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

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

背景

有别于“裸奔”的程序,类似于FreeRTOS或者Uc/OS II之类的实时系统都必备一个强大的任务调度器,基于此用户可以实现各种“乱七八糟”或者“丰富多彩”的功能。而“裸奔”的用户似乎与只能在main函数中,或者中断函数中苦苦挣扎求生存。当项目小的时候,我相信程序员有能力能够hold住。一旦项目变得复杂或者成熟后,有时候一点点需求的变动都会让整个项目都变得伤痕累累-各种补丁和注释。

是不是可以既得实时系统的优点-任务调度器,又可以兼备裸奔的“简洁”呢?我们想要的无外非一个可以把我们所有的任务都管理的仅仅有条而且有条不紊的管家而已,那些妖艳的队列,互斥信号量或者邮箱我们其实可以不用,毕竟有时候杀鸡也用不了牛刀。对此类实时系统的源码或者书籍做了一番解读以后,那我们是不是可以模仿它的行为来构建一个简化版的任务调度器?
我们迫切想要实现一个功能罗列如下:

  1. 随时可以注册任务,即将一个任务加入到list里面
  2. 可以删除任务,即将一个任务从list中移除
  3. 任务可以delay,即暂时的挂起。被挂起的任务将会自动被调度器忽略,当delay时间到来后,自动取消挂起。
  4. 任务可以sleep,即被永久的挂起。被挂起的任务将会自动被调度器忽略,直到被手动解除休眠,即取消挂起。
  5. 把一个或者多个注册过的任务依次执行。
  6. 每一个任务不在delay或者sleep的任务都可以从任务调度器哪里得到cpu的控制权后,在完成任务后自动返还cpu控制权,即任务调度器重新得到cpu的控制权,以便其继续运行list中的下一个任务。
  7. 需要一个中断作为调度器的节拍器,也就是心脏。

所以我们这里涉及的简化版任务调度器不具备:

  1. 抢占优先级,所有注册后的任务都是一个优先级,排队依次被调用
  2. 基于堆栈的task进入/退出方法,这个将会和后面介绍的状态机结合在一起,基于状态机实现一个复杂的任务。
  3. 没有队列,信号量,邮箱等数据同步/冲突解决方案,简单一点,别整的这么复杂。

这样最大的好处在于:

  1. 任务调度器是自行开发的,不存在维护困难问题。
  2. 基于任务调度器的应用程序二次开发将会具备最大化的灵活性。
  3. 支持多个task“同时”运行,想想都要流口水。

下面开始吹牛,欢迎拍砖

正文

下面先声明一个任务的状态的各种可能,这里用枚举类型

typedef enum 
{
	Task_Ready 			= 2,
	Task_ReadyWithTimer	= 3,
	Task_Error 			= 4,
	Task_Delay 			= 5,
	Task_Sleep 			= 6
}TASK_StatusEnum;

然后再定义一个结构体,正如其名任务统计。这个变量将可以被所有注册过的任务访问。

/*
	the follow struct was shared by all task in the tlt
*/

typedef struct
{
		uint32_t		active_signal_num;
		uint32_t		active_task_num;
}TaskStatisticsStr;

接下来在定义一个结构体,统称为任务句柄。通过这个句柄,就可以访问该任务的状态等所有的相关的变量。

typedef struct BSPTaskHandleStr
{
/*
	high level status of the task, the task should be execute or not was depend on the high level task status
	only in Task_Ready or Task_ReadyWithTimer will make task_handle active. otherwise will execute the special
	function like enqueue or dequeue or just pass it to next tle.
	delay_us was only active when task_status == Task_Delay. it was differ to timer_us
*/
	TASK_StatusEnum	task_status;
	int32_t 		delay_us;
/*
	low level stask of the task, it was the detail about how the task run and transmit to other sub-status
	the timer_us was used for sub_status, in some situation there need a timeout alarm to push the sub-status
	back to idle or other.
*/
	uint32_t		subtask_status;
	int32_t 		timer_us;
/*
	define a para to delivery into task_process
*/
	void *			para;
	int32_t			(*task_process)(struct BSPTaskHandleStr*,void *);

	TaskStatisticsStr *tss;
}BSPTaskHandleStr;

