之前,为了实现任务的阻塞延时,在任务控制块中内置了一个延时变量、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 中定义。

  1. static List_t xDelayedTaskList1; //1
  2. static List_t xDelayedTaskList2; //2
  3. static List_t * volatile pxDelayedTaskList; //3
  4. static List_t * volatile pxOverflowDelayedTaskList; //4
  1. FreeRTOS 定义了两个任务延时列表,当系统时基计数器xTickCount 没有溢出时,用一条列表,当 xTickCount 溢出后, 用另外一条列表。
  2. 任务延时列表指针, 指向 xTickCount 没有溢出时使用的那条列表。
  3. 任务延时列表指针, 指向 xTickCount 溢出时使用的那条列表。

8.2.2 任务延时列表初始化

任务延时列表属于任务列表的一种,在 prvInitialiseTaskLists()函数中初始化

  1. // 就绪列表初始化
  2. void prvInitialiseTaskLists( void )
  3. {
  4. UBaseType_t uxPriority;
  5. for(uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++)
  6. {
  7. vListInitialise(& (pxReadyTasksLists[ uxPriority ]));
  8. }
  9. vListInitialise(&xDelayedTaskList1); //1
  10. vListInitialise(&xDelayedTaskList2); //2
  11. pxDelayedTaskList = &xDelayedTaskList1; //3
  12. pxOverflowDelayedTaskList = &xDelayedTaskList2; //4
  13. }

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 行

  1. void vTaskStartScheduler( void )
  2. {
  3. /*======================================创建空闲任务start==============================================*/
  4. TCB_t *pxIdleTaskTCBBuffer = NULL;
  5. StackType_t *pxIdleTaskStackBuffer = NULL;
  6. uint32_t ulIdleTaskStackSize;
  7. /* 获取空闲任务的内存:任务栈和任务TCB */
  8. vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
  9. &pxIdleTaskStackBuffer,
  10. &ulIdleTaskStackSize );
  11. xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
  12. (char *)"IDLE", /* 任务名称,字符串形式 */
  13. (uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
  14. (void *) NULL, /* 任务形参 */
  15. (UBaseType_t) tskIDLE_PRIORITY, /* 任务优先级,数值越大,优先级越高 */
  16. (StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
  17. (TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */
  18. /*======================================创建空闲任务end================================================*/
  19. xNextTaskUnblockTime = portMAX_DELAY;
  20. xTickCount = ( TickType_t ) 0U;
  21. /* 启动调度器 */
  22. if( xPortStartScheduler() != pdFALSE )
  23. {
  24. /* 调度器启动成功,则不会返回,即不会来到这里 */
  25. }
  26. }

8.3 修改代码,支持任务延时列表

8.3.1 修改vTaskDelay() 函数

  1. // 阻塞延时
  2. void vTaskDelay( const TickType_t xTicksToDelay )
  3. {
  4. TCB_t *pxTCB = NULL;
  5. /* 获取当前任务的 TCB */
  6. pxTCB = pxCurrentTCB;
  7. /* 设置延时时间 */
  8. //pxTCB->xTicksToDelay = xTicksToDelay;
  9. /* 将任务插入到延时列表 */
  10. prvAddCurrentTaskToDelayedList( xTicksToDelay ); // 调用这个函数
  11. /* 任务切换 */
  12. taskYIELD();
  13. }

1. prvAddCurrentTaskToDelayedList()函数

  1. static void prvAddCurrentTaskToDelayedList( TickType_t xTicksTowait )
  2. {
  3. TickType_t xTimeToWake;
  4. /* 获取系统时基计数器 xTickCount 的值 */
  5. const TickType_t xConstTickCount = xTickCount;
  6. /* 将任务从就绪列表中移除 返回值是这个链表的剩余节点数*/
  7. if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
  8. {
  9. /* 将任务在优先级位图中对应的位清除 位图操作*/
  10. portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
  11. }
  12. /* 计算任务延时到期时,系统时基计数器 xTickCount 的值是多少 */
  13. xTimeToWake = xConstTickCount + xTicksTowait;
  14. /* 将延时到期的值设置为节点的排序值*/
  15. listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
  16. /* 溢出 苏醒的时间线 < 计数的时间 说明溢出了 */
  17. if (xTimeToWake < xConstTickCount)
  18. {
  19. vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
  20. }
  21. else // 没有溢出
  22. {
  23. vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
  24. /* 更新下一个任务解锁时刻变量xNextTaskUnblockTime的值 */
  25. /* xNextTaskUnblockTime 下个任务解锁时刻 */
  26. /* xTimeToWake 下个任务需要醒来的时间 */
  27. /* xTimeToWake 这个是判断是不是延时最短的任务的时间 可能上个任务的延时比这个多,还
  28. 需要等待更长时间,把需要等待的最短的时间进行提前*/
  29. if( xTimeToWake < xNextTaskUnblockTime )
  30. {
  31. xNextTaskUnblockTime = xTimeToWake;
  32. }
  33. }
  34. }
  1. 用函数 uxListRemove()将任务从就绪列表移除, uxListRemove()会返回当前链表下节点的个数,如果为 0,则表示当前链表下没有任务就绪,则调用函数portRESET_READY_PRIORITY()将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。 因为 FreeRTOS 支持同一个优先级下可以有多个任务,所以在清除优先级位图表uxTopReadyPriority 中对应的位时要判断下该优先级下的就绪列表是否还有其它的任务。 目前为止, 我们还没有支持同一个优先级下有多个任务的功能, 这个功能我们将在下一章“支持时间片”里面实现。
  2. xTimeToWake 溢出, 将任务插入到溢出任务延时列表。溢出?什么意思? xTimeToWake 等于系统时基计数器 xTickCount 的值加上任务需要延时的时间xTicksToWait。举例: 如果当前 xTickCount 的值等于 0xfffffffdUL, xTicksToWait 等于0x03,那么 xTimeToWake = 0xfffffffdUL + 0x03 = 1,显然得出的值比任务需要延时的时间0x03 还小,这肯定不正常,说明溢出了,这个时候需要将任务插入到溢出任务延时列表。
  3. xTimeToWake 没有溢出, 则将任务插入到正常任务延时列表
  4. 更新下一个任务解锁时刻变量 xNextTaskUnblockTime 的值。 这一步很重要, 在 xTaskIncrementTick()函数中,我们只需要让系统时基计数器 xTickCount 与xNextTaskUnblockTime 的值先比较就知道延时最快结束的任务是否到期。


8.3.2 修改 xTaskIncrementTick()函数

xTaskIncrementTick()函数改动较大

  1. void xTaskIncrementTick( void )
  2. {
  3. TCB_t *pxTCB = NULL;
  4. TickType_t xItemValue;
  5. BaseType_t i = 0;
  6. /* 更新系统时基计数器 xTickCount, xTickCount 是一个在 port.c 中定义的全局变量 */
  7. const BaseType_t xConstTickCount = xTickCount + 1;
  8. xTickCount = xConstTickCount;
  9. /* 如果 xConstTickCount 溢出,则切换延时列表 */
  10. if ( xConstTickCount == (TickType_t) 0U )
  11. {
  12. taskSWITCH_DELAYED_LISTS(); // 这个后面有
  13. }
  14. /* xNextTaskUnblockTime存放要期的任务 */
  15. if ( xConstTickCount >= xNextTaskUnblockTime)
  16. {
  17. for(;;)
  18. {
  19. if ( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
  20. {
  21. /* 延时列表为空,设置 xNextTaskUnblockTime 为可能的最大值 */
  22. xNextTaskUnblockTime = portMAX_DELAY;
  23. break;
  24. }
  25. else
  26. {
  27. // 获取排在第一个 也就是会最先到达延迟的任务
  28. pxTCB = (TCB_t *) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
  29. xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
  30. /* 直到将延时列表中所有延时到期的任务移除才跳出 for 循环 */
  31. if( xConstTickCount < xItemValue)
  32. {
  33. xNextTaskUnblockTime = xItemValue;
  34. break;
  35. }
  36. /* 将任务从延时列表移除,消除等待状态 */
  37. ( void )uxListRemove( &( pxTCB->xStateListItem ) );
  38. /* 将解除等待的任务添加到就绪列表 */
  39. prvAddTaskToReadyList( pxTCB );
  40. }
  41. }
  42. }
  43. /* 任务切换 */
  44. portYIELD();
  45. }
  1. 如果系统时基计数器 xTickCount 溢出,则切换延时列表。taskSWITCH_DELAYED_LISTS()函数在 task.c 中定义。

    1. /*
    2. * 当系统时基计数器溢出的时候,延时列表pxDelayedTaskList 和
    3. * pxOverflowDelayedTaskList要互相切换
    4. */
    5. #define taskSWITCH_DELAYED_LISTS()\
    6. {\
    7. List_t *pxTemp;\
    8. pxTemp = pxDelayedTaskList;\
    9. pxDelayedTaskList = pxOverflowDelayedTaskList;\
    10. pxOverflowDelayedTaskList = pxTemp;\
    11. xNumOfOverflows++;\
    12. prvResetNextTaskUnblockTime();\
    13. }

    prvResetNextTaskUnblockTime

    1. static void prvResetNextTaskUnblockTime( void )
    2. {
    3. TCB_t *pxTCB;
    4. if ( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE)
    5. {
    6. /* 当前延时列表为空,则设置 xNextTaskUnblockTime 等于最大值 */
    7. xNextTaskUnblockTime = portMAX_DELAY;
    8. }
    9. else
    10. {
    11. /* 当前列表不为空,则有任务在延时,则获取当前列表下第一个节点的排序值
    12. 然后将该节点的排序值更新到 xNextTaskUnblockTime */
    13. ( pxTCB ) = ( TCB_t *) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
    14. xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
    15. }
    16. }

8.3.3 修改 taskRESET_READY_PRIORITY()函数

在没有添加任务延时列表之前,与任务相关的列表只有一个,就是就绪列表,无论任务在延时还是就绪都只能通过扫描就绪列表来找到任务的 TCB,从而实现系统调度。 所以在上一章“支持多优先级”中,实现 taskRESET_READY_PRIORITY()函数的时候,不用先判断当前优先级下就绪列表中的链表的节点是否为 0,而是直接把任务在优先级位图表uxTopReadyPriority 中对应的位清零。 因为当前优先级下就绪列表中的链表的节点不可能为0, 目前我们还没有添加其它列表来存放任务的 TCB,只有一个就绪列表。
但是从本章开始,我们额外添加了延时列表,当任务要延时的时候,将任务从就绪列表移除,然后添加到延时列表,同时将任务在优先级位图表 uxTopReadyPriority 中对应的位清除。在清除任务在优先级位图表 uxTopReadyPriority 中对应的位的时候, 与上一章不同的是需要判断就绪列表 pxReadyTasksLists[]在当前优先级下对应的链表的节点是否为 0,只有当该链表下没有任务时才真正地将任务在优先级位图表 uxTopReadyPriority 中对应的位清零。
那什么情况下就绪列表的链表里面会有多个任务节点?即同一优先级下有多个任务?这个就是
我们下一章“支持时间片” 。

  1. #define taskRESET_READY_PRIORITY( uxPriority ) \
  2. { \
  3. if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \
  4. { \
  5. portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
  6. } \
  7. }
  8. #if 0
  9. #define taskRESET_READY_PRIORITY( uxPriority ) \
  10. { \
  11. portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
  12. }
  13. #endif

8.4 小总结

  1. 增加了定义延时的列表,然后初始化。其实就是两个指针分别指向不同的列表。原来的为就绪列表。

    1. static List_t xDelayedTaskList1; //1
    2. static List_t xDelayedTaskList2; //2
    3. static List_t * volatile pxDelayedTaskList; //3
    4. static List_t * volatile pxOverflowDelayedTaskList; //4

    为什么要增加两个队列?为的是计数器溢出的时候可以切换两个队列。防止出错。

  2. 定义xNextTaskUnblockTime,并初始化为portMAX_DELAY(0xffffff),也就是最大值。这个时间是用来记录任务睡眠后,苏醒的时间 xNextTaskUnblockTime = xtickCount + delayTime,然后和tick时间比较,进行切换。

  3. 修改vTaskDelay() 函数,增加了prvAddCurrentTaskToDelayedList()函数,将任务放入等待列表

    1. 从就绪列表移除该任务
    2. 计算延迟到期的时间
    3. 按照延迟的时间xItemValue插入到delay链表中
    4. 需要判断是否溢出 溢出要添加到溢出链表中
    5. 否则插入到正在运行的延时列表,并判断是否需要更新xNextTaskUnblockTime时间,因为xNextTaskUnblockTime总是指向的是最早就绪的任务的wake时间
  4. 修改 xTaskIncrementTick()函数,改动比较大。之前是通过遍历就绪列表,查看dely的时间是否到了,需要全部的遍历,不方便。在这里只会查找在延时列表中的任务。

    1. 先更新xTickCount
    2. 判断是否有益处,也就是xTickCount = 0。要进行链表的切换
      1. 交换了两个延时列表指针的指向(因为这个在中断进程中更改了值,所以有volatile修饰)
      2. 然后判断新的延时链表有没任务,没有就设置xNextTaskUnblockTime = portMAX_DELAY有的话会获取最早延时完成的任务,然后更新xNextTaskUnblockTime。
    3. 当延时任务到期 xConstTickCount >= xNextTaskUnblockTime。要开始将所有延时到达的任务设置为就绪态。通过for循环。
      1. 如果没有任务,直接跳出
      2. 有任务,获取最新的任务,然后判断这个任务要延时的时间是否大于xTickCount,大于说明延时时间没到直接退出。如果小于xTickCount,说明任务就绪,然后从延时链表溢出,添加到就绪链表中。
    4. 调用的是prvAddTaskToReadyList 宏。这个宏做了两件事
      1. 在位图中标记任务准备好了
      2. 插入到链表Index的后面
    5. 切换任务

这个Index很有意思:

  1. 1. 在任务就绪的时候是插入到Index的后面(调用的是vListInsertEnd函数)
  2. 1. 设置到最高优先级的时候Inex = Indxe->Next(就是刚插入的那个任务) 这个时候INdex 的指针是动了的listGET_OWNER_OF_NEXT_ENTRY。也就是有多个任务要插入,会插入到Index的后面,然后一个一个的运行。
  3. 1. 删除的时候:Index 如果指向要删除的任务——> pxList->pxIndex = pxItemToRemove->pxPrevious

8. 任务延时列表的实现 - 图1

8.5 实验结果

和上一节一样,没有变化。只不过是调度的方式发生了改变。
image.png