4. Tickless模式配置实战:在STM32L4上配置FreeRTOS Tickless模式

好,咱们进入正题。Tickless模式,说白了就是让RTOS在没事干的时候「打盹」,而不是傻乎乎地每秒几万次地检查有没有活干。我刚开始接触低功耗时,总觉得系统空闲了直接进睡眠不就完了?后来才发现,FreeRTOS的Tickless远不止「睡一觉」那么简单。

这一章,咱们就在STM32L4上把Tickless模式真正跑起来。重点就两个:改配置、写钩子。

4.1 先搞清楚Tickless在干什么

先看一张图,理解一下Tickless的核心逻辑。

FreeRTOS Tickless 模式核心流程 正常 Tick 中断 空闲任务执行 计算可睡眠 Tick 数 配置低功耗定时器(LPTIM) 进入 STOP 模式 LPTIM 唤醒 → 补偿 Tick 关键点: • 空闲任务不是「空转」,而是进入低功耗模式 • 用 LPTIM 替代 SysTick 做唤醒定时器 • 唤醒后要补偿丢失的 Tick 计数

看到没?核心就三步:算时间、睡下去、醒来补Tick。但每一步都有坑,咱们一个一个填。

4.2 修改 FreeRTOSConfig.h

这是最直接的一步。我习惯把配置项分成「必开」和「可选」两类,先保证能跑起来。

4.2.1 必开配置

/* FreeRTOSConfig.h */
#define configUSE_TICKLESS_IDLE         1           // 启用 Tickless 模式
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2    // 至少空闲 2 个 Tick 才睡

configUSE_TICKLESS_IDLE 设为 1,告诉内核:空闲时别傻等,去睡。

configEXPECTED_IDLE_TIME_BEFORE_SLEEP 这个值很关键。设太小,频繁进出睡眠反而费电;设太大,响应变慢。我一般设 2~5,具体看你的 Tick 周期。

我的经验:如果 Tick 是 1ms,设 5 意味着至少空闲 5ms 才睡。对于大部分 IoT 应用,这个值够用。曾经有个项目设了 1,结果频繁唤醒,功耗反而比不睡还高。

4.2.2 可选但推荐配置

#define configTICK_RATE_HZ              ((TickType_t)1000)  // 1ms Tick
#define configSYSTICK_CLOCK_HZ          ((uint32_t)1000000) // 1MHz
#define configPRE_SLEEP_PROCESSING(x)    PreSleepProcessing(x)
#define configPOST_SLEEP_PROCESSING(x)   PostSleepProcessing(x)

configPRE_SLEEP_PROCESSING 和 configPOST_SLEEP_PROCESSING 是两个宏,分别在睡前和醒后调用。这是咱们做低功耗的主战场。

4.3 实现睡前和醒后钩子

这两个函数写在 main.c 或者专门的 lowpower.c 里。我习惯单独建文件,方便维护。

4.3.1 睡前处理:PreSleepProcessing

void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
    /* 1. 关闭不用的外设时钟 */
    __HAL_RCC_USART1_CLK_DISABLE();
    __HAL_RCC_SPI1_CLK_DISABLE();
    __HAL_RCC_I2C1_CLK_DISABLE();

    /* 2. 配置 LPTIM1 作为唤醒定时器 */
    HAL_LPTIM_TimeOut_Start_IT(&hlptim1, ulExpectedIdleTime, 1000);

    /* 3. 进入 STOP 模式 */
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWER_REGULATOR_ON, PWR_STOPENTRY_WFI);
}

注意顺序:先关外设,再配定时器,最后进 STOP。关外设的顺序也有讲究——我一般先关通信接口,再关传感器,最后关调试接口。

踩坑提醒:千万不要在睡前关掉 LPTIM 的时钟!我曾经犯过这个错,结果系统睡下去就再也醒不来了。LPTIM 的时钟源要保留,通常是 LSE 或 LSI。

4.3.2 醒后处理:PostSleepProcessing

