有别于“裸奔”的程序,类似于FreeRTOS或者Uc/OS II之类的实时系统都必备一个强大的任务调度器,基于此用户可以实现各种“乱七八糟”或者“丰富多彩”的功能。而“裸奔”的用户似乎与只能在main函数中,或者中断函数中苦苦挣扎求生存。当项目小的时候,我相信程序员有能力能够hold住。一旦项目变得复杂或者成熟后,有时候一点点需求的变动都会让整个项目都变得伤痕累累-各种补丁和注释。
是不是可以既得实时系统的优点-任务调度器,又可以兼备裸奔的“简洁”呢?我们想要的无外非一个可以把我们所有的任务都管理的仅仅有条而且有条不紊的管家而已,那些妖艳的队列,互斥信号量或者邮箱我们其实可以不用,毕竟有时候杀鸡也用不了牛刀。对此类实时系统的源码或者书籍做了一番解读以后,那我们是不是可以模仿它的行为来构建一个简化版的任务调度器?
我们迫切想要实现一个功能罗列如下:
所以我们这里涉及的简化版任务调度器不具备:
这样最大的好处在于:
下面开始吹牛,欢迎拍砖
下面先声明一个任务的状态的各种可能,这里用枚举类型
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;
这里小小的介绍一下链表:
链表用于实现将不连续(也可以连续分布)分布的数据串联在一起,形成两种(目前我仅知道两种)链表:
下面只介绍环形链表,因为我们这里将会应用这个链表结构
当链表中仅包含一个元素的时候:
当链表中包含两个或以上元素的时候:
可以看到,但一个链表中包含一个以上元素(这里我们称之为tle:tast list element)的时候,每一个tle里面的prev指向前一个tle,每一个next指向后一个tle,同时task handle也指向了各自的任务句柄实体。通过这个句柄就可以通过函数指针(task_process)(struct BSPTaskHandleStr,void *)访问具体的任务函数实体。
下面通过图片演示一下链表中元素入列和出列
首先是入列,如下图所示将tle2放置在原来的tle1和tle3中,上图是未插入之前的状态,后者是插入后的状态。
下图中绿色加粗的虚线就是入列后,对整个链表的影响。
下面是入列实现的源码,注意这里每一次入列将会将任务统计中的有效任务数量+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删除,上图是未删入之前的状态,后者是删后的状态。
下图中绿色加粗的虚线就是入列后,对整个链表的影响。
到目前为止,大家应该对整个任务调度器的原理有了最基本的认识:
原来就是利用链表,通过轮询链表中的tle的状态,来执行tle中的任务句柄。
下面再看一眼,任务调度器的心脏,中断函数的实现,其实仅仅实现了:
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):
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,用于验证调度器真的在工作,下面是应用的初始函数,里面完成了:
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中的状态机一样。
我们随后有时间再展开。