4. 实战:FreeRTOS Tickless Idle移植与配置——在STM32L4上开启Tickless,配置最低功耗模式
好,咱们直接进入正题。Tickless Idle 模式,说白了就是让 FreeRTOS 在空闲任务里睡大觉,而不是空转。我刚开始接触 RTOS 低功耗时,总觉得系统空闲时 CPU 跑个 while(1) 也没什么,直到用示波器一看电流——好家伙,几十毫安。嗯,从那以后,Tickless 就成了我每个低功耗项目的标配。
4.1 Tickless 的核心原理
先理清一个概念:普通 FreeRTOS 在空闲任务里会不断调用 vApplicationIdleHook,CPU 一直跑着。Tickless 则不同——它计算下一次任务唤醒的时间,然后让 MCU 进入深度睡眠,直到定时器或外部中断把它叫醒。
关键点: Tickless 不是让系统永远睡下去,而是“算好时间睡,准时醒”。
我画了一张流程图,帮你理解这个决策过程:
4.2 移植前的准备工作
移植 Tickless 需要改几个关键文件。我习惯先把 FreeRTOSConfig.h 打开,确认以下宏定义存在:
| 宏定义 | 作用 | 推荐值 |
|---|---|---|
configUSE_TICKLESS_IDLE |
开启 Tickless 模式 | 1 或 2(2 表示自定义模式) |
configEXPECTED_IDLE_TIME |
空闲时间阈值(tick 数) | 2 ~ 5 |
configTICK_RATE_HZ |
系统 tick 频率 | 1000(1ms 一个 tick) |
注意: configTICK_RATE_HZ 不要设太高。1000Hz 已经够用,太高会导致 Tickless 频繁退出,低功耗效果大打折扣。我曾经在一个项目里设了 5000Hz,结果电流只降了 10%,后来才发现是 tick 太密了。
4.3 实现两个关键回调函数
FreeRTOS 在 Tickless 模式下会调用两个回调函数,我们需要在 port.c 或单独的文件里实现它们:
4.3.1 vApplicationSleep
这个函数是 Tickless 的核心。它接收一个参数——预计睡眠的 tick 数。我们需要做三件事:
- 关闭 SysTick 中断,防止睡眠期间被 tick 打断。
- 配置一个低功耗定时器(比如 LPTIM1),设置唤醒时间。
- 调用 WFI 指令,进入 STOP 模式。
代码示例:
void vApplicationSleep(TickType_t xExpectedIdleTime)
{
// 1. 关闭 SysTick
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
// 2. 配置 LPTIM1 作为唤醒定时器
// 假设 LPTIM1 时钟为 32.768kHz
uint32_t ulWakeTime = xExpectedIdleTime * (configTICK_RATE_HZ / 32768);
LL_LPTIM_SetAutoReload(LPTIM1, ulWakeTime);
LL_LPTIM_EnableIT_ARRM(LPTIM1);
LL_LPTIM_StartCounter(LPTIM1, LL_LPTIM_OPERATING_MODE_ONESHOT);
// 3. 进入 STOP 模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 4. 醒来后重新配置 SysTick
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;
}
小技巧: 我习惯在进入 STOP 前把 GPIO 也配置成低功耗状态——该拉高的拉高,该下拉的下拉。否则有些引脚浮空会漏电,白白浪费了睡眠省下来的电流。
4.3.2 vApplicationTickHook
这个函数在每次 tick 中断时被调用。Tickless 模式下,我们需要用它来补偿丢失的 tick:
void vApplicationTickHook(void)
{
// 如果是从睡眠中醒来,需要手动补偿 tick 计数
if (ulSleepTime > 0)
{
vTaskStepTick(ulSleepTime);
ulSleepTime = 0;
}
}
4.4 配置最低功耗模式
STM32L4 有多种低功耗模式。Tickless 模式下,我推荐使用 STOP 模式,因为它能在低功耗和唤醒速度之间取得平衡。
| 模式 | 典型电流 | 唤醒时间 | 适用场景 |
|---|---|---|---|
| SLEEP | ~1 mA | ~1 µs | 短睡眠(< 1ms) |
| STOP | ~10 µA | ~5 µs | 推荐:Tickless 默认 |
| STANDBY | ~0.3 µA | ~50 µs | 长睡眠(> 1s) |
配置 STOP 模式时,有几个寄存器需要留意:
- PWR_CR1:选择稳压器模式(低功耗稳压器 LP 或主稳压器 MR)。
- PWR_CR2:配置唤醒引脚(如 RTC 闹钟、LPTIM 等)。
- PWR_SCR:清除唤醒标志,防止误唤醒。
实测数据: 我在 STM32L476 开发板上测试过,开启 Tickless + STOP 模式后,空闲电流从 12 mA 降到了 45 µA。如果再加上 RTC 定时唤醒,可以做到 10 µA 以下。
4.5 避坑指南
移植过程中有几个坑,我一个个说:
- 坑一:LPTIM 时钟没配好。 我遇到过 LPTIM 用的内部 RC,精度不够,导致唤醒时间偏差。后来改用 LSE(32.768kHz),问题解决。
- 坑二:中断优先级。 Tickless 用到的 LPTIM 中断优先级必须高于 FreeRTOS 的临界区优先级(configMAX_SYSCALL_INTERRUPT_PRIORITY),否则会死锁。
- 坑三:调试器干扰。 用 ST-Link 调试时,SWD 引脚会阻止 MCU 进入 STOP 模式。我一般用串口打印电流,或者直接看万用表。
曾经踩过的坑: 有一次我忘了在睡眠前关闭 SysTick 中断,结果 MCU 刚睡下就被 tick 叫醒,电流一点没降。查了两天才发现是 SysTick 没关。嗯,从那以后我把这个步骤写成了 checklist。
4.6 验证与调试
移植完成后,怎么验证 Tickless 生效了?我一般做三步:
- 看电流: 用万用表串在电源上,空闲时电流应该明显下降。
- 看 GPIO: 在进入睡眠前翻转一个 GPIO,用示波器看低电平持续时间是否等于预期睡眠时间。
- 看任务调度: 创建一个周期性任务,确认它能在睡眠期间准时被唤醒执行。
如果发现任务唤醒延迟,先检查 LPTIM 的时钟频率是否准确,再检查 vTaskStepTick 的补偿逻辑是否正确。