FreeRTOS时间管理
主要要了解延时函数:
相对延时:指每次延时都是从执行函数vTaskDelay()开始,直到延时指定的时间结束。
绝对延时:指将整个任务的运行周期看成一个整体,适用于需要按照一定频率运行的任务。
函数 vTaskDelayUntil()是绝对模式(绝对延时函数)。函数 vTaskDelay()在文件 tasks.c 中有定义,要使用此函数的话宏 INCLUDE_vTaskDelay 必须为 1,
void vTaskDelay( const TickType_t xTicksToDelay )
具体的函数代码如下:
void vTaskDelay( const TickType_t xTicksToDelay )//
{
BaseType_t xAlreadyYielded = pdFALSE;
/* A delay time of zero just forces a reschedule. */
if( xTicksToDelay > ( TickType_t ) 0U )
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();
/*通过调用vTaskSuspendAll挂起所有任务,
这是为了安全地更新任务的状态和延迟列表,
防止在操作过程中发生中断导致的数据不一致。*/
{
traceTASK_DELAY();
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
/*调用prvAddCurrentTaskToDelayedList
将当前任务添加到延迟列表中,xTicksToDelay
指定了延迟的时间,pdFALSE表示此任务
在延迟期满时不需要立即运行。*/
}
xAlreadyYielded = xTaskResumeAll();
/*通过调用xTaskResumeAll尝试恢复之前挂起的任务。
如果在挂起期间有任务变为就绪状态,
xTaskResumeAll会返回pdTRUE,
表示已经触发了任务切换,否则返回pdFALSE。*/
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
/*如果xTaskResumeAll返回pdFALSE,这
意味着在挂起所有任务和恢复任务切换的过程中,
没有其他任务变为就绪状态,从而没有自动触发
任务切换。但是,当前任务通过调用vTaskDelay
已经表达了它愿意让出CPU。为了确保这种意愿得
到尊重,即使xTaskResumeAll没有触发任务切换,
也通过调用portYIELD_WITHIN_API强制进行一
次任务调度。这样做确保了调度器会重新评估哪个任
务应该运行,即使当前任务的延迟时间为0,也会按照
优先级选择另一个任务运行,如果有的话。*/
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
接下来让我们来看这个函数prvAddCurrentTaskToDelayedList()
,该函数就是用于将当前任务添加到等待列表。
函数声明:static void prvAddCurrentTaskToDelayedList(TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely)
声明了一个静态函数,接受两个参数:xTicksToWait(任务应该被延迟的tick数)和xCanBlockIndefinitely(一个布尔值,指示任务是否可以无限期地阻塞)
。
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,
const BaseType_t xCanBlockIndefinitely )
{
TickType_t xTimeToWake;
const TickType_t xConstTickCount = xTickCount;
/*获取当前的tick计数(xTickCount),
这是系统启动以来经过的tick数。*/
#if ( INCLUDE_xTaskAbortDelay == 1 )
{
pxCurrentTCB->ucDelayAborted = pdFALSE;
}/*重置延迟中止标志(如果启用了INCLUDE_xTaskAbortDelay):
这部分代码通过将pxCurrentTCB->ucDelayAborted设置为pdFALSE,
确保当任务被移动到延迟列表时,任何之前的延迟中止请求都被清除。*/
#endif
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}/*将当前任务从就绪列表中移除以后
还要取消任务在 uxTopReadyPriority
中的就绪标记。也就是将 uxTopReadyPriority
中对应的 bit 清零。*/
else
{
mtCOVERAGE_TEST_MARKER();
}
#if ( INCLUDE_vTaskSuspend == 1 )
{
/*这部分代码检查任务是否请求无限期等待
(xTicksToWait == portMAX_DELAY)。
portMAX_DELAY通常定义为可表示的最大延时,
意味着任务希望无限期挂起。同时,它检查
xCanBlockIndefinitely标志,确保任务允
许无限期阻塞。如果两个条件都满足,任务会被加
入到挂起任务列表(xSuspendedTaskList)的末尾。
这意味着任务将不会被调度,直到明确地被唤醒。*/
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
{
listINSERT_END( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
/*如果任务不是无限期挂起,那么它请求有限的延时。
这部分代码计算任务应当被唤醒的时间点(xTimeToWake),
并将这个时间设置为任务状态列表项的值。*/
else
{
xTimeToWake = xConstTickCount + xTicksToWait;
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
if( xTimeToWake < xConstTickCount )
{
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/*这里检查是否存在时间溢出的情况。如果xTimeToWake小于当前的xConstTickCount,
说明发生了溢出,任务被插入到溢出延时任务列表(pxOverflowDelayedTaskList)。
否则,任务插入到正常的延时任务列表(pxDelayedTaskList)。*/
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
/*当任务进入阻塞状态时(例如,等待一个事件或延时),
它的唤醒时间会被计算并设置。这时,系统会检查这个唤
醒时间是否早于当前的xNextTaskUnblockTime:如果早
于:这意味着系统中有一个新的最早唤醒时间,因此需要
更新xNextTaskUnblockTime为这个新时间。这样可以确
保调度器能够在正确的时间唤醒任务。如
果晚于或等于:xNextTaskUnblockTime不需要更新,
因为已经存在一个更早或相同时间的任务需要被唤醒。*/
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
#else /* INCLUDE_vTaskSuspend */
{
xTimeToWake = xConstTickCount + xTicksToWait;
/* The list item will be inserted in wake time order. */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
if( xTimeToWake < xConstTickCount )
{
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
( void ) xCanBlockIndefinitely;
}
#endif /* INCLUDE_vTaskSuspend */
}
1: if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0
)在FreeRTOS中,uxListRemove
函数用于从列表中移除一个项,并返回该项所在列表中的剩余项数。这个函数的返回值在某些情况下用于判断是否需要进行额外的操作,比如更新调度器的状态或做一些清理工作。具体到if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
这行代码的意义,我们来详细解释一下:
-
移除任务从就绪列表:这行代码的主要目的是从就绪列表中移除当前任务的状态列表项(
pxCurrentTCB->xStateListItem
)。在FreeRTOS中,每个任务都有一个与之关联的列表项,用于将该任务链接到不同的任务列表中,例如就绪列表、延迟列表等。当任务需要被延迟或阻塞时,它必须首先从就绪列表中移除。 -
判断列表项是否是列表中的最后一个:通过检查
uxListRemove
的返回值是否为0,这行代码实际上是在判断移除操作后,原列表是否为空。如果返回值为0,意味着在移除当前任务之前,它是列表中的唯一任务项。这种情况下,就绪列表变为空,可能需要进行一些额外的操作,比如调整就绪任务的优先级位图。 -
调整优先级位图:如果当前任务是其优先级队列中的唯一任务,移除它后,该优先级队列变为空。在这种情况下,需要调用
portRESET_READY_PRIORITY
宏(或类似的操作),来在就绪优先级位图中清除相应优先级的位。这是因为,如果一个优先级队列为空,那么调度器在选择下一个要运行的任务时,就不应该考虑这个优先级了。 -
保持调度器的正确性:这个判断和随后的操作确保了调度器能够正确地反映当前系统的状态,避免在选择下一个要运行的任务时,考虑到已经没有任务的优先级队列。
2:listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
在FreeRTOS中,listSET_LIST_ITEM_VALUE
是一个宏,用于设置列表项的值。这个宏通常用于与任务控制块(TCB)相关的列表项,以跟踪特定的信息,如任务的唤醒时间。
这行代码的作用是设置当前任务(由pxCurrentTCB
指向)的状态列表项(xStateListItem
)的值为xTimeToWake
。这里,xTimeToWake
是计算出的任务应当被唤醒的时间点。
-
pxCurrentTCB: 是指向当前任务控制块(Task Control Block)的指针。每个任务在FreeRTOS中都有一个TCB,其中包含了管理和调度任务所需的所有信息。
-
xStateListItem: 是TCB中的一个成员,是一个
ListItem_t
结构体。这个结构体用于将任务链接到不同的列表中,例如就绪列表、延时列表等。通过这种方式,FreeRTOS的调度器可以管理和调度多个任务。 -
listSET_LIST_ITEM_VALUE: 这个宏接受两个参数,第一个参数是列表项的地址,第二个参数是要设置的值。在这个上下文中,它用于设置任务的唤醒时间。这个值随后用于确定何时将任务从延时列表移动到就绪列表,以便调度器可以重新调度该任务。
函数 vTaskDelayUntil()
函数 vTaskDelayUntil()
会阻塞任务,阻塞时间是一个绝对时间,那些需要按照一定的频率运行的任务可以使用函数 vTaskDelayUntil()。此函数再文件 tasks.c 中有如下定义:
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement )
/*这是xTaskDelayUntil函数的定义,接受两个参数:
pxPreviousWakeTime是指向上一次唤醒时间的指针,
xTimeIncrement是两次唤醒之间的时间间隔。*/
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;/*定义局部变量xTimeToWake来
存储下一次唤醒的时间点,xAlreadyYielded用于指示是否已经进行了任务切换,xShouldDelay标志是否需要延迟。*/
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();//调用vTaskSuspendAll来暂停所有任务调度,这是为了防止在更新计数器和计算下一次唤醒时间时发生中断。
{
/* Minor optimisation. The tick count cannot change in this
* block. */
const TickType_t xConstTickCount = xTickCount;
//获取当前的tick计数并保存到xConstTickCount中,这个值在这个代码块中不会改变,用于后续的时间计算。
/* 计算下一次唤醒的时间点,即上一次唤醒时间加上时间间隔。 */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
/* 计算下一次唤醒的时间点,即上一次唤醒时间加上时间间隔。 */
if( xConstTickCount < *pxPreviousWakeTime )
{
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
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 )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xShouldDelay;
}
下面我们要分析计数器溢出的几种情况,为了更好理解溢出的几种情况。可以根据下面这个图更好去理解这个过程:
if( xConstTickCount < *pxPreviousWakeTime )
{
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
根据图 12.3.1 可以看出,理论上 xConstTickCount
要大于 pxPreviousWakeTime
的,但是也有一种情况会导致 xConstTickCount
小于 pxPreviousWakeTime
,那就是 xConstTickCount
溢出了!既然 xConstTickCount
都溢出了,那么计算得到的任务唤醒时间点肯定也是要溢出的,并且 xTimeToWake
肯定也是要大于 xConstTickCount
的。接下来就是分情况去讨论:
还有其他两种情况,一:只有 xTimeToWake
溢出,二:都没有溢出。只有 xTimeToWake
溢出的话如图 12.3.3 所示:
其实使用函数 vTaskDelayUntil()延时的任务也不一定就能周期性的运行,使用函数vTaskDelayUntil()只能保证你按照一定的周期取消阻塞,进入就绪态。如果有更高优先级或者中断的话你还是得等待其他的高优先级任务或者中断服务函数运行完成才能轮到你。这个绝对延时只是相对于 vTaskDelay()这个简单的延时函数而言的。