FreeRTOS 延时函数

  • 相对延迟:相对于当前时间点进行延迟。
  • 绝对延迟:以上一次调用函数的时间点为基础进行延迟。

    vTaskDelay()相对模式的延迟函数

    在 FreeRTOS 中延时函数也有相对模式和绝对模式,不过在 FreeRTOS 中不同的模式用的函数不同,其中函数 vTaskDelay()是相对模式(相对延时函数),函数 vTaskDelayUntil()是绝对模式(绝对延时函数)。函数 vTaskDelay()在文件 tasks.c 中有定义,要使用此函数的话宏INCLUDE_vTaskDelay必须为 1。

    1. void vTaskDelay( const TickType_t xTicksToDelay )
    2. {
    3. BaseType_t xAlreadyYielded = pdFALSE;
    4. /*
    5. * 延时时间由参数 xTicksToDelay 来确定,为要延时的时间节拍数,延时时间肯定要大
    6. * 于 0。否则的话相当于直接调用函数 portYIELD()进行任务切换
    7. * /
    8. if( xTicksToDelay > ( TickType_t ) 0U )
    9. {
    10. configASSERT( uxSchedulerSuspended == 0 );
    11. //调用函数 vTaskSuspendAll()挂起任务调度器。
    12. vTaskSuspendAll();
    13. {
    14. traceTASK_DELAY();
    15. //将要延时的任务添加到延时列表中
    16. prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
    17. }
    18. //恢复任务调度器
    19. xAlreadyYielded = xTaskResumeAll();
    20. }
    21. else
    22. {
    23. mtCOVERAGE_TEST_MARKER();
    24. }
    25. //如果函数 xTaskResumeAll()没有进行任务调度的话那么在这里就得进行任务调度。
    26. if( xAlreadyYielded == pdFALSE )
    27. {
    28. //调用函数 portYIELD_WITHIN_API()进行一次任务调度
    29. portYIELD_WITHIN_API();
    30. }
    31. else
    32. {
    33. mtCOVERAGE_TEST_MARKER();
    34. }
    35. }

    prvAddCurrentTaskToDelayedList()添加任务到延迟列表

    函数 prvAddCurrentTaskToDelayedList()用于将当前任务添加到等待列表中,函数在文件tasks.c 中有定义

    1. static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
    2. {
    3. TickType_t xTimeToWake;
    4. /*
    5. * 读取进入函数 prvAddCurrentTaskToDelayedList()的时间点并保存在 xConstTickCount 中,
    6. * 后面计算任务唤醒时间点的时候要用到。xTickCount 是时钟节拍计数器,每个滴答定时器中断
    7. * xTickCount 都会加一。
    8. */
    9. const TickType_t xConstTickCount = xTickCount;
    10. #if( INCLUDE_xTaskAbortDelay == 1 )
    11. {
    12. //如果使能函数 xTaskAbortDelay()的话复位任务控制块的 ucDelayAborted 字段为
    13. //pdFALSE。
    14. pxCurrentTCB->ucDelayAborted = pdFALSE;
    15. }
    16. #endif
    17. //要将当前正在运行的任务添加到延时列表中,肯定要先将当前任务从就绪列表中移除。
    18. if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
    19. {
    20. /*
    21. * 将当前任务从就绪列表中移除以后还要取消任务在 uxTopReadyPriority 中的就绪标记。
    22. * 也就是将 uxTopReadyPriority 中对应的 bit 清零。
    23. */
    24. portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
    25. }
    26. else
    27. {
    28. mtCOVERAGE_TEST_MARKER();
    29. }
    30. #if ( INCLUDE_vTaskSuspend == 1 )
    31. {
    32. /*
    33. * 延 时 时 间 为 最 大 值 portMAX_DELAY ,并且 xCanBlockIndefinitely 不 为
    34. * pdFALSE(xCanBlockIndefinitely 不为 pdFALSE 的话表示允许阻塞任务)的话直接将当前任务添
    35. * 加到挂起列表中,任务就不用添加到延时列表中。
    36. */
    37. if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
    38. {
    39. //将当前任务添加到挂起列表 xSuspendedTaskList 的末尾
    40. vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
    41. }
    42. else
    43. {
    44. //计算任务唤醒时间点
    45. xTimeToWake = xConstTickCount + xTicksToWait;
    46. //将计算到的任务唤醒时间点值 xTimeToWake 写入到任务列表中壮态列表项的相应字段中。
    47. listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
    48. /*
    49. * 计算得到的任务唤醒时间点小于 xConstTickCount,说明发生了溢出。全局变量
    50. * xTickCount 是 TickType_t 类型的,这是个 32 位的数据类型,因此在用 xTickCount 计算任务唤
    51. * 醒时间点xTimeToWake的时候的肯定会出现溢出的现象。FreeRTOS针对此现象专门做了处理,
    52. * 在 FreeROTS 中定义了两个延时列表 xDelayedTaskList1 和 xDelayedTaskList2,并且也定义了两
    53. * 个指针 pxDelayedTaskList 和 pxOverflowDelayedTaskList 来访问这两个列表,在初始化列表函数
    54. * prvInitialiseTaskLists() 中指针 pxDelayedTaskList 指 向 了 列 表 xDelayedTaskList1 ,指针
    55. * pxOverflowDelayedTaskList 指向了列表 xDelayedTaskList2。这样发生溢出的话就将任务添加到
    56. * pxOverflowDelayedTaskList 所指向的列表中,如果没有溢出的话就添加到 pxDelayedTaskList 所
    57. * 指向的列表中。
    58. */
    59. if( xTimeToWake < xConstTickCount ) {
    60. //如果发生了溢出的话就将当前任务添加到pxOverflowDelayedTaskList所指向的列表中。
    61. vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
    62. }
    63. else
    64. {
    65. //如果没有发生溢出的话就将当前任务添加到 pxDelayedTaskList 所指向的列表中
    66. vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
    67. //xNextTaskUnblockTime 是个全局变量,保存着距离下一个要取消阻塞的任务最小时间点值。 当 xTimeToWake 小于 xNextTaskUnblockTime 的话说明有个更小的时间点来了
    68. if( xTimeToWake < xNextTaskUnblockTime )
    69. {
    70. //更新 xNextTaskUnblockTime 为 xTimeToWake
    71. xNextTaskUnblockTime = xTimeToWake;
    72. }
    73. else
    74. {
    75. mtCOVERAGE_TEST_MARKER();
    76. }
    77. }
    78. }
    79. }
    80. }

    vTaskDelayUntil()绝对延迟函数

    函数 vTaskDelayUntil()会阻塞任务,阻塞时间是一个绝对时间,那些需要按照一定的频率运行的任务可以使用函数 vTaskDelayUntil()。此函数再文件 tasks.c 中有如下定义。

    1. void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,const TickType_t xTimeIncrement )
    2. {
    3. TickType_t xTimeToWake;
    4. BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
    5. configASSERT( pxPreviousWakeTime );
    6. configASSERT( ( xTimeIncrement > 0U ) );
    7. configASSERT( uxSchedulerSuspended == 0 );
    8. //挂起任务调度器。
    9. vTaskSuspendAll();
    10. {
    11. //记录进入函数 vTaskDelayUntil()的时间点值,并保存在 xConstTickCount 中
    12. const TickType_t xConstTickCount = xTickCount;
    13. /*
    14. * 根据延时时间 xTimeIncrement 来计算任务下一次要唤醒的时间点,并保存在
    15. * xTimeToWake 中。可以看出这个延时时间是相对于 pxPreviousWakeTime 的,也就是上一次任务
    16. * 被唤醒的时间点。
    17. */
    18. xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
    19. //理论上 xConstTickCount 要大于 pxPreviousWakeTime 的,但是也有一种情况会导致 xConstTickCount 小于 pxPreviousWakeTime,那就是 xConstTickCount 溢出了!
    20. if( xConstTickCount < *pxPreviousWakeTime )
    21. {
    22. //既然 xConstTickCount 都溢出了,那么计算得到的任务唤醒时间点肯定也是要溢出的,并且 xTimeToWake 肯定也是要大于 xConstTickCount 的
    23. if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake >xConstTickCount ) )
    24. {
    25. xShouldDelay = pdTRUE;
    26. }
    27. else
    28. {
    29. mtCOVERAGE_TEST_MARKER();
    30. }
    31. }
    32. else
    33. {
    34. //还有其他两种情况,一:只有 xTimeToWake 溢出,二:都没有溢出。
    35. if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
    36. {
    37. //将 pdTRUE 赋值给 xShouldDelay,标记允许延时。
    38. xShouldDelay = pdTRUE; (8)
    39. }
    40. else
    41. {
    42. mtCOVERAGE_TEST_MARKER();
    43. }
    44. }
    45. //更新 pxPreviousWakeTime 的值,更新为 xTimeToWake,为本函数的下一次执行做准备。
    46. *pxPreviousWakeTime = xTimeToWake;
    47. //允许进行任务延时
    48. if( xShouldDelay != pdFALSE )
    49. {
    50. traceTASK_DELAY_UNTIL( xTimeToWake );
    51. /*
    52. * 调用函数 prvAddCurrentTaskToDelayedList()进行延时。函数的第一个参数是设置任务
    53. * 的阻塞时间,前面我们已经计算出了任务下一次唤醒时间点了,那么任务还需要阻塞的时间就
    54. * 是下一次唤醒时间点 xTimeToWake 减去当前的时间 xConstTickCount。而在函数 vTaskDelay()中
    55. * 只是简单的将这参数设置为 xTicksToDelay。
    56. */
    57. prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
    58. }
    59. else
    60. {
    61. mtCOVERAGE_TEST_MARKER();
    62. }
    63. }
    64. //恢复任务调度器
    65. xAlreadyYielded = xTaskResumeAll();
    66. if( xAlreadyYielded == pdFALSE )
    67. {
    68. ortYIELD_WITHIN_API();
    69. }
    70. else
    71. {
    72. mtCOVERAGE_TEST_MARKER();
    73. }
    74. }

    参数

  • pxPreviousWakeTime: 上一次任务延时结束被唤醒的时间点,任务中第一次调用函数vTaskDelayUntil的话需要将pxPreviousWakeTime初始化进入任务的 while()循环体的时间点值。在以后的运行中函数vTaskDelayUntil()会自动更新pxPreviousWakeTime

  • xTimeIncrement:任务需要延时的时间节拍数(相对于 pxPreviousWakeTime 本次延时的节拍数)。

