目录
- FreeRTOS 延时函数
在 FreeRTOS 中延时函数有相对模式和绝对模式,FreeRTOS 中不同的模式用的函数不同,其中函数 vTaskDelay()是相对模式(相对延时函数), 函数 vTaskDelayUntil()是绝对模式(绝对延时函数)。
1.函数 vTaskDelay()在文件 tasks.c 中有定义, 要使用此函数的话宏 INCLUDE_vTaskDelay 必须为 1,函数代码如下:
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
//延时时间要大于 0
if( xTicksToDelay > ( TickType_t ) 0U )
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();
{
traceTASK_DELAY();
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
- 延时时间由参数 xTicksToDelay 来确定,为要延时的时间节拍数,延时时间肯定要大于 0。否则的话相当于直接调用函数 portYIELD()进行任务切换。
- 调用函数 vTaskSuspendAll()挂起任务调度器。
- 调用函数 prvAddCurrentTaskToDelayedList() 将要延时的任务添加到延时列表pxDelayedTaskList 或 者 pxOverflowDelayedTaskList() 中 。 后 面 会 具 体 分 析 函 数prvAddCurrentTaskToDelayedList()。
- 调用函数 xTaskResumeAll()恢复任务调度器。
- 如果函数 xTaskResumeAll()没有进行任务调度的话那么在这里就得进行任务调度。
- 调用函数 portYIELD_WITHIN_API()进行一次任务调度。
2.函数 vTaskDelayUntil()会阻塞任务,阻塞时间是一个绝对时间,那些需要按照一定的频率运行的任务可以使用函数 vTaskDelayUntil()。此函数再文件 tasks.c 中有如下定义:
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();
{
const TickType_t xConstTickCount = xTickCount;
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
if( xConstTickCount < *pxPreviousWakeTime )
{
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake >\
xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
*pxPreviousWakeTime = xTimeToWake;
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
xAlreadyYielded = xTaskResumeAll();
if( xAlreadyYielded == pdFALSE )
{
ortYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxPreviousWakeTime: 上一次任务延时结束被唤醒的时间点,任务中第一次调用函数vTaskDelayUntil 的话需要将 pxPreviousWakeTime 初始化进入任务的 while()循环体的时间点值。在以后的运行中函数 vTaskDelayUntil()会自动更新 pxPreviousWakeTime。
xTimeIncrement:任务需要延时的时间节拍数(相对于 pxPreviousWakeTime 本次延时的节拍数)。
- 挂起任务调度器。
- 记录进入函数 vTaskDelayUntil()的时间点值,并保存在 xConstTickCount 中。
- 根据延时时间 xTimeIncrement 来计算任务下一次要唤醒的时间点, 并保存在xTimeToWake 中。可以看出这个延时时间是相对于 pxPreviousWakeTime 的,也就是上一次任务被唤醒的时间点。pxPreviousWakeTime、xTimeToWake、xTimeIncrement 和xConstTickCount 的关系如下所示。
- 根据上图 可以看出,理论上 xConstTickCount 要大于 pxPreviousWakeTime 的,但是也有一种情况会导致 xConstTickCount 小于 pxPreviousWakeTime,那就是 xConstTickCount 溢出了!
- 既然 xConstTickCount 都溢出了,那么计算得到的任务唤醒时间点肯定也是要溢出的,并且 xTimeToWake 肯定也是要大于 xConstTickCount 的。这种情况如图 下
如果满足条件的话就将 pdTRUE 赋值给 xShouldDelay,标记允许延时。
还有其他两种情况,一:只有 xTimeToWake 溢出,二:都没有溢出。只有 xTimeToWake溢出的话如图下:
都不溢出的话就如图 1所示,这两种情况都允许进行延时。
- 将 pdTRUE 赋值给 xShouldDelay,标记允许延时。
- 更新 pxPreviousWakeTime 的值,更新为 xTimeToWake,为本函数的下一次执行做准备。
- 经过前面的判断,允许进行任务延时。
- 调用函数 prvAddCurrentTaskToDelayedList()进行延时。函数的第一个参数是设置任务的阻塞时间,前面我们已经计算出了任务下一次唤醒时间点了,那么任务还需要阻塞的时间就是下一次唤醒时间点 xTimeToWake 减去当前的时间 xConstTickCount。而在函数 vTaskDelay()中只是简单的将这参数设置为 xTicksToDelay。
- 调用函数 xTaskResumeAll()恢复任务调度器。函数 vTaskDelayUntil()的使用方法如下:
void TestTask( void * pvParameters )
{
TickType_t PreviousWakeTime;
//延时 50ms,但是函数 vTaskDelayUntil()的参数需要设置的是延时的节拍数,不能直接
//设置延时时间,因此使用函数 pdMS_TO_TICKS 将时间转换为节拍数。
const TickType_t TimeIncrement = pdMS_TO_TICKS( 50 );
PreviousWakeTime = xTaskGetTickCount(); //获取当前的系统节拍值for( ;; )
{
/******************************************************************/
/*************************任务主体*********************************/
/******************************************************************/
//调用函数vTaskDelayUntil 进行延时
vTaskDelayUntil( &PreviousWakeTime, TimeIncrement);
}
}
- FreeRTOS 系统时钟节拍
不管是什么系统,运行都需要有个系统时钟节拍,前面已经提到多次了,xTickCount 就是FreeRTOS 的系统时钟节拍计数器。每个滴答定时器中断中 xTickCount 就会加一,xTickCount 的具体操作过程是在函数 xTaskIncrementTick()中进行的,此函数在文件 tasks.c 中有定义,如下:
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB; TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
//每个时钟节拍中断(滴答定时器中断)调用一次本函数,增加时钟节拍计数器 xTickCount 的
//值,并且检查是否有任务需要取消阻塞。
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
const TickType_t xConstTickCount = xTickCount + 1;
//增加系统节拍计数器 xTickCount 的值,当为 0,也就是溢出的话就交换延时和溢出列
//表指针值。
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//判断是否有任务延时时间到了,任务都会根据唤醒时间点值按照顺序(由小到大的升
//序排列)添加到延时列表中,这就意味这如果延时列表中第一个列表项对应的任务的
//延时时间都没有到的话后面的任务就不用看了,肯定也没有到。
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 ) );
if( xConstTickCount < xItemValue )
{
}
else
{
}
//任务延时时间还没到,但是 xItemValue 保存着下一个即将解除
//阻塞态的任务对应的解除时间点,所以需要用 xItemValue 来更新
//变量xNextTaskUnblockTime
xNextTaskUnblockTime = xItemValue;
break;
mtCOVERAGE_TEST_MARKER();
//将任务从延时列表中移除
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
//任务是否还在等待其他事件?如信号量、队列等,如果是的话就将这些
//任务从相应的事件列表中移除。相当于等待事件超时退出!
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) !\ = NULL )
{
}
else
{
}
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
mtCOVERAGE_TEST_MARKER();
//将任务添加到就绪列表中
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
//使用抢占式内核,判断解除阻塞的任务优先级是否高于当前正在
//运行的任务优先级,如果是的话就需要进行一次任务切换!
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
}
else
{
}
}
xSwitchRequired = pdTRUE;
mtCOVERAGE_TEST_MARKER();
#endif /* configUSE_PREEMPTION */
}
}
}
//如果使能了时间片的话还需要处理同优先级下任务之间的调度
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )(17)
{
if( listCURRENT_LIST_LENGTH( &( \
pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
}
else
{
}
}
xSwitchRequired = pdTRUE;
mtCOVERAGE_TEST_MARKER();
#endif
//使用时钟节拍钩子函数
#if ( configUSE_TICK_HOOK == 1 )
{
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
}
else
{
}
}
vApplicationTickHook();
mtCOVERAGE_TEST_MARKER();
#endif /* configUSE_TICK_HOOK */
}
- 判断任务调度器是否被挂起。
- 将时钟节拍计数器 xTickCount 加一,并将结果保存在 xConstTickCount 中,下一行程序会将 xConstTickCount 赋值给 xTickCount,相当于给 xTickCount 加一。
- xConstTickCount 为 0,说明发生了溢出!
- 如果发生了溢出的话使用函数 taskSWITCH_DELAYED_LISTS 将延时列表指针pxDelayedTaskList 和溢出列表指针 pxOverflowDelayedTaskList 所指向的列表进行交换,函数taskSWITCH_DELAYED_LISTS()本质上是个宏,在文件 tasks.c 中有定义,将这两个指针所指向的列表交换以后还需要更新 xNextTaskUnblockTime 的值。
- 变量 xNextTaskUnblockTime 保存着下一个要解除阻塞的任务的时间点值,如果xConstTickCount 大于 xNextTaskUnblockTime 的话就说明有任务需要解除阻塞了。
- 判断延时列表是否为空。
- 如果延时列表为空的话就将 xNextTaskUnblockTime 设置为 portMAX_DELAY。
- 延时列表不为空,获取延时列表第一个列表项对应的任务控制块。
- 获取(8)中获取到的任务控制块中的壮态列表项值。
- 任务控制块中的壮态列表项值保存了任务的唤醒时间点,如果这个唤醒时间点值大于当前的系统时钟(时钟节拍计数器值),说明任务的延时时间还未到。
- 任务延时时间还未到,而且 xItemValue 已经保存了下一个要唤醒的任务的唤醒时间点,所以需要用 xItemValue 来更新 xNextTaskUnblockTime。
- 任务延时时间到了,所以将任务先从延时列表中移除。
- 检查任务是否还等待某个事件,比如等待信号量、队列等。如果还在等待的话就任务从相应的事件列表中移除。因为超时时间到了!
- 将任务从相应的事件列表中移除。
- 任务延时时间到了,并且任务已经从延时列表或者事件列表中已经移除。所以这里需要将任务添加到就绪列表中。
- 延时时间到的任务优先级高于正在运行的任务优先级,所以需要进行任务切换了,标记 xSwitchRequired 为pdTRUE,表示需要进行任务切换。
- 如果使能了时间片调度的话,还要处理跟时间片调度有关的工作
- 如果使能了时间片钩子函数的话就执行时间片钩子函数 vApplicationTickHook(),函数的具体内容由用户自行编写。
- 如果调用函数 vTaskSuspendAll()挂起了任务调度器的话在每个滴答定时器中断就不不会更新 xTickCount 了。取而代之的是用 uxPendedTicks 来记录调度器挂起过程中的时钟节拍数。这样在调用函数 xTaskResumeAll()恢复任务调度器的时候就会调用 uxPendedTicks 次函数xTaskIncrementTick(),这样 xTickCount 就会恢复,并且那些应该取消阻塞的任务都会取消阻塞。函数 xTaskResumeAll()中相应的处理代码如下:
BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE; configASSERT( uxSchedulerSuspended );
taskENTER_CRITICAL();
/************************************************************************/
/****************************省略部分代码********************************/
/************************************************************************/
UBaseType_t uxPendedCounts = uxPendedTicks; if( uxPendedCounts > ( UBaseType_t ) 0U )
{
//do-while()循环体,循环次数为 uxPendedTicks do
{
if( xTaskIncrementTick() != pdFALSE ) //调用函数xTaskIncrementTick
{
}
else
{
xYieldPending = pdTRUE; //标记需要进行任务调度。
mtCOVERAGE_TEST_MARKER();
}
--uxPendedCounts;
} while( uxPendedCounts > ( UBaseType_t ) 0U );
uxPendedTicks = 0; //循环执行完毕,uxPendedTicks 清零
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/************************************************************************/
/****************************省略部分代码********************************/
/************************************************************************/
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
- uxPendedTicks 是个全局变量,在文件 tasks.c 中有定义,任务调度器挂起以后此变量用来记录时钟节拍数。
- 、有时候调用其他的 API 函数会使用变量 xYieldPending 来标记是否需要进行上下文切换,后面具体遇到具体分析。
- 、返回 xSwitchRequired 的值,xSwitchRequired 保存了是否进行任务切换的信息,如果为 pdTRUE 的话就需要进行任务切换, pdFALSE 的话就不需要进行任务切换。函数xPortSysTickHandler()中调用 xTaskIncrementTick()的时候就会判断返回值,并且根据返回值决定是否进行任务切换。