之前,为了实现任务的阻塞延时,在任务控制块中内置了一个延时变量、xTicksToDelay。每当任务需要延时的时候,就初始化 xTicksToDelay 需要延时的时间, 然后将任务挂起,这里的挂起只是将任务在优先级位图表 uxTopReadyPriority 中对应的位清零,并不会将任务从就绪列表中删除。 当每次时基中断(SysTick 中断) 来临时, 就扫描就绪列表中的每个任务的 xTicksToDelay, 如果 xTicksToDelay 大于 0 则递减一次,然后判断xTicksToDelay 是否为 0,如果为 0 则表示延时时间到,将该任务就绪(即将任务在优先级位图表uxTopReadyPriority 中对应的位置位) ,然后进行任务切换。 这种延时的缺点是,在每个时基中断中需要对所有任务都扫描一遍,费时,优点是容易理解。
8.1 任务延时列表的工作原理
在 FreeRTOS 中, 有一个任务延时列表(实际上有两个,为了方便讲解原理,我们假装合并为一个,其实两个的作用是一样的) ,当任务需要延时的时候, 则先将任务挂起,即先将任务从就绪列表删除,然后插入到任务延时列表,同时更新下一个任务的解锁时刻变量: xNextTaskUnblockTime 的值。
xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。 当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。 与 RT-Thread 和 μC/OS 在解锁延时任务时要扫描定时器列表这种时间不确定性的方法相比, FreeRTOS 这个 xNextTaskUnblockTime全局变量设计的非常巧妙。
任务延时列表表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。 当每次时基中断(SysTick 中断) 来临时, 就拿系统时基计数器的值 xTickCount 与下一个任务的解锁时刻变量 xNextTaskUnblockTime 的值相比较, 如果相等, 则表示有任务延时到期, 需要将该任务就绪, 否则只是单纯地更新系统时基计数器xTickCount 的值, 然后进行任务切换。
8.2 实现任务延时列表
接下来具体讲解下 FreeRTOS 中任务延时列表的实现
8.2.1 定义任务延时列表
任务延时列表在 task.c 中定义。
static List_t xDelayedTaskList1; //1
static List_t xDelayedTaskList2; //2
static List_t * volatile pxDelayedTaskList; //3
static List_t * volatile pxOverflowDelayedTaskList; //4
- FreeRTOS 定义了两个任务延时列表,当系统时基计数器xTickCount 没有溢出时,用一条列表,当 xTickCount 溢出后, 用另外一条列表。
- 任务延时列表指针, 指向 xTickCount 没有溢出时使用的那条列表。
- 任务延时列表指针, 指向 xTickCount 溢出时使用的那条列表。
8.2.2 任务延时列表初始化
任务延时列表属于任务列表的一种,在 prvInitialiseTaskLists()函数中初始化
// 就绪列表初始化
void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;
for(uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++)
{
vListInitialise(& (pxReadyTasksLists[ uxPriority ]));
}
vListInitialise(&xDelayedTaskList1); //1
vListInitialise(&xDelayedTaskList2); //2
pxDelayedTaskList = &xDelayedTaskList1; //3
pxOverflowDelayedTaskList = &xDelayedTaskList2; //4
}
8.2.3 定义xNextTaskUnblockTime
xNextTaskUnblockTime 是一个在 task.c 中定义的静态变量,用于表示下一个任务的解锁时刻。 xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。
8.2.4 初始化 xNextTaskUnblockTime
xNextTaskUnblockTime 在 vTaskStartScheduler()函数中初始化为 portMAX_DELAY(portMAX_DELAY 是一个 portmacro.h 中定义的宏,默认为 0xffffffffUL) 在第 22 行
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 ); /* 任务控制块 */
/*======================================创建空闲任务end================================================*/
xNextTaskUnblockTime = portMAX_DELAY;
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
8.3 修改代码,支持任务延时列表
8.3.1 修改vTaskDelay() 函数
// 阻塞延时
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的 TCB */
pxTCB = pxCurrentTCB;
/* 设置延时时间 */
//pxTCB->xTicksToDelay = xTicksToDelay;
/* 将任务插入到延时列表 */
prvAddCurrentTaskToDelayedList( xTicksToDelay ); // 调用这个函数
/* 任务切换 */
taskYIELD();
}
1. 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的值 */
/* xNextTaskUnblockTime 下个任务解锁时刻 */
/* xTimeToWake 下个任务需要醒来的时间 */
/* xTimeToWake 这个是判断是不是延时最短的任务的时间 可能上个任务的延时比这个多,还
需要等待更长时间,把需要等待的最短的时间进行提前*/
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
}
}
- 用函数 uxListRemove()将任务从就绪列表移除, uxListRemove()会返回当前链表下节点的个数,如果为 0,则表示当前链表下没有任务就绪,则调用函数portRESET_READY_PRIORITY()将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。 因为 FreeRTOS 支持同一个优先级下可以有多个任务,所以在清除优先级位图表uxTopReadyPriority 中对应的位时要判断下该优先级下的就绪列表是否还有其它的任务。 目前为止, 我们还没有支持同一个优先级下有多个任务的功能, 这个功能我们将在下一章“支持时间片”里面实现。
- xTimeToWake 溢出, 将任务插入到溢出任务延时列表。溢出?什么意思? xTimeToWake 等于系统时基计数器 xTickCount 的值加上任务需要延时的时间xTicksToWait。举例: 如果当前 xTickCount 的值等于 0xfffffffdUL, xTicksToWait 等于0x03,那么 xTimeToWake = 0xfffffffdUL + 0x03 = 1,显然得出的值比任务需要延时的时间0x03 还小,这肯定不正常,说明溢出了,这个时候需要将任务插入到溢出任务延时列表。
- xTimeToWake 没有溢出, 则将任务插入到正常任务延时列表
- 更新下一个任务解锁时刻变量 xNextTaskUnblockTime 的值。 这一步很重要, 在 xTaskIncrementTick()函数中,我们只需要让系统时基计数器 xTickCount 与xNextTaskUnblockTime 的值先比较就知道延时最快结束的任务是否到期。
8.3.2 修改 xTaskIncrementTick()函数
xTaskIncrementTick()函数改动较大
void xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
TickType_t xItemValue;
BaseType_t i = 0;
/* 更新系统时基计数器 xTickCount, xTickCount 是一个在 port.c 中定义的全局变量 */
const BaseType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
/* 如果 xConstTickCount 溢出,则切换延时列表 */
if ( xConstTickCount == (TickType_t) 0U )
{
taskSWITCH_DELAYED_LISTS(); // 这个后面有
}
/* xNextTaskUnblockTime存放要期的任务 */
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 );
}
}
}
/* 任务切换 */
portYIELD();
}
如果系统时基计数器 xTickCount 溢出,则切换延时列表。taskSWITCH_DELAYED_LISTS()函数在 task.c 中定义。
/*
* 当系统时基计数器溢出的时候,延时列表pxDelayedTaskList 和
* pxOverflowDelayedTaskList要互相切换
*/
#define taskSWITCH_DELAYED_LISTS()\
{\
List_t *pxTemp;\
pxTemp = pxDelayedTaskList;\
pxDelayedTaskList = pxOverflowDelayedTaskList;\
pxOverflowDelayedTaskList = pxTemp;\
xNumOfOverflows++;\
prvResetNextTaskUnblockTime();\
}
prvResetNextTaskUnblockTime
static void prvResetNextTaskUnblockTime( void )
{
TCB_t *pxTCB;
if ( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE)
{
/* 当前延时列表为空,则设置 xNextTaskUnblockTime 等于最大值 */
xNextTaskUnblockTime = portMAX_DELAY;
}
else
{
/* 当前列表不为空,则有任务在延时,则获取当前列表下第一个节点的排序值
然后将该节点的排序值更新到 xNextTaskUnblockTime */
( pxTCB ) = ( TCB_t *) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
}
}
8.3.3 修改 taskRESET_READY_PRIORITY()函数
在没有添加任务延时列表之前,与任务相关的列表只有一个,就是就绪列表,无论任务在延时还是就绪都只能通过扫描就绪列表来找到任务的 TCB,从而实现系统调度。 所以在上一章“支持多优先级”中,实现 taskRESET_READY_PRIORITY()函数的时候,不用先判断当前优先级下就绪列表中的链表的节点是否为 0,而是直接把任务在优先级位图表uxTopReadyPriority 中对应的位清零。 因为当前优先级下就绪列表中的链表的节点不可能为0, 目前我们还没有添加其它列表来存放任务的 TCB,只有一个就绪列表。
但是从本章开始,我们额外添加了延时列表,当任务要延时的时候,将任务从就绪列表移除,然后添加到延时列表,同时将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。在清除任务在优先级位图表 uxTopReadyPriority 中对应的位的时候, 与上一章不同的是需要判断就绪列表 pxReadyTasksLists[]在当前优先级下对应的链表的节点是否为 0,只有当该链表下没有任务时才真正地将任务在优先级位图表 uxTopReadyPriority 中对应的位清零。
那什么情况下就绪列表的链表里面会有多个任务节点?即同一优先级下有多个任务?这个就是
我们下一章“支持时间片” 。
#define taskRESET_READY_PRIORITY( uxPriority ) \
{ \
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \
{ \
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
} \
}
#if 0
#define taskRESET_READY_PRIORITY( uxPriority ) \
{ \
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
}
#endif
8.4 小总结
增加了定义延时的列表,然后初始化。其实就是两个指针分别指向不同的列表。原来的为就绪列表。
static List_t xDelayedTaskList1; //1
static List_t xDelayedTaskList2; //2
static List_t * volatile pxDelayedTaskList; //3
static List_t * volatile pxOverflowDelayedTaskList; //4
为什么要增加两个队列?为的是计数器溢出的时候可以切换两个队列。防止出错。
定义xNextTaskUnblockTime,并初始化为portMAX_DELAY(0xffffff),也就是最大值。这个时间是用来记录任务睡眠后,苏醒的时间 xNextTaskUnblockTime = xtickCount + delayTime,然后和tick时间比较,进行切换。
修改vTaskDelay() 函数,增加了prvAddCurrentTaskToDelayedList()函数,将任务放入等待列表
- 从就绪列表移除该任务
- 计算延迟到期的时间
- 按照延迟的时间xItemValue插入到delay链表中
- 需要判断是否溢出 溢出要添加到溢出链表中
- 否则插入到正在运行的延时列表,并判断是否需要更新xNextTaskUnblockTime时间,因为xNextTaskUnblockTime总是指向的是最早就绪的任务的wake时间
修改 xTaskIncrementTick()函数,改动比较大。之前是通过遍历就绪列表,查看dely的时间是否到了,需要全部的遍历,不方便。在这里只会查找在延时列表中的任务。
- 先更新xTickCount
- 判断是否有益处,也就是xTickCount = 0。要进行链表的切换
- 交换了两个延时列表指针的指向(因为这个在中断进程中更改了值,所以有volatile修饰)
- 然后判断新的延时链表有没任务,没有就设置xNextTaskUnblockTime = portMAX_DELAY有的话会获取最早延时完成的任务,然后更新xNextTaskUnblockTime。
- 当延时任务到期 xConstTickCount >= xNextTaskUnblockTime。要开始将所有延时到达的任务设置为就绪态。通过for循环。
- 如果没有任务,直接跳出
- 有任务,获取最新的任务,然后判断这个任务要延时的时间是否大于xTickCount,大于说明延时时间没到直接退出。如果小于xTickCount,说明任务就绪,然后从延时链表溢出,添加到就绪链表中。
- 调用的是prvAddTaskToReadyList 宏。这个宏做了两件事
- 在位图中标记任务准备好了
- 插入到链表Index的后面
- 切换任务
这个Index很有意思:
1. 在任务就绪的时候是插入到Index的后面(调用的是vListInsertEnd函数)
1. 设置到最高优先级的时候Inex = Indxe->Next(就是刚插入的那个任务) 这个时候INdex 的指针是动了的listGET_OWNER_OF_NEXT_ENTRY。也就是有多个任务要插入,会插入到Index的后面,然后一个一个的运行。
1. 删除的时候:Index 如果指向要删除的任务——> pxList->pxIndex = pxItemToRemove->pxPrevious
8.5 实验结果
和上一节一样,没有变化。只不过是调度的方式发生了改变。