官网介绍:https://www.freertos.org/low-power-tickless-rtos.html
freeRTOS 提供了一个叫做TickLess 的低功耗模式。
一、 如何降低功耗?
空闲任务进入低功耗。
简单应用中处理器大量的时间都在处理空闲任务,所以我们就可以考虑当处理器处理空闲任务的时候就进入低功耗模式。
freertos 就是通过在处理器处理空闲任务的时候将处理器设置为低功耗模式来降低能耗。一般会在空闲任务的钩子函数中执行低功耗相关处理,比如设置处理器进入低功耗模式、关闭其他外设时钟、降低系统主频等等。
进入Tickles模式,当处理器进入空闲任务周期以后,就关闭系统节拍中断。
系统时钟是由滴答定时器中断来提供的,系统时钟频率越高,那么滴答定时器中断频率也就越高,中断可以将芯片从睡眠模式中唤醒,周期性的滴答定时器中断就会导致芯片周期性的进去和退出睡眠模式中,这样导致的结果就是低功耗模式的作用被大大的削弱。
二、 进入低功耗后,需要记录时间的两个操作:
应用层任务需要唤醒低功耗模式,开启一个定时器,定时器的定时周期设置为下一个任务运行的时间。
记录下系统节拍中断关闭的时间,当系统节拍中断再次开启运行的时候补上这段时间就行了。这里需要另外一个定时器来记录这段该补上的时间, 低功耗定时器 或者系统时钟或者普通定时器。
三、 Tickless 具体实现:
关键参数:
configUSE_TICKLESS_IDLE, 启用tickless 模式。
设置 configUSE_TICKLESS_IDLE 为1 。
通过在FreeRTOSConfig.h中将configUSE_TICKLESS_IDLE定义为1(对于支持此功能的端口),可以启用内置tickless idle functionality。
通过在FreeRTOSConfig.h中将configUSE_TICKLESS_IDLE定义为2,可以为任何FreeRTOS端口(包括那些包含内置实现的端口)提供用户定义的tickless idle functionality。
portSUPPRESS_TICKS_AND_SLEEP(),tickless 低功耗代码的实现,此函数被空闲任务调用。
A. 调用条件:
当启用tickless 模式时,如果以下两个条件都为真,则内核将调用宏portSUPPRESS_TICKS_AND_SLEEP()来处理低功耗相关的工作,
空闲任务是唯一能够运行的任务,因为所有应用程序任务都处于阻止状态或处于暂停状态。
当系统运行于低功耗模式的时钟节拍数大于等于参数configEXPECTED_IDLE_TIME_BEFORE_SLEEP时,系统才可以进入到低功耗模式。此参数默认已经在 FreeRTOS.h文件进行定义了。用户也可以在 FreeRTOSConfig.h 文件中重新定义此参数。 默认定义的大小是 2 个系统时钟节拍,且用户自定义的话,不可以小于 2 个系统时钟节拍
B. 函数参数说明:
portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ), 参数xExpectedIdleTime用来指定还有多长时间将有任务进入就绪态,其实就是处理器进入低功耗模式的时长(单位为时钟节拍数)。
C. 函数说明:
portSUPPRESS_TICKS_AND_SLEEP()应该是由用户根据自己所选择的平台来编写的,但是, 对于 Cortex-M3 和 M4 内核来说,FreeRTOS 已经提供了 tickless 低功耗代码的实现,具体定义如下:
portmacro.h:
/* Tickless idle/low power functionality. */
#ifndef portSUPPRESS_TICKS_AND_SLEEP
extern void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime );
#define portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ) vPortSuppressTicksAndSleep( xExpectedIdleTime )
#endif
port.c
#if( configUSE_TICKLESS_IDLE == 1 )
__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
}
用户自己编写的话,configUSE_TICKLESS_IDLE设置为2。
d. 函数源码说明:
#if( configUSE_TICKLESS_IDLE == 1 )
__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements;
TickType_t xModifiableIdleTime;
/* Make sure the SysTick reload value does not overflow the counter. */
//确保滴答定时器的重载值不会溢出,也就是不能超过滴答定时器最大计数值。
if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks )
{
xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
}
/* Stop the SysTick momentarily. The time the SysTick is stopped for
is accounted for as best it can be, but using the tickless mode will
inevitably result in some tiny drift of the time maintained by the
kernel with respect to calendar time. */
//停止滴答定时器
portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;
/* Calculate the reload value required to wait xExpectedIdleTime
tick periods. -1 is used because this code will execute part way
through one of the tick periods. */
//根据参数xExpectedIdleTime来计算滴答定时器的重装载值。
ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + ( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );
if( ulReloadValue > ulStoppedTimerCompensation )
{
ulReloadValue -= ulStoppedTimerCompensation;
}
/* Enter a critical section but don't use the taskENTER_CRITICAL()
method as that will mask interrupts that should exit sleep mode. */
__disable_irq();
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
/* If a context switch is pending or a task is waiting for the scheduler
to be unsuspended then abandon the low power entry. */
//判断是否进入低功耗模式
if( eTaskConfirmSleepModeStatus() == eAbortSleep ) //表示不能进入低功耗模式,重新恢复滴答定时器的运行。
{
/* Restart from whatever is left in the count register to complete
this tick period. */
portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;
/* Restart SysTick. */
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
/* Reset the reload register to the value required for normal tick
periods. */
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
/* Re-enable interrupts - see comments above __disable_irq() call
above. */
__enable_irq(); //重新打开中断。
}
else
{ //进入低功耗模式,完成低功耗相关设置。
/* Set the new reload value. */
portNVIC_SYSTICK_LOAD_REG = ulReloadValue;
/* Clear the SysTick count flag and set the count value back to
zero. */
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* Restart SysTick. */
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
/* Sleep until something happens. configPRE_SLEEP_PROCESSING() can
set its parameter to 0 to indicate that its implementation contains
its own wait for interrupt or wait for event instruction, and so wfi
should not be executed again. However, the original expected idle
time variable must remain unmodified, so a copy is taken. */
xModifiableIdleTime = xExpectedIdleTime;
configPRE_SLEEP_PROCESSING( xModifiableIdleTime );//进入低功耗之前,用户做的一些操作,比如降低系统主频,关闭某些外设。
if( xModifiableIdleTime > 0 )
{
__dsb( portSY_FULL_READ_WRITE );
__wfi();
__isb( portSY_FULL_READ_WRITE );
}
//以下函数,在进入睡眠模式后,是不执行的,等到从睡眠模式唤醒后,才去执行。
configPOST_SLEEP_PROCESSING( xExpectedIdleTime );//退出低功耗之后,用户做的一些操作,恢复系统主频,打开外设,电源。
/* Re-enable interrupts to allow the interrupt that brought the MCU
out of sleep mode to execute immediately. see comments above
__disable_interrupt() call above. */
__enable_irq();
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
/* Disable interrupts again because the clock is about to be stopped
and interrupts that execute while the clock is stopped will increase
any slippage between the time maintained by the RTOS and calendar
time. */
__disable_irq();
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
/* Disable the SysTick clock without reading the
portNVIC_SYSTICK_CTRL_REG register to ensure the
portNVIC_SYSTICK_COUNT_FLAG_BIT is not cleared if it is set. Again,
the time the SysTick is stopped for is accounted for as best it can
be, but using the tickless mode will inevitably result in some tiny
drift of the time maintained by the kernel with respect to calendar
time*/
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT );
/* Determine if the SysTick clock has already counted to zero and
been set back to the current reload value (the reload back being
correct for the entire expected idle time) or if the SysTick is yet
to count to zero (in which case an interrupt other than the SysTick
must have brought the system out of sleep mode). */
if( ( portNVIC_SYSTICK_CTRL_REG & portNVIC_SYSTICK_COUNT_FLAG_BIT ) != 0 ) //不同的中断,不同的补偿方式。
{
uint32_t ulCalculatedLoadValue;
/* The tick interrupt is already pending, and the SysTick count
reloaded with ulReloadValue. Reset the
portNVIC_SYSTICK_LOAD_REG with whatever remains of this tick
period. */
ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ) - ( ulReloadValue - portNVIC_SYSTICK_CURRENT_VALUE_REG );
/* Don't allow a tiny value, or values that have somehow
underflowed because the post sleep hook did something
that took too long. */
if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation ) || ( ulCalculatedLoadValue > ulTimerCountsForOneTick ) )
{
ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL );
}
portNVIC_SYSTICK_LOAD_REG = ulCalculatedLoadValue;
/* As the pending tick will be processed as soon as this
function exits, the tick value maintained by the tick is stepped
forward by one less than the time spent waiting. */
ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
}
else
{
/* Something other than the tick interrupt ended the sleep.
Work out how long the sleep lasted rounded to complete tick
periods (not the ulReload value which accounted for part
ticks). */
ulCompletedSysTickDecrements = ( xExpectedIdleTime * ulTimerCountsForOneTick ) - portNVIC_SYSTICK_CURRENT_VALUE_REG;
/* How many complete tick periods passed while the processor
was waiting? */
ulCompleteTickPeriods = ulCompletedSysTickDecrements / ulTimerCountsForOneTick;
/* The reload value is set to whatever fraction of a single tick
period remains. */
portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1UL ) * ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements;
}
/* Restart SysTick so it runs from portNVIC_SYSTICK_LOAD_REG
again, then set portNVIC_SYSTICK_LOAD_REG back to its standard
value. */
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
vTaskStepTick( ulCompleteTickPeriods ); //重点:补偿系统时钟。将睡眠的时间补偿给系统时钟。
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
/* Exit with interrpts enabled. */
__enable_irq();
}
}
#endif /* #if configUSE_TICKLESS_IDLE */
下面是 FreeRTOS 实现低功耗 tickless 模式的代码框架,方便用户对 tickles 模式有一个认识,同时也方便 FreeRTOS 没有支持的微控制器,用户可以参考实现。 当然,不局限于这种方法,用户有更好的方法,也可以的。 其中函数 vTaskStepTick 和 eTaskConfirmSleepModeStatus 是 FreeRTOS 提供的,其余的函数是需要用户实现的。
e. 程序者需要编写的代码:
configPRE_SLEEP_PROCESSING( xModifiableIdleTime ); //进入低功耗之前,用户做的一些操作,比如降低系统主频,关闭某些外设。
在低功耗设计中,不仅仅是将处理器设置到低功耗模式就行,还需要做一些其他的处理,比如:
将处理器降低到合适的频率,因为频率越低功耗越小,甚至可以在进入低功耗模式以后关闭系统时钟。
修改时钟源,晶振的功耗肯定比处理器内部的时钟源高,进入到低功耗模式以后,可以切换到内部时钟源。
关闭其他外设时钟,比如IO口的时钟。
关闭单子上其他功能模块电源。MOS管。
configPOST_SLEEP_PROCESSING( xExpectedIdleTime ); //退出低功耗之后,用户做的一些操作,恢复系统主频,打开外设,电源。
总结:
如果RTOS使能tickless空闲功能,每当只有空闲任务被执行时,系统节拍时钟中断将会停止,微控制器进入低功耗模式。当微控制器退出低功耗后,系统节拍计数器必须被调整,将进入低功耗的时间弥补上。
低功耗顺序:
获取下一个任务运行时刻,
初始化一个定时器,LPTIM, SYSTICK, TIMER,
降低系统主频,关闭某些外设,关闭某些器件的电源(比如蜂鸣器)
WFI指令让CPU进入低功耗
退出低功耗模式、中断,SYSTICK
恢复系统主频,打开外设,电源
补偿系统计数器/系统时钟。
处理应用层任务。。。。