void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
    /* 1. 重新配置系统时钟 */
    SystemClock_Config();

    /* 2. 重新开启外设时钟 */
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_SPI1_CLK_ENABLE();
    __HAL_RCC_I2C1_CLK_ENABLE();

    /* 3. 重新初始化外设(有些外设醒来后需要重新配置) */
    MX_USART1_UART_Init();
    MX_SPI1_Init();
    MX_I2C1_Init();
}

醒后的第一件事永远是恢复时钟。STM32L4 从 STOP 模式唤醒后,系统时钟默认回到 MSI,需要手动切回你想要的频率。

4.4 配置 LPTIM 定时器

LPTIM 是 Tickless 模式的核心。它要在系统睡眠时替代 SysTick 计时。

参数 推荐值 说明
时钟源 LSE (32.768kHz) 精度高,功耗低
预分频 128 得到 256Hz 的计数频率
计数模式 连续计数 支持多次唤醒
中断优先级 最低 避免影响其他中断
/* LPTIM1 初始化示例 */
void MX_LPTIM1_Init(void)
{
    hlptim1.Instance = LPTIM1;
    hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_LSE;
    hlptim1.Init.Clock.Prescaler = LPTIM_PRESCALER_128;
    hlptim1.Init.CounterSource = LPTIM_COUNTERSOURCE_INTERNAL;
    hlptim1.Init.OutputPolarity = LPTIM_OUTPUTPOLARITY_HIGH;
    hlptim1.Init.UpdateMode = LPTIM_UPDATE_IMMEDIATE;
    HAL_LPTIM_Init(&hlptim1);
}
小技巧:LSE 的起振时间比较长(几百毫秒),如果系统频繁进出 STOP 模式,可以考虑用 LSI(约 32kHz)。虽然精度差一点,但起振快,适合短睡眠场景。

4.5 验证 Tickless 是否生效

配置完了,怎么知道它真的在睡?我一般用三种方法验证:

  1. 电流测量:用万用表或功耗分析仪看平均电流。STOP 模式下 STM32L4 应该降到 µA 级别。
  2. GPIO 示波器:在睡前拉高一个 GPIO,醒后拉低。用示波器看脉冲宽度,就是实际睡眠时间。
  3. FreeRTOS 统计:调用 vTaskGetRunTimeStats() 看空闲任务占比。
/* 调试用:在睡前醒后翻转 GPIO */
void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
    HAL_GPIO_WritePin(DEBUG_GPIO_Port, DEBUG_Pin, GPIO_PIN_SET);  // 拉高
    // ... 其他操作
}

void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
    HAL_GPIO_WritePin(DEBUG_GPIO_Port, DEBUG_Pin, GPIO_PIN_RESET); // 拉低
    // ... 其他操作
}

关键指标对照:

  • 正常 Tick 模式:电流约 1~5 mA(视外设而定)
  • Tickless + STOP 模式:电流约 5~50 µA
  • 理想情况(无外设):可低至 1 µA 以下

4.6 常见问题与避坑

这部分是我最想说的。Tickless 配置本身不难,但坑都在细节里。

  • 唤醒后系统时钟不对:STOP 模式唤醒后,HCLK 默认是 MSI。记得在 PostSleepProcessing 里重新调用 SystemClock_Config()。
  • 外设醒来后不工作:有些外设(如 USART、I2C)在 STOP 模式下会丢失配置,需要重新初始化。我习惯把所有外设的初始化函数在醒后都调一遍。
  • 中断丢失:如果睡眠期间有中断发生,系统会立即唤醒。但要注意,某些中断标志位需要在醒后手动清除。
  • LPTIM 精度问题:LSE 的精度很高,但 LSI 受温度影响较大。如果产品工作环境温度变化大,建议用 LSE。
我曾经踩过的坑:有一次产品在低温环境下频繁死机,查了两天才发现是 LSI 频率漂移导致 LPTIM 计时不准,系统误以为睡眠时间到了,提前唤醒后又因为时钟没准备好而卡死。后来换成 LSE,问题解决。