4. 电源管理配置与Kconfig

电源管理,说白了就是让设备在“干活”和“歇着”之间找到最佳平衡点。很多工程师一上来就盯着芯片的数据手册,看各种低功耗模式的电流数值,结果代码一跑,功耗根本下不来。为什么?因为光有硬件能力不够,你得让操作系统知道什么时候该睡、睡多深、怎么叫醒。

Zephyr 的电源管理子系统,就是干这个活的。它帮你把硬件 PM 能力抽象成一套统一的框架。我个人习惯是先理解它的状态机,再去看策略配置,这样思路会清晰很多。

4.1 Zephyr PM 状态机:从清醒到深睡

Zephyr 定义了一套标准电源状态,从全速运行到几乎完全断电。每个状态对应不同的功耗和唤醒延迟。我把它整理成了一张表,方便你对照:

状态 枚举值 CPU 状态 外设状态 RAM 保持 典型唤醒时间
Active PM_STATE_ACTIVE 运行 全开 是 N/A
Idle PM_STATE_IDLE WFI/WFE 可配置 是 ~1 µs
Standby PM_STATE_STANDBY 关闭 大部分关闭 是 ~100 µs
Suspend-to-RAM PM_STATE_SUSPEND_TO_RAM 关闭 关闭 是 ~1 ms
Suspend-to-Disk PM_STATE_SUSPEND_TO_DISK 关闭 关闭 否 ~100 ms
Off PM_STATE_OFF 断电 断电 否 重启

嗯,这里要注意:不是所有芯片都支持全部状态。比如一些低成本的 MCU,可能只有 Active 和 Idle 两个状态。Zephyr 的做法是,你通过设备树或 Kconfig 告诉系统你的硬件支持哪些状态,框架会自动适配。

核心观点:状态切换不是你想切就能切。系统会检查是否有任务在运行、是否有锁阻止睡眠。只有所有条件都满足,才会进入更深层的睡眠。

4.2 PM 策略配置:谁来决定何时睡?

状态机是骨架,策略才是灵魂。Zephyr 提供了两种策略模式,你可以根据产品需求来选择。

4.2.1 主动策略 (Active Policy)

应用层主动调用 pm_state_force() 来强制进入某个状态。这种方式控制力最强,但需要开发者对业务逻辑非常清楚。我曾在做一个数据采集器时用过这个模式——采集完一轮数据后,直接强制进入 Standby,等 RTC 闹钟唤醒。

#include <zephyr/pm/pm.h>

void data_collection_done(void)
{
    /* 强制进入 Standby 状态 */
    pm_state_force(PM_STATE_STANDBY);
    /* 系统将在下一次 idle 时进入 Standby */
}

4.2.2 被动策略 (Passive Policy)

这是默认模式。系统在 idle 线程中自动决策,根据当前的约束条件选择最深的可用状态。你只需要配置好约束条件就行。

约束条件通过 pm_constraint 机制来管理。比如,某个外设正在工作,它就可以加一个锁,阻止系统进入太深的状态。

#include <zephyr/pm/pm.h>

void uart_tx_start(void)
{
    /* 阻止系统进入 Standby 及更深状态 */
    pm_constraint_set(PM_STATE_STANDBY);
}

void uart_tx_done(void)
{
    /* 释放约束 */
    pm_constraint_clear(PM_STATE_STANDBY);
}

我的经验:被动策略适合大多数场景。你只需要在关键外设操作前后加约束,剩下的交给系统。但要注意,约束加多了,系统就永远睡不深,功耗下不来。

4.3 设备电源管理:每个外设都要管

系统级睡眠是一方面,每个外设的独立电源管理同样重要。Zephyr 通过设备树中的 pm_device 属性来控制外设的运行时电源。

举个例子,一个 SPI 外设,平时不用时可以关掉时钟甚至断电。Zephyr 的 PM 框架会在系统进入低功耗状态前,依次调用每个设备的 pm_device_action 回调。

