合作式调度器Cooperative Scheduler

背景

单片机开发,入门从最开始的IO置位(点亮LED)开始,裸机开发来说整个是面向过程开发,最终所有的功能都在一个While循环之中,这样的好处在于模块逻辑很直观,流程比较清晰,但是在程序功能增多的时候整体功能会显得不直观。
且需要很多的标志位,同时由于所有的功能循环在一起运行,对于任务的运行间隔并不能很好的确定,有时也会因一个任务时间过长导致别的任务没有及时得到响应。
现在市面上的嵌入式RTOS也很多,这些RTOS可以很好设置任务间隔,这样用户可以根据自己应用来定义。
不过RTOS一般都需要一些汇编的知识,入门门槛较高,且RTOS成功用于应用需要对RTOS有一个很好的了解,且应用编写也需要一定的规则与要求,对于初学者来说难度较大。
合作式调度器是一种简单且可预测的调度方式,能较好地满足,任务按时调度的需求。同时合作式调度器全部有C语言实现,便于理解与移植。可以应用于很多的电子类产品,简单的复杂的,消费品及工业品

合作式调度器

调度器

可以看作是一个简单的操作系统,允许以周期性或单次方式来调用任务。从底层来看,为不同任务提供定时器中断服务,即初始化一个定时器为不同任务报价定时调用的功能。
嵌入式的RTOS也是一种调度器,一般都是可抢占的调度器,类似于freertos。抢占式调度器,通常较为复杂,需要实现任务的上下文切换,添加各种安全机制,锁机制,临界段,消息机制等。
合作式调度器,只是想实现定时调度任务,可以不需要额外的机制,实现简单。同时对过程来说,是很可控的。不会因为一些作务的切换导致运行不正常。

实现步骤

  1. 数据结构
  2. 初始化任务空间
  3. 定时器中断服务程序,用于刷新调度器
  4. 向调度器增加任务的函数
  5. 调度函数,可在运行循环函数内运行,即调度器更新去运行准备好的任务
  6. 删除任务函数,可以不添加,因为有些系统可以不删除任务

代码示例

任务数据结构

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可以很快的移植过去使用。
当然合作式调度器使用中也有一点地方要注意的。

  1. 每个任务的运行时间不可以过长,否则会影响系统的整体时效性。因此一些长时间运行的任务需要进行分解,使每次任务调用,不用过长的时间
  2. 所有的运行任务采用一个相同的delay,可能会导致任务的运行时间会有轻微偏差,如果对这一点要求过高,可以不同任务采用不同的delay,使调度任务时,尽量只有一个任务是需要调度的

实际的现实效果,可以在系统完成,添加一些测量函数,来评估下当前调度器的运行状态。
总体来说,合作式用于简单的电子应用,是一个很好的实现机制。
示例代码

参考书籍:时间触发嵌入式系统设计模式

posted @ 2022-02-24 10:54  cau_par  阅读(560)  评论(0编辑  收藏  举报