低功耗 Tickless 模式
电池设备的续航关键:为什么周期性 tick 中断会浪费电,Tickless Idle 如何在空闲时抑制节拍并让 MCU 深睡,唤醒后又如何补偿丢失的时间。
电池设备的续航关键:为什么周期性 tick 中断会浪费电,Tickless Idle 如何在空闲时抑制节拍并让 MCU 深睡,唤醒后又如何补偿丢失的时间。
configTICK_RATE_HZ,常见 1000Hz)维持时间基准与延时。哪怕整个系统都在空闲,MCU 也会每 1ms 被这个中断唤醒一次去做记账,无法进入深度睡眠。对电池供电设备,这种"无谓唤醒"是续航头号杀手。configUSE_TICKLESS_IDLE 开启。xExpectedIdleTime,作为本次睡眠的最长时长。它是延时任务、定时器到期时间的最小值。只有当它超过阈值 configEXPECTED_IDLE_TIME_BEFORE_SLEEP(默认 2 tick)才值得进入 tickless(睡太短反而不划算)。vTaskStepTick 补偿 tick → 恢复 SysTick。你可以重定义它以适配自己的低功耗定时器(如 STM32 的 LPTIM、RTC 唤醒)。vTaskStepTick(n) 把系统 tick 计数一次性推进 n 步,让所有延时/定时器的到期判断正确。若睡眠被外部中断提前打断,补偿的是"实际睡眠时间"而非"预期时间",这样时间才不会漂移。configPRE_SLEEP_PROCESSING(x) 和 configPOST_SLEEP_PROCESSING(x) 两个宏,让你在真正睡眠前后插入自定义代码:睡前关闭不需要的外设时钟、降压、配置唤醒源;睡醒后恢复时钟、重新初始化外设。参数可被改写以跳过默认 WFI(若你在钩子里已自行处理睡眠)。它们是把 FreeRTOS 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))
用 STM32CubeMX 时,勾选 FreeRTOS → Config → USE_TICKLESS_IDLE 会生成一套基于 SysTick 的默认 tickless(进 Sleep 模式,主时钟仍在,省电有限)。若要进 Stop 模式(关主时钟、省电几十倍),必须把 configUSE_TICKLESS_IDLE 设为 2 并自己实现 portSUPPRESS_TICKS_AND_SLEEP,改用 LPTIM 或 RTC 作为睡眠计时与唤醒源,因为 SysTick 在 Stop 下不工作。
/* 睡前:关掉不需要的外设时钟,降低睡眠电流 */ 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(); }
/* 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 章的规则。
| 模式 | 主时钟 | SysTick | 唤醒延迟 | 省电 | tickless 计时源 |
|---|---|---|---|---|---|
| Run | 开 | 开 | — | 无 | — |
| Sleep | 开 | 开 | 极快 | 低 | SysTick 可用 |
| Stop | 关 | 停 | 较快(µs~ms) | 高 | 必须用 LPTIM/RTC |
| Standby | 关 | 停 | 慢(复位级) | 极高 | RAM 丢失,不适合 RTOS 续跑 |
睡眠计时用的低功耗定时器(如 32.768kHz 的 LPTIM/RTC)分辨率有限,且启停有固定开销,因此 tickless 下的延时精度略低于满速 tick,长期可能有微小漂移。对严格定时的场景(精确采样、通信时序)不要依赖 tickless 期间的软件延时,应交给硬件定时器(第 7 章)。tickless 适合"大部分时间空闲、偶尔醒来干活"的低占空比设备。
| 做法 | 效果 |
|---|---|
| 开启 configUSE_TICKLESS_IDLE | 空闲时停 tick 深睡,最核心的一步 |
| 用事件/中断驱动代替轮询 | 让任务尽量阻塞,拉长空闲时间 |
| 合并周期任务的唤醒时刻 | 减少唤醒次数(多个任务同时醒,睡得更整) |
| 睡前关闭无关外设时钟 | 降低睡眠基础电流 |
| 选够深的低功耗模式(Stop) | 关主时钟,省电数量级提升 |
| 降低 configTICK_RATE_HZ | 唤醒本身更省,但延时分辨率变粗,需权衡 |
| 用真实电流计实测各阶段 | 避免"以为省了其实没省",定位漏电外设 |
默认的周期性 tick 中断会每毫秒唤醒 MCU 做记账,让它无法深睡,是电池设备续航的大敌。Tickless Idle(configUSE_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 有时间量化误差,严格定时交给硬件定时器。优化实践:多阻塞少轮询、合并唤醒、关无关外设、选够深的模式、实测电流。