然后最关键的一步,定义一个结构体来表示链表元素tle,该结构体具备三个指针,前两个用来形成链表,后面一个指针作为具体的任务句柄的入口。

typedef struct TaskListElementStr
{
	struct TaskListElementStr * Next;
	struct TaskListElementStr * Prev;
	
	BSPTaskHandleStr 			*task_handle;
}TaskListElementStr;

这里小小的介绍一下链表:
链表用于实现将不连续(也可以连续分布)分布的数据串联在一起,形成两种(目前我仅知道两种)链表:

  1. 环形链表
  2. 单向链表(都是随便取得名字,呵呵)

下面只介绍环形链表,因为我们这里将会应用这个链表结构

ARM设计:简化版任务调度器的实现和应用(1)_第1张图片

当链表中仅包含一个元素的时候:

ARM设计:简化版任务调度器的实现和应用(1)_第2张图片

当链表中包含两个或以上元素的时候:

可以看到,但一个链表中包含一个以上元素(这里我们称之为tle:tast list element)的时候,每一个tle里面的prev指向前一个tle,每一个next指向后一个tle,同时task handle也指向了各自的任务句柄实体。通过这个句柄就可以通过函数指针(task_process)(struct BSPTaskHandleStr,void *)访问具体的任务函数实体。

下面通过图片演示一下链表中元素入列和出列

首先是入列,如下图所示将tle2放置在原来的tle1和tle3中,上图是未插入之前的状态,后者是插入后的状态。
下图中绿色加粗的虚线就是入列后,对整个链表的影响。
ARM设计:简化版任务调度器的实现和应用(1)_第3张图片
ARM设计:简化版任务调度器的实现和应用(1)_第4张图片

下面是入列实现的源码,注意这里每一次入列将会将任务统计中的有效任务数量+1,用来显示当前链表中有几个tle是有效的

// to insert the tle before the target. this function only work when there are at least one tle in the list
void task_enqueue_insert_before(TaskListElementStr * insert_tle, TaskListElementStr * target_tle)
{
	assert_param(__Check_Point_Is_Valid(target_tle));
	assert_param(__Check_Point_Is_Valid(insert_tle));

	insert_tle->Prev = target_tle->Prev;
	insert_tle->Next = target_tle;

	target_tle->Prev->Next = insert_tle;
	target_tle->Prev = insert_tle;
	if(target_tle == head_tle)
		head_tle = insert_tle;
	insert_tle->task_handle->tss->active_task_num++;
}


// to insert the tle after the target. this function only work when there are at least one tle in the list
void task_enqueue_insert_after(TaskListElementStr * insert_tle, TaskListElementStr * target_tle)
{
	assert_param(__Check_Point_Is_Valid(target_tle));
	assert_param(__Check_Point_Is_Valid(insert_tle));
	
	insert_tle->Prev = target_tle;
	insert_tle->Next = target_tle->Next;

	target_tle->Next->Prev = insert_tle;
	target_tle->Next = insert_tle;
	if(target_tle == tail_tle)
		tail_tle = insert_tle;
	insert_tle->task_handle->tss->active_task_num++;
}

下面在介绍出列,如下图所示将tle2删除,上图是未删入之前的状态,后者是删后的状态。
下图中绿色加粗的虚线就是入列后,对整个链表的影响。
ARM设计:简化版任务调度器的实现和应用(1)_第5张图片
ARM设计:简化版任务调度器的实现和应用(1)_第6张图片

到目前为止,大家应该对整个任务调度器的原理有了最基本的认识:
原来就是利用链表,通过轮询链表中的tle的状态,来执行tle中的任务句柄。

下面再看一眼,任务调度器的心脏,中断函数的实现,其实仅仅实现了:

  1. 轮训一遍,从head tle开始
  2. 检查每一个tle的状态,如果是delay,那么就将其句柄中的delay计数器减去相应的值。如果delay数小于零,将状态重新置为Ready
  3. 如果是Task_ReadyWithTimer状态,那么就将句柄中的定时器减去相应的值,这个定时器将会用来提示任务是否超时
  4. 最后将active_task_num++,该变量保存在任务统计里,将会用来指导任务调度器的工作是否开始
  5. 同时在整个执行的过程中暂时禁用了中断,避免冲突
