在写FreeRTOS的应用程序时,经常需要使用到延时函数,当执行到延时函数时,会将任务从就绪状态变为延时等待状态,这里会放弃CPU的占用权进入阻塞态,将CPU让给其它任务使用,直到延时时间结束会重新变为就绪态。FreeRTOS中的延时函数有两种模式,一种是相对模式,另一种是绝对模式。相对延时函数使用 vTaskDelay() ,相对延时函数是指每次执行都是从函数接口处开始计时,计时到指定时间结束回来继续往下执行。绝对延时函数使用 vTaskDelayUntil() ,绝对延时函数是指每隔一段指定的时间执行一次 vTaskDelayUntil() ,那些需要按照一定频率运行的任务可以使用绝对模式。
先分析一下相对模式的应用方式,一般我们是这样使用的:
void led_task(void *pvParameters)
{
while(1)
{
LED0=!LED0;
vTaskDelay(500); /* LED灯每隔500ms亮灭一次 */
}
}
函数的实现如下:
#if ( 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 )
{
/* 如果 xTaskResumeAll 函数没有进行调度的话就在这里进行任务切换,此 */
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
上面主要涉及到几个函数:
vTaskSuspendAll
prvAddCurrentTaskToDelayedList
xTaskResumeAll
portYIELD_WITHIN_API
void vTaskSuspendAll( void )
{
/* 挂起调度器不需要关闭可屏蔽中断,这个变量递增后在systick中断函数里面判断到 uxSchedulerSuspended
不为 pdFALSE就不会去判断延时列表是否有任务可以执行,这里挂起主要为了避免调度器和即将运行的函数操作发生冲突,
在systick操作了延时列表和计数器,接下来的函数操作中也会操作延时列表和计数器,所以需暂时屏蔽 */
/* 挂起计数器(支持嵌套) */
++uxSchedulerSuspended;
}
portYIELD_WITHIN_API() 实际上就是操作中断控制及状态寄存器ICSR,请求一次PendSV中断,PendSV任务切换具体步骤前面任务“FreeRTOS-任务切换源码分析”已经分析了,
#define portYIELD() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
/* Barriers are normally not required but do ensure the code is completely \
within the specified behaviour for the architecture. */ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
延时函数主要就是将任务从就绪状态切换为等待状态,使用 prvAddCurrentTaskToDelayedList 来实现:
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
TickType_t xTimeToWake;
const TickType_t xConstTickCount = xTickCount;
/* 将任务从就绪列表中删除 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* 清除对应就绪优先级标志 */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 如果允许任务挂起 */
#if ( INCLUDE_vTaskSuspend == 1 )
{
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
{
/* 如果设置了最大延时时间并且形参传入pdTURE则无限期阻塞任务,即放入挂起列表 */
vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 当前定时器计数值(xConstTickCount在systick中断递增)+延时时间=唤醒时间 */
xTimeToWake = xConstTickCount + xTicksToWait;
/* 将唤醒时间作为状态列表项值 */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
/* 如果唤醒时间溢出了(int类型为4字节,当计数到0xFFFFFFFF,再加1就会溢出变为0) */
if( xTimeToWake < xConstTickCount )
{
/* 计数值溢出了将任务插入到溢出延时列表 */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 计数值没溢出将任务插入到延时列表 */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
if( xTimeToWake < xNextTaskUnblockTime )
{
/* 如果下一个任务的唤醒时间比当前任务的唤醒时间要长,就重置一下 xNextTaskUnblockTime 变量 */
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
#else /* 如果没有开启任务挂起(少了挂起部分的代码,其余部分和上面一样) */
{
/* 当前定时器计数值(xConstTickCount在systick中断递增)+延时时间=唤醒时间 */
xTimeToWake = xConstTickCount + xTicksToWait;
/* 将唤醒时间作为列表项值 */
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
}
在将任务插入到延时或溢出列表后会恢复调度器,使用函数 xTaskResumeAll :
BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
configASSERT( uxSchedulerSuspended );
/* 进入临界区 */
taskENTER_CRITICAL();
{
/* 计数值递减,用于解挂调度器 */
--uxSchedulerSuspended;
/* 如果为pdFALSE说明所有挂起都已解除,调度器可以开始运行了 */
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* 如果当前就绪任务数量大于0 */
if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
{
/* 挂起就绪列表不为空(有任务获取到了相应的队列事件而解除了阻塞状态后本应该放到就绪列表中,
但调度器还处于挂起状态,就会将任务暂时添加到xPendingReadyList这个列表) */
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
{
/* 获取挂起就绪列表中的项目,一个个取出来 */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
/* 将任务从事件列表中移除 */
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
/* 将任务从状态列表中移除 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/* 把任务添加到就绪列表中 */
prvAddTaskToReadyList( pxTCB );
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
/* 如果这个任务的优先级高于当前任务的优先级则标记进行一次切换 */
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 挂起就绪列表不为空,即获取到了一个任务 */
if( pxTCB != NULL )
{
/* 重置下一个任务的解锁定时间 */
prvResetNextTaskUnblockTime();
}
{
/* 获取调度器休眠时 systick 计数器的递增值,此时xTickCount不递增,
使用 uxPendedTicks 代替 xTickCount 作为计数器 */
UBaseType_t uxPendedCounts = uxPendedTicks;
/* 调度器休眠时systick计数器计数值大于0 */
if( uxPendedCounts > ( UBaseType_t ) 0U )
{
do
{
/* 补回调度器休眠时 systick 的计时值 */
if( xTaskIncrementTick() != pdFALSE )
{
/* 如果这期间有任务延时时间到了,或者同优先级下有就绪任务,标记需要进行调度 */
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 计数值递减,直到补回所有调度器休眠时定时器的计数值 */
--uxPendedCounts;
} while( uxPendedCounts > ( UBaseType_t ) 0U );
uxPendedTicks = 0;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( xYieldPending != pdFALSE )
{
#if( configUSE_PREEMPTION != 0 )
{
/* 标记在 xTaskResumeAll 中进行了任务切换 */
xAlreadyYielded = pdTRUE;
}
#endif
/* 进行一次任务切换 */
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 退出临界区 */
taskEXIT_CRITICAL();
/* 如果为pdTURE表示在此函数中进行了任务切换,返回pdFALSE表示没有进行切换 */
return xAlreadyYielded;
}
上面恢复任务调度器主要内容是处理调度器在休眠时造成有些任务(延时结束、就绪任务)延迟处理了,将这些延迟处理的任务都挂回到就绪列表,启动一次任务切换。其中有关计数器递增和处理延时列表,将延时列表任务挂回就绪列表等相关时间的代码都涉及到一个函数,就是“ xTaskIncrementTick ”,上面恢复调度器的函数中补回调度器休眠时的计数值就是使用了这个计数器递增的函数,其实在 systick 定时器中断函数也使用了这个函数,他是调度器的关键函数之一,这个计数器是整个系统的时基,systick 定时器中断处理函数的定义如下:
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//调度器已经运行
{
xPortSysTickHandler();
}
}
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
{
/* 计数值递增 */
if( xTaskIncrementTick() != pdFALSE )
{
/* 往中断控制及状态寄存器ICSR(地址:0xE000_ED04)的bit28写1挂起一次PendSV中断 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
/* 开中断 */
vPortClearBASEPRIFromISR();
}
下面看下核心函数 “ xTaskIncrementTick() ”的实现
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* 定时器计数值递增 */
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
xTickCount = xConstTickCount;
/* 计数值溢出(int类型为4字节,当计数到0xFFFFFFFF,再加1就会溢出变为0) */
if( xConstTickCount == ( TickType_t ) 0U )
{
/* 因为在延时函数里面唤醒时间一旦溢出就会插入到溢出列表,所以执行到这里说明延时列表任务已经全部清空了,
只剩溢出延时列表有任务待执行了,所以这里交换一下溢出列表和延时列表指针值,经过交换后继续处理延时
列表中的任务,并且更新下一个延时任务的解锁时间 xNextTaskUnblockTime,函数分析放在下面 */
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 计数值大于或等于下一个任务解锁时间(即延时列表的第一个列表项) */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* 延时列表没任务,复位xNextTaskUnblockTime变量为最大值 */
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
/* 延时列表有任务,获取列表中第一个任务的状态列表项值(即延时值) */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
/* 任务延时时间还没到,但是 xItemValue 保存的是下一个任务的解锁时间,
将这个变量赋值给 xNextTaskUnblockTime */
xNextTaskUnblockTime = xItemValue;
/* 退出此循环 */
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 如果任务延时时间到了将任务从延时列表中删除 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
/* 如果此任务有等待的事件(信号量、队列等)则从相应的事件列表中删除 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 添加到就绪列表中 */
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
/* 如果优先级大于等于当前优先级 */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
/* 标记一次任务调度请求 */
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
/* 处理同优先级下任务之间的调度 */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
/* 当前任务优先级就绪列表中是否有任务 */
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
/* 有任务的话标记一次任务切换请求 */
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
/* 使用时钟节拍钩子函数 */
#if ( configUSE_TICK_HOOK == 1 )
{
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
/* 时间片钩子函数,需要自己实现 */
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
}
else /* 如果调度器挂起了 */
{
/* 调度器休眠时,systick计数器的递增值,此时xTickCount不递增,使用uxPendedTicks代替 */
++uxPendedTicks;
/* 使能时间片钩子函数 */
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
/* 标记一次任务切换请求 */
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
/* 返回是否进行任务切换结果 */
return xSwitchRequired;
}
上面延时列表和溢出列表进行交换的代码如下:
#define taskSWITCH_DELAYED_LISTS() \
{ \
List_t *pxTemp; \
\
configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) ); \
\
pxTemp = pxDelayedTaskList; \
pxDelayedTaskList = pxOverflowDelayedTaskList; \
pxOverflowDelayedTaskList = pxTemp; \
xNumOfOverflows++; \
prvResetNextTaskUnblockTime(); \
}
代码比较简单,就是交换两个列表的指针。
分析完相对延时函数 vTaskDelay() 后再分析一下绝对延时函数 vTaskDelayUntil(),应用方式如下:
void led_task(void *pvParameters)
{
TickType_t PreviousWakeTime;
/* 延时时间需要转换为系统节拍数, 这里延时100ms*/
const TickType_t TimerIncrement = pdMS_TO_TICKS0(100);
/* 获取系统当前系统节拍数用于vTaskDelayUntil形参 */
PreviousWakeTime = xTaskGetTickCount();
while(1)
{
/* 任务主体 */
LED0=!LED0;
/* 进行延时 */
vTaskDelayUntil(&PreviousWakeTime, TimerIncrement); /* LED灯每隔500ms亮灭一次 */
}
}
使用 vTaskDelayUntil 可以达到周期性执行任务主体,但这个周期并不准确,因为如果有高优先级任务,还需要等到高优先级任务执行完才会执行继续执行,下面看下源码的实现:
#if ( INCLUDE_vTaskDelayUntil == 1 )
/* pxPreviousWakeTime: 上一次任务延时结束被唤醒的时间点 */
/* xTimeIncrement: 任务需要延时的时间节拍数(相对于pxPreviousWakeTime本次延时的节拍数) */
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();
{
/* 获取系统当前时钟节拍(此时任务主体执行完后的时间值xConstTickCount = *pxPreviousWakeTime + 任务主体执行时间) */
const TickType_t xConstTickCount = xTickCount;
/* 唤醒时间 = 上次任务唤醒的时间点 + 需要延时的时间 */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
/* 计数值溢出(唤醒时间 xTimeToWake 肯定也溢出,因为 xTimeToWake 理论上比 xConstTickCount 大) */
if( xConstTickCount < *pxPreviousWakeTime )
{
/* 唤醒时间 xTimeToWake 和 xConstTickCount 都溢出了 */
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
/* 需要将任务添加到延时列表 */
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else/* 计数器没溢出 */
{
/* 只有唤醒时间 xTimeToWake 溢出或者都没溢出 */
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 )
{
/* 如果xTaskResumeAll没有进行调度的话就在这里进行任务切换 */
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
总结:
整个系统的时间管理都是围绕systick定时器中断的递增计数值为时基来进行的,当任务需要延时时,会将任务阻塞进入等待状态,具体等待多久由用户自己定义,等待时间依照时基来推断,在延时阻塞任务后还会进行一次任务切换,将执行权让给其它延时结束或已经就绪的任务去执行。由于时基是个4字节的变量,系统还做了一些细节上的处理,时基已经溢出说明正常延时列表的任务基本都判断处理完了(这里需要结合代码自己去理解一下),部分有些任务刚好在延时后唤醒的计数值会溢出,导致变量变为0重新叠加上去,此时系统会将这些溢出的任务放入到延时溢出列表,在systick定时器中断处理函数中判断到计数值溢出的时候会将这个溢出列表重新赋值给延时列表,进入新一轮的判断处理。还有个细节处理是在进行延时列表操作时系统调度器会挂起,挂起后某些任务可能会在此时从其他状态回到就绪状态,但由于调度器挂起了,这些就绪的任务无法得到执行,所以会先记录下来,等到恢复调度器的时候去处理这些任务。