Chapter 10 · FreeRTOS 实时操作系统

低功耗 Tickless 模式

电池设备的续航关键:为什么周期性 tick 中断会浪费电,Tickless Idle 如何在空闲时抑制节拍并让 MCU 深睡,唤醒后又如何补偿丢失的时间。

课程进度83%

关键概念

系统节拍 (Tick) 与它的功耗代价
默认情况下 FreeRTOS 靠一个周期性中断(通常 SysTick,频率 configTICK_RATE_HZ,常见 1000Hz)维持时间基准与延时。哪怕整个系统都在空闲,MCU 也会每 1ms 被这个中断唤醒一次去做记账,无法进入深度睡眠。对电池供电设备,这种"无谓唤醒"是续航头号杀手。
Tickless Idle(无节拍空闲)
FreeRTOS 的低功耗机制:当空闲任务发现"接下来一段时间没有任务要运行",就停掉周期性 tick,把 MCU 置入低功耗模式睡眠,直到下一个任务该被唤醒(或外部中断到来)才醒来。醒来后再把睡眠期间"跳过"的 tick 一次性补记,时间基准无缝衔接。用 configUSE_TICKLESS_IDLE 开启。
预期空闲时间 (Expected Idle Time)
内核在进入 tickless 前会算出"到最近一个任务解除阻塞还有多少个 tick",即 xExpectedIdleTime,作为本次睡眠的最长时长。它是延时任务、定时器到期时间的最小值。只有当它超过阈值 configEXPECTED_IDLE_TIME_BEFORE_SLEEP(默认 2 tick)才值得进入 tickless(睡太短反而不划算)。
portSUPPRESS_TICKS_AND_SLEEP
Tickless 的核心宏/函数,由空闲任务调用,参数是预期空闲 tick 数。移植层的默认实现会:停 SysTick → 用一个低功耗定时器计时 → 执行 WFI 进入睡眠 → 唤醒后计算实际睡了多久 → 用 vTaskStepTick 补偿 tick → 恢复 SysTick。你可以重定义它以适配自己的低功耗定时器(如 STM32 的 LPTIM、RTC 唤醒)。
tick 补偿 (vTaskStepTick)
睡眠结束后,内核并不知道"真实过了多久",需要读低功耗定时器算出实际经过的 tick 数,调用 vTaskStepTick(n) 把系统 tick 计数一次性推进 n 步,让所有延时/定时器的到期判断正确。若睡眠被外部中断提前打断,补偿的是"实际睡眠时间"而非"预期时间",这样时间才不会漂移。
唤醒源 (Wakeup Source)
让 MCU 从低功耗模式醒来的事件:低功耗定时器到期(对应 FreeRTOS 里某个任务该运行了)、外部中断(按键、通信、传感器 INT 引脚)、RTC 闹钟等。设计低功耗系统的关键是确保所有需要响应的事件都被配成有效唤醒源,否则事件会被"睡过去",直到定时器超时才处理,造成延迟。
睡眠深度与外设时钟
MCU 有多档低功耗模式(如 STM32 的 Sleep / Stop / Standby),越深省电越多但唤醒越慢、保留的状态越少。Stop 模式会关掉主时钟,SysTick 也停摆——这正是 tickless 必须换用独立低功耗时钟(LPTIM/RTC)计时的原因。选哪一档要权衡功耗、唤醒延迟、以及睡眠中需保持工作的外设。
Pre/Post Sleep 处理钩子
configPRE_SLEEP_PROCESSING(x)configPOST_SLEEP_PROCESSING(x) 两个宏,让你在真正睡眠前后插入自定义代码:睡前关闭不需要的外设时钟、降压、配置唤醒源;睡醒后恢复时钟、重新初始化外设。参数可被改写以跳过默认 WFI(若你在钩子里已自行处理睡眠)。它们是把 FreeRTOS tickless 接到具体芯片低功耗流程的关键接缝。

Tick 中断的功耗浪费 vs Tickless

