FreeRTOS-空闲任务、低功耗源码分析

FreeRTOS在启动任务调度时会自动创建一个空闲任务,空闲任务主要在系统没有其它任务或任务都处于挂起状态时执行,它被系统设置为最低优先级,不会去抢占其它高优先级的任务,从而既能保证系统总有至少一个任务可以运行又不干扰到其它任务。空闲任务里面可以执行一些辅助操作,比如任务删除自身时由于无法立马释放掉自己的内存,这时可以做个标记,在空闲任务里面去删除。还有个非常重要的功能就是实现低功耗,进入空闲任务一般意味着系统当前没事做了,此时如果关闭某些外设或时钟就能达到降低功率的效果。
从启动任务调度器的代码中可以看到系统创建了一个空闲任务:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;

/* 静态创建空闲任务 */
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
	.....
	/* 静态内存方式创建空闲任务 */
	xIdleTaskHandle = xTaskCreateStatic(	prvIdleTask,
											configIDLE_TASK_NAME,
											ulIdleTaskStackSize,
											( void * ) NULL, 
											portPRIVILEGE_BIT,
											pxIdleTaskStackBuffer,
											pxIdleTaskTCBBuffer ); 

	.....
}
#else
{
	//动态方式创建空闲任务
	xReturn = xTaskCreate(	prvIdleTask,
							configIDLE_TASK_NAME,
							configMINIMAL_STACK_SIZE,
							( void * ) NULL,
							portPRIVILEGE_BIT, 
							&xIdleTaskHandle ); 
}
#endif

其中空闲任务的函数指针 prvIdleTask 是一个宏定义:

#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )
static portTASK_FUNCTION( prvIdleTask, pvParameters )

可以看出 portTASK_FUNCTION 的形参为 prvIdleTask ,所以最终 portTASK_FUNCTION 就是空闲任务 prvIdleTask,这里分析一下 portTASK_FUNCTION 这个函数的源码:

