今天我们来回想一下我们学习单片机的历程,从大三开始学习单片机到现在有四年了吧。编写的程序还只是一个大while循环。。。也就是所谓的前后台程序,对于多任务的处理,要不就是顺序执行(if、switch等等),要不就是放在中断。虽然做法很low,但是应付大多数的程序都够了。
随着程序复杂度的提升,使用前后台程序就显得有些混乱了。假如说一个ECU的底层,ECU要采集实时踏板、水温、油压、轨压、空气压力、转速、进气量等等各种信息,还要实时计算这些信息控制燃油的喷射,还要能提供整车控制任务,甚至还要能输出诊断信息。可以想象这任务量是很庞大的,单单使用顺序执行,中断显然是不够的。这种层次的任务量才是真正的多任务,能够处理这种多任务才算是真正入了门。
操作系统是很多人的选择,操作系统就是为了多任务的调度而生的,调度也是操作系统的核心。我们今天讲一个比操作系统第一个层次的多任务调度方法,就是时间片轮询法。我之前虽然用到了,但是一直没有系统性的规划这种方法。今天我们就尝试用这种方法搭建一个系统性质的运行环境。
1、首先来说明定时器的复用
上一贴我们使用STM搭建了1ms定时的底层,并且开了匹配中断。我们现在需要在此基础上得到10ms、20ms、100ms的定时,需要怎么做呢? 不难想,我们只需要设定一个定时值,假如10,每次中断减一,判断标志位为0就得到了10ms的定时。这就是定时器的复用。代码上,我们可以参考以下的方式:
#define TASK_NUM (3) // 这里定义的任务数为3,表示有三个任务会使用此定时器定时。
uint16 TaskCount[TASK_NUM] // 这里为三个任务定义三个变量来存放定时值
uint8 TaskMark[TASK_NUM]; // 同样对应三个标志位,为0表示时间没到,为1表示定时时间到。
/* FunctionName : TimerInterrupt() * Description : 定时中断服务函数 * EntryParameter : None * ReturnValue : None*/
/*代码解释:定时中断服务函数,在中断中逐个判断,如果定时值为0了,表示没有使用此定时器或此定时器已经完成定时,不做处理。否则定时器减一,知道为零时,相应标志位值1,表示此任务的定时值到了。*/
void TimerInterrupt(void)
{
uint8 i;
for (i=0; i<TASKS_NUM; i++)
{
if (TaskCount)
{
TaskCount--;
if (TaskCount == 0)
{
TaskMark = 0x01;
}
}
}
TaskCount[0] = 20; // 延时20ms
TaskMark[0] = 0x00; // 启动此任务的定时器
大家可以体会一下以上代码,我们可以在1ms的基础定时上,得到20ms的定时。
2、时间片轮询法
时间片其实和以上是一个道理,也是利用定时器的复用,增加的一点就是任务调度。就是在相应的定时时间到了调度相应的任务,而其他任务则仍然处于延时当中。相当于是在其他任务空闲的时候运行调度任务,这样就能充分利用CPU时间,我们的操作系统就是这样的,当然操作系统要更复杂。接下来我们用代码来实现:
首先定义一个任务结构体,该结构体包含一个运行任务的所有信息。
typedef struct _TASK_COMPONENTS
{
uint8 Run; // 程序运行标记:0-不运行,1运行
uint8 Timer; // 计时器
uint8 ItvTime; // 任务运行间隔时间
void (*TaskHook)(void); // 要运行的任务函数
} TASK_COMPONENTS; // 任务定义
接下来定义一个任务标志处理函数,类似于中断服务函数。作用是处理任务标志位:
void TaskRemarks(void)
{
uint8 i;
for (i=0; i<TASKS_MAX; i++) // 逐个任务时间处理
{
if (TaskComps.Timer) // 时间不为0
{
TaskComps.Timer--; // 减去一个节拍
if (TaskComps.Timer == 0) // 时间减完了
{
TaskComps.Timer = TaskComps.ItvTime; // 恢复计时器值,从新下一次
TaskComps.Run = 1; // 任务可以运行
}
}
}
}
这个函数要放在定时中断里面,和我们定时复用里面讲的一样,这里就是各个任务定时的核心。
接下来是任务处理函数,这个函数可以说是系统任务处理的核心。只需放在while循环里面,随着时间片的流转,该函数不断调用任务函数,完成轮询调度的工作。
void TaskProcess(void)
{
uint8 i;
for (i=0; i<TASKS_MAX; i++) // 逐个任务时间处理
{
if (TaskComps.Run) // 时间不为0
{
TaskComps.TaskHook(); // 运行任务
TaskComps.Run = 0; // 标志清0
}
}
}
说到这里,整个系统结构就很清楚了。在定时里面完成任务定时的复用,然后在主循环里面不停的轮询调用任务函数,这样已完成多任务的调度。
接下来我们给出一个例子:
static TASK_COMPONENTS TaskComps[] =
{
{0, 1000, 1000, TaskDisplayClock}, // 显示时钟
{0, 20, 20, TaskKeySan}, // 按键扫描
{0, 30, 30, TaskDispStatus}, // 显示工作状态
// 这里添加你的任务。。。。
};
这里我们有三个任务,显示当前时钟,1s显示一次,因为我们基础定时为1ms,所以定时值为1000;按键扫描为了消抖,所以定为20ms的任务;最后一个是显示工作状态,定为30ms的任务。
typedef enum _TASK_LIST
{
TAST_DISP_CLOCK, // 显示时钟
TAST_KEY_SAN, // 按键扫描
TASK_DISP_WS, // 工作状态显示
// 这里添加你的任务。。。。
TASKS_MAX // 总的可供分配的定时任务数目
} TASK_LIST;
定义任务列表为枚举类型。
接下来则是各个任务的定义;
/* FunctionName : TaskDisplayClock() * Description : 显示任务 * EntryParameter : None * ReturnValue : None */
void TaskDisplayClock(void)
{
}
/* FunctionName : TaskKeySan() * Description : 扫描任务 * EntryParameter : None * ReturnValue : None */
void TaskKeySan(void)
{
}
/* FunctionName : TaskDispStatus() * Description : 工作状态显示 * EntryParameter : None * ReturnValue : None*/
void TaskDispStatus(void)
{
}
最后是主函数部分:
/* FunctionName : main() * Description : 主函数 * EntryParameter : None * ReturnValue : None */
int main(void)
{
InitSys(); // 初始化
while (1)
{
TaskProcess(); // 任务处理
}
再添加好系统初始化之后,就可以在while循环放入任务调出函数。当然不要忘了在定时中断里面放入标志位处理函数。
有没有发现这个系统都很清晰,定义好这些以后,我们只需要定义任务的信息和任务函数就完了,更本不用管主函数。只要定义好任务时间,一切都会按部就班的工作。一瞬间世界安静了。。。。。。
大家可以在实际中应用,体会这种调度方法的妙处。
好了今天的帖子就到这里,我们下次再唠。
(参考文献:Killoser. 牛人分享: 单片机应用程序架构的经验总结. https://forum.mianbaoban.cn/topic/60374_1_1.html?from=timeline.2017)
只有注册用户才能在此添加评论。 如果您已经注册,请登录。 如果您还没有注册,请注册并登录。