3. 任务调度与功耗管理:空闲任务的深度定制

做嵌入式系统,尤其是电池供电的设备,功耗管理是个绕不开的话题。很多工程师觉得功耗管理就是让CPU睡大觉,其实没那么简单。你想想看,RTOS里任务调度跑得正欢,突然要进入低功耗模式,这里面的门道可不少。

我个人习惯把功耗管理分成两个层面:一个是硬件层面的时钟门控和电压调节,另一个就是咱们今天要聊的——软件层面的空闲任务定制。说白了,就是让CPU在没事干的时候,知道怎么去“睡觉”,而且还得睡得安稳、醒得及时。

3.1 空闲任务的本质

每个RTOS都有一个空闲任务,它优先级最低,只有当所有其他任务都阻塞或挂起时才会运行。我见过不少开发者直接让空闲任务跑个空循环,这在低功耗场景下简直是灾难。

核心观点:空闲任务不是“没事干”的任务,而是功耗管理的最后一道防线。

空闲任务的典型执行流程是这样的:

  1. 检查是否有更高优先级的任务就绪
  2. 如果没有,进入空闲任务
  3. 在空闲任务中执行低功耗操作
  4. 等待中断唤醒

3.2 空闲钩子函数(Idle Hook)

大多数RTOS都提供了空闲钩子函数,比如FreeRTOS的vApplicationIdleHook。这个函数会在空闲任务的每次循环中被调用。嗯,这里要注意:钩子函数里不能调用任何可能阻塞的API,否则系统就乱套了。

我曾经在一个项目中,工程师在空闲钩子里调用了vTaskDelay,结果系统响应变得极其迟钝。排查了半天才发现是空闲任务把自己给阻塞了,导致其他任务无法被调度。

3.2.1 空闲钩子的典型用法

// FreeRTOS 空闲钩子示例
void vApplicationIdleHook(void)
{
    // 进入睡眠模式
    __WFI();  // Wait For Interrupt
}

这段代码看起来简单,但实际项目中要考虑的事情远不止这些。比如,进入睡眠前要不要关掉外设时钟?要不要保存上下文?不同的低功耗模式怎么选?

3.3 低功耗模式的选择策略

不同的MCU支持不同的低功耗模式,我整理了一个对比表,方便你根据场景选择:

模式 功耗 唤醒延迟 上下文保留 适用场景
睡眠模式 mA级 几微秒 全部保留 短时间空闲,频繁唤醒
停止模式 μA级 几十微秒 部分保留 中等空闲时间
待机模式 nA级 毫秒级 几乎不保留 长时间休眠

我个人习惯的做法是:在空闲钩子里根据下一次任务唤醒的时间动态选择模式。如果距离下次任务执行还有1ms以上,就进停止模式;如果只有几十微秒,睡眠模式就够了。

3.4 空闲任务的深度定制实战

光在空闲钩子里调用__WFI()是不够的。真正的低功耗设计需要系统级的配合。下面是我在一个物联网终端项目中的实际做法:

3.4.1 系统级低功耗框架

// 系统级低功耗管理
typedef enum {
    SLEEP_MODE_LIGHT,   // 浅睡眠
    SLEEP_MODE_DEEP,    // 深睡眠
    SLEEP_MODE_SHUTDOWN // 关机
} SleepMode_t;

void System_EnterLowPower(SleepMode_t mode)
{
    // 1. 通知所有驱动准备进入低功耗
    for (int i = 0; i < DRIVER_COUNT; i++) {
        Driver_PrepareSleep(i);
    }
    
    // 2. 保存关键寄存器
    SaveCriticalRegisters();
    
    // 3. 配置唤醒源
    ConfigureWakeUpSource();
    
    // 4. 进入指定低功耗模式
    switch (mode) {
        case SLEEP_MODE_LIGHT:
            MCU_EnterSleep();
            break;
        case SLEEP_MODE_DEEP:
            MCU_EnterStop();
            break;
        case SLEEP_MODE_SHUTDOWN:
            MCU_EnterStandby();
            break;
    }
    
    // 5. 唤醒后恢复
    RestoreCriticalRegisters();
    
    // 6. 通知驱动恢复
    for (int i = 0; i < DRIVER_COUNT; i++) {
        Driver_Resume(i);
    }
}

避坑指南:我曾经在唤醒后忘记恢复外设时钟配置,导致UART通信乱码。排查了整整两天才发现是时钟树没恢复。记住:进入低功耗前保存什么,唤醒后就要恢复什么,一个都不能少。

3.4.2 空闲任务与低功耗的协同

空闲任务和低功耗管理的协同,说白了就是回答三个问题:

  • 什么时候睡?——空闲任务运行时
  • 睡多深?——根据下次唤醒时间决定
  • 怎么醒?——配置好中断源

我画了一张流程图,帮你理清这个逻辑:

空闲任务低功耗决策流程 空闲任务低功耗决策流程 空闲任务开始 检查是否有任务就绪 是 返回调度器 否 计算下次唤醒时间 选择低功耗模式 进入低功耗模式 关键决策点 根据下次唤醒时间 动态选择睡眠深度 唤醒后恢复 恢复外设时钟 恢复寄存器状态

3.5 实战中的避坑指南

做低功耗任务调度这些年,我踩过不少坑。挑几个典型的说说:

技巧:在空闲钩子里不要做任何耗时操作。我曾经见过有人在空闲钩子里打印调试信息,结果系统功耗反而比正常运行时还高。空闲钩子应该是“秒进秒出”的。

  • 坑1:中断唤醒后直接返回空闲任务

    唤醒后应该重新调度,而不是直接回到空闲任务。否则高优先级任务可能得不到执行。

  • 坑2:忘记处理Tick中断

    进入深度睡眠时,SysTick可能停止。唤醒后需要重新校准系统Tick,否则时间就乱了。

  • 坑3:外设状态不一致

    进入低功耗前,确保所有外设都处于安全状态。比如DMA正在传输时进入睡眠,数据就丢了。

我曾经踩过的坑:在一个电池供电的传感器项目中,我用了停止模式,功耗降到了10μA以下,看起来完美。结果产品量产1000台后,有200台在低温环境下无法唤醒。排查发现是低温导致晶振起振时间变长,唤醒代码里没有等待晶振稳定。从那以后,我在所有低功耗代码里都加了超时重试机制。

3.6 性能与功耗的平衡

低功耗不是越低越好,关键是要找到平衡点。我一般用这个公式来评估:

平均功耗 = (工作时间 × 工作功耗 + 空闲时间 × 空闲功耗) / 总时间

你想想看,如果频繁进入深度睡眠,虽然空闲功耗低了,但唤醒延迟增加了,工作时间变长,平均功耗可能反而更高。所以,选择低功耗模式要结合任务的时间特性。

我个人习惯的做法是:在系统启动时跑一段功耗测试,记录不同模式下的功耗和唤醒延迟,然后根据实际任务负载动态调整。说白了,就是让系统自己学会“聪明地睡觉”。