FreeRTOS-空闲任务prvIdleTask()函数解析
以下源码为FreeRTOS v9.0.0版本,不同版本源码可能会有所区别,但实现的逻辑差不多。
需要空闲任务的原因:处理器总是需要代码来执行——所以至少要有一个任务处于运行态。为了保证这一点,当调用vTaskStartScheduler()时,调度器会自动创建一个空闲任务。
空闲任务主要用于处理待删除任务列表和低功耗,它拥有最低优先级(优先级 0)以保证其不会妨碍具有更高优先级的应用任务进入运行态——当然,没有任何限制说是不能把应用任务创建在与空闲任务相同的优先级上;如果需要的话,你一样可以和空闲任务一起共享优先级,不过这样系统就会运行该同等优先级任务了,不会去运行空闲任务(除非该同等优先级任务被挂起)。
prvIdleTask()函数
/*
这个宏函数 portTASK_FUNCTION() 用于允许编译器特定的语言扩展。
此函数的等效原型为 void prvIdleTask( void *pvParameters );
它实际长这样
#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )
*/
static portTASK_FUNCTION(prvIdleTask, pvParameters)
{
/* 防止编译器警告的 */
(void)pvParameters;
/*rtos的空闲任务---当调度程序启动时自动创建 */
for (;;)
{
/* 查看是否有任务删除了自己---如果是,则空闲任务负责释放已删除任务的TCB和堆栈
在该函数内实现被删除任务内存资源的释放 */
prvCheckTasksWaitingTermination();
/* 抢占式调度的宏为 0 时需要强制进行任务切换,不然高优先级任务无法及时执行 */
#if (configUSE_PREEMPTION == 0)
{
/* 如果没有使用抢占式调度,那么会强制任务切换,看看是否有其他任务可用。
如果使用了抢占,就不需要这样做,因为任何可用的任务都会自动获得处理器 */
taskYIELD();
}
#endif /* configUSE_PREEMPTION */
/* 使用抢占式调度并且空闲任务应该让出时间片的宏为1的时候,空闲任务需要让出时间片给同等优先级的其他就绪任务,
宏 configIDLE_SHOULD_YIELD 用于使能空闲任务可以被同优先级的任务抢占 */
#if ((configUSE_PREEMPTION == 1) && (configIDLE_SHOULD_YIELD == 1))
{
/* 抢占式调度时,与空闲任务同等优先级的任务将会抢占空闲任务执行并且沿用空闲任务剩余的时间片
获取与空闲任务优先级一样的任务数量,当前空闲任务优先级下的就绪列表有任务,那就进行任务切换,执行这些任务 */
if (listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[tskIDLE_PRIORITY])) > (UBaseType_t)1)
{
taskYIELD();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) */
/* 空闲任务钩子 */
#if (configUSE_IDLE_HOOK == 1)
{
extern void vApplicationIdleHook(void);
/* 从空闲任务中调用用户定义的函数。这允许应用程序设计人员在不增加单独任务开销的情况下添加后台功能。
注意:vApplicationIdleHook()在任何情况下都不能调用可能阻塞的函数,因为空闲任务不能被阻塞 */
vApplicationIdleHook(); // 空闲任务钩子函数
}
#endif /* configUSE_IDLE_HOOK */
/* 这个条件编译应该使用 != 0 的不等式,而不是等于1。这是为了确保当用户定义的
低功耗模式实现要求将 configUSE_TICKLESS_IDLE 设置为 1 以外的值时调用
portSUPPRESS_TICKS_AND_SLEEP() */
/* 低功耗模式 */
#if (configUSE_TICKLESS_IDLE != 0)
{
TickType_t xExpectedIdleTime; // 预期空闲时间
/* 获取预期空闲时间,即进入低功耗模式多久 */
xExpectedIdleTime = prvGetExpectedIdleTime();
/* 只有进入低功耗时间大于等于最短时长 configEXPECTED_IDLE_TIME_BEFORE_SLEEP
系统才会进入低功耗模式节省资源,此次计算的结果可能不是最终进入低功耗的时长,因为可能受到任务调度的影响 */
if (xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP)
{
vTaskSuspendAll();
{
configASSERT(xNextTaskUnblockTime >= xTickCount);
/* 重新计算进入低功耗模式的时间,此时调度器被挂起,所以本次的计算结果
就是进入响应低功耗模式的时长 */
xExpectedIdleTime = prvGetExpectedIdleTime();
/* 只有进入低功耗时间大于等于该宏,系统才会进入低功耗模式节省资源 */
if (xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP)
{
/* 调试用的 */
traceLOW_POWER_IDLE_BEGIN();
/* 该宏函数就是让MCU进入相应低功耗模式的,传入需要进入低功耗模式的时长 */
portSUPPRESS_TICKS_AND_SLEEP(xExpectedIdleTime);
traceLOW_POWER_IDLE_END();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
(void)xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICKLESS_IDLE */
}
}
1.调用 prvCheckTasksWaitingTermination() 函数检查是否有任务删除了自己,如果有则在该函数内进行TCB控制块和任务堆栈内存的释放。
2.判断是否有与空闲任务同等优先级的任务存在,如果有并且启用了相关的宏那就进行任务切换,转之去运行该同等优先级的任务。
3.是否启动空闲任务钩子函数,空闲任务构造函数是在空闲任务执行时被调用的函数(类似与回调函数),这允许开发者在不单独增加任务创建的开销情况下执行一些其他动作,比如检测系统处理裕量。
4.是否可以进入低功耗模式,使用函数 prvGetExpectedIdleTime() 获取进入低功耗模式的预期时间,只有大于进入低功耗时间的最短时长才会允许进入低功耗模式。最后通过使用 portSUPPRESS_TICKS_AND_SLEEP 进入低功耗模式。
prvCheckTasksWaitingTermination() 函数
查看是否有任务删除了自己---如果是,则是空闲任务负责释放已删除任务的TCB和堆栈在该函数内实现被删除任务内存资源的释放。
static void prvCheckTasksWaitingTermination(void)
{
/* 这个函数从rtos的空闲任务中调用 */
/* 只有启用了任务删除的宏才会执行该函数 */
#if (INCLUDE_vTaskDelete == 1)
{
BaseType_t xListIsEmpty;
/* 等待清理的被删除任务大于0,说明有任务被删除,需要空闲任务释放内存 */
while (uxDeletedTasksWaitingCleanUp > (UBaseType_t)0U)
{
/* 挂起调度器,挂起调度器会阻止上下文切换,如果调度器被挂起时,中断请求切换上下文,
那么请求将会被挂起。而且只有在调度器恢复(取消挂起)时才会执行 */
vTaskSuspendAll();
{
/* 等待删除列表是否为空,不为空说明需要处理 */
xListIsEmpty = listLIST_IS_EMPTY(&xTasksWaitingTermination);
}
(void)xTaskResumeAll();
/* 待删除任务列表中的任务数量不为 0 */
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();
/* 调用删除任务释放堆栈和TCB内存 */
prvDeleteTCB(pxTCB);
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
#endif /* INCLUDE_vTaskDelete */
}
该函数很简单,就是执行被删除任务资源的释放,并更新等待终止列表中已被删除任务的数量。内部通过调用 prvDeleteTCB() 函数进行已删除任务的资源释放。
prvGetExpectedIdleTime() 函数
/* 获取预期空闲时间,即进入低功耗模式多久 */
static TickType_t prvGetExpectedIdleTime(void)
{
TickType_t xReturn;
UBaseType_t uxHigherPriorityReadyTasks = pdFALSE;
/* uxHigherPriorityReadyTasks 用于处理 configUSE_PREEMPTION(1使用抢占式内核,0使用协程) 为0的情况,
因为可能有高于空闲优先级任务的任务处于 Ready 状态,即使空闲任务正在运行 */
#if (configUSE_PORT_OPTIMISED_TASK_SELECTION == 0)
{
if (uxTopReadyPriority > tskIDLE_PRIORITY)
{
uxHigherPriorityReadyTasks = pdTRUE;
}
}
#else
{
/* 最低有效位 */
const UBaseType_t uxLeastSignificantBit = (UBaseType_t)0x01;
/* 是否有大于优先级0的任务就绪了,0x01指bit0被置1了,bit0表示优先级0的任务
只要后面不管哪个bit被置1都是大于0x01的,即有更高优先级任务就绪了,所以高优先级
任务标志位设置为 pdTRUE */
if (uxTopReadyPriority > uxLeastSignificantBit)
{
uxHigherPriorityReadyTasks = pdTRUE;
}
}
#endif
/* 如果当前执行的任务优先级大于空闲任务优先级是无法进入低功耗的,低功耗模式是在
空闲任务执行期间进入的 */
if (pxCurrentTCB->uxPriority > tskIDLE_PRIORITY)
{
xReturn = 0;
}
/* 与空闲任务优先级 0 一样的任务数量大于1,也无法进入低功耗,需要转去执行优先级和空闲任务
一样为0的这个任务 */
else if (listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[tskIDLE_PRIORITY])) > 1)
{
xReturn = 0;
}
/* 存在优先级高于空闲任务的就绪任务 */
else if (uxHigherPriorityReadyTasks != pdFALSE)
{
xReturn = 0;
}
else
{
/* 下一个任务解除阻塞时间 - 当前滴答计数器值 = 进入低功耗的时间
在排除了前面这么多种情况之后,就只剩空闲任务在运行,可以进入低功耗
模式了,使用下个任务解除阻塞时间(如50) - 当前计数值(如20) = 30
即可以进入低功耗模式的时间就是30 */
xReturn = xNextTaskUnblockTime - xTickCount;
}
return xReturn;
}
该函数主要用于获取即将要进入的低功耗时间,如果当前存在优先级大于空闲任务优先级则无法进入低功耗模式。故将 uxHigherPriorityReadyTasks 标志位置1,最终通过返回 xReturn 来确定进入低功耗模式的时长,如果返回的是0则不会进入低功耗模式。