普通 tick 模式 vs Tickless Idle(空闲 20ms 期间) ═══════════════════════════════════════════════════════════ 普通模式(1kHz tick):空闲期间每 1ms 被 SysTick 唤醒一次 电流 ┤ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ ← 20 次无谓唤醒 └──────────────────────────────────────► t 睡不深、醒 20 次,平均电流高 Tickless 模式:停 tick,一觉睡到"下一个任务该运行" 电流 ┤ █ █ ← 仅 2 次唤醒 └──────────────────────────────────────► t 深睡 20ms,醒来 vTaskStepTick(20) 补偿,平均电流骤降 收益:空闲越久、tick 频率越高,省电越明显(可达数十倍)

portSUPPRESS_TICKS_AND_SLEEP 的工作流程

一次 Tickless 睡眠的完整时序 空闲任务运行 │ 预期空闲 xExpectedIdleTime ≥ 阈值? ▼ 是 portSUPPRESS_TICKS_AND_SLEEP(xExpectedIdleTime) │ ├─► 进入临界区,再次确认可以睡(eTaskConfirmSleepModeStatus) ├─► 停止 SysTick ├─► 配置低功耗定时器 = min(预期空闲, 定时器最大量程) ├─► configPRE_SLEEP_PROCESSING() 关外设时钟/降压 ├─► __WFI() ← MCU 深睡,等唤醒源 │ (定时器到期 或 外部中断 唤醒) ├─► configPOST_SLEEP_PROCESSING() 恢复时钟 ├─► 读定时器算出实际睡了多少 tick ├─► vTaskStepTick(实际tick数) 补偿时间 └─► 重启 SysTick,退出临界区 → 调度继续

开启 Tickless 的配置

/* FreeRTOSConfig.h */

/* 1 = 用内核默认的 tickless 实现(基于 SysTick 重载值)
   2 = 用你自己在移植层重定义的 portSUPPRESS_TICKS_AND_SLEEP */
#define configUSE_TICKLESS_IDLE   1

/* 低功耗要求空闲任务不抢 CPU,通常保持默认 */
#define configUSE_IDLE_HOOK       1

/* 预期空闲不足此 tick 数就不进 tickless(睡太短不划算) */
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP   3

/* 睡前/睡后钩子:接到芯片的低功耗流程 */
#define configPRE_SLEEP_PROCESSING(x)   prvPreSleep(&(x))
#define configPOST_SLEEP_PROCESSING(x)  prvPostSleep(&(x))
STM32 的两种典型做法

用 STM32CubeMX 时,勾选 FreeRTOS → Config → USE_TICKLESS_IDLE 会生成一套基于 SysTick 的默认 tickless(进 Sleep 模式,主时钟仍在,省电有限)。若要进 Stop 模式(关主时钟、省电几十倍),必须把 configUSE_TICKLESS_IDLE 设为 2 并自己实现 portSUPPRESS_TICKS_AND_SLEEP,改用 LPTIM 或 RTC 作为睡眠计时与唤醒源,因为 SysTick 在 Stop 下不工作。

Pre/Post Sleep 钩子示例

/* 睡前:关掉不需要的外设时钟,降低睡眠电流 */
void prvPreSleep(TickType_t *pxExpectedIdle) {
  (void) pxExpectedIdle;
  HAL_SuspendTick();               /* 停 HAL 的 tick,避免误唤醒 */
  disable_unused_peripherals();     /* 关 ADC/USART 时钟等 */
  /* 若把 *pxExpectedIdle 置 0,内核会跳过默认 WFI,
     由你在钩子里自行进入 Stop 模式 */
}

/* 睡后:恢复时钟与外设 */
void prvPostSleep(TickType_t *pxExpectedIdle) {
  (void) pxExpectedIdle;
  SystemClock_Config();            /* Stop 唤醒后主时钟需重配 */
  HAL_ResumeTick();
  reenable_peripherals();
}

自定义 STOP 模式 + LPTIM 睡眠骨架

