void vTaskDelay( const TickType_t xTicksToDelay )
函数是FreeRTOS中非常重要的函数,用来延时xTicksToDelay
个时钟节拍。
vTaskDelay
函数源码
#if ( INCLUDE_vTaskDelay == 1 )
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
/* A delay time of zero just forces a reschedule. */
if( xTicksToDelay > ( TickType_t ) 0U )//延时的时间是否大于0
{
configASSERT( uxSchedulerSuspended == 0 );//断言 如果任务调度器是挂起的调用本函数会报错。
vTaskSuspendAll();//挂起任务调度器
{
traceTASK_DELAY();
/* A task that is removed from the event list while the
scheduler is suspended will not get placed in the ready
list or removed from the blocked list until the scheduler
is resumed.
This task cannot be in an event list as it is the currently
executing task. */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );//将任务添加到延时列表
}
xAlreadyYielded = xTaskResumeAll();//恢复任务调度器
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Force a reschedule if xTaskResumeAll has not already done so, we may
have put ourselves to sleep. */
if( xAlreadyYielded == pdFALSE )//如果没有做任务切换就做一次任务切换
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskDelay */
函数的逻辑很简单,注释写的很清楚了。
1、如果延时时间大于0,那么将当前任务添加到延时列表。
2、根据需要做一次任务切换
我们重点看一下prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
的内部实现
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely )
{
TickType_t xTimeToWake;
const TickType_t xConstTickCount = xTickCount;
#if( INCLUDE_xTaskAbortDelay == 1 )
{
/* About to enter a delayed list, so ensure the ucDelayAborted flag is
reset to pdFALSE so it can be detected as having been set to pdTRUE
when the task leaves the Blocked state. */
pxCurrentTCB->ucDelayAborted = pdFALSE;
}
#endif
/* Remove the task from the ready list before adding it to the blocked list
as the same list item is used for both lists. */
//将当前任务的状态列表项从就绪列表中移除,
//为什么这么肯定就是就绪列表呢? 因为调用vTaskDelay函数一定是当前正在运行的任务(也就是任务自身),
//不可能说A任务调用vTaskDelay让B任务延时,这是不存在的。
//既然是当前正在运行的任务调用vTaskDelay,那么说明他一定在就绪列表中。
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* The current task must be in a ready list, so there is no need to
check, and the port reset macro can be called directly. */
//当前优先级下没有就绪任务就复位优先级
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
#if ( INCLUDE_vTaskSuspend == 1 )
{
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
{
/* Add the task to the suspended task list instead of a delayed task
list to ensure it is not woken by a timing event. It will block
indefinitely. */
//如果延时portMAX_DELAY个时钟节拍就添加到挂起列表中,也就是挂起任务,
//但是调用vTaskDelay函数即使传递参数portMAX_DELAY也不能挂起任务 因为xCanBlockIndefinitely固定传递的是pdFALSE
vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* Calculate the time at which the task should be woken if the event
does not occur. This may overflow but this doesn't matter, the
kernel will manage it correctly. */
xTimeToWake = xConstTickCount + xTicksToWait;//计算出唤醒时间点
/* The list item will be inserted in wake time order. */
//将唤醒时间点的值写入到当前任务控制块的状态列表项中,后续会把状态列表项挂载到延时列表
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
if( xTimeToWake < xConstTickCount )//如果下一个时间点小于当前时间点
{
/* Wake time has overflowed. Place this item in the overflow
list. */
//这种说明"唤醒点"会在xTickCount溢出之后到来 溢出就挂载到pxOverflowDelayedTaskList指针指向的延时列表上
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* The wake time has not overflowed, so the current block list
is used. */
//这种情况说名"唤醒点"会在xTickCount溢出之前到来
//"唤醒点"在xTickCount溢出之前到来就挂载到pxDelayedTaskList指针指向的延时列表上
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* If the task entering the blocked state was placed at the
head of the list of blocked tasks then xNextTaskUnblockTime
needs to be updated too. */
//xNextTaskUnblockTime保存了最近一个任务的解锁时间
//如果新添加到阻塞列表中的"唤醒点"小于最近一个任务解锁时刻
//那么就更新xNextTaskUnblockTime
if( xTimeToWake < xNextTaskUnblockTime )
{
//更新xNextTaskUnblockTime
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
#else /* INCLUDE_vTaskSuspend */
{
/* Calculate the time at which the task should be woken if the event
does not occur. This may overflow but this doesn't matter, the kernel
will manage it correctly. */
xTimeToWake = xConstTickCount + xTicksToWait;
/* The list item will be inserted in wake time order. */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
if( xTimeToWake < xConstTickCount )
{
/* Wake time has overflowed. Place this item in the overflow list. */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* The wake time has not overflowed, so the current block list is used. */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* If the task entering the blocked state was placed at the head of the
list of blocked tasks then xNextTaskUnblockTime needs to be updated
too. */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* Avoid compiler warning when INCLUDE_vTaskSuspend is not 1. */
( void ) xCanBlockIndefinitely;
}
#endif /* INCLUDE_vTaskSuspend */
}
函数的核心思想就是将当前任务从就绪列表中移除,添加到延时列表中,这其中有一个头疼的问题就是系统时钟节拍溢出。解决方案就是溢出就挂载到另一条延时列表上,两条列表交替使用,完美解决了此问题。
总结:
阻塞的任务在xTickCount
溢出之前唤醒就挂载到pxDelayedTaskList
延时列表上。
阻塞的任务在xTickCount
溢出之后唤醒就挂载到pxOverflowDelayedTaskList
延时列表上。
那么问题来了,pxDelayedTaskList
和pxOverflowDelayedTaskList
又是怎么维护的呢?
pxDelayedTaskList
和pxOverflowDelayedTaskList
是在xTaskIncrementTick()
函数中维护的,xTaskIncrementTick()
函数会在滴答定时中断服务函数中调用
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
* executes all interrupts must be unmasked. There is therefore no need to
* save and then restore the interrupt mask value as its value is already
* known - therefore the slightly faster vPortRaiseBASEPRI() function is used
* in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
vPortRaiseBASEPRI();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
* the PendSV interrupt. Pend the PendSV interrupt. */
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 + 1;
xTickCount = xConstTickCount; //实现了时钟节拍xTickCount自增 每次加1,
//xTickCount 每次加1,只加1,不是跳跃式的加,所以xTickCount 加到最大值以后再加就是0,也就是溢出了。
if( xConstTickCount == ( TickType_t ) 0U )//xConstTickCount为0说明时钟节拍溢出
{
taskSWITCH_DELAYED_LISTS();//交换延时列表的指针指向
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//当前时钟节拍大于等于下一个任务解锁时间点 说明有任务需要解锁了
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )//用for循环把所有到解锁时间的任务都解锁
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* The delayed list is empty. Set xNextTaskUnblockTime
to the maximum possible value so it is extremely
unlikely that the
if( xTickCount >= xNextTaskUnblockTime ) test will pass
next time through. */
//如果延时列表是空的 xNextTaskUnblockTime赋值后则直接跳出
//这种情况也是存在的 比如说系统中只有一个空闲任务的时候
//或者说用户目前没有创建任务直接调用开启任务调度器函数
xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
break;
}
else
{
/* The delayed list is not empty, get the value of the
item at the head of the delayed list. This is the time
at which the task at the head of the delayed list must
be removed from the Blocked state. */
//如果列表不为空 获取延时列表中第一个列表项的任务控制块 只要找到任务控制块一切都好办了
//为什么是第一个列表项 因为列表项是从小到大排序的,最先解除阻塞的任务肯定排在列表前边。
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );//获取任务状态列表项的值也就是解除阻塞的时刻点
if( xConstTickCount < xItemValue )//如果当前时刻 小于 延时列表中最近的一个解锁任务的解锁时间 说明解锁时刻还没有到来
{
/* It is not time to unblock this item yet, but the
item value is the time at which the task at the head
of the blocked list must be removed from the Blocked
state - so record the item value in
xNextTaskUnblockTime. */
xNextTaskUnblockTime = xItemValue;
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* It is time to remove the item from the Blocked state. */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );//将TCB的状态列表项从阻塞列表中移除
/* Is the task waiting on an event also? If so remove
it from the event list. */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )//如果任务正在等待事件 也不等了 进入就绪态
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Place the unblocked task into the appropriate ready
list. */
prvAddTaskToReadyList( pxTCB );//添加到就绪列表
/* A task being unblocked cannot cause an immediate
context switch if preemption is turned off. */
#if ( configUSE_PREEMPTION == 1 )
{
/* Preemption is on, but a context switch should
only be performed if the unblocked task has a
priority that is equal to or higher than the
currently executing task. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )//如果解锁的任务优先级高于当前任务的优先级
{
xSwitchRequired = pdTRUE; //标记需要做任务切换 因为RTOS 时刻都在运行就绪态中优先级最高的任务
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
/* Tasks of equal priority to the currently running task will share
processing time (time slice) if preemption is on, and the application
writer has not explicitly turned time slicing off. */
#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 /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
#if ( configUSE_TICK_HOOK == 1 )
{
/* Guard against the tick hook being called when the pended tick
count is being unwound (when the scheduler is being unlocked). */
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
}
else//任务调度器挂起期间
{
++uxPendedTicks;//任务调度器挂起期间 uxPendedTicks变量++ 记录时钟节拍个数
/* The tick hook gets called at regular intervals, even if the
scheduler is locked. */
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
return xSwitchRequired;//返回是否进行任务切换的标志
}
这里重点介绍一下时钟节拍溢出以后交换两条延时列表指针的原理,
taskSWITCH_DELAYED_LISTS
是一个宏,如下:
#define taskSWITCH_DELAYED_LISTS() \
{ \
List_t *pxTemp; \
\
/* The delayed tasks list should be empty when the lists are switched. */ \
configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) ); \
\
pxTemp = pxDelayedTaskList; \
pxDelayedTaskList = pxOverflowDelayedTaskList; \
pxOverflowDelayedTaskList = pxTemp; \
xNumOfOverflows++; \
prvResetNextTaskUnblockTime(); \
}
PRIVILEGED_DATA static List_t xDelayedTaskList1; //延时列表1
PRIVILEGED_DATA static List_t xDelayedTaskList2; //延时列表2
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList; //指向延时列表的指针
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList; //指向延时溢出列表的指针
系统创建任务时判断,如果创建第一个任务会初始化系统中所有的列表。其中两条延时列表和延时列表指针初始化如下:
pxDelayedTaskList = &xDelayedTaskList1;
pxOverflowDelayedTaskList = &xDelayedTaskList2;
两条列表在和列表指针在系统中运行关系如下图,
两个指针交替指向两条延时列表。
pxDelayedTaskList
指向当前延时的列表,pxOverflowDelayedTaskList
指向延时溢出列表。如果一个任务延时的时间会在xTickCount
溢出之前到来,那么该任务会挂载到pxDelayedTaskList指向的延时列表,否则挂载到pxOverflowDelayedTaskList
指向的列表。
xTickCount
溢出以后指针指向会交换,交换以后原来的溢出列表变成当前延时列表,当前延时列表变成了溢出列表。下次溢出再交换指向,就这样周而复始的运行。
不知道我这里我是否表达清楚。这是FreeRTOS中写的很巧妙的一段代码。