与总体产品联调时,需要各个单机系统严格按照总体要求,进行数据输出,时间的偏差将出现系统异常,控制失败等不稳定情况产生,甚至影响到产品安全。
因此必须确保某些关键任务的优先执行。单片机任务优先级一般有两种方式实现,基于单片机中断服务的中断函数进行实现和基于实时操作系统的任务调度实现。
基于中断服务函数实现的任务优先级对单片机硬件资源有要求,而对于实时操作系统的任务调度方式,仅需一个定时器就可完成多任务多优先级的管理。
参与某产品联调时,总体要求每间隔5ms向总控发
送一次关键数据。
当系统联调运行时,总控会产生超时报警,报警内容是通信超时。
经过排查排除了硬件问题、电磁干扰问题、程序逻辑错误未正常发送数据等问题。
通过报警时间比对,发现该报警出现时间没有规律性。通过示波器查看发现,其发送数据周期没有严格按照5ms 间隔时间发送,发送时间落在 5ms 区间段内,任意时间点都可能会进行关键数据的传递,无法预测下次一次发送数据的准确时间,当系统在规定时间内未接收到数据时,产生系统报警。
经过对程序进行逻辑分析,出现问题原因是单片机运
行任务是顺序执行,只有轮到发送数据任务执行时,才能发送数据,如果其他任务占用执行时间过长,将会导致发送任务不能在5ms时间内再次获得运行机会,因此也无法按时发送数据,造成数据超时问题。
1 关键任务优先执行方法
1.1 查找问题
下位机程序任务流程如图1所示:
下位机程序按照项目功能需求,将不同功能划分为不
同任务,根据每个任务特点,制定的间隔时间不一致,
如 对RS485等通信口监听时,其响应时间在50ms满足要求,自然环境下温度变化缓慢,因此温度采集500ms一次也满足要求。通过计时器进行技术,当5ms时置位5ms任务标志位,10ms时置位5ms和10ms任务标志位,通过任务标志位定义了任务执行频率,优先级高的任务得到更多执行次数。该种任务执行方式称为任务协同方式,当一个任务执行时,必须等到该任务执行完成,才能执行下一个任务。当某一时刻,多个时间任务被置位时,其按照顺序结构运行程序,任务需要排队执行,实时性不高。
下位机程序使用任务协同方式进行运行,分别定义了
5ms,10ms,20ms,50ms,100ms,200ms,500ms 等任务。
所有的任务基于顺序执行,其中5ms程序critical_task
作为关键任务。某个时刻,如定时器在计数到500ms时,其上的5ms,10ms,20ms,50ms,100ms,200ms 时间标志位被置位,任务均得到执行,导致500ms这一时刻需要顺序执行很多任务,如在5ms内不能执行完全部任务,那么下一次的关键任务程序 critical_task 将不能按时被执行,导致输出超时情况产生。 为解决超时问题,必须提升critical_task 任务的优先级,提升任务优先级的方式较多,常用的方式有中断服务函数(前后台系统)、实时操作系统实现。
1.2 关键任务任务由前后台系统保证
单片机是单核处理器,不能同时执行多个任务。
从主程序架构上看,该种顺序执行方式不能保证关键
任务critical_task 的优先执行。因此应该使用某种方式
能够中断当前正在顺序执行的任务,转向执行优先级较高的任务。
单片机中断是指正在执行一项任务A,然后突然停止
任务A去执行任务B,执行完任务B再回来继续执行任务
A 的过程。单片机中断有很多触发源,如定时器中断、外部按键中断、通信发送、接收数据中断,每个中断源都可以打断正在执行的任务,转向执行中断任务,中断任务执行完毕后,继续回到当前的任务进行未完成的操作,利用单片机的中断特性,能够保证某些代码的及时执行。将某些关键任务放入中断服务函数中,就能打断顺序执行任务而优先执行中断任务,使用该种方式提升了关键任务的优先级。
单片机中断源的产生有很多方式,与产品联调问题是
不能在5ms时准确的进行数据传输,因此需要在5ms时产生一次任务中断,以执行发送任务。为了满足上述要求,选择定时器中断可满足要求。
定时器中断是指单片机内部有一个从0开始向上(向
下)计数的计数器,每一次计数时间均相同,设置一个计
数目标值,当计数器计数到目标值时,会产生一个计数中断,中断后单片机可以打断当前正在执行的程序跳转执行中断服务程序。在中断服务程序中,清除中断服务向量,使得计数器归0重新计数,以此不断循环达到每隔一定时间就产生一次服务中断的工作模式。
我们将原程序中的critical_task任务从5ms任务重
移除,添加到 Count5ms_OnInterrupt 中断服务程序中,通过前后台方式实现打断其他正在执行任务来保证
critical_task 的优先执行。程序更改后critical_task
能够5ms一次准确输出,系统报警现象被消除。采用此种方法虽然简单,但是突出问题有几个:
(1)使用中断方式来保证优先级需要占用一个中断
来完成,浪费资源。
(2)当有多个关键任务需要执行时,会出现中断嵌
套,关键任务仍然会被打断执行。
(3)违背了中断中只执行不耗时
简单操作的原则,
仍然存在隐患。
(4)不能对不同任务进行不同权重的CPU使用权划
分。
C51 单片机任务协同的实现可以通过定时器中断和任务标志位来完成。在 C51 中,定时器的配置和中断处理稍有不同于现代的 ARM Cortex 系列单片机,但基本原理是相似的。下面是一个简单的示例程序,演示如何使用 C51 编写一个任务协同的程序,涉及10ms、30ms、50ms、100ms 的任务执行:
#include <REG51.H>
// 定义任务标志位
bit task_10ms_flag = 0;
bit task_30ms_flag = 0;
bit task_50ms_flag = 0;
bit task_100ms_flag = 0;
// 定义定时器0的初值
#define TIMER0_INIT_VALUE 65536 - (12000 / 12) // 12MHz晶振,计时1ms
// 定时器0中断处理函数
void Timer0_ISR(void) interrupt 1
{
static unsigned char count_10ms = 0;
static unsigned char count_30ms = 0;
static unsigned char count_50ms = 0;
static unsigned char count_100ms = 0;
// 10ms任务
count_10ms++;
if (count_10ms >= 10)
{
count_10ms = 0;
task_10ms_flag = 1;
}
// 30ms任务
count_30ms++;
if (count_30ms >= 30)
{
count_30ms = 0;
task_30ms_flag = 1;
}
// 50ms任务
count_50ms++;
if (count_50ms >= 50)
{
count_50ms = 0;
task_50ms_flag = 1;
}
// 100ms任务
count_100ms++;
if (count_100ms >= 100)
{
count_100ms = 0;
task_100ms_flag = 1;
}
}
void main(void)
{
// 初始化定时器0
TMOD = 0x01; // 定时器0工作在模式1
TH0 = TIMER0_INIT_VALUE >> 8; // 预设初值
TL0 = TIMER0_INIT_VALUE & 0xFF;
TR0 = 1; // 启动定时器0
ET0 = 1; // 允许定时器0中断
EA = 1; // 允许总中断
while (1)
{
// 执行10ms任务
if (task_10ms_flag)
{
task_10ms_flag = 0;
// 在此处执行10ms的任务
}
// 执行30ms任务
if (task_30ms_flag)
{
task_30ms_flag = 0;
// 在此处执行30ms的任务
}
// 执行50ms任务
if (task_50ms_flag)
{
task_50ms_flag = 0;
// 在此处执行50ms的任务
}
// 执行100ms任务
if (task_100ms_flag)
{
task_100ms_flag = 0;
// 在此处执行100ms的任务
}
}
}
程序说明:
-
定时器设置:使用定时器0(TMOD = 0x01),工作在模式1下,计时器1ms。通过中断每1ms计时,并根据计数器判断是否到达设定的任务执行时间。
-
任务标志位:使用四个标志位
task_10ms_flag
、task_30ms_flag
、task_50ms_flag
、task_100ms_flag
来表示对应的任务是否需要执行。 -
任务执行:在主循环中,根据任务标志位的状态执行对应的任务,每次任务完成后将对应的标志位清零。
注意事项:
-
定时器配置:根据具体的晶振频率和需求调整
TIMER0_INIT_VALUE
的值,确保定时器产生1ms的中断。 -
任务执行顺序:任务按照设定的时间间隔执行,且串行执行,适合简单控制任务。
-
实时性:由于是基于定时器中断,任务的实时性取决于定时器的精确性和系统负载。
这个简单的示例展示了如何在 C51 单片机上实现基于定时器和任务标志位的任务协同方式,适用于对实时性要求不是非常高的简单控制和数据采集应用。
在上述的程序中,的确存在一种可能性,即30ms和50ms的任务同时在处理的情况。这是因为任务标志位 task_30ms_flag
和 task_50ms_flag
可能会在同一个定时器中断周期内同时被置位,从而导致它们在主循环中几乎同时被检测到并执行对应的任务代码段。
要解决这个问题,可以考虑以下几点改进:
-
任务优先级管理:在任务标志位设置时,考虑将更高优先级的任务标志位先置位,确保高优先级任务优先执行。例如,在定时器中断处理函数中,先处理100ms任务标志位,然后是50ms、30ms、最后是10ms任务标志位。
-
任务执行顺序控制:在主循环中,确保每次只处理一个任务标志位,并且每个任务完成后清除对应的标志位。这样可以避免同时处理多个任务的情况。
-
优化任务时间间隔:考虑调整任务的时间间隔,使得不同任务的触发时间点尽可能错开,减少同时触发的可能性。
下面是改进后的代码示例,演示如何根据任务的优先级顺序处理任务标志位:
#include <REG51.H>
// 定义任务标志位
bit task_10ms_flag = 0;
bit task_30ms_flag = 0;
bit task_50ms_flag = 0;
bit task_100ms_flag = 0;
// 定义定时器0的初值
#define TIMER0_INIT_VALUE 65536 - (12000 / 12) // 12MHz晶振,计时1ms
// 定时器0中断处理函数
void Timer0_ISR(void) interrupt 1
{
static unsigned char count_10ms = 0;
static unsigned char count_30ms = 0;
static unsigned char count_50ms = 0;
static unsigned char count_100ms = 0;
// 100ms任务
count_100ms++;
if (count_100ms >= 100)
{
count_100ms = 0;
task_100ms_flag = 1;
}
// 50ms任务
if (count_100ms % 50 == 0)
{
task_50ms_flag = 1;
}
// 30ms任务
if (count_100ms % 30 == 0)
{
task_30ms_flag = 1;
}
// 10ms任务
count_10ms++;
if (count_10ms >= 10)
{
count_10ms = 0;
task_10ms_flag = 1;
}
}
void main(void)
{
// 初始化定时器0
TMOD = 0x01; // 定时器0工作在模式1
TH0 = TIMER0_INIT_VALUE >> 8; // 预设初值
TL0 = TIMER0_INIT_VALUE & 0xFF;
TR0 = 1; // 启动定时器0
ET0 = 1; // 允许定时器0中断
EA = 1; // 允许总中断
while (1)
{
// 执行100ms任务
if (task_100ms_flag)
{
task_100ms_flag = 0;
// 在此处执行100ms的任务
}
// 执行50ms任务
if (task_50ms_flag)
{
task_50ms_flag = 0;
// 在此处执行50ms的任务
}
// 执行30ms任务
if (task_30ms_flag)
{
task_30ms_flag = 0;
// 在此处执行30ms的任务
}
// 执行10ms任务
if (task_10ms_flag)
{
task_10ms_flag = 0;
// 在此处执行10ms的任务
}
}
}
改进说明:
-
优先级处理:在定时器中断处理函数中,先处理更高优先级的任务标志位(例如100ms任务),然后逐级处理较低优先级的任务标志位。这样可以确保在同一个定时器中断周期内只有一个任务标志位被置位。
-
顺序执行:在主循环中,每次只检测并执行一个任务标志位的任务,确保任务的顺序执行,并在每个任务完成后清除对应的标志位。
通过这样的改进,可以有效避免30ms和50ms任务同时在处理的情况,保证任务执行的可控性和稳定性。