本篇文章主要对 FreeRTOS 中任务管理相关的函数进行了详解,最最重要的是解析了 绝对延时 vTaskDelayUntil()” 为什么能够确保任务执行周期准确???
一部分代码和图片参考野火 FreeRTOS 教程。
任务的挂起主要涉及调度器方面,要让调度器不调度这个被挂起的函数:
/*-----------------------------------------------------------*/
#if (INCLUDE_vTaskSuspend == 1)
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
/* 如果在此处传递null,则它正在被挂起的正在运行的任务。*/
pxTCB = prvGetTCBFromHandle(xTaskToSuspend);
traceTASK_SUSPEND(pxTCB);
/* 从就绪/阻塞列表中删除任务并放入挂起列表中。*/
if (uxListRemove(&(pxTCB->xStateListItem)) == (UBaseType_t)0) {
taskRESET_READY_PRIORITY(pxTCB->uxPriority);
} else {
mtCOVERAGE_TEST_MARKER();
}
/* 如果任务在等待事件,也从等待事件列表中移除 */
if (listLIST_ITEM_CONTAINER(&(pxTCB->xEventListItem)) != NULL) {
(void)uxListRemove(&(pxTCB->xEventListItem));
} else {
mtCOVERAGE_TEST_MARKER();
}
/* 将任务状态添加到挂起列表中 */
vListInsertEnd(&xSuspendedTaskList, &(pxTCB->xStateListItem));
}
taskEXIT_CRITICAL();
if (xSchedulerRunning != pdFALSE) {
/* 重置下一个任务的解除阻塞时间。
重新计算一下还要多长时间执行下一个任务。
如果下个任务的解锁,刚好是被挂起的那个任务,
那么变量NextTaskUnblockTime 就不对了,
所以要重新从延时列表中获取一下。*/
taskENTER_CRITICAL();
{
prvResetNextTaskUnblockTime();
}
taskEXIT_CRITICAL();
} else {
mtCOVERAGE_TEST_MARKER();
}
if (pxTCB == pxCurrentTCB) {
if (xSchedulerRunning != pdFALSE) {
/* 当前的任务已经被挂起。*/
configASSERT(uxSchedulerSuspended == 0);
/* 调度器在运行时,如果这个挂起的任务是当前任务,立即切换任务。*/
portYIELD_WITHIN_API();
} else {
/* 调度器未运行(xSchedulerRunning == pdFALSE ),
但 pxCurrentTCB 指向的任务刚刚被暂停,
所以必须调整 pxCurrentTCB 以指向其他任务。
首先调用函数listCURRENT_LIST_LENGTH()
判断一下系统中所有的任务是不是都被挂起了,
也就是查看列表xSuspendedTaskList
的长度是不是等于uxCurrentNumberOfTasks,
事实上并不会发生这种情况,
因为空闲任务是不允许被挂起和阻塞的,
必须保证系统中无论如何都有一个任务可以运行*/
if (listCURRENT_LIST_LENGTH(&xSuspendedTaskList) == uxCurrentNumberOfTasks) {
/* 没有其他任务准备就绪,因此将pxCurrentTCB设置回NULL,
以便在创建下一个任务时pxCurrentTCB将被设置为指向它,
实际上并不会执行到这里 */
pxCurrentTCB = NULL;
} else {
/* 有其他任务,则切换到其他任务 */
vTaskSwitchContext();
}
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskSuspend */
/*-----------------------------------------------------------*/
这个函数的功能是挂起一个任务。具体步骤如下:
恢复被挂起的任务,使其从挂起的状态立刻回到就绪的状态。
#if (INCLUDE_vTaskSuspend == 1)
void vTaskResume(TaskHandle_t xTaskToResume)
{
/* 根据xTaskToResume获取对应的任务控制块 */
TCB_t *const pxTCB = (TCB_t *)xTaskToResume;
/* 检查要恢复的任务是否被挂起,
如果没被挂起,恢复调用任务没有意义 */
configASSERT(xTaskToResume);
/* 该参数不能为 NULL,
同时也无法恢复当前正在执行的任务,
因为当前正在运行的任务不需要恢复,
只能恢复处于挂起态的任务 */
if ((pxTCB != NULL) && (pxTCB != pxCurrentTCB)) {
/* 进入临界区 */
taskENTER_CRITICAL();
{
if (prvTaskIsTaskSuspended(pxTCB) != pdFALSE) {
traceTASK_RESUME(pxTCB);
/* 由于我们处于临界区,
即使任务被挂起,我们也可以访问任务的状态列表。
将要恢复的任务从挂起列表中删除 */
(void)uxListRemove(&(pxTCB->xStateListItem));
/* 将要恢复的任务添加到就绪列表中去 */
prvAddTaskToReadyList(pxTCB);
/* 如果刚刚恢复的任务优先级比当前任务优先级更高
则需要进行任务的切换 */
if (pxTCB->uxPriority >= pxCurrentTCB->uxPriority) {
/* 因为恢复的任务在当前情况下的优先级最高
调用taskYIELD_IF_USING_PREEMPTION()进行一次任务切换*/
taskYIELD_IF_USING_PREEMPTION();
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL(); /* 退出临界区 */
} else {
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskSuspend */
/*-----------------------------------------------------------*/
这个函数的功能是恢复一个挂起的任务。具体步骤如下:
函数名的意思是挂起所有任务,实际上就是挂起任务调度器,使所有任务都不参与调度。
注意,调用多少次挂起任务调度器,相应的恢复的时候就要调用恢复任务调度器函数对应的次数。
用 uxSchedulerSuspended 记录调用的次数。
void vTaskSuspendAll( void )
{
++uxSchedulerSuspended;
}
/*----------------------------------------------------------*/
BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
/* If uxSchedulerSuspended is zero then this function does not match a
previous call to vTaskSuspendAll(). */
configASSERT( uxSchedulerSuspended );
/* It is possible that an ISR caused a task to be removed from an event
list while the scheduler was suspended. If this was the case then the
removed task will have been added to the xPendingReadyList. Once the
scheduler has been resumed it is safe to move all the pending ready
tasks from this list into their appropriate ready list. */
taskENTER_CRITICAL();
{
--uxSchedulerSuspended;
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
{
/* Move any readied tasks from the pending list into the
appropriate ready list. */
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
/* If the moved task has a priority higher than the current
task then a yield must be performed. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( pxTCB != NULL )
{
/* A task was unblocked while the scheduler was suspended,
which may have prevented the next unblock time from being
re-calculated, in which case re-calculate it now. Mainly
important for low power tickless implementations, where
this can prevent an unnecessary exit from low power
state. */
prvResetNextTaskUnblockTime();
}
/* If any ticks occurred while the scheduler was suspended then
they should be processed now. This ensures the tick count does
not slip, and that any delayed tasks are resumed at the correct
time. */
{
UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. */
if( uxPendedCounts > ( UBaseType_t ) 0U )
{
do
{
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 )
{
xAlreadyYielded = pdTRUE;
}
#endif
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
这个函数的功能是恢复所有被挂起的任务,并在需要时进行任务切换。具体步骤如下:
关键是要区分是否是自删除任务和非自删除任务,也就是是否在一个任务中执行删除自己的操作。
这个函数的设计思路和关键点可以简洁地总结如下:
#if ( INCLUDE_vTaskDelete == 1 )
void vTaskDelete( TaskHandle_t xTaskToDelete )
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
/* If null is passed in here then it is the calling task that is
being deleted. */
pxTCB = prvGetTCBFromHandle( xTaskToDelete );
/* Remove task from the ready list. */
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Is the task waiting on an event also? */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Increment the uxTaskNumber also so kernel aware debuggers can
detect that the task lists need re-generating. This is done before
portPRE_TASK_DELETE_HOOK() as in the Windows port that macro will
not return. */
uxTaskNumber++;
if( pxTCB == pxCurrentTCB )
{
/* A task is deleting itself. This cannot complete within the
task itself, as a context switch to another task is required.
Place the task in the termination list. The idle task will
check the termination list and free up any memory allocated by
the scheduler for the TCB and stack of the deleted task. */
vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
/* Increment the ucTasksDeleted variable so the idle task knows
there is a task that has been deleted and that it should therefore
check the xTasksWaitingTermination list. */
++uxDeletedTasksWaitingCleanUp;
/* The pre-delete hook is primarily for the Windows simulator,
in which Windows specific clean up operations are performed,
after which it is not possible to yield away from this task -
hence xYieldPending is used to latch that a context switch is
required. */
portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
}
else
{
--uxCurrentNumberOfTasks;
prvDeleteTCB( pxTCB );
/* Reset the next expected unblock time in case it referred to
the task that has just been deleted. */
prvResetNextTaskUnblockTime();
}
traceTASK_DELETE( pxTCB );
}
taskEXIT_CRITICAL();
/* Force a reschedule if it is the currently running task that has just
been deleted. */
if( xSchedulerRunning != pdFALSE )
{
if( pxTCB == pxCurrentTCB )
{
configASSERT( uxSchedulerSuspended == 0 );
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
#endif /* INCLUDE_vTaskDelete */
/*-----------------------------------------------------------*/
这个函数的功能是删除一个任务,并在需要时进行任务切换。具体步骤如下:
当任务自删除的时候,只是对任务进行删除标记,直到执行空闲任务的时候才对其进行删除和内存清理。
这个函数的设计思路如下:
static void prvCheckTasksWaitingTermination( void )
{
/** THIS FUNCTION IS CALLED FROM THE RTOS IDLE TASK **/
#if ( INCLUDE_vTaskDelete == 1 )
{
BaseType_t xListIsEmpty;
/* ucTasksDeleted is used to prevent vTaskSuspendAll() being called
too often in the idle task. */
while( uxDeletedTasksWaitingCleanUp > ( UBaseType_t ) 0U )
{
vTaskSuspendAll();
{
xListIsEmpty = listLIST_IS_EMPTY( &xTasksWaitingTermination );
}
( void ) xTaskResumeAll();
if( xListIsEmpty == pdFALSE )
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xTasksWaitingTermination ) );
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
--uxCurrentNumberOfTasks;
--uxDeletedTasksWaitingCleanUp;
}
taskEXIT_CRITICAL();
prvDeleteTCB( pxTCB );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
#endif /* INCLUDE_vTaskDelete */
这个函数的功能是检查是否有任务等待终止,并在需要时删除这些任务。函数会在RTOS的空闲任务中被调用。具体步骤如下:
这个函数之前我们已经详细介绍过:
【学习日记】【FreeRTOS】空闲任务与阻塞延时
我们想要绿色以 150ms 的周期执行:
代码主要分为几部分:
#if ( INCLUDE_vTaskDelayUntil == 1 )
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();
{
/* Minor optimisation. The tick count cannot change in this
block. */
const TickType_t xConstTickCount = xTickCount;
/* Generate the tick time at which the task wants to wake. */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
if( xConstTickCount < *pxPreviousWakeTime )
{
/* The tick count has overflowed since this function was
lasted called. In this case the only time we should ever
actually delay is if the wake time has also overflowed,
and the wake time is greater than the tick time. When this
is the case it is as if neither time had overflowed. */
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
/* The tick time has not overflowed. In this case we will
delay if either the wake time has overflowed, and/or the
tick time is less than the wake time. */
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* Update the wake time ready for the next call. */
*pxPreviousWakeTime = xTimeToWake;
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
/* prvAddCurrentTaskToDelayedList() needs the block time, not
the time to wake, so subtract the current tick count. */
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
xAlreadyYielded = xTaskResumeAll();
/* 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_vTaskDelayUntil */
/*-----------------------------------------------------------*/
因为绝对延时能根据任务主体的耗时来调整具体的延时时间:任务耗时长(比如被其他任务抢占耽误了一会),延时就短;任务耗时短,延时就长。最终达到执行周期相对准确的目的。
具体的图解:
可以看到,当把绝对延时函数放置与 while 中的任务主体代码第一行时,只有第一个任务之前的延时过长,后面就以准确的周期执行任务了。
如果想使第一个任务之前的延时也准确,只需要把绝对延时函数放在任务主体的最后一行。
void vTaskA(void* pvParameters)
{
/* 用于保存上次时间。调用后系统自动更新 */
static portTickType PreviousWakeTime;
/* 设置延时时间,将时间转为节拍数 */
const portTickType TimeIncrement = pdMS_TO_TICKS(1000);
/* 获取当前系统时间 */
PreviousWakeTime = xTaskGetTickCount();
while (1)
{
/* 调用绝对延时函数,任务时间间隔为 1000 个 tick */
vTaskDelayUntil(&PreviousWakeTime, TimeIncrement);
// ...
// 这里为任务主体代码
// ...
}
}
如果您觉得本文写得不错,可以点个赞激励一下作者!
如果您发现本文的问题,欢迎在评论区或者私信共同探讨!
共勉!