简化版任务调度器的实现和应用(2)
背景
再上一文简化版任务调度器的实现和应用(1)中简单介绍了如何实现简化版的任务调度器,这里再做一些回忆:
- 该任务调度器基于链表,即将待执行的task函数指针以tle(链表元素)的形式上下串联。在调度器被激活以后依次执行所链表内的task(没有优先级)
- 该任务调度器基于systick中断作为节拍器,中断函数用于激活任务调度器。
- 支持sleep,delay以及定时等模式
- 任务可随时入列或者出列
基于此,我们可得到下面的任务调度进度图:
这里对图片做个简单的介绍:
- 水平方向所有的箭头表示CPU的控制权占有情况,即ARM的CPU会被4个函数来回切换,这包含了两个task函数,1在前用绿色表示,2在后黄色表示,调度器为灰色,systick中断函数为棕色。
- 垂直方向的虚线箭头表示当前CPU的跳转,即从一个函数跳到另一个函数。其中黑色虚线表示中断函数的进入和退出,紫色虚线表示任务调度器控制下的CPU跳转。其中黑色虚线是强制性的,周期性的,在这里可以使1ms触发一次或者100us触发一次。只要触发时间达到,CPU立马跳入Systick的中断函数中去。
- 进一步介绍,我们假定初始时刻恰好来了一个systick中断,即Tick,随后CPU来到了中断函数,中断函数激活了调度器(CNt = CNT+1,结果就是CNT=1),而中断函数返回也回到了调度器.
- 调度器判断出CNT>0后,即开始展开任务的调度,同时将CNT=CNT-1,即得到CNT=0.图中灰色的实心箭头,第一个要进入的就是Task1
- 进入到Task1后,运行了一段时间(绿色剪头)后返回调度器
- 调度器判断后面还有Task2,然后准备进去Task2
- 进入到Task2后,运行了一段时间(黄色箭头)后返回调度器
- 此时调度器观察到没有后续的Task了,同时CNT==0,所以调度器进入IDLE,即灰色空心箭头,等待下一个Tick
- 第二个Tick达到后,同样是强制性的来到中断函数,中断函数激活了调度器(CNt = CNT+1,结果就是CNT=1),而中断函数返回也回到了调度器.
- 调度器判断出CNT>0后,即开始展开任务的调度,同时将CNT=CNT-1,即得到CNT=0.图中灰色的实心箭头,第一个要进入的就是Task1
- 进入到Task1后,运行了一段时间(绿色剪头)后返回调度器
- 调度器判断后面还有Task2,然后准备进去Task2
- 进入到Task2后,运行了很长一段时间(黄色箭头),此时仍未回到调度器第三个Tick就来了,那CPU只能强制来到中断函数,中断函数激活了调度器(CNt = CNT+1,结果就是CNT=1),而中断函数返回却回到了Task2
- 待Task2完成后回到了调度器,此时调度器观察到后续没有Task,正准备挂起的时候发现此时的CNT!=0,随后调度器又急忙开始下一轮的任务调度,即又来到Task1.(注意此时调度器没有IDLE,而是立即展开下一轮的调度)
讲到这里,大家差不多应该了解了我们任务调度器的核心功能。 那么回到本文的重点,我们如何最大化的利用这个调度器来为我们服务呢?否者如果只是简单的先后执行各种Task,那还不如就将其放到一个While(1)里,还省点事情呢?
这就是本文想要解释说明的,基于状态机的复杂TASK的实现。
正文
不知道大家是否了解状态机,这个是在FPGA设计过程中不可或缺的重要环节,基于此状态机才使得所有的逻辑资源有条不紊的展开。那么回到ARM中,我们常常利用ARM实现非常繁琐的任务,这些任务可能都非常简单,却有着非常严格的先后依赖关系,如下图所示:
上图几乎涵盖了我们对Task的所有期待:
- 根据条件判断是否跳出当前状态或者保持当前状态不变
- 超时机制,即一个状态的持续时间超过一定的范围就会判定为超时,根据情况状态机到其他状态。
- Delay机制,即强制delay多少时间后进入下一个状态。
虽然上图仅仅是一个最简单的例程,但其他更加复杂的应用实际上就是上述三个元素的组合叠加而已。
同时,在复杂的Task在任务调度器看来就只有两种情况:
- 如果Task是Ready的,那就进去执行
- 如果Task是Delay或者Sleep的,那就直接pass,去看下一个任务的激活情况。
所以设计的思路就是,第一步我们定义几个状态
typedef enum
{
Task_Idle = ((uint32_t)0),
Task_Process = ((uint32_t)1),
Task_Timeout = ((uint32_t)2),
Task_Success = ((uint32_t)3),
Task_Delay = ((uint32_t)4)
}TASK_StatusEnum;
从名字上来看就非常的直观,这里不再絮叨。
然后再来看一下我们定义的休眠以及Delay的宏
#define __TASK_BEGIN begin:
#define __TASK_END {\
end:\
return 1;\
}
#define __TASK_RESTART goto begin;
#define __TASK_RETURN goto end;
#define __TASK_DELAY(task_handle, delay_in_us) {\
task_handle->delay_us = delay_in_us;\
task_handle->task_status = Task_Delay;\
__TASK_RETURN\
}
#define __TASK_SLEEP(task_handle) {\
task_handle->task_status = Task_Sleep;\
__TASK_RETURN\
}
#define __TASK_ENABLE_TIMER_RETURN(task_handle, timeout_us) {\
task_handle->timer_us = timeout_us;\
task_handle->task_status = Task_ReadyWithTimer;\
__TASK_RETURN\
}
#define __TASK_ENABLE_TIMER_RESTART(task_handle, timeout_us) {\
task_handle->timer_us = timeout_us;\
task_handle->task_status = Task_ReadyWithTimer;\
__TASK_RESTART\
}
#define __TASK_DISABLE_TIMER_RESTART(task_handle) {\
task_handle->timer_us = 0;\
task_handle->task_status = Task_Ready;\
__TASK_RESTART\
}
#define __TASK_DISABLE_TIMER_RETURN(task_handle) {\
task_handle->timer_us = 0;\
task_handle->task_status = Task_Ready;\
__TASK_RETURN\
}
我们在构建Task主体的时候,会大量的利用这些宏,基于这些宏定义,可以实现:
- task begin 定义任务的开始位置方便跳转
- task end 定义任务的结束位置,方便跳转
- restart,即再次执行当前task,为了加速某些应用,一般情况下这个时候的状态机会执行与上一次不一样的状态
- return,即马上退出当前task
- enable timer,将状态机的主状态设置为ready_timer,同时给timer计数器正确的赋值,然后restart或者return
- diasable timer,将状态机的主状态设置为ready,同时给timer计数器赋值为0,然后restart或者return
- delay,将状态机的主状态置为delay,同时给delay的计数器正确的赋值,然后return
- sleep,将状态机的主状态置为sleep,同时return
利用这些宏就可以实现很复杂的Task,也包括上面的例程
然后开始构建我们的Task主体部分
int32_t Task_Commu_Process_Handler(BSPTaskHandleStr* task_handle, void * para)
{
uint8_t flag_timeout = 0;
// check timeout or not and keep the flag of timeout;
if(task_handle->task_status == Task_ReadyWithTimer)
flag_timeout = (task_handle->timer_us < 0)? 1 : 0;
__TASK_BEGIN
switch((TASK_CommuStatusEnum)task_handle->subtask_status)
{
case Task_Idle:
{
if(a == 0)
{
//如果期望的信号没有来到,保持idle,立即跳到__TASK_END
task_handle->subtask_status = (uint32_t)Task_Idle;
__TASK_RETURN
}
else
{
//如果期望的信号已经来到,开启定时器后,状态置为process,重启状态机,即跳回到__TASK_BEGIN处继续执行
task_handle->subtask_status = (uint32_t)Task_Process;
__TASK_ENABLE_TIMER_RESTART(task_handle, 1000)
}
}
case Task_Process:
{
if(done == 1)
{
//如果期望的信号已经来到,关闭定时器,将状态置为success,重启状态机,即跳回到__TASK_BEGIN处继续执行
task_handle->subtask_status = (uint32_t)Task_Success;
__TASK_DISABLE_TIMER_RESTART(task_handle)
}
else if (flag_timeout == 1)
{
//如果期望的信号没有来到,但是超时的信号来到,关闭定时器,将状态置为Timeout,重启状态机,即跳回到__TASK_BEGIN处继续执行
task_handle->subtask_status = (uint32_t)Task_Timeout;
__TASK_DISABLE_TIMER_RESTART(task_handle)
}
else
{
//如果都没有,这保持状态不变,同时立即跳到__TASK_END
task_handle->subtask_status = (uint32_t)Task_Process;
__TASK_RETURN
}
}
case Task_Timeout:
{
//默认将状态置为Idle,同时立即跳到__TASK_END
task_handle->subtask_status = (uint32_t)Task_Idle;
__TASK_RETURN
}
case Task_Success:
{
//默认将状态置为delay,同时设置delay,然后立即跳到__TASK_END
task_handle->subtask_status = (uint32_t)Task_Delay;
__TASK_DELAY(task_handle, 1000000)
}
case Task_Delay:
{
//默认将状态置为Idle,同时立即跳到__TASK_END
task_handle->subtask_status = (uint32_t)Task_Idle;
__TASK_RETURN
}
default:
{
//默认将状态置为Idle,同时立即跳到__TASK_END
task_handle->subtask_status = (uint32_t)Task_Idle;
__TASK_RETURN
}
}
__TASK_END
}
上面的源码的注释很清楚的解释了整个状态跳转的所有信息,实际上就是基于条件来判断下一个状态,然后决定是否跳出状态机或者在次进入状态机。
需要解释的是:
- 在delay状态,实际上任务调度器不会再进入Task,直到其delay的时候消耗完,Task才会再次被激活,如下图所示
第二个的Tick后,任务调度器并没有进入Task1,因为在此之前就被Delay或者Sleep了,因此将其pass了,直接进入Task2
等待delay或者sleep被解除后,任务调度器才会重新进入Task1. - 任务初始化函数(包括入列)以及任务调度器和systick中断函数并没有被包含在内,因为上一篇已经介绍过了。
思考
到目前为止,基于简化版任务调度器的实现和应用就都完成了,希望大家能够理解,并将其应用到自己的程序中去,以应对复杂多变的固件需求。