第一章 Zephyr低功耗管理基础:电源管理框架概述

做嵌入式开发的朋友都知道,功耗是产品落地的硬指标。尤其搞物联网设备,电池一挂,用户体验直接归零。Zephyr的电源管理框架,说白了就是一套帮你把功耗「压到最低」的系统级方案。我个人习惯把它的核心拆成四块来看:系统电源状态、设备电源管理、低功耗定时器,还有整个框架的调度逻辑。

嗯,咱们先搭个整体认知。下面这张图是我自己梳理的框架结构,你看一眼就能明白各模块怎么配合。

Zephyr 电源管理框架总览 应用层 / 业务逻辑 电源管理核心 (Power Management Core) 策略决策 · 状态切换 · 通知回调 系统电源状态 (PM_STATE) ACTIVE / SLEEP / DEEP SLEEP / OFF 设备电源管理 (DPM) ACTIVE / SUSPEND / OFF 低功耗定时器 (LPTIMER) 唤醒源 · 周期唤醒 · 微调补偿 约束管理 (Constraints) 锁状态 · 最小延迟 · 设备依赖 硬件抽象层 (SoC / MCU 具体实现)

1.1 系统电源状态 (PM_STATE)

Zephyr 把系统电源状态定义成一套枚举,从浅睡到深睡,一共四个等级。你想想看,这跟 Linux 的 cpuidle 框架思路有点像,但更轻量。

状态 枚举值 CPU 状态 RAM 保持 典型唤醒延迟
PM_STATE_ACTIVE 0 运行 是 —
PM_STATE_SLEEP_1 1 时钟门控 是 ~10 µs
PM_STATE_SLEEP_2 2 深度睡眠 是 ~100 µs
PM_STATE_SLEEP_3 3 掉电 否 ~ms 级

我在项目中遇到过最典型的场景:一个 BLE 传感器节点,大部分时间处于 SLEEP_2,RAM 保持数据,每 500ms 被 RTC 唤醒一次,发完数据包立刻再睡回去。整机平均电流从 3mA 降到了 12µA。嗯,这里要注意——不是所有 MCU 都支持 SLEEP_3,有些芯片掉电后 RAM 内容就丢了,你得提前把关键数据刷进 Flash。

核心原则:选择哪个睡眠状态,取决于你的唤醒延迟容忍度和 RAM 保持需求。唤醒越快,功耗越高;睡得越死,唤醒越慢。

1.2 设备电源管理 (DPM)

系统睡归睡,但外设不能乱睡。比如你正在通过 UART 往外吐日志,突然把 UART 的时钟关了,数据就丢了。Zephyr 的设备电源管理,就是给每个驱动挂一套电源回调。

每个设备驱动需要实现 pm_control 回调函数,框架在切换系统状态时会逐个通知设备。设备可以拒绝进入低功耗状态——比如 SPI 总线还在传输,驱动就返回 -EBUSY。

// 设备电源回调示例
static int my_uart_pm_control(const struct device *dev,
                               enum pm_device_action action)
{
    switch (action) {
    case PM_DEVICE_ACTION_SUSPEND:
        // 关闭 UART 时钟,保存寄存器
        uart_disable(dev);
        break;
    case PM_DEVICE_ACTION_RESUME:
        // 恢复时钟,重新初始化
        uart_enable(dev);
        break;
    case PM_DEVICE_ACTION_TURN_OFF:
        // 完全掉电
        uart_power_off(dev);
        break;
    default:
        return -ENOTSUP;
    }
    return 0;
}

我个人习惯把设备分成三类:

  • 关键设备(如 RTC、PMIC)—— 永远不睡,或者只睡到最低等级。
  • 条件设备(如 I2C、SPI)—— 总线空闲时才允许 suspend。
  • 非关键设备(如 LED、调试 UART)—— 系统睡眠时直接关掉。

避坑指南:我曾经在调试一个传感器节点时,发现系统进入 SLEEP_2 后电流还是偏高。排查了两天,最后发现是 GPIO 的 pull-up 没关,电流从引脚漏出去了。设备电源管理不只是关时钟,还要管引脚状态。

1.3 低功耗定时器 (LPTIMER)

系统睡死了怎么醒?靠低功耗定时器。Zephyr 里 LPTIMER 是一个独立于 CPU 主时钟的定时器外设,通常由 32.768kHz 晶振驱动,能在睡眠状态下继续计数,到达阈值后触发中断唤醒 CPU。

它的典型用法是周期唤醒:

// 配置 LPTIMER 每 100ms 唤醒一次
struct k_timer wake_timer;

void wakeup_handler(struct k_timer *timer_id)
{
    // 这里只做最轻量的事,比如置一个标志位
    wakeup_flag = 1;
}

void main(void)
{
    k_timer_init(&wake_timer, wakeup_handler, NULL);
    k_timer_start(&wake_timer, K_MSEC(100), K_MSEC(100));

    while (1) {
        // 进入睡眠,等待 LPTIMER 唤醒
        pm_state_force(0, &(struct pm_state_info){
            .state = PM_STATE_SLEEP_2,
            .substate = 0,
        });
        // 醒来后处理任务
        if (wakeup_flag) {
            sensor_read_and_send();
            wakeup_flag = 0;
        }
    }
}

你想想看,如果不用 LPTIMER,用主定时器来唤醒,主时钟就得一直跑,功耗直接翻几倍。LPTIMER 的功耗通常在 µA 级别,而主时钟跑起来至少几十 µA。

定时器类型 时钟源 睡眠时运行 典型功耗
主定时器 (TIM) 系统主时钟 否 ~50 µA
低功耗定时器 (LPTIM) 32.768 kHz 是 ~1 µA
RTC 闹钟 32.768 kHz 是 ~0.5 µA

注意:LPTIMER 的分辨率通常不如主定时器。32.768kHz 晶振一个 tick 约 30.5µs,如果你需要微秒级的精确唤醒,LPTIMER 可能不够用。这时候得考虑用 RTC 的 sub-second 模式,或者干脆不睡那么深。

1.4 电源管理框架的调度逻辑

框架怎么决定什么时候睡、睡多深?Zephyr 的 idle 线程里跑了一套策略:

  1. 系统 idle 时,检查是否有设备持有 PM_DEVICE_ACTION_TURN_OFF 的约束锁。
  2. 如果没有约束,查询当前允许的最深睡眠状态(由 pm_state_cpu_get_allowed 决定)。
  3. 逐个通知设备进入 suspend,如果某个设备返回 -EBUSY,降级到浅一级的睡眠状态。
  4. 执行 WFI/WFE 指令进入睡眠。
  5. 被 LPTIMER 或外部中断唤醒后,逐个通知设备 resume。

这个流程看起来简单,但实际调试时坑不少。我记得有一次,系统明明进了 SLEEP_2,但电流就是降不下来。后来发现是一个 I2C 传感器驱动在 suspend 回调里没有释放总线时钟,导致外设一直处于半工作状态。

经验总结:调试低功耗时,别只盯着 CPU 的电流。用电流探头夹整板,然后逐个拔外设,看电流变化。哪个外设拔掉后电流骤降,问题就在那。