static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
	( void ) pvParameters;//防止报错

	portTASK_CALLS_SECURE_FUNCTIONS();

	for( ;; )
	{
		/* 进入空闲任务后会不停的查看是否有任务需要删除,在这个函数内进行删除 */
		prvCheckTasksWaitingTermination();

		#if ( configUSE_PREEMPTION == 0 )
		{
			/* 如果没有使用抢占,会不停的强制一次任务切换查看是否有其他任务需要执行
			   使用了抢占的话就不需要这一步了,因为有高优先级任务就绪后会自动抢占 */
			taskYIELD();
		}
		#endif

		/* 如果使能了抢占并使能时间片调度的话执行这个分支 */
		#if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )
		{
			/* 查看除了空闲任务以外同等优先级下有没有任务等待执行,有的话将时间片剩余的时间让给同优先级的就绪任务 */
			if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )
			{
				taskYIELD();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif 

		/* 定义了这个宏的话当进入空闲任务时就会执行下面这个钩子函数 */
		#if ( configUSE_IDLE_HOOK == 1 )
		{
			/* 这个钩子函数由用户自己定义使用,可做一些低功耗处理,但是一般建议使用 tickless 来做低功耗操作 */
			extern void vApplicationIdleHook( void );
			
			vApplicationIdleHook();
		}
		#endif 

		/* 当使能这个宏时就使能低功耗 tickless 模式 */
		#if ( configUSE_TICKLESS_IDLE != 0 )
		{
		TickType_t xExpectedIdleTime;

			/* 获取下一个任务的解锁时间(即进入低功耗模式的时长) */
			xExpectedIdleTime = prvGetExpectedIdleTime();

			/* 下一个任务的解锁时间必须大于用户定义的最小空闲休眠时间阈值,否则不休眠 */
			if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
			{
				/* 挂起任务调度器 */
				vTaskSuspendAll();
				{
					configASSERT( xNextTaskUnblockTime >= xTickCount );
					/* 重新采集一次时间值,这次的时间值可以使用 */
					xExpectedIdleTime = prvGetExpectedIdleTime();

					configPRE_SUPPRESS_TICKS_AND_SLEEP_PROCESSING( xExpectedIdleTime );

					/* 下一个任务的解锁时间必须大于用户定义的最小空闲休眠时间阈值,否则不休眠 */
					if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
					{
						traceLOW_POWER_IDLE_BEGIN();
						/* 进入Tickless模式 */
						portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime );
						traceLOW_POWER_IDLE_END();
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				/* 恢复任务调度器 */
				( void ) xTaskResumeAll();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif
	}
}

查看是否有任务需要删除,内存未释放的,使用这个函数 prvCheckTasksWaitingTermination 实现:

static void prvCheckTasksWaitingTermination( void )
{

	#if ( INCLUDE_vTaskDelete == 1 )
	{
		TCB_t *pxTCB;

		/* 有等待删除的任务 */
		while( uxDeletedTasksWaitingCleanUp > ( UBaseType_t ) 0U )
		{
			/* 进入临界区 */
			taskENTER_CRITICAL();
			{
				/* 获取等待删除列表的任务 */
				pxTCB = listGET_OWNER_OF_HEAD_ENTRY( ( &xTasksWaitingTermination ) );
				/* 将任务从状态列表中删除 */
				( void ) uxListRemove( &( pxTCB->xStateListItem ) );
				/* 当前任务数量减一 */
				--uxCurrentNumberOfTasks;
				/* 等待删除任务减一 */
				--uxDeletedTasksWaitingCleanUp;
			}
			/* 退出临界区 */
			taskEXIT_CRITICAL();

			/* 释放任务控制块内存 */
			prvDeleteTCB( pxTCB );
		}
	}
	#endif 
}

这两个函数都比较简单,其中还可以看到 configUSE_IDLE_HOOK 这个宏的实现,可以做到当进入空闲任务时执行用户定义的回调函数,用户可以自行在里面做降低功耗的处理。接下来分析一下系统自带的 tickless 低功耗模式的处理,什么时候进入低功耗、退出低功耗,有两种方式,一种是延时触发一个指定时间的定时器中断,当延时时间到了后触发中断返回正常状态,还有就是外设中断的触发也会导致系统退出低功耗。在第一种方式下我们需要指定一个定时时间,这个时间可以定义为下一个任务的解锁时间,一旦下一个要运行的任务时间片到了后就退出低功耗去执行对应的任务,如何获取下一个任务的解锁时间,使用的是 prvGetExpectedIdleTime,源码分析如下:

#if ( configUSE_TICKLESS_IDLE != 0 )

	static TickType_t prvGetExpectedIdleTime( void )
	{
	TickType_t xReturn;
	UBaseType_t uxHigherPriorityReadyTasks = pdFALSE;

		/* 寻找下一个运行任务使用通用方法(软件方式,C语言实现,效率低,不限制最大优先级数目) */
		#if( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )
		{
			if( uxTopReadyPriority > tskIDLE_PRIORITY )
			{
				/* 当前有就绪任务 */
				uxHigherPriorityReadyTasks = pdTRUE;
			}
		}
		/* 寻找下一个运行任务使用特殊方法(硬件方式,硬件实现,效率高,限制最大优先级数目) */
		#else
		{
			const UBaseType_t uxLeastSignificantBit = ( UBaseType_t ) 0x01;

			if( uxTopReadyPriority > uxLeastSignificantBit )
			{
				/* 当前有就绪任务 */
				uxHigherPriorityReadyTasks = pdTRUE;
			}
		}
		#endif

		if( pxCurrentTCB->uxPriority > tskIDLE_PRIORITY )
		{
			xReturn = 0;
		}
		else if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > 1 )
		{
			xReturn = 0;
		}
		else if( uxHigherPriorityReadyTasks != pdFALSE )
		{
			xReturn = 0;
		}
		else
		{
			/* 下一个任务的解锁时间减去当前时间 */
			xReturn = xNextTaskUnblockTime - xTickCount;
		}

		return xReturn;
	}

#endif

获取到下一个任务的解锁时间后,就可以去重置 systick 定时器中断的重载值,在下一个任务需要执行的时候退出低功耗模式,使用函数 vPortSuppressTicksAndSleep 来实现,源码分析如下:

#if( configUSE_TICKLESS_IDLE == 1 )

	__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
	{
	uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements;
	TickType_t xModifiableIdleTime;

		/* 确保systick定时器的重载值没有溢出,也就是不能超过滴答定时器最大计数值 */
		if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks )
		{
			/* xMaximumPossibleSuppressedTicks 会在  vPortSetupTimerInterrupt中被重新赋值 */
			xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
		}

		/* 停止系统滴答定时器 */
		portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;

		/* 根据 xExpectedIdleTime 计算滴答定时器的重载值 */
		ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + ( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );
		if( ulReloadValue > ulStoppedTimerCompensation )
		{
			/* ulStoppedTimerCompensation 在 vPortSetupTimerInterrupt() 中被重新赋值 */
			ulReloadValue -= ulStoppedTimerCompensation;
		}

		/* 进入临界区(关闭中断) */
		/* __disable_irq 用于设置PRIMASK,在执行WFI前设置寄存器PRIMASK的话处理器可以由中断唤醒但不会处理这些中断,
		   退出低功耗模式后通过清除寄存器PRIMASK来使ISR得到执行 */
		__disable_irq();
		__dsb( portSY_FULL_READ_WRITE );
		__isb( portSY_FULL_READ_WRITE );

		/* 确认是否可以进入低功耗模式,检查是否还有就绪任务来决定能不能进入低功耗模式 */
		if( eTaskConfirmSleepModeStatus() == eAbortSleep )
		{
			/* 不能进入低功耗模式,重启滴答定时器 */
			portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;
			portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
			portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;

			/* 使能中断 */
			__enable_irq();
		}
		else
		{
			/* 可以进入低功耗模式,设置滴答定时器的重载值 */
			portNVIC_SYSTICK_LOAD_REG = ulReloadValue;
			portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
			portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;

			xModifiableIdleTime = xExpectedIdleTime;
			/* 在进入低功耗模式之前可能有一些其他的事情要处理,
			   比如降低系统时钟、关闭外设时钟等都在这个宏里面
			   实现(configPRE_SLEEP_PROCESSING),需用户自己定义 */
			configPRE_SLEEP_PROCESSING( xModifiableIdleTime );
			if( xModifiableIdleTime > 0 )
			{
				__dsb( portSY_FULL_READ_WRITE );
				/* 使用wfi指令进入睡眠模式 */
				__wfi();
				__isb( portSY_FULL_READ_WRITE );
			}

			/* 执行到这里说明已经退出低功耗模式,用户自己定义这个宏的实现,执行前面对应的退出操作 */
			configPOST_SLEEP_PROCESSING( xExpectedIdleTime );

			/* 使能中断 */
			__enable_irq();
			__dsb( portSY_FULL_READ_WRITE );
			__isb( portSY_FULL_READ_WRITE );

			/* 失能中断 */
			__disable_irq();
			__dsb( portSY_FULL_READ_WRITE );
			__isb( portSY_FULL_READ_WRITE );
			
			portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT );

			/* 判断导致退出低功耗模式是由外部中断引起的还是滴答定时器计时时间到引起的 */
			if( ( portNVIC_SYSTICK_CTRL_REG & portNVIC_SYSTICK_COUNT_FLAG_BIT ) != 0 )
			{
				uint32_t ulCalculatedLoadValue;

				/* 休眠定时时间到了,复位重载值 */
				ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ) - ( ulReloadValue - portNVIC_SYSTICK_CURRENT_VALUE_REG );

				if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation ) || ( ulCalculatedLoadValue > ulTimerCountsForOneTick ) )
				{
					ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL );
				}

				portNVIC_SYSTICK_LOAD_REG = ulCalculatedLoadValue;

				ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
			}
			/* 外部中断唤醒的,需要进行时间补偿 */
			else
			{
				/* 获取补偿值 */
				ulCompletedSysTickDecrements = ( xExpectedIdleTime * ulTimerCountsForOneTick ) - portNVIC_SYSTICK_CURRENT_VALUE_REG;
				
				/* 将补偿值转换为节拍值 */
				ulCompleteTickPeriods = ulCompletedSysTickDecrements / ulTimerCountsForOneTick;

				portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1UL ) * ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements;
			}

			/* 重启滴答定时器,滴答定时器的重载值设置为正常值 */
			portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
			portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
			/* 补偿系统时钟, ulCompleteTickPeriods 是要补充的值*/
			vTaskStepTick( ulCompleteTickPeriods );
			portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
			
			/* 使能中断 */
			__enable_irq();
		}
	}

#endif

其中对系统时钟进行补偿的函数也比较简单:

void vTaskStepTick( const TickType_t xTicksToJump )
{
	configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
	xTickCount += xTicksToJump;
	traceINCREASE_TICK_COUNT( xTicksToJump );
}

上面展示出来的代码只是使用 WFI 指令进入了睡眠模式,如果想降低功耗,就必须在进入和退出时做关闭相应时钟的操作,从代码中可以看出进入和退出调用了这个两个接口 configPRE_SLEEP_PROCESSING 和 configPOST_SLEEP_PROCESSING,他们是需要用户自行去实现的宏,定义在 FreeRTOSConfig.h 示例如下:

extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);


//进入低功耗模式前要做的处理
#define configPRE_SLEEP_PROCESSING	PreSleepProcessing

//退出低功耗模式后要做的处理
#define configPOST_SLEEP_PROCESSING	PostSleepProcessing

之后在相应的应用代码中实现这两个接口即可:

/* 低功耗处理函数 */
void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, DISABLE);
}

void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
}

你可能感兴趣的:(FreeRTOS)