void SysTick_Handler(void)
{
	TaskListElementStr * lookup_tle;

	//make sure that before handle run, there are at least one or more tle in the list
	assert_param(__Check_Point_Is_Valid(head_tle));
	
	// enter critical code	
	__disable_irq();


	// initial the lookup_tle
	lookup_tle = head_tle;

/*
	1. traversal all tle in the list one by one to find which tle was in Task_Delay
	2. if it was Task_Delay then sub the delay_us with US_PER_TICK so does Task_ReadyWithTimer
	3. after subtraction then check the delay_us agian. to pull it to ready again if necessary 
*/
	do
	{
		
		if(lookup_tle->task_handle->task_status == Task_Delay)
		{
			if(lookup_tle->task_handle->delay_us > 0)
				lookup_tle->task_handle->delay_us -= US_PER_TICK;

			if(lookup_tle->task_handle->delay_us <= 0)
				lookup_tle->task_handle->task_status = Task_Ready;
		}
		else if(lookup_tle->task_handle->task_status == Task_ReadyWithTimer)
			lookup_tle->task_handle->timer_us -= US_PER_TICK;
		else
		{
			lookup_tle->task_handle->delay_us = 0;
			lookup_tle->task_handle->timer_us = 0;
		}
		lookup_tle = lookup_tle->Next;
	}while(lookup_tle != head_tle);

	//this signal will be used for 
	lookup_tle->task_handle->tss->active_signal_num++;
	// leave critical code	
	__enable_irq();
}

到这里基本上已经完成介绍,接下来开始写任务调度器本身,源码如下,是不是很简单,其实就是一个while(1):

  1. 当active_signal_num不为0后,调度器就正式开始工作
  2. 检查当前tle是否是ready,如果不是直接pass
  3. 如果是就利用函数指针进入并工作,此时就要离开任务调度器去具体的实现函数里
  4. 当完成当前任务后,回到任务调度器,将当前tle指向下一个tle
  5. 如果当前tle是header,意味着已经从头到尾完成了一轮,等待下一个active_signal_num不为0.
void	task_scheduler()
{
	while(1)
	{
		if((realtime_tle->task_handle->tss->active_signal_num >= 1)&&(__Check_Num_Is_Positive(realtime_tle->task_handle->tss->active_task_num)))
		{
			realtime_tle->task_handle->tss->active_signal_num--;
			while(1)
			{
				__TASK_RECORD_ENTRY_TIME(realtime_tle->task_handle)
				switch(realtime_tle->task_handle->task_status)
			   	{
					case Task_Ready:
					{
						realtime_tle->task_handle->delay_us = 0;
						realtime_tle->task_handle->timer_us = 0;
						realtime_tle->task_handle->task_process(realtime_tle->task_handle,realtime_tle->task_handle->para2);
						break;
					}						
					case Task_ReadyWithTimer:
					{
						realtime_tle->task_handle->delay_us = 0;						
						realtime_tle->task_handle->task_process(realtime_tle->task_handle,realtime_tle->task_handle->para2);
						break;
					}
					case Task_Error:
					{
						// should not run to here
						assert_param(1==0);
						break;
					}
					case Task_Delay:
					{
						realtime_tle->task_handle->timer_us = 0;
						break;
					}
					case Task_Sleep:
					{
						break;
					}
					default:
					{
						// should not run to here
						assert_param(1==0);
						break;
					}					
				}
/*
				after currenttask has been conplete, the next task should be call
*/
				__TASK_RECORD_EXIT_TIME(realtime_tle->task_handle)
				realtime_tle = realtime_tle->Next;


/*
				it means the all task in the list has been execute one time, should quit the while loop
*/
				if( (uint32_t)head_tle == (uint32_t)realtime_tle )
					break;
				
			}
        }	
	}
}

整理一下思路,到目前为止已经完成了任务调度器的基本框架,是时候搞一个真是的应用跑一跑了。

应用

下年简单的例化了一个类似于万年历的task,用于验证调度器真的在工作,下面是应用的初始函数,里面完成了:

  1. 将句柄的指针赋值给tle,在这里是dtw_tle
  2. 将句柄中的状态置为ready
  3. 将任务函数指针赋值给dtw_tle->task_handle->task_process,以及其会调用的参数dtw_tle->task_handle->para = (HSIStructure *)bsp_hsi;
  4. 最重要的一步,将tle入列,该入列函数上文没有介绍到,其实就是将tle放置到队尾,表示其最后执行