pxPreviousWakeTime、xTimeToWake、xTimeIncrement 和 xConstTickCount 的关系。
12.时间管理 - 图1
(1)为任务主体,也就是任务真正要做的工作,(2)是任务函数中调用vTaskDelayUntil()对任务进行延时,(3)为其他任务在运行。任务的延时时间是 xTimeIncrement,这个延时时间是相对于 pxPreviousWakeTime 的,可以看出任务总的执行时间一定要小于任务的延时时间 xTimeIncrement!也就是说如果使用 vTaskDelayUntil()的话任务相当于任务的执行周期永远都是 xTimeIncrement,而任务一定要在这个时间内执行完成。这样就保证了任务永远按照一定的频率运行了,这个延时值就是绝对延时时间,因此函数 vTaskDelayUntil()也叫做绝对延时函数。
xConstTickCount 都溢出了的示意图:
12.时间管理 - 图2
只有 xTimeToWake溢出的示意图
12.时间管理 - 图3
vTaskDelayUntil()的使用方法如下:

  1. void TestTask( void * pvParameters )
  2. {
  3. TickType_t PreviousWakeTime;
  4. //延时 50ms,但是函数 vTaskDelayUntil()的参数需要设置的是延时的节拍数,不能直接
  5. //设置延时时间,因此使用函数 pdMS_TO_TICKS 将时间转换为节拍数。
  6. const TickType_t TimeIncrement = pdMS_TO_TICKS( 50 );
  7. PreviousWakeTime = xTaskGetTickCount(); //获取当前的系统节拍值
  8. for( ;; )
  9. {
  10. /******************************************************************/
  11. /*************************任务主体*********************************/
  12. /******************************************************************/
  13. //调用函数 vTaskDelayUntil 进行延时
  14. vTaskDelayUntil( &PreviousWakeTime, TimeIncrement);
  15. }
  16. }

