4. 实战:FreeRTOS Tickless Idle移植与配置——在STM32L4上开启Tickless,配置最低功耗模式

好,咱们直接进入正题。Tickless Idle 模式,说白了就是让 FreeRTOS 在空闲任务里睡大觉,而不是空转。我刚开始接触 RTOS 低功耗时,总觉得系统空闲时 CPU 跑个 while(1) 也没什么,直到用示波器一看电流——好家伙,几十毫安。嗯,从那以后,Tickless 就成了我每个低功耗项目的标配。

4.1 Tickless 的核心原理

先理清一个概念:普通 FreeRTOS 在空闲任务里会不断调用 vApplicationIdleHook,CPU 一直跑着。Tickless 则不同——它计算下一次任务唤醒的时间,然后让 MCU 进入深度睡眠,直到定时器或外部中断把它叫醒。

关键点: Tickless 不是让系统永远睡下去,而是“算好时间睡,准时醒”。

我画了一张流程图,帮你理解这个决策过程:

FreeRTOS Tickless 空闲决策流程 系统进入空闲任务 计算下次任务唤醒时间 睡眠时间 > 阈值? 短睡眠/空转 配置唤醒定时器 进入 STOP / STANDBY 模式 说明: 阈值由 configEXPECTED_IDLE_TIME 控制,通常设为 2~5 个 tick。 太短则频繁进出睡眠,反而费电。

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 数。我们需要做三件事:

  1. 关闭 SysTick 中断,防止睡眠期间被 tick 打断。
  2. 配置一个低功耗定时器(比如 LPTIM1),设置唤醒时间。
  3. 调用 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 生效了?我一般做三步:

  1. 看电流: 用万用表串在电源上,空闲时电流应该明显下降。
  2. 看 GPIO: 在进入睡眠前翻转一个 GPIO,用示波器看低电平持续时间是否等于预期睡眠时间。
  3. 看任务调度: 创建一个周期性任务,确认它能在睡眠期间准时被唤醒执行。

如果发现任务唤醒延迟,先检查 LPTIM 的时钟频率是否准确,再检查 vTaskStepTick 的补偿逻辑是否正确。