static int my_spi_pm_action(const struct device *dev,
                            enum pm_device_action action)
{
    switch (action) {
    case PM_DEVICE_ACTION_SUSPEND:
        /* 关闭 SPI 时钟,保存上下文 */
        break;
    case PM_DEVICE_ACTION_RESUME:
        /* 恢复时钟,恢复上下文 */
        break;
    default:
        return -ENOTSUP;
    }
    return 0;
}

PM_DEVICE_DEFINE(my_spi, my_spi_pm_action);

我曾经踩过一个坑:某个 I2C 传感器在 Suspend 后,唤醒时没有重新初始化,导致读数全是 0。后来才发现,它的 PM 回调里只关了时钟,没保存寄存器状态。嗯,唤醒后的恢复逻辑一定要写完整。

4.4 状态机与策略的完整流程

为了让你更直观地理解整个 PM 流程,我画了一张图。这张图展示了从系统空闲到进入睡眠,再到被唤醒的完整路径。

Zephyr PM 状态机与策略流程 系统运行 (Active) Idle 线程触发 检查约束条件 有约束?保持当前状态 选择最深可用状态 调用设备 PM 回调 进入睡眠状态 唤醒事件发生 恢复设备上下文 唤醒后,系统从设备 PM 回调恢复 然后回到 Active 状态

避坑提醒:我曾经在一个项目中,设备 PM 回调里做了延时操作,结果导致系统唤醒时间变长,影响了实时性。记住,PM 回调里不要做耗时操作,尤其是不能在中断上下文里调用。

4.5 配置实战:Kconfig 与设备树

理论说完了,咱们看看实际怎么配。Zephyr 的 PM 配置主要分两步:

  1. Kconfig 使能 PM 框架
  2. 设备树中声明支持的电源状态

先看 Kconfig:

# 使能电源管理框架
CONFIG_PM=y
# 使能设备运行时电源管理
CONFIG_PM_DEVICE=y
# 使能被动策略(默认)
CONFIG_PM_POLICY_DEFAULT=y

再看设备树。以 nRF52840 为例,在 nrf52840.dtsi 中定义支持的电源状态:

cpu0: cpu@0 {
    compatible = "arm,cortex-m4f";
    reg = <0>;
    #address-cells = <1>;
    #size-cells = <1>;

    power-states {
        idle: idle {
            compatible = "zephyr,power-state";
            power-state-name = "suspend-to-idle";
            min-residency-us = <100>;
            exit-latency-us = <1>;
        };
        standby: standby {
            compatible = "zephyr,power-state";
            power-state-name = "standby";
            min-residency-us = <1000>;
            exit-latency-us = <100>;
        };
    };
};

这里有两个关键参数:

  • min-residency-us:进入该状态后,至少停留多长时间才划算。如果很快就要唤醒,还不如不睡。
  • exit-latency-us:从该状态唤醒需要的时间。系统会根据这个值来判断是否值得进入。

我的建议:这两个参数一定要根据实际硬件测量来填。别照抄参考设计,不同板子、不同晶振、不同负载,数值可能差好几倍。我习惯用逻辑分析仪抓唤醒引脚,实测 exit-latency。

4.6 调试 PM:怎么看系统睡了没?

你配置了半天,怎么知道系统真的睡下去了?Zephyr 提供了几个调试手段。

第一,打开 PM 调试日志:

CONFIG_PM_LOG_LEVEL_DBG=y

第二,用 pm_stats 命令(如果使能了 Shell):

uart:~$ pm stats
State                Count    Time(ms)
Active               1        12345
Suspend-to-Idle      5        67890
Standby              2        1234

第三,也是最直接的——量电流。我每次调 PM,桌上必放一个万用表。代码说睡了,电流没降,那肯定是有地方没处理好。

嗯,说到调试,我记得有一次,系统明明进了 Standby,电流却还是 5mA。查了半天,发现是一个 GPIO 没配置成唤醒源,导致它一直在漏电。所以,硬件设计和软件配置一定要对齐,别各干各的。