2、RTOS任务调度与功耗:Tickless Idle机制详解

大家好,我是你们的嵌入式系统讲师。今天我们来聊一个很实在的话题——RTOS任务调度和功耗的关系。

说实话,我早年做产品的时候,对功耗这事没太在意。觉得只要功能跑通了,功耗大点就大点呗。直到有一次,一个手持设备项目,电池续航死活达不到客户要求的72小时。我查了三天三夜,最后发现罪魁祸首居然是——空闲任务在疯狂跑圈。

嗯,从那以后,我就把Tickless Idle机制当成了必修课。

2.1 为什么要有Tickless Idle?

先说说传统RTOS是怎么工作的。

你想想看,一个典型的RTOS,系统时钟节拍(SysTick)会定期产生中断。比如每1ms一次。这个中断用来做什么?用来检查是否有任务需要切换,或者是否有定时器到期。

问题来了——当所有任务都挂起等待时,系统其实没事干。但SysTick还在那里“滴答滴答”地响着。每次中断,CPU都要从睡眠中醒来,处理一下,再睡回去。

这就像你半夜睡着了,闹钟每1分钟响一次,你按掉再睡。一晚上下来,你根本没睡踏实。

Tickless Idle的核心思想就是:当系统空闲时,干脆把SysTick关掉,让CPU深度睡眠。等到下一个任务该执行的时候,再唤醒。

关键数据对比:

模式 空闲时功耗 唤醒延迟 适用场景
传统Tick模式 中等(频繁中断) 低(微秒级) 对响应要求极高
Tickless Idle 极低(深度睡眠) 较高(毫秒级) 电池供电设备

2.2 FreeRTOS的Tickless模式配置

FreeRTOS对Tickless Idle的支持很成熟。我个人习惯在项目初期就把这个功能打开,省得后期改起来麻烦。

配置其实就两步:

  1. 在FreeRTOSConfig.h中使能宏
  2. 实现两个底层接口函数

第一步:使能配置宏

/* FreeRTOSConfig.h */
#define configUSE_TICKLESS_IDLE         1
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP   2  /* 单位:tick */

这里有个小细节——configEXPECTED_IDLE_TIME_BEFORE_SLEEP的值设多少合适?

我建议设2~5。设太小了,系统刚睡下就被唤醒,反而浪费能量。设太大了,响应延迟会变长。我在一个传感器采集项目里试过,设2效果最好。

第二步:实现底层接口

你需要实现两个函数:

/* 进入低功耗模式前调用 */
void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime)
{
    /* 1. 计算可以睡眠的最长时间 */
    uint32_t ulSleepTime = xExpectedIdleTime;
    
    /* 2. 关闭SysTick中断 */
    SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
    
    /* 3. 进入深度睡眠模式 */
    __WFI();  /* Wait For Interrupt */
    
    /* 4. 醒来后重新配置SysTick */
    SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;
}

/* 系统空闲任务钩子(可选) */
void vApplicationIdleHook(void)
{
    /* 可以在这里做一些低功耗准备工作 */
}

注意:不同MCU的深度睡眠模式差异很大。STM32有Stop模式、Standby模式;nRF52有System ON、System OFF。一定要查芯片手册,别照搬代码。

2.3 调度策略对功耗的影响

很多人以为Tickless Idle打开就万事大吉了。其实不然。调度策略的选择,对功耗影响非常大。

我举个例子。假设你有三个任务:

  • 任务A:每10ms执行一次,耗时1ms
  • 任务B:每100ms执行一次,耗时5ms
  • 任务C:事件触发,平均每小时执行一次

如果用抢占式调度,任务A每10ms打断一次。即使Tickless Idle生效,系统也要频繁醒来处理任务A的切换。空闲时间被切得很碎,深度睡眠很难持续。

如果用协作式调度(或者说时间片轮转),任务A、B、C依次执行完再让出CPU。空闲时间更连续,深度睡眠效果更好。

我的经验:对于电池供电设备,我倾向于用协作式调度 + Tickless Idle。虽然响应速度慢一点,但功耗能降30%~50%。

2.4 避坑指南

我曾经踩过一个坑,分享给大家。

有一次做NB-IoT终端,用了FreeRTOS的Tickless模式。测试时发现,设备偶尔会“死机”——其实就是睡过去再也醒不来了。

查了很久才发现问题:

  • 某个外设的中断优先级设置得比SysTick还低
  • 系统进入深度睡眠后,外设中断来了,但CPU无法响应
  • 因为SysTick被关了,也没有其他唤醒源
  • 设备就永远睡下去了

解决方案:确保所有可能唤醒CPU的中断,优先级都高于或等于SysTick。或者在进入睡眠前,检查是否有挂起的中断。

/* 安全进入睡眠的改进版 */
void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime)
{
    /* 先关中断,防止竞态 */
    portDISABLE_INTERRUPTS();
    
    /* 检查是否有挂起的中断 */
    if(/* 有挂起中断 */) {
        portENABLE_INTERRUPTS();
        return;  /* 不睡了,先处理中断 */
    }
    
    /* 再进入睡眠 */
    __WFI();
    
    portENABLE_INTERRUPTS();
}

2.5 总结一下

Tickless Idle不是银弹。它适合那些空闲时间占比高、对响应延迟不敏感的场景。如果你的系统需要微秒级响应,那还是老老实实用传统Tick模式吧。

但话说回来,现在IoT设备、可穿戴设备、传感器节点,哪个不是电池供电?学会用好Tickless Idle,是嵌入式工程师的必修课。

下一章,我们会深入讲讲动态电压频率调整(DVFS),那是另一个功耗优化的利器。

公众号:蓝海资料掘金营,微信deep3321