void app_dtw_initial(void * bsp_hsi)
{
	dtw_tle->task_handle = &DTW_TASK_HANDLE;

	dtw_tle->task_handle->task_status = Task_Ready;
	dtw_tle->task_handle->delay_us = 0;
/*
	low level stask of the task, it was the detail about how the task run and transmit to other sub-status
	the timer_us was used for sub_status, in some situation there need a timeout alarm to push the sub-status
	back to idle or other.
*/
	dtw_tle->task_handle->subtask_status = 0;
	dtw_tle->task_handle->timer_us = 0;
	
/*
	define para to delivery into task_process
*/
	dtw_tle->task_handle->para = (HSIStructure		*)bsp_hsi;
	
	dtw_tle->task_handle->task_process = Task_DTW_Process_Handler;

	dtw_tle->task_handle->tss = tss;
	task_enqueue_tail(dtw_tle);

}

面是任务本体的实现,就是一个万年历的例子,不在深究,特别需要注意里面调用了 __TASK_DELAY(task_handle, 1000000)
该宏如下,其实就是将状态置为Task_Delay,然后返回而已

#define __TASK_DELAY(task_handle, delay_in_us)	{\
												task_handle->delay_us = delay_in_us;\
												task_handle->task_status = Task_Delay;\
												__TASK_RETURN\
											}
BSPTaskHandleStr	DTW_TASK_HANDLE;
TaskListElementStr	DTW_TLE;
TaskListElementStr	*dtw_tle = &DTW_TLE;	

static uint8_t Month[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
#define LeapYear(x) (((x%4 == 0)&&(x%100!=0))||(x%400==0)) ? 1:0


int32_t Task_DTW_Enqueue_Handler(BSPTaskHandleStr* task_handle, void * para)
{
	task_handle->task_status = Task_Ready;
	return 1;
}

int32_t Task_DTW_Dequeue_Handler(BSPTaskHandleStr* task_handle, void * para)
{
	return 1;
}





int32_t Task_DTW_Process_Handler(BSPTaskHandleStr* task_handle, void * para)
{
/*
	to pass parameter into function, at here it was a usart_handle
*/
	HSIStructure	* bsp_hsi = (HSIStructure	*)para;

	if(task_handle->task_status != Task_Ready)
	{
		//shouldn't run to here
		assert_param(0);
		__TASK_RETURN	
	}
	
	
	__TASK_BEGIN
	
	
	bsp_hsi->time_reg.str.second += 1;
	if(bsp_hsi->time_reg.str.second >= 60)
	{
		bsp_hsi->time_reg.str.second -= 60;
		bsp_hsi->time_reg.str.minute += 1;
		if(bsp_hsi->time_reg.str.minute >= 60)
		{
			bsp_hsi->time_reg.str.minute -= 60;
			bsp_hsi->time_reg.str.hour += 1;
			if(bsp_hsi->time_reg.str.hour >= 24)
			{
				bsp_hsi->time_reg.str.hour -= 24;
				bsp_hsi->date_reg.str.day += 1;
				if(bsp_hsi->date_reg.str.day > Month[bsp_hsi->date_reg.str.month])
				{
					bsp_hsi->date_reg.str.day = 1;
					bsp_hsi->date_reg.str.month += 1;
					if(bsp_hsi->date_reg.str.month > 12)
					{
						bsp_hsi->date_reg.str.month = 1;
						bsp_hsi->date_reg.str.year += 1;
						if(LeapYear(bsp_hsi->date_reg.str.year))
							Month[2] = 29;
						else
							Month[2] = 28;
					}
				}
			}
		}
	}

	bsp_hsi->fault_code_reg.str.fault_code++;
	bsp_hsi_latch_fault();
	__TASK_DELAY(task_handle, 1000000)

	__TASK_END	
}

到目前为止,整个介绍都已经完成,虽然这个例子很简单,没能将我们的任务调度器的优点都发挥出来。

思考

如何最大化的利用这个任务调度器来实现复杂任务,这里将会涉及到一种特殊的规则,基于这个规则写出来的任务,可以非常复杂,就像FPGA中的状态机一样。

我们随后有时间再展开。

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