ARM设计: 简化版任务调度器的实现和应用(2)

简化版任务调度器的实现和应用(2)

背景

再上一文简化版任务调度器的实现和应用(1)中简单介绍了如何实现简化版的任务调度器,这里再做一些回忆:

  1. 该任务调度器基于链表,即将待执行的task函数指针以tle(链表元素)的形式上下串联。在调度器被激活以后依次执行所链表内的task(没有优先级)
  2. 该任务调度器基于systick中断作为节拍器,中断函数用于激活任务调度器。
  3. 支持sleep,delay以及定时等模式
  4. 任务可随时入列或者出列

基于此,我们可得到下面的任务调度进度图:
ARM设计: 简化版任务调度器的实现和应用(2)_第1张图片
这里对图片做个简单的介绍:

  1. 水平方向所有的箭头表示CPU的控制权占有情况,即ARM的CPU会被4个函数来回切换,这包含了两个task函数,1在前用绿色表示,2在后黄色表示,调度器为灰色,systick中断函数为棕色。
  2. 垂直方向的虚线箭头表示当前CPU的跳转,即从一个函数跳到另一个函数。其中黑色虚线表示中断函数的进入和退出,紫色虚线表示任务调度器控制下的CPU跳转。其中黑色虚线是强制性的,周期性的,在这里可以使1ms触发一次或者100us触发一次。只要触发时间达到,CPU立马跳入Systick的中断函数中去。
  3. 进一步介绍,我们假定初始时刻恰好来了一个systick中断,即Tick,随后CPU来到了中断函数,中断函数激活了调度器(CNt = CNT+1,结果就是CNT=1),而中断函数返回也回到了调度器.
  4. 调度器判断出CNT>0后,即开始展开任务的调度,同时将CNT=CNT-1,即得到CNT=0.图中灰色的实心箭头,第一个要进入的就是Task1
  5. 进入到Task1后,运行了一段时间(绿色剪头)后返回调度器
  6. 调度器判断后面还有Task2,然后准备进去Task2
  7. 进入到Task2后,运行了一段时间(黄色箭头)后返回调度器
  8. 此时调度器观察到没有后续的Task了,同时CNT==0,所以调度器进入IDLE,即灰色空心箭头,等待下一个Tick
  9. 第二个Tick达到后,同样是强制性的来到中断函数,中断函数激活了调度器(CNt = CNT+1,结果就是CNT=1),而中断函数返回也回到了调度器.
  10. 调度器判断出CNT>0后,即开始展开任务的调度,同时将CNT=CNT-1,即得到CNT=0.图中灰色的实心箭头,第一个要进入的就是Task1
  11. 进入到Task1后,运行了一段时间(绿色剪头)后返回调度器
  12. 调度器判断后面还有Task2,然后准备进去Task2
  13. 进入到Task2后,运行了很长一段时间(黄色箭头),此时仍未回到调度器第三个Tick就来了,那CPU只能强制来到中断函数,中断函数激活了调度器(CNt = CNT+1,结果就是CNT=1),而中断函数返回却回到了Task2
  14. 待Task2完成后回到了调度器,此时调度器观察到后续没有Task,正准备挂起的时候发现此时的CNT!=0,随后调度器又急忙开始下一轮的任务调度,即又来到Task1.(注意此时调度器没有IDLE,而是立即展开下一轮的调度)

讲到这里,大家差不多应该了解了我们任务调度器的核心功能。 那么回到本文的重点,我们如何最大化的利用这个调度器来为我们服务呢?否者如果只是简单的先后执行各种Task,那还不如就将其放到一个While(1)里,还省点事情呢?

这就是本文想要解释说明的,基于状态机的复杂TASK的实现。

正文

不知道大家是否了解状态机,这个是在FPGA设计过程中不可或缺的重要环节,基于此状态机才使得所有的逻辑资源有条不紊的展开。那么回到ARM中,我们常常利用ARM实现非常繁琐的任务,这些任务可能都非常简单,却有着非常严格的先后依赖关系,如下图所示:

ARM设计: 简化版任务调度器的实现和应用(2)_第2张图片
上图几乎涵盖了我们对Task的所有期待:

  1. 根据条件判断是否跳出当前状态或者保持当前状态不变
  2. 超时机制,即一个状态的持续时间超过一定的范围就会判定为超时,根据情况状态机到其他状态。
  3. Delay机制,即强制delay多少时间后进入下一个状态。

虽然上图仅仅是一个最简单的例程,但其他更加复杂的应用实际上就是上述三个元素的组合叠加而已。
同时,在复杂的Task在任务调度器看来就只有两种情况:

  1. 如果Task是Ready的,那就进去执行
  2. 如果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主体的时候,会大量的利用这些宏,基于这些宏定义,可以实现:

  1. task begin 定义任务的开始位置方便跳转
  2. task end 定义任务的结束位置,方便跳转
  3. restart,即再次执行当前task,为了加速某些应用,一般情况下这个时候的状态机会执行与上一次不一样的状态
  4. return,即马上退出当前task
  5. enable timer,将状态机的主状态设置为ready_timer,同时给timer计数器正确的赋值,然后restart或者return
  6. diasable timer,将状态机的主状态设置为ready,同时给timer计数器赋值为0,然后restart或者return
  7. delay,将状态机的主状态置为delay,同时给delay的计数器正确的赋值,然后return
  8. 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	
}

上面的源码的注释很清楚的解释了整个状态跳转的所有信息,实际上就是基于条件来判断下一个状态,然后决定是否跳出状态机或者在次进入状态机。

需要解释的是:

  1. 在delay状态,实际上任务调度器不会再进入Task,直到其delay的时候消耗完,Task才会再次被激活,如下图所示
    ARM设计: 简化版任务调度器的实现和应用(2)_第3张图片
    第二个的Tick后,任务调度器并没有进入Task1,因为在此之前就被Delay或者Sleep了,因此将其pass了,直接进入Task2
    等待delay或者sleep被解除后,任务调度器才会重新进入Task1.
  2. 任务初始化函数(包括入列)以及任务调度器和systick中断函数并没有被包含在内,因为上一篇已经介绍过了。

思考

到目前为止,基于简化版任务调度器的实现和应用就都完成了,希望大家能够理解,并将其应用到自己的程序中去,以应对复杂多变的固件需求。

你可能感兴趣的:(ARM设计)