FreeRTOS空闲任务与低功耗
FreeRTOS 低功耗这块,最容易混在一起的是三个东西:HAL 的基础时钟、FreeRTOS 的 Tick、CPU 进入低功耗的时机。原笔记里提到 SysTick、TIM6、空闲钩子和 Tickless,本质上都围绕这几个问题。
一、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 的最大睡眠时间
原笔记里提到“最大也就 99ms?”。这个值不是 FreeRTOS 固定规定的,而是和移植层、时钟频率、SysTick 装载寄存器宽度、CubeMX/HAL 配置都有关系。
Cortex-M SysTick 是 24 位递减计数器,最大重装值是 0xFFFFFF。如果直接用 SysTick 做 Tickless 的睡眠定时,最大可睡眠时间受这个 24 位计数器限制:
1 | 最大时间 ≈ 0xFFFFFF / SysTick 时钟频率 |
如果 SysTick 时钟很高,比如几十 MHz,那么一次能表示的最长时间确实可能只有几十到几百毫秒。某些工程里看到大约 99ms,往往就是当前时钟和移植层计算出来的限制,不是所有工程都固定为 99ms。
有些芯片会用低功耗定时器 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 时基是否还在走、外设时钟是否会被低功耗模式关闭、唤醒源是否配置正确。