/* configUSE_TICKLESS_IDLE = 2 时,自行实现该函数 */
void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime) {
  eSleepModeStatus st;

  /* 关中断进临界区,防止竞态 */
  __disable_irq();
  st = eTaskConfirmSleepModeStatus();
  if (st == eAbortSleep) {           /* 有任务刚就绪,别睡了 */
    __enable_irq();
    return;
  }

  stop_systick();
  lptim_start(xExpectedIdleTime);   /* 低功耗定时器计时 */

  if (st != eNoTasksWaitingTimeout) {
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
  }

  uint32_t slept = lptim_elapsed_ticks();  /* 实际睡了多少 tick */
  SystemClock_Config();            /* 恢复主时钟 */
  restart_systick();

  __enable_irq();
  vTaskStepTick(slept);             /* 补偿丢失的 tick */
}
忘了配唤醒源 = 事件被"睡过去"

进入 Stop 模式后,只有被配成唤醒源的中断能叫醒 MCU。如果按键/串口/传感器 INT 没有被使能为唤醒中断,它们的事件会一直挂着,直到低功耗定时器超时才被处理——表现为"按键要过几秒才响应"。务必在 prvPreSleep 里确保所有需要即时响应的外设都配成 EXTI/唤醒源,且它们的中断优先级符合第 9 章的规则。

低功耗档位对比(以 STM32 为例)

模式主时钟SysTick唤醒延迟省电tickless 计时源
Run
Sleep极快SysTick 可用
Stop较快(µs~ms)必须用 LPTIM/RTC
Standby慢(复位级)极高RAM 丢失,不适合 RTOS 续跑
Tickless 会引入时间量化误差

睡眠计时用的低功耗定时器(如 32.768kHz 的 LPTIM/RTC)分辨率有限,且启停有固定开销,因此 tickless 下的延时精度略低于满速 tick,长期可能有微小漂移。对严格定时的场景(精确采样、通信时序)不要依赖 tickless 期间的软件延时,应交给硬件定时器(第 7 章)。tickless 适合"大部分时间空闲、偶尔醒来干活"的低占空比设备。

功耗优化实践清单

做法效果
开启 configUSE_TICKLESS_IDLE空闲时停 tick 深睡,最核心的一步
用事件/中断驱动代替轮询让任务尽量阻塞,拉长空闲时间
合并周期任务的唤醒时刻减少唤醒次数(多个任务同时醒,睡得更整)
睡前关闭无关外设时钟降低睡眠基础电流
选够深的低功耗模式(Stop)关主时钟,省电数量级提升
降低 configTICK_RATE_HZ唤醒本身更省,但延时分辨率变粗,需权衡
用真实电流计实测各阶段避免"以为省了其实没省",定位漏电外设
本章小结

默认的周期性 tick 中断会每毫秒唤醒 MCU 做记账,让它无法深睡,是电池设备续航的大敌。Tickless IdleconfigUSE_TICKLESS_IDLE)在空闲任务发现"接下来一段时间无事可做"时,停掉 tick、把 MCU 置入低功耗模式,睡到"最近的任务该运行"或外部中断到来。内核先算出预期空闲时间 xExpectedIdleTime(超过 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 才值得睡),调用 portSUPPRESS_TICKS_AND_SLEEP:停 SysTick → 启低功耗定时器 → configPRE_SLEEP_PROCESSING 关外设/降压 → WFI 深睡 → 醒来 configPOST_SLEEP_PROCESSING 恢复 → 读实际睡眠时长 → vTaskStepTick 补偿 tick → 重启 SysTick。STM32 进 Stop 模式需把配置设为 2 并自实现该函数、改用 LPTIM/RTC 计时(SysTick 在 Stop 下停摆)。务必把所有需即时响应的事件配成唤醒源,否则会被睡过去。tickless 有时间量化误差,严格定时交给硬件定时器。优化实践:多阻塞少轮询、合并唤醒、关无关外设、选够深的模式、实测电流。