FreeRTOS空闲任务与低功耗
FreeRTOS 低功耗这里主要关注:HAL 的基础时钟、FreeRTOS 的 Tick、CPU 进入低功耗的时机。
一、HAL 时基和 __weak
STM32 HAL 里很多函数依赖一个毫秒级时基,最典型的是 HAL_Delay() 和 HAL_GetTick()。HAL 默认通常使用 SysTick 作为时基,每 1ms 进入一次 SysTick 中断,在中断里增加 HAL 的 tick 计数。
HAL 里有不少函数使用 __weak 修饰。弱函数的意思是:库里提供一个默认实现,用户如果在自己的工程里写了同名强定义函数,就可以覆盖这个默认实现。HAL 时基相关函数就是典型例子。
比如 HAL 默认会有类似这样的弱函数:
1 | __weak void HAL_IncTick(void) |
用户可以通过 CubeMX 或手动代码,把 HAL 的时基从 SysTick 改成 TIM6、TIM7 等普通定时器。这样 HAL 的毫秒计数就不再依赖 SysTick,而是由对应 TIM 的中断来驱动。
二、不使用 FreeRTOS 时的 SysTick
不使用 FreeRTOS 时,SysTick 经常被 HAL 作为默认基础时钟。它的作用主要是周期性产生 1ms tick,用于 HAL 的延时和超时判断。
这种情况下,HAL_Delay(10) 大致就是轮询 HAL_GetTick() 的变化,等 HAL tick 过去 10ms。它不是让 CPU 自动进入低功耗,也不是任务级阻塞,只是 HAL 层面的时间等待。
如果工程完全不用 HAL_Delay()、HAL 超时判断、HAL tick 相关逻辑,理论上停掉 HAL 的 SysTick 对这部分功能影响不大。但实际工程里 HAL 驱动内部经常会用 tick 做超时,所以不能简单认为“停掉 SysTick 对系统运行无影响”。要看具体外设驱动和代码有没有依赖 HAL tick。
三、使用 FreeRTOS 时的 SysTick
使用 FreeRTOS 后,SysTick 通常要作为 FreeRTOS 的系统节拍。它负责产生 RTOS Tick,驱动任务延时、超时等待、时间片轮转等逻辑。SysTick 中断里会更新 FreeRTOS 的 tick 计数,并在需要时触发调度请求。
因此,在 FreeRTOS 工程里,不建议再让 HAL 和 FreeRTOS 同时争用 SysTick。常见做法是:
SysTick 交给 FreeRTOS 使用。
HAL 时基改用其他定时器,比如 TIM6 或 TIM7。
任务里尽量使用
vTaskDelay()、vTaskDelayUntil(),少用HAL_Delay()。
CubeMX 里可以在 SYS 配置中把 Timebase Source 从 SysTick 改成 TIM6。这样会生成 TIM6 相关初始化,并用 TIM6 中断去调用 HAL 的 tick 增加逻辑。此时 TIM6 不是“完全代替 SysTick”,更准确地说是:TIM6 代替 SysTick 成为 HAL 的基础时钟;SysTick 仍然服务于 FreeRTOS。
这个关系可以这样理解:
| 场景 | SysTick 用途 | HAL 时基 |
|---|---|---|
| 不使用 FreeRTOS | 常作为 HAL 时基 | 默认 SysTick |
| 使用 FreeRTOS | 作为 RTOS Tick | 建议 TIM6/TIM7 |
如果 HAL 时基用 TIM6,那么关闭 TIM6 会影响 HAL_Delay()、HAL_GetTick() 以及 HAL 内部依赖 tick 的超时判断,不只是影响一个手写延时函数。原笔记里“只影响 HAL_Delay”这个说法要收窄,实际要看 HAL 驱动是否用到 tick。
四、空闲任务
FreeRTOS 会自动创建空闲任务,优先级为 0。只要没有其他就绪任务可以运行,调度器就会运行空闲任务。
空闲任务不是没有作用。它至少有几个职责:
保证系统始终有任务可运行。
清理被删除任务的动态内存。
如果启用了空闲钩子函数,可以在空闲任务里执行用户代码。
如果启用了 Tickless 低功耗,空闲任务路径会参与进入低功耗判断。
因此,空闲任务需要有运行机会。比如某个高优先级任务一直 while(1) 运行,不阻塞、不 vTaskDelay(),空闲任务就没有机会运行,被删除任务的内存也可能无法回收,低功耗也进不去。
五、空闲钩子函数
空闲钩子函数是 vApplicationIdleHook()。要使用它,需要在 FreeRTOSConfig.h 中启用:
1 |
然后用户实现:
1 | void vApplicationIdleHook(void) |
或者使用 HAL 的接口进入 Sleep:
1 | void vApplicationIdleHook(void) |
原笔记里的写法是:
1 | void vApplicationIdleHook(void) |
这里要看具体 STM32 型号和 HAL 支持的低功耗模式。有些系列的 Sleep 模式参数支持 PWR_MAINREGULATOR_ON 或 PWR_LOWPOWERREGULATOR_ON,有些系列参数含义不完全一样。写代码时要以当前芯片 HAL 头文件为准。
空闲钩子里不能调用会阻塞的函数。比如不能在里面 vTaskDelay()、不能等待队列、不能等待信号量。原因很直接:Idle Hook 本来就在空闲任务里执行,阻塞空闲任务没有意义,还可能影响内核清理删除任务等工作。
空闲钩子里的代码应该非常短,典型做法就是执行 WFI 让 CPU 等待中断唤醒。下一次 SysTick、外设中断或其他唤醒源到来后,CPU 会继续运行。
六、直接在 Idle Hook 里 Sleep 的特点
在 vApplicationIdleHook() 里执行 WFI 或 HAL_PWR_EnterSLEEPMode(),属于比较直接的低功耗方式。只要系统空闲,就让 CPU 进入 Sleep,下一次中断来时再醒来。
这种方式的优点是简单,不需要 FreeRTOS 停止 Tick,也不需要补偿 tick。缺点是 SysTick 仍然按固定周期运行,比如 1ms 一次。也就是说,即使没有任务要运行,CPU 也会被 SysTick 周期性唤醒。
所以 Idle Hook 进 Sleep 更适合轻量降功耗,主要减少空闲期间 CPU 空转。它不能消除周期 Tick 带来的频繁唤醒。
示例:
1 | void vApplicationIdleHook(void) |
WFI 表示 Wait For Interrupt。DSB/ISB 是屏障指令,部分低功耗进入流程里会加上,确保前面的内存访问和指令状态处理完再进入等待中断。
七、Tickless 低功耗
Tickless Idle 是 FreeRTOS 提供的低功耗机制。它的核心思路是:当系统发现接下来一段时间没有任务需要运行,就临时停止周期性 Tick,让 CPU 睡更久;醒来后再根据睡眠时间补回丢掉的 Tick。
启用配置通常是:
1 |
普通 Tick 模式下,如果 Tick 是 1ms,CPU 每 1ms 至少会被 SysTick 叫醒一次。Tickless 模式下,FreeRTOS 会计算“距离下一个任务超时/延时到期还有多少 Tick”,如果这个时间足够长,就进入低功耗,并在唤醒后通过 vTaskStepTick() 一类机制补偿系统 tick。
流程大致是:
1 | 所有可运行任务都阻塞 |
原笔记里写“会计算睡眠的节拍数,然后补回来”,这个理解是对的。Tickless 的关键就是预计睡眠时间和醒来后的 tick 补偿。
八、Tickless 的最大睡眠时间
这个值不是 FreeRTOS 固定规定的,而是和移植层、时钟频率、SysTick 装载寄存器宽度、CubeMX/HAL 配置都有关系。
Cortex-M SysTick 是 24 位递减计数器,最大重装值是 0xFFFFFF。如果直接用 SysTick 做 Tickless 的睡眠定时,最大可睡眠时间受这个 24 位计数器限制:
1 | 最大时间 ≈ 0xFFFFFF / SysTick 时钟频率 |
如果 SysTick 时钟很高,比如几十 MHz,那么一次能表示的最长时间确实可能只有几十到几百毫秒。
有些芯片会用低功耗定时器 RTC/LPTIM 做更长时间的 Tickless 睡眠,这时最大睡眠时间又由低功耗定时器决定。STM32 具体能做到哪一步,要看使用的 FreeRTOS 移植层和 CubeMX 生成代码。
九、进入低功耗前要注意的外设
低功耗不是只让 CPU 执行一条 WFI。很多问题来自外设状态。
- HAL 时基
如果 HAL 时基使用 TIM6,而 Sleep/Stop 模式下 TIM6 不运行,HAL_Delay() 和 HAL 超时判断可能会异常。Sleep 模式下一些总线和定时器仍可运行,Stop 模式下很多外设时钟会停,需要按芯片手册确认。
- 调试器
连接调试器时,低功耗表现可能和脱机运行不同。有些 MCU 在调试模式下会冻结定时器,或者不真正进入低功耗。
- 唤醒源
进入 Sleep 后,SysTick、外部中断、串口中断等都可能唤醒 CPU。进入 Stop/Standby 时,能唤醒的源更受限制,需要单独配置 EXTI、RTC、LPTIM 等。
- 任务阻塞状态
只有任务都阻塞或挂起、没有高优先级任务持续就绪时,系统才有机会进入空闲任务。任务里如果一直轮询标志位,就算 CPU 看起来“没做什么”,FreeRTOS 也不会认为系统空闲。
更好的写法是让任务阻塞等待事件:
1 | void SensorTask(void *argument) |
而不是:
1 | void SensorTask(void *argument) |
后者会一直占用 CPU,空闲任务没有机会运行,也就谈不上低功耗。
十、注意事项
使用 FreeRTOS 后,SysTick 通常给 FreeRTOS 做系统 Tick;HAL 时基建议改用 TIM6/TIM7 等其他定时器。
TIM6 作为 HAL 时基时,关闭 TIM6 影响的不只是 HAL_Delay(),也可能影响 HAL 内部依赖 tick 的超时判断。
空闲任务有实际作用,包括清理被删除任务内存和执行 Idle Hook。不要让高优先级任务长期不阻塞地占用 CPU。
vApplicationIdleHook() 里不能调用阻塞函数,也不要写复杂业务逻辑。它适合放很短的低功耗入口代码。
Idle Hook 里直接 WFI 简单,但 SysTick 仍会周期性唤醒 CPU。
Tickless Idle 会尝试停止周期 Tick,让 CPU 睡更久,醒来后补偿 Tick。它的最大睡眠时间不是固定值,取决于定时器和移植层实现。
低功耗调试时要同时看任务是否真的阻塞、HAL 时基是否还在走、外设时钟是否会被低功耗模式关闭、唤醒源是否配置正确。
补充:STM32HAL中的低功耗
看门狗解决“程序跑飞后怎么自恢复”,低功耗解决“CPU 没事干时怎么少耗电”
看门狗:
1 | HAL_IWDG_Refresh(&hiwdg); |
程序正常运行时,要定期执行“喂狗”,如果程序死循环、任务卡死、跑飞,导致长时间没有喂狗,看门狗计时超时,就会程序异常导致无法喂狗,然后Watchdog 超时,产生系统复位,MCU 重新启动
程序已经出错之后,让系统能够自动恢复。对于无人值守设备非常重要。
两种看门狗:
1 | IWDG:Independent Watchdog,独立看门狗 |
IWDG 独立看门狗
IWDG 使用一个独立低速时钟 LSI:LSI->Prescaler 分频->递减计数器->0->System Reset
程序不断重新装载计数器,只要整个循环能够正常运行,就一直喂狗。
Independent指的是它的时钟不是 CPU 主时钟,即便主时钟异常,IWDG 的LSI仍然可以继续运行,因此 IWDG 的可靠性更高。
WWDG 窗口看门狗
设置一个合法的喂狗时间,比如正常一个任务执行是在30ms,但是2ms的时候就喂狗,那也是有问题的
低功耗:
STM32程序没干活”不代表“CPU 没耗电
MCU 功耗很大一部分来自CPU 时钟翻转、外设时钟、PLL(锁相环用来根据一个比较稳定低速的参考时钟通过倍频和分频产生CPU和外设需要的高速时钟)、外设。假设 CPU 什么事都没有,它依然在高速执行branch(跳转指令,)
STM32 常见可以粗略理解成:
正常 Run Sleep Stop Standby Shutdown
功耗越来越低 唤醒代价越来越大 保留内容越来越少
Sleep 是最轻的一种,
CPU 停止 外设继续工作 SRAM 保留 寄存器保留,比如UART RX 中断,或者timer中断,CPU 就醒了,
核心代码HAL_PWR_EnterSLEEPMode 对应ARM指令__WFI();Wait For Interrupt
WFI 和 WFE 有什么区别:Wait For Interrupt,Wait For Event WFI 主要等中断;WFE 等事件,配合 Event Register 和 SEV,适合事件同步。
裸机开发时,CPU 大部分时间都在判断,busy loop
FreeRTOS 中如果所有任务都 blocked,就只剩Idle Task,这时候可以进入Sleep
正常 FreeRTOS,SysTick每 1 ms 唤醒 CPU 一次,即使完全没事情,也睡1ms醒一次,如果使用Tickless Idle 会计算下一个任务 500 ms 后才需要执行,然后sleep500ms,功耗进一步降低。如果算不出来,全阻塞,freertos内核会直接WFI。Tickless 真正计算的是“最长安全睡眠时间”
stop模式:
Stop 比 Sleep 更深。Stop 唤醒以后程序通常从睡眠位置继续执行,并不是从 main() 重新开始。
CPU停止,高速时钟停止,PLL 关闭,SRAM保留,寄存器 保留,外设一部分停止,但是比如(RTC,IWDG,还有取决不同芯片,比如一部分低功耗芯片的 UART,EXTI)
比如:
但是睡醒之后要SystemClock_Config();因为高速时钟已经停止了
Standby 模式
CPU OFF 主 SRAM 大部分不保留 系统时钟 OFF 大部分寄存器 丢失 RTC/Backup 可以保留
功耗非常低,唤醒行为很接近一次 Reset。
Wakeup Pin RTC Alarm 芯片启动 Reset_Handler main()
所以如果进入 Standby 前有重要数据,可能要放到Flash EEPROM
越深的低功耗模式,关闭的模块越多,功耗越低,但唤醒时间更长、需要恢复的系统状态也更多。
唤醒:
常见唤醒源包括:
1 | GPIO / EXTI |
不是所有低功耗模式都支持所有唤醒源,比如有些都已经关了,比如进入standby模式后,高速的timer已经停下了
还有看门狗和低功耗之间的冲突:不能简单认为“CPU 睡了,看门狗也睡了”。Watchdog 在 Sleep/Stop/Standby 中是否继续计数
解决思路是:
1 | 把 watchdog 超时时间设计得大于睡眠时间 |
RTC 为什么 CPU 睡觉以后还能工作:
1.RTC 和 CPU 可以不使用同一个时钟和电源域
LSE 32.768kHz ───────→ RTC 2的15次方
HSE是给 CPU AHB apb
2.STM32 中 RTC 通常属于Backup Domain也就是备份域
CPU 外设 内存是主域,在深度掉电后Backup Domain里面的Backup Register RTC LSE仍然工作,