FreeRTOS 系统时钟节拍

不管是什么系统,运行都需要有个系统时钟节拍,前面已经提到多次了,xTickCount 就是FreeRTOS 的系统时钟节拍计数器。每个滴答定时器中断中 xTickCount 就会加一,xTickCount 的具体操作过程是在函数 xTaskIncrementTick()中进行的,此函数在文件 tasks.c 中有定义

  1. BaseType_t xTaskIncrementTick( void )
  2. {
  3. TCB_t * pxTCB;
  4. TickType_t xItemValue;
  5. BaseType_t xSwitchRequired = pdFALSE;
  6. //每个时钟节拍中断(滴答定时器中断)调用一次本函数,增加时钟节拍计数器 xTickCount 的
  7. //值,并且检查是否有任务需要取消阻塞。
  8. traceTASK_INCREMENT_TICK( xTickCount );
  9. //判断任务调度器是否被挂起
  10. if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
  11. {
  12. //将时钟节拍计数器 xTickCount 加一,并将结果保存在 xConstTickCount 中,下一行程序会将 xConstTickCount 赋值给 xTickCount,相当于给 xTickCount 加一。
  13. const TickType_t xConstTickCount = xTickCount + 1;
  14. //增加系统节拍计数器 xTickCount 的值,当为 0,也就是溢出的话就交换延时和溢出列
  15. //表指针值。
  16. xTickCount = xConstTickCount;
  17. //xConstTickCount 为 0,说明发生了溢出
  18. if( xConstTickCount == ( TickType_t ) 0U )
  19. {
  20. /*
  21. * 如果发生了溢出的话使用函数 taskSWITCH_DELAYED_LISTS 将延时列表指针
  22. * pxDelayedTaskList 和溢出列表指针 pxOverflowDelayedTaskList 所指向的列表进行交换,函数
  23. * taskSWITCH_DELAYED_LISTS()本质上是个宏,在文件 tasks.c 中有定义,将这两个指针所指向
  24. * 的列表交换以后还需要更新 xNextTaskUnblockTime 的值
  25. */
  26. taskSWITCH_DELAYED_LISTS();
  27. }
  28. else
  29. {
  30. mtCOVERAGE_TEST_MARKER();
  31. }
  32. //判断是否有任务延时时间到了,任务都会根据唤醒时间点值按照顺序(由小到大的升
  33. //序排列)添加到延时列表中,这就意味这如果延时列表中第一个列表项对应的任务的
  34. //延时时间都没有到的话后面的任务就不用看了,肯定也没有到。
  35. if( xConstTickCount >= xNextTaskUnblockTime )
  36. {
  37. for( ;; )
  38. {
  39. //判断延时列表是否为空
  40. if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
  41. {
  42. //延时列表为空,设置 xNextTaskUnblockTime 为最大值。
  43. xNextTaskUnblockTime = portMAX_DELAY;
  44. break;
  45. }
  46. else
  47. {
  48. //延时列表不为空,获取延时列表的第一个列表项的值,根据判断这个值
  49. //判断任务延时时间是否到了, 如果到了的话就将任务移除延时列表。
  50. pxTCB = ( TCB_t * )listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
  51. xItemValue =listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
  52. //任务控制块中的壮态列表项值保存了任务的唤醒时间点,如果这个唤醒时间点值大于当前的系统时钟(时钟节拍计数器值),说明任务的延时时间还未到。
  53. if( xConstTickCount < xItemValue )
  54. {
  55. //任务延时时间还没到,但是 xItemValue 保存着下一个即将解除
  56. //阻塞态的任务对应的解除时间点,所以需要用 xItemValue 来更新
  57. //变量 xNextTaskUnblockTime
  58. xNextTaskUnblockTime = xItemValue;
  59. break;
  60. }
  61. else
  62. {
  63. mtCOVERAGE_TEST_MARKER();
  64. }
  65. //将任务从延时列表中移除
  66. ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
  67. //任务是否还在等待其他事件?如信号量、队列等,如果是的话就将这些
  68. //任务从相应的事件列表中移除。相当于等待事件超时退出!
  69. if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
  70. {
  71. ( void ) uxListRemove( &( pxTCB->xEventListItem ) ); (14)
  72. }
  73. else
  74. {
  75. mtCOVERAGE_TEST_MARKER();
  76. }
  77. //将任务添加到就绪列表中
  78. prvAddTaskToReadyList( pxTCB );
  79. #if ( configUSE_PREEMPTION == 1 )
  80. {
  81. //使用抢占式内核,判断解除阻塞的任务优先级是否高于当前正在
  82. //运行的任务优先级,如果是的话就需要进行一次任务切换!
  83. if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) {
  84. xSwitchRequired = pdTRUE;
  85. }
  86. else
  87. {
  88. mtCOVERAGE_TEST_MARKER();
  89. }
  90. }
  91. #endif /* configUSE_PREEMPTION */
  92. }
  93. }
  94. }
  95. //如果使能了时间片的话还需要处理同优先级下任务之间的调度
  96. #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
  97. {
  98. if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
  99. {
  100. xSwitchRequired = pdTRUE;
  101. }
  102. else
  103. {
  104. mtCOVERAGE_TEST_MARKER();
  105. }
  106. }
  107. #endif
  108. //使用时钟节拍钩子函数
  109. #if ( configUSE_TICK_HOOK == 1 )
  110. {
  111. if( uxPendedTicks == ( UBaseType_t ) 0U )
  112. {
  113. //如果使能了时间片钩子函数的话就执行时间片钩子函数 vApplicationTickHook(),函数的具体内容由用户自行编写。
  114. vApplicationTickHook();
  115. }
  116. else
  117. {
  118. mtCOVERAGE_TEST_MARKER();
  119. }
  120. }
  121. #endif /* configUSE_TICK_HOOK */
  122. }
  123. else //任务调度器挂起
  124. {
  125. /*
  126. * 如果调用函数 vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不
  127. * 不会更新 xTickCount 了。取而代之的是用 uxPendedTicks 来记录调度器挂起过程中的时钟节拍
  128. * 数。这样在调用函数 xTaskResumeAll()恢复任务调度器的时候就会调用 uxPendedTicks 次函数
  129. * xTaskIncrementTick(),这样 xTickCount 就会恢复,并且那些应该取消阻塞的任务都会取消阻塞。
  130. */
  131. ++uxPendedTicks;
  132. #if ( configUSE_TICK_HOOK == 1 )
  133. {
  134. vApplicationTickHook();
  135. }
  136. #endif
  137. }
  138. #if ( configUSE_PREEMPTION == 1 )
  139. {
  140. //有时候调用其他的 API 函数会使用变量 xYieldPending 来标记是否需要进行上下文切换
  141. if( xYieldPending != pdFALSE )
  142. {
  143. xSwitchRequired = pdTRUE;
  144. }
  145. else
  146. {
  147. mtCOVERAGE_TEST_MARKER();
  148. }
  149. }
  150. #endif /* configUSE_PREEMPTION */
  151. /*
  152. * 返回 xSwitchRequired 的值,xSwitchRequired 保存了是否进行任务切换的信息,如果
  153. * 为 pdTRUE 的话就需要进行任务切换,pdFALSE 的话就不需要进行任务切换。函数
  154. * xPortSysTickHandler()中调用 xTaskIncrementTick()的时候就会判断返回值,并且根据返回值决定
  155. * 是否进行任务切换。
  156. */
  157. return xSwitchRequired;
  158. }