背景
单片机开发,入门从最开始的IO置位(点亮LED)开始,裸机开发来说整个是面向过程开发,最终所有的功能都在一个While循环之中,这样的好处在于模块逻辑很直观,流程比较清晰,但是在程序功能增多的时候整体功能会显得不直观。
且需要很多的标志位,同时由于所有的功能循环在一起运行,对于任务的运行间隔并不能很好的确定,有时也会因一个任务时间过长导致别的任务没有及时得到响应。
现在市面上的嵌入式RTOS也很多,这些RTOS可以很好设置任务间隔,这样用户可以根据自己应用来定义。
不过RTOS一般都需要一些汇编的知识,入门门槛较高,且RTOS成功用于应用需要对RTOS有一个很好的了解,且应用编写也需要一定的规则与要求,对于初学者来说难度较大。
合作式调度器是一种简单且可预测的调度方式,能较好地满足,任务按时调度的需求。同时合作式调度器全部有C语言实现,便于理解与移植。可以应用于很多的电子类产品,简单的复杂的,消费品及工业品
合作式调度器
调度器
可以看作是一个简单的操作系统,允许以周期性或单次方式来调用任务。从底层来看,为不同任务提供定时器中断服务,即初始化一个定时器为不同任务报价定时调用的功能。
嵌入式的RTOS也是一种调度器,一般都是可抢占的调度器,类似于freertos。抢占式调度器,通常较为复杂,需要实现任务的上下文切换,添加各种安全机制,锁机制,临界段,消息机制等。
合作式调度器,只是想实现定时调度任务,可以不需要额外的机制,实现简单。同时对过程来说,是很可控的。不会因为一些作务的切换导致运行不正常。
实现步骤
- 数据结构
- 初始化任务空间
- 定时器中断服务程序,用于刷新调度器
- 向调度器增加任务的函数
- 调度函数,可在运行循环函数内运行,即调度器更新去运行准备好的任务
- 删除任务函数,可以不添加,因为有些系统可以不删除任务
代码示例
任务数据结构
typedef struct sTask
{
void (*pTask)(void);
uint32 delay;
uint32 period;
uint8 runMe;
}sTask;
总的任务空间
#define SCH_MAX_TASKS 10
sTask schTaskGroup[SCH_MAX_TASKS];
初始化任务空间
void Task_Framework_Init(void)
{
uint8 i = 0;
for(i = 0; i < SCH_MAX_TASKS; i++)
{
schTaskGroup[i].pTask = 0;
schTaskGroup[i].delay = 0;
schTaskGroup[i].period = 0;
schTaskGroup[i].runMe = 0;
}
}
定时器中断更新调度器
void Task_Framework_Update(void)
{
uint8 index = 0;
for(index = 0; index < SCH_MAX_TASKS; index++)
{
if(schTaskGroup[index].pTask)
{
if(schTaskGroup[index].delay == 0)
{
schTaskGroup[index].runMe += 1;
if(schTaskGroup[index].period)
{
schTaskGroup[index].delay = schTaskGroup[index].period - 1;
}
}
else
{
schTaskGroup[index].delay -= 1;
}
}
}
}
添加任务
uint8 Task_Framework_Add_Task(void (*pTask_)(void), uint32 delay_, uint32 period_)
{
uint8 index = 0;
while((schTaskGroup[index].pTask != 0) && index < SCH_MAX_TASKS)
{
index++;
}
if(index == SCH_MAX_TASKS)
{
return SCH_MAX_TASKS;
}
schTaskGroup[index].pTask = pTask_;
schTaskGroup[index].delay = delay_;
schTaskGroup[index].period = period_;
schTaskGroup[index].runMe = 0;
return index;
}
调度函数
调度函数可以在定时器中断中,在更新完直接调用,也可以在主循环中调用。个人倾向于在主循环中调用,减少在中断操作。
void Task_Framework_Dispatch_Tasks(void)
{
uint8 index = 0;
for(index = 0; index < SCH_MAX_TASKS; index++)
{
if(schTaskGroup[index].runMe > 0)
{
(*(schTaskGroup[index].pTask))();
schTaskGroup[index].runMe -= 1;
if(schTaskGroup[index].period == 0)
{
Task_Framework_Delete_Task(index);
}
}
}
}
删除任务
没有删除任务需求,这个函数也可以不添加。
uint8 Task_Framework_Delete_Task(uint8 taskId_)
{
uint8 errorCode = 0;
if(taskId_ < SCH_MAX_TASKS)
{
schTaskGroup[taskId_].pTask = 0;
schTaskGroup[taskId_].delay = 0;
schTaskGroup[taskId_].period = 0;
schTaskGroup[taskId_].runMe = 0;
}
return errorCode;
}
demo
int main( void )
{
System_Framework_Init();
Task_Framework_Add_Task(App_Test, 0, 100);
Task_Framework_Add_Task(App_Polling, 0, 200);
while(1)
{
Task_Framework_Dispatch_Tasks();
}
}
void timer_interrupt(void)
{
Task_Framework_Update();
}
总结
通过上述的代码就可以实现一个合作式调度器,对于不同的MCU可以很快的移植过去使用。
当然合作式调度器使用中也有一点地方要注意的。
- 每个任务的运行时间不可以过长,否则会影响系统的整体时效性。因此一些长时间运行的任务需要进行分解,使每次任务调用,不用过长的时间
- 所有的运行任务采用一个相同的delay,可能会导致任务的运行时间会有轻微偏差,如果对这一点要求过高,可以不同任务采用不同的delay,使调度任务时,尽量只有一个任务是需要调度的
实际的现实效果,可以在系统完成,添加一些测量函数,来评估下当前调度器的运行状态。
总体来说,合作式用于简单的电子应用,是一个很好的实现机制。
示例代码
参考书籍:时间触发嵌入式系统设计模式