Skip to content

11 低功耗支持

11.1 省电简介

FreeRTOS 通过空闲任务(IDLE task)钩子函数和 Tickless Idle 模式,提供了便捷的低功耗能力。

在 FreeRTOS 运行的微控制器上,常见做法是使用空闲任务钩子将 MCU 置于低功耗状态,从而降低功耗。
但这种方式的节能效果会受到限制,因为系统仍必须周期性退出低功耗状态去处理中断 tick,随后再重新进入低功耗。
此外,如果 tick 中断频率过高(即空闲唤醒过于频繁),那么每次 tick 都要付出的“进入低功耗 + 退出低功耗”的时间与能耗开销,可能会抵消节能收益(除非是非常轻量级的省电模式)。

FreeRTOS 支持一种可周期性进出低功耗状态的机制。FreeRTOS 的 Tickless Idle 模式会在空闲时段(即没有应用任务可执行时)停止周期 tick 中断,使 MCU 能够持续停留在更深度的节能状态,直到出现中断,或者 RTOS 内核需要将某个任务转换为 Ready 状态。
当 tick 中断恢复后,内核会对 RTOS 的 tick 计数进行校正。
FreeRTOS Tickless 模式的基本原理是:当 MCU 在执行空闲任务时进入低功耗模式,以降低系统功耗。

11.2 FreeRTOS 睡眠模式

FreeRTOS 支持三种睡眠模式状态:

  1. eAbortSleep —— 表示已有任务被置为就绪、已有上下文切换被挂起,或在调度器挂起期间 tick 中断已经发生但被挂起。该状态用于通知 RTOS 中止进入睡眠模式。

  2. eStandardSleep —— 允许进入一种持续时间不超过预期空闲时长的睡眠模式。

  3. eNoTasksWaitingTimeout —— 当没有任务在等待超时时进入该状态;此时可以安全地进入一种只能由外部中断或复位唤醒的睡眠模式。

11.3 函数与内建 Tickless Idle 功能的启用

在支持该特性的端口上,可通过在 FreeRTOSConfig.h 中将 configUSE_TICKLESS_IDLE 定义为 1 来启用内建 Tickless Idle 功能。
对于任意 FreeRTOS 端口(包括已提供内建实现的端口),也可以通过在 FreeRTOSConfig.h 中将 configUSE_TICKLESS_IDLE 定义为 2,提供用户自定义的 Tickless Idle 实现。

当启用 Tickless Idle 功能后,若满足以下两个条件,内核将调用 portSUPPRESS_TICKS_AND_SLEEP() 宏:

  1. 仅有 Idle 任务可运行,即所有应用任务都处于 Blocked 或 Suspended 状态。

  2. 在内核需要把某个应用任务从 Blocked 状态转出之前,至少还会经过 n 个完整 tick 周期;其中 n 由 FreeRTOSConfig.h 中的 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 定义。

11.3.1 portSUPPRESS_TICKS_AND_SLEEP()

portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime )

清单 11.1 portSUPPRESS_TICKS_AND_SLEEP 宏原型

portSUPPRESS_TICKS_AND_SLEEP() 的参数 xExpectedIdleTime,等于任务下一次需要被移入 Ready 状态前的总 tick 周期数。
因此,该参数值也就表示了在抑制 tick 中断时,微控制器能够安全保持深度睡眠的时间。

11.3.2 vPortSuppressTicksAndSleep 函数

vPortSuppressTicksAndSleep() 函数在 FreeRTOS 中定义,可用于实现 Tickless 模式。该函数在 FreeRTOS Cortex-M 端口层中以 weak 方式定义,应用开发者可以覆写它。

void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime );

清单 11.2 vPortSuppressTicksAndSleep API 函数原型

11.3.3 eTaskConfirmSleepModeStatus 函数

eTaskConfirmSleepModeStatus API 返回睡眠模式状态,用于判断是否可以继续进入睡眠,以及是否可以无限期睡眠。该功能仅在 configUSE_TICKLESS_IDLE 设为 1 时可用。

eSleepModeStatus eTaskConfirmSleepModeStatus( void );

清单 11.3 eTaskConfirmSleepModeStatus API 函数原型

如果在 portSUPPRESS_TICKS_AND_SLEEP() 内部调用 eTaskConfirmSleepModeStatus() 时返回 eNoTasksWaitingTimeout,那么微控制器可以无限期保持在深度睡眠状态。
eTaskConfirmSleepModeStatus() 仅会在以下条件都满足时返回 eNoTasksWaitingTimeout

  • 未使用软件定时器,因此调度器未来任意时刻都不需要执行定时器回调函数。

  • 所有应用任务要么处于 Suspended 状态,要么处于超时值为 portMAX_DELAY 的 Blocked 状态,因此调度器未来任意固定时刻都不需要将任务从 Blocked 状态转出。

为避免竞态,FreeRTOS 会在调用 portSUPPRESS_TICKS_AND_SLEEP() 之前挂起调度器,并在其执行完成后恢复调度器。
这可确保微控制器退出低功耗状态后到 portSUPPRESS_TICKS_AND_SLEEP() 执行完毕前,应用任务不会运行。
此外,portSUPPRESS_TICKS_AND_SLEEP() 还必须在“停止定时器”与“进入睡眠模式”之间创建一个短临界区,以确保此时确实可以进入睡眠。eTaskConfirmSleepModeStatus() 应在该临界区内调用。

另外,FreeRTOS 还在 FreeRTOSConfig.h 中为用户提供了两个接口宏。它们分别允许应用开发者在 MCU 进入低功耗状态前后加入附加处理步骤。

11.3.4 configPRE_SLEEP_PROCESSING 配置

