在本节之前,为了实现任务的阻塞延时,在任务控制块中内置了一个延时变量xTicksToDelay。每当任务需要延时的时候,就初始化 xTicksToDelay 需要延时的时间,然后将任务挂起,这里的挂起只是将任务在优先级位图表 uxTopReadyPriority 中对应的位清零,并不会将任务从就绪列表中删除。当每次时基中断(SysTick 中断)来临时,就扫描就绪列表中的每个任务的 xTicksToDelay,如果 xTicksToDelay 大于 0 则递减一次,然后判断 xTicksToDelay 是否为 0,如果为 0 则表示延时时间到,将该任务就绪(即将任务在优先级位图表 uxTopReadyPriority 中对应的位置位),然后进行任务切换。这种延时的缺点是,在每个时基中断中需要对所有任务都扫描一遍,费时,优点是容易理解。之所以先这样讲解是为了慢慢地过度到 FreeRTOS 任务延时列表的讲解。
任务延时列表的工作原理
在 FreeRTOS 中,有一个任务延时列表(实际上有两个,为了方便讲解原理,我们假装合并为一个,其实两个的作用是一样的),当任务需要延时的时候,则先将任务挂起,即先将任务从就绪列表删除,然后插入到任务延时列表,同时更新下一个任务的解锁时刻变量:xNextTaskUnblockTime的值。
xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。与 RT-Thread 和 μC/OS 在解锁延时任务时要扫描定时器列表这种时间不确定性的方法相比,FreeRTOS 这个xNextTaskUnblockTime 全局变量设计的非常巧妙。
实现任务延时列表
定义任务延时列表
任务延时列表在 task.c 中定义
static List_t xDelayedTaskList1;//①
static List_t xDelayedTaskList2;//②
static List_t * volatile pxDelayedTaskList;//③
static List_t * volatile pxOverflowDelayedTaskList;//④
①②FreeRTOS定义了两个任务延时列表,当系统时基计数器xTickCount没有溢出时,用一条列表,当 xTickCount 溢出后,用另外一条列表。
③任务延时列表指针,指向 xTickCount 没有溢出时使用的那条列表。
④任务延时列表指针,指向 xTickCount 溢出时使用的那条列表。
任务延时列表初始化
任务延时列表属于任务列表的一种,在 prvInitialiseTaskLists() 函数中初始化
/* 初始化任务相关的列表 */
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++)
{
vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
}
vListInitialise( &xDelayedTaskList1 );
vListInitialise( &xDelayedTaskList2 );
pxDelayedTaskList = &xDelayedTaskList1;
pxOverflowDelayedTaskList = &xDelayedTaskList2;
}
定义 xNextTaskUnblockTime
xNextTaskUnblockTime 是一个在 task.c 中定义的静态变量,用于表示下一个任务的解锁时刻。xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。
初始化 xNextTaskUnblockTime
xNextTaskUnblockTime 在 vTaskStartScheduler() 函 数 中 初 始 化 为 portMAX_DELAY(portMAX_DELAY 是一个 portmacro.h 中定义的宏,默认为 0xffffffffUL)。
#if( configUSE_16_BIT_TICKS == 1)
typedef uint16_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffff
#else
typedef uint32_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
#endif
初始化 xNextTaskUnblockTime
void vTaskStartScheduler( void )
{
/*======================================创建空闲任务start==============================================*/
TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务TCB */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
(char *)"IDLE", /* 任务名称,字符串形式 */
(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(UBaseType_t) tskIDLE_PRIORITY, /* 任务优先级,数值越大,优先级越高 */
(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
(TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */ /* 任务控制块指针 */
// /* 将任务添加到就绪列表 */
// vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
///*======================================创建空闲任务end================================================*/
//
// /* 手动指定第一个运行的任务 */
// pxCurrentTCB = &Task1TCB;
xNextTaskUnblockTime = portMAX_DELAY;
/* 初始化系统时基计数器 */
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
修改代码,支持任务延时列表
修改 vTaskDelay() 函数
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* ①设置延时时间 */
//pxTCB->xTicksToDelay = xTicksToDelay;
/* ②将任务插入到延时列表 */
prvAddCurrentTaskToDelayedList( xTicksToDelay );
/* 任务切换 */
taskYIELD();
}
①从本节开始,添加了任务的延时列表,延时的时候不用再依赖任务TCB 中内置的延时变量 xTicksToDelay。
②将任务插入到延时列表。函数prvAddCurrentTaskToDelayedList() 在task.c 中定义。
prvAddCurrentTaskToDelayedList() 函数
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait )
{
TickType_t xTimeToWake;
/* ①获取系统时基计数器xTickCount的值 */
const TickType_t xConstTickCount = xTickCount;
/* ②将任务从就绪列表中移除 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* 将任务在优先级位图中对应的位清除 */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
/* ③计算延时到期时,系统时基计数器xTickCount的值是多少 */
xTimeToWake = xConstTickCount + xTicksToWait;
/* ④将延时到期的值设置为节点的排序值 */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
/* ⑤溢出 */
if( xTimeToWake < xConstTickCount )
{
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else /* ⑥没有溢出 */
{
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* ⑦更新下一个任务解锁时刻变量xNextTaskUnblockTime的值 */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
}
}
①获取系统时基计数器 xTickCount 的值,xTickCount 是一个在 task.c 中定义的全局变量,用于记录 SysTick 的中断次数。
②调用函数 uxListRemove() 将任务从就绪列表移除,uxListRemove() 会返回当前链表下节点的个数,如果为 0,则表示当前列表下没有任务就绪,则调用函数portRESET_READY_PRIORITY() 将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。因为 FreeRTOS 支持同一个优先级下可以有多个任务,所以在清除优先级位图表 uxTopReadyPriority 中对应的位时要判断下该优先级下的就绪列表是否还有其他的任务。目前为止,我们还没有支持同一个优先级下有多个任务的功能,这个功能我们将在下一节“支持时间片”里面实现。
③计算任务延时到期时,系统时基计数器 xTickCount 的值是多少。
④将任务延时到期的值设置为节点的排序值。将任务插入到延时列表时就是根据这个值来做升序排列的,最先延时到期的任务排在最前面。
⑤:xTimeToWake 溢出,将任务插入到溢出任务延时列表。溢出?什么意思?xTimeToWake 等于系统时基计数器 xTickCount 的值加上任务需要延时的时间 xTicksToWait。举例:如果当前 xTickCount 的值等于 0xfffffffdUL,xTicksToWait 等于 0x03,那么xTimeToWake = 0xfffffffdUL + 0x03 = 1,显然得出的值比任务需要延时的时间 0x03 还小,这肯定不正常,说明溢出了,这个时候需要将任务插入到溢出任务延时列表。
⑥xTimeToWake 没有溢出,则将任务插入到正常任务延时列表。
⑦更新下一个任务解锁时刻变量 xNextTaskUnblockTime 的值。这一步很重要,在 xTaskIncrementTick() 函数中,我们只需要让系统时基计数器 xTickCount 与xNextTaskUnblockTime 的值先比较就知道延时最快结束的任务是否到期。
修改 xTaskIncrementTick() 函数
void xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
const TickType_t xConstTickCount = xTickCount + 1;//①
xTickCount = xConstTickCount;
/* ②如果xConstTickCount溢出,则切换延时列表 */
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
/* ③最近的延时任务延时到期 */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )//④
{
/* 延时列表为空,设置xNextTaskUnblockTime为可能的最大值 */
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else /* ⑤延时列表不为空 */
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );//⑥
/* ⑦直到将延时列表中所有延时到期的任务移除才跳出for循环 */
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
/* ⑧将任务从延时列表移除,消除等待状态 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/* ⑨将解除等待的任务添加到就绪列表 */
prvAddTaskToReadyList( pxTCB );
}
}
}/* xConstTickCount >= xNextTaskUnblockTime */
/* ⑩任务切换 */
portYIELD();
}
①更新系统时基计数器 xTickCount 的值。
②如果系统时基计数器 xTickCount 溢出,则切换延时列表。taskSWITCH_DELAYED_LISTS() 函数在 task.c 中定义
taskSWITCH_DELAYED_LISTS() 函数
/*
* 当系统时基计数器溢出的时候,延时列表pxDelayedTaskList 和
* pxOverflowDelayedTaskList要互相切换
*/
#define taskSWITCH_DELAYED_LISTS()
{
List_t *pxTemp;//(1)
pxTemp = pxDelayedTaskList;
pxDelayedTaskList = pxOverflowDelayedTaskList;
pxOverflowDelayedTaskList = pxTemp;
xNumOfOverflows++;
prvResetNextTaskUnblockTime();//②
}
(1)切换延时列表,实际就是更换pxDelayedTaskList和pxOverflowDelayedTaskList 这两个指针的指向。
(2)复位 xNextTaskUnblockTime 的值。prvResetNextTaskUnblockTime() 函数在 task.c 中定义。
prvResetNextTaskUnblockTime 函数
static void prvResetNextTaskUnblockTime( void )
{
TCB_t *pxTCB;
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* (1)The new current delayed list is empty. Set xNextTaskUnblockTime to
the maximum possible value so it is extremely unlikely that the
if( xTickCount >= xNextTaskUnblockTime ) test will pass until
there is an item in the delayed list. */
xNextTaskUnblockTime = portMAX_DELAY;
}
else
{
/* (2)The new current delayed list is not empty, get the value of
the item at the head of the delayed list. This is the time at
which the task at the head of the delayed list should be removed
from the Blocked state. */
( pxTCB ) = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
}
}
(1)当前延时列表为空,则设置 xNextTaskUnblockTime 等于最大值。
(2)当前列表不为空,则有任务在延时,则获取当前列表下第一个节点的排序值,然后将该节点的排序值更新到xNextTaskUnblockTime。
③有任务延时到期,则进入下面的 for 循环,一一将这些延时到期的任务从延时列表移除。
④延时列表为空,则将 xNextTaskUnblockTime 设置为最大值,然后跳出 for 循环。
⑤延时列表不为空,则需要将延时列表里面延时到期的任务删除,并将它们添加到就绪列表。
⑥取出延时列表第一个节点的排序辅助值。
⑦直到将延时列表中所有延时到期的任务移除才跳出 for 循环。延时列表中有可能存在多个延时相等的任务。
⑧将任务从延时列表移除,消除等待状态。
⑨将解除等待的任务添加到就绪列表。
⑩执行一次任务切换。
修改 taskRESET_READY_PRIORITY() 函数
在没有添加任务延时列表之前,与任务相关的列表只有一个,就是就绪列表,无论任务在延时还是就绪都只能通过扫描就绪列表来找到任务的 TCB,从而实现系统调度。所以在上一节“支持多优先级”中,实现 taskRESET_READY_PRIORITY() 函数的时候,不用先判断当前优先级下就绪列表中的列表的节点是否为 0,而是直接把任务在优先级位图表uxTopReadyPriority 中对应的位清零。因为当前优先级下就绪列表中的列表的节点不可能为 0,目前我们还没有添加其他列表来存放任务的 TCB,只有一个就绪列表。
但是从本节开始,我们额外添加了延时列表,当任务要延时的时候,将任务从就绪列表移除,然后添加到延时列表,同时将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。在清除任务在优先级位图表 uxTopReadyPriority 中对应的位的时候,与上一章不同的是需要判断就绪列表pxReadyTasksLists[] 在当前优先级下对应的链表的节点是否为 0,只有当该列表下没有任务时才真正地将任务在优先级位图表 uxTopReadyPriority 中对应的位清零。
taskRESET_READY_PRIORITY() 函数的具体修改见代码如下。那什么情况下就绪列表的列表里面会有多个任务节点?即同一优先级下有多个任务?这个就是我们下一节“支持时间片”要讲的内容。
#if 0 //本节实现方法
#define taskRESET_READY_PRIORITY( uxPriority )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )
{
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );
}
}
#else //上一节实现方法
#define taskRESET_READY_PRIORITY( uxPriority )
{
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );
}
#endif
main函数
int main(void)
{
/* 硬件初始化 */
/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
/* 创建任务 */
Task1_Handle = xTaskCreateStatic( ( TaskFunction_t )Task1_Entry,
(char *)"Task1",
(uint32_t)TASK1_STACK_SIZE,
(void *)NULL,
(UBaseType_t) 1,
(StackType_t *)Task1Stack,
(TCB_t *)&Task1TCB);
// /* 将任务添加到就绪列表 */
// vListInsertEnd( &( pxReadyTasksLists[1]), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
Task2_Handle = xTaskCreateStatic( ( TaskFunction_t )Task2_Entry,
(char *)"Task2",
(uint32_t)TASK2_STACK_SIZE,
(void *)NULL,
(UBaseType_t) 2,
(StackType_t *)Task2Stack,
(TCB_t *)&Task2TCB);
// /* 将任务添加到就绪列表 */
// vListInsertEnd( &( pxReadyTasksLists[2]), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
/* 在启动调度器前,关闭中断 */
portDISABLE_INTERRUPTS();
/* 启动调度器,开始多任务调度,启动成功则不返回 */
vTaskStartScheduler();
for(;;)
{
/*啥事不干*/
}
}
实验现象
实验现象与上一节一样,虽说一样,但是实现延时的方法本质却变了,需要好好理解代码的实现,特别是当系统时基计数器 xTickCount 发生溢出时,延时列表的更换是难点。
参考资料:《FreeRTOS 内核实现与应用开发实战—基于RT1052》