3、系统电源状态管理:ACTIVE / SUSPEND / OFF 的定义与切换

好,咱们进入第三章。系统电源状态管理,说白了就是回答一个问题:我的设备现在该不该睡?该睡多深?

我在做第一款低功耗物联网终端时,就踩过这个坑——芯片明明进了休眠,电流却还有 2mA。查了半天,发现是某个外设没关干净。嗯,从那以后,我对电源状态的定义和切换流程就格外较真了。

3.1 系统电源状态定义

Zephyr 把系统电源状态抽象成三个等级:ACTIVESUSPENDOFF。你想想看,这其实对应了我们日常使用电子设备时的三种场景:正在干活、待机、彻底关机。

状态 含义 CPU 状态 外设状态 RAM 保持
ACTIVE 正常运行 全速运行 按需开启
SUSPEND 浅度休眠 时钟门控或暂停 大部分关闭
OFF 深度断电 完全掉电 全部关闭

重点理解:SUSPEND 状态下,RAM 内容保留,所以唤醒后可以快速恢复现场。OFF 状态下 RAM 丢失,需要重新初始化。

我个人习惯把 SUSPEND 再细分为两级:SUSPEND_TO_IDLESUSPEND_TO_RAM。前者只停 CPU 时钟,后者连 PLL 和部分电源域都关了。Zephyr 的 pm_state 结构体里,用 pm_state_info 来描述这些细节。

3.2 状态转换机制

状态转换不是你想切就能随便切的。Zephyr 内部维护了一套严格的转换规则:

  • ACTIVE → SUSPEND:系统空闲且无任务可运行时触发
  • SUSPEND → ACTIVE:中断或唤醒事件触发
  • ACTIVE → OFF:通常由用户指令或低电量触发
  • OFF → ACTIVE:冷启动,走完整启动流程

这里有个容易忽略的点:从 SUSPEND 唤醒后,CPU 会从中断向量表重新执行,而不是从休眠点继续。所以你的 ISR 里必须处理好恢复流程。我曾经在项目里忘了恢复外设时钟,结果唤醒后 UART 死活发不出数据——排查了整整一下午。

避坑指南:千万不要在临界区(如关中断后)直接调用休眠 API。Zephyr 的电源管理框架依赖调度器来协调状态切换,关中断会导致死锁。

3.3 状态切换 API 详解

Zephyr 提供了一套简洁的 API 来控制电源状态。咱们直接看代码:

#include <zephyr/pm/pm.h>

/* 请求进入 SUSPEND 状态 */
int pm_state_force(PM_STATE_SUSPEND_TO_RAM);

/* 查询当前状态 */
enum pm_state pm_state_get(void);

/* 注册唤醒源 */
void pm_wakeup_event(struct device *dev);

实际项目中,我更推荐用 pm_policy_next_state 这个函数。它会根据系统负载自动选择最合适的休眠深度。你想想看,如果系统刚处理完一个突发任务,直接进深度休眠反而浪费了缓存里的热数据——这时候浅度休眠更合理。

我的小技巧:在调试阶段,可以用 pm_state_dump() 打印当前状态机的详细信息。这个函数会列出所有支持的电源状态、当前状态、以及上次唤醒原因。我在调一个蓝牙门锁项目时,就是靠它发现唤醒源配置错了。

3.4 实际应用中的状态机设计

讲个真实案例。我做过一个电池供电的传感器节点,要求 5 年不换电池。它的状态机是这样设计的:

  1. ACTIVE:采集数据 + 发送,持续 200ms
  2. SUSPEND:等待下一个采集周期,持续 10 秒
  3. OFF:电池电压低于 3.0V 时进入,彻底断电

这里有个关键点:SUSPEND 状态下,RTC 必须保持运行,否则没法定时唤醒。Zephyr 的 pm_device_runtime 子系统可以单独控制每个外设的电源域,我习惯把 RTC 标记为 PM_DEVICE_FLAG_KEEP_ACTIVE

/* 在设备树中标记 RTC 为常开 */
rtc0: rtc@40002000 {
    compatible = "vendor,rtc";
    reg = <0x40002000 0x1000>;
    pm-keep-active;
};

嗯,这里要注意:pm-keep-active 属性只在支持电源域管理的 SoC 上有效。如果你用的是 STM32L0 这类简单芯片,可能得手动在应用层保证 RTC 不被关掉。

3.5 状态切换的时序约束

最后聊一个容易被忽视的点:状态切换的时序。从 ACTIVE 到 SUSPEND,Zephyr 会依次执行:

  • 通知所有注册的电源管理回调
  • 暂停调度器
  • 关闭外设时钟
  • 进入 WFI/WFE 指令

整个过程必须在 微秒级 完成。如果你的某个外设驱动在回调里做了延时操作(比如等待硬件就绪),那整个系统的休眠延迟就会被拖长。我见过一个项目,就因为 SPI 驱动在休眠回调里加了 10ms 的忙等,导致系统平均功耗飙升了 30%。

经验总结:电源管理回调里不要做任何阻塞操作。如果必须等待硬件,用 k_busy_wait() 替代 k_sleep(),或者干脆把硬件关闭操作放到设备驱动自己的电源管理接口里。

好了,这一章的内容就到这里。记住:电源状态管理不是简单的「睡或不睡」,而是要在性能、功耗、响应速度之间找到平衡点。下一章我们会深入设备电源管理,看看怎么单独控制每个外设的功耗。