configPRE_SLEEP_PROCESSING( xExpectedIdleTime )

清单 11.4 configPRE_SLEEP_PROCESSING 宏原型

在用户让 MCU 进入低功耗模式前,必须调用 configPRE_SLEEP_PROCESSING() 配置系统参数以降低系统功耗,例如关闭其它外设时钟、降低系统频率。

11.3.5 configPOST_SLEEP_PROCESSING 配置

configPOST_SLEEP_PROCESSING( xExpectedIdleTime )

清单 11.5 configPOST_SLEEP_PROCESSING 宏原型

退出低功耗模式后,用户应调用 configPOST_SLEEP_PROCESSING() 函数,恢复系统主频和外设功能。

11.4 实现 portSUPPRESS_TICKS_AND_SLEEP()

如果当前使用的 FreeRTOS 端口未提供 portSUPPRESS_TICKS_AND_SLEEP() 的默认实现,则应用开发者可以在 FreeRTOSConfig.h 中定义 portSUPPRESS_TICKS_AND_SLEEP() 来提供自己的实现。
如果当前端口已经提供默认实现,应用开发者同样可以在 FreeRTOSConfig.h 中重新定义 portSUPPRESS_TICKS_AND_SLEEP() 以覆盖默认实现。

下面的源码展示了应用开发者可如何实现 portSUPPRESS_TICKS_AND_SLEEP()。该示例是基础版本,会在内核维护时间与日历时间之间引入一定偏差。示例中出现的函数调用里,只有 vTaskStepTick()eTaskConfirmSleepModeStatus() 属于 FreeRTOS API,其余函数都与具体硬件时钟和低功耗模式相关,因此需要由应用开发者自行提供。

/* First define the portSUPPRESS_TICKS_AND_SLEEP() macro.  The parameter is the
    time, in ticks, until the kernel next needs to execute. */

#define portSUPPRESS_TICKS_AND_SLEEP( xIdleTime ) vApplicationSleep( xIdleTime )

/* Define the function that is called by portSUPPRESS_TICKS_AND_SLEEP(). */
void vApplicationSleep( TickType_t xExpectedIdleTime )
{
     unsigned long ulLowPowerTimeBeforeSleep, ulLowPowerTimeAfterSleep;

     eSleepModeStatus eSleepStatus;

     /* Read the current time from a time source that will remain operational
         while the microcontroller is in a low power state. */
     ulLowPowerTimeBeforeSleep = ulGetExternalTime();

     /* Stop the timer that is generating the tick interrupt. */
     prvStopTickInterruptTimer();

     /* Enter a critical section that will not effect interrupts bringing the MCU
         out of sleep mode. */
     disable_interrupts();

     /* Ensure it is still ok to enter the sleep mode. */
     eSleepStatus = eTaskConfirmSleepModeStatus();

     if( eSleepStatus == eAbortSleep )
     {
          /* A task has been moved out of the Blocked state since this macro was
              executed, or a context siwth is being held pending.  Do not enter a
              sleep state.  Restart the tick and exit the critical section. */
          prvStartTickInterruptTimer();
          enable_interrupts();
     }
     else
     {
          if( eSleepStatus == eNoTasksWaitingTimeout )
          {
                /* It is not necessary to configure an interrupt to bring the
                    microcontroller out of its low power state at a fixed time in 
                    the future. */
                prvSleep();
          }
          else
          {
                /* Configure an interrupt to bring the microcontroller out of its low
                    power state at the time the kernel next needs to execute.  The
                    interrupt must be generated from a source that remains operational
                    when the microcontroller is in a low power state. */
                vSetWakeTimeInterrupt( xExpectedIdleTime );

                /* Enter the low power state. */
                prvSleep();

                /* Determine how long the microcontroller was actually in a low power
                    state for, which will be less than xExpectedIdleTime if the
                    microcontroller was brought out of low power mode by an interrupt
                    other than that configured by the vSetWakeTimeInterrupt() call.
                    Note that the scheduler is suspended before
                    portSUPPRESS_TICKS_AND_SLEEP() is called, and resumed when
                    portSUPPRESS_TICKS_AND_SLEEP() returns.  Therefore no other tasks will
                    execute until this function completes. */
                ulLowPowerTimeAfterSleep = ulGetExternalTime();

                /* Correct the kernels tick count to account for the time the
                    microcontroller spent in its low power state. */
                vTaskStepTick( ulLowPowerTimeAfterSleep - ulLowPowerTimeBeforeSleep );
          }

          /* Exit the critical section - it might be possible to do this immediately
              after the prvSleep() calls. */
          enable_interrupts();

          /* Restart the timer that is generating the tick interrupt. */
          prvStartTickInterruptTimer();
     }
}

清单 11.6 用户自定义 portSUPPRESS\_TICKS\_AND\_SLEEP() 的实现示例

11.5 空闲任务钩子函数

空闲任务可以选择调用一个由应用定义的钩子(或回调)函数——Idle Hook。
空闲任务运行在最低优先级,因此只有当没有更高优先级任务可运行时,该 Idle Hook 才会执行。
这使 Idle Hook 成为让处理器进入低功耗状态的理想位置——在系统无处理工作时自动节能。
仅当 FreeRTOSConfig.h 中 configUSE_IDLE_HOOK 设为 1 时,Idle Hook 才会被调用。

void vApplicationIdleHook( void );

清单 11.7 vApplicationIdleHook API 函数原型

只要空闲任务在运行,Idle Hook 就会被反复调用。必须确保 Idle Hook 函数不调用任何可能导致阻塞的 API。
另外,如果应用使用了 vTaskDelete() API,那么 Idle Hook 必须允许周期性返回,因为空闲任务负责清理 RTOS 内核为已删除任务分配的资源。