FreeRTOS-空闲任务及钩子函数
- FreeRTOS中空闲任务是开启任务调度器自动创建的一个任务,这样可以保证系统中有任务可以运行,这个任务优先级是最低的,如果有其他任务处于就绪态,那么空闲任务就会主动让出CPU使用权给其他任务。那么空闲任务只是为了保证系统中有任务可以运行吗?其实不是,空闲任务其实有两个重要功能,其一,如果某任务调用了vTaskDelete()将自身删除了,那么这个任务的任务控制块和任务堆栈不会同时释放,而是在空闲任务中释放掉,所以空闲任务可以理解为清理垃圾的。其二,由于不是所有的RTOS都有Tickless模式,所以在空闲任务期间可以用来进入低功耗模式,这种方法使用与几乎所以的RTOS系统。下面来看一下空闲任务创建过程及相关函数。
空闲任务创建及函数
vTaskStartScheduler()
- 空闲任务是在开启任务调度器中创建的,函数定义如下。
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
"IDLE",
ulIdleTaskStackSize,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer );
xReturn = xTaskCreate( prvIdleTask,
"IDLE", configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
//其余部分省略
...
...
( void ) xIdleTaskHandle;
}
- 从上面空闲任务创建可以看出,空闲任务函数名为prvIdleTask,接下来我们看一下该函数中进行了什么操作。
prvIdleTask()
- 实际上,在寻找该函数的时候是找不到的,因为该函数是通过宏定义来实现的,其定义如下。
#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )
- 真正调用的函数是portTASK_FUNCTION(),函数定义如下。
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
/* Stop warnings. */
( void ) pvParameters;
for( ;; )
{
prvCheckTasksWaitingTermination();----1
#if ( configUSE_PREEMPTION == 0 )----2
{
taskYIELD();
}
#endif /* configUSE_PREEMPTION */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )----3
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )----4
{
taskYIELD();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) */
#if ( configUSE_IDLE_HOOK == 1 )----5
{
extern void vApplicationIdleHook( void );
vApplicationIdleHook();
}
#endif /* configUSE_IDLE_HOOK */
#if ( configUSE_TICKLESS_IDLE != 0 )
{
TickType_t xExpectedIdleTime;
xExpectedIdleTime = prvGetExpectedIdleTime();----6
if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )----7
{
vTaskSuspendAll();
{
configASSERT( xNextTaskUnblockTime >= xTickCount );
xExpectedIdleTime = prvGetExpectedIdleTime();----8
if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )----9
{
traceLOW_POWER_IDLE_BEGIN();
portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime );
traceLOW_POWER_IDLE_END();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
( void ) xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICKLESS_IDLE */
}
}
- 检查是否有已经删除的函数没有释放内存,该函数会在下面分析
- 如果没有使用抢占式内核,在这里进行一次任务调度
- 如果使用了抢占式内核,并且需要进行任务调度
- 判断是否有准备好的任务,如果有则进行任务调度
- 如果使用了钩子函数,则调用钩子函数
- 如果开启了低功耗模式,则计算下一次唤醒时间
- 判断唤醒时间是否有效,即是否大于2个时钟节拍
- 再次获取唤醒时间
- 再次判断,如果仍旧成立,则调用函数portSUPPRESS_TICKS_AND_SLEEP()进入低功耗模式
- 从这里我们就可以知道,内存清除、钩子函数调用、低功耗模式的进入都是在空闲任务中进行的。下面来分析prvCheckTasksWaitingTermination()函数
prvCheckTasksWaitingTermination()
- 函数定义如下。
static void prvCheckTasksWaitingTermination( void )
{
#if ( INCLUDE_vTaskDelete == 1 )
{
BaseType_t xListIsEmpty;
while( uxDeletedTasksWaitingCleanUp > ( UBaseType_t ) 0U )---1
{
vTaskSuspendAll();
{
xListIsEmpty = listLIST_IS_EMPTY( &xTasksWaitingTermination );---2
}
( void ) xTaskResumeAll();
if( xListIsEmpty == pdFALSE )---3
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xTasksWaitingTermination ) );---4
( void ) uxListRemove( &( pxTCB->xStateListItem ) );---5
--uxCurrentNumberOfTasks;---6
--uxDeletedTasksWaitingCleanUp;---7
}
taskEXIT_CRITICAL();
prvDeleteTCB( pxTCB );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
#endif /* INCLUDE_vTaskDelete */
}
- 判断是否还有被删除了但没有释放内存的任务
- 判断等待释放内存的任务列表是否为空
- 如果等待释放内存的任务列表不为空
- 获取列表头指针
- 将任务从列表中移除
- 系统中任务数量-1
- 等待释放内存的任务数量-1
钩子函数
- FreeRTOS中钩子函数如下表所示
宏 | 描述 |
---|---|
configUSE_IDLE_HOOK | 空闲任务钩子函数,空闲任务执行时会调用 |
configSUE_TICK_HOOK | 时间片钩子函数,xTaskIncrementTick()会调用该函数 |
configUSE_MALLOC_FAILED_HOOK | 内存申请失败钩子函数,使用pvPortMalloc()申请内存失败的时候会调用该函数 |
configUSE_DAEMON_TASK_STARTUP_HOOK | 定时器服务任务钩子函数 |
- 这里我们仅就空闲任务钩子函数做一个用法示例,其他函数用法类似。使用空闲任务钩子函数的时候,当调用该函数的时候使系统进入低功耗模式,这种低功耗模式相对于tickless模式效果要差些,但是几乎适用于所有RTOS系统。其运行对比如下图所示。
- 从上面可以看出,调用空闲任务钩子函数实现的低功耗模式,在每个系统节拍处必然不在低功耗模式状态,这个即是没有其他任务处于就绪态,系统也会被反复唤醒。而FreeRTOS提供的tickless模式则要好很多。
- 下面是通过空闲任务进入低功耗模式配置的源码
#define configUSE_IDLE_HOOK 1
//进入低功耗模式前的工作
void PreSleepProcessing(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,DISABLE);
}
//退出低功耗模式后的工作
void PostSleepProcessing(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG,ENABLE);
}
void vApplicationIdleHook( void )
{
__disable_irq();
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
PreSleepProcessing();
__dsb( portSY_FULL_READ_WRITE );
__wfi();
__isb( portSY_FULL_READ_WRITE );
PostSleepProcessing();
__enable_irq();
}
-
下面仅就vApplicationIdleHook()中的代码做一个简要分析。首先调用__disable_irq()函数关闭系统中断请求,然后调用PreSleepProcessing()做进入睡眠模式前的一些低功耗处理,这里仅关闭了GPIO的时钟,接着调用__wfi()进入WFI低功耗模式,当调用PostSleepProcessing()的时候说明已经退出低功耗模式了,然后恢复外设时钟,调用__enable_irq();开启中断请求
-
至此就将FreeRTOS空闲任务及钩子函数讲述完了