2、电源管理子系统初始化:PM子系统初始化流程、设备树中的电源管理配置、电源管理设备驱动注册
好,咱们接着聊电源管理子系统的初始化。
说实话,这部分内容我刚开始接触Zephyr时也绕了不少弯路。你想想看,一个系统要管理功耗,首先得知道自己能管什么、怎么管。这就涉及到初始化流程、设备树配置,还有驱动注册这三个核心环节。
2.1 PM子系统初始化流程
Zephyr的PM子系统初始化,说白了就是一套「先搭架子,再填内容」的过程。系统启动时,内核会按固定顺序调用各个初始化函数。
我个人习惯把初始化流程分成三个阶段:
- 早期初始化:PM子系统的基础数据结构被创建。比如状态机、约束列表、通知链等。
- 设备注册阶段:各设备驱动通过宏注册自己的PM回调函数。
- 后期就绪:系统进入空闲循环后,PM子系统才开始真正工作。
具体代码入口在哪里?我带你看看关键部分。
// 在 pm.c 中,初始化函数如下:
static int pm_init(const struct device *dev)
{
// 初始化状态机
k_mutex_init(&pm_mutex);
// 初始化约束列表
sys_slist_init(&pm_constraints);
// 注册系统通知
pm_notifier_register(&pm_sys_notifier);
return 0;
}
// 通过 SYS_INIT 宏注册为 POST_KERNEL 级别
SYS_INIT(pm_init, POST_KERNEL, CONFIG_PM_INIT_PRIORITY);
这里有个细节要注意:CONFIG_PM_INIT_PRIORITY 的默认值通常是 50。为什么是这个数?我当初也纳闷。后来翻代码发现,它比大部分设备驱动的初始化优先级低,但又比应用层高。这样能保证设备驱动先注册好,PM子系统再接管。
核心要点:PM子系统初始化不负责具体设备的电源管理,它只搭建管理框架。真正的「干活」是在设备驱动里。
2.2 设备树中的电源管理配置
设备树(Device Tree)在Zephyr里扮演的角色,我理解就是「硬件配置清单」。电源管理相关的配置,主要写在两个地方:
- 节点属性:比如
power-domain、regulator、wakeup-source - 绑定文件:YAML格式,定义这些属性怎么解析
举个例子,一个典型的UART设备节点,带电源管理配置:
uart0: serial@40002000 {
compatible = "vendor,uart";
reg = <0x40002000 0x1000>;
interrupts = <5 0>;
// 电源管理相关
power-domain = <&pwr_domain_always_on>;
wakeup-source;
// 时钟和电压
clocks = <&clk_uart0>;
regulator = <®_vuart>;
};
这里 power-domain 指定了该设备属于哪个电源域。wakeup-source 告诉系统:这个设备可以唤醒系统。我在项目中遇到过一个问题:某个传感器明明支持唤醒,但系统就是睡不醒。查了半天,发现是设备树里漏写了 wakeup-source 属性。
避坑指南:设备树中的电源管理属性,不是写了就生效的。驱动代码里必须调用对应的API去解析这些属性。我曾经以为设备树配好了就万事大吉,结果功耗死活降不下来——原来是驱动根本没读 power-domain 属性。
另外,Zephyr 3.0以后引入了 pm_device 结构体,设备树中的配置会直接映射到这个结构体里。你可以在驱动中通过 DEVICE_DT_DEFINE 宏来获取这些信息。
2.3 电源管理设备驱动注册
驱动注册这块,是PM子系统初始化的重头戏。每个设备驱动需要告诉PM子系统:我能进入什么状态?怎么进入?怎么退出?
Zephyr提供了一套标准的注册机制。我习惯用 PM_DEVICE_DT_DEFINE 宏,它把设备树节点和PM回调函数绑定在一起。
// 定义一个PM回调函数
static int uart_pm_control(const struct device *dev,
enum pm_device_action action)
{
switch (action) {
case PM_DEVICE_ACTION_SUSPEND:
// 关闭UART时钟,保存寄存器
uart_suspend(dev);
break;
case PM_DEVICE_ACTION_RESUME:
// 恢复时钟,恢复寄存器
uart_resume(dev);
break;
case PM_DEVICE_ACTION_TURN_ON:
// 完全上电
uart_power_on(dev);
break;
case PM_DEVICE_ACTION_TURN_OFF:
// 完全断电
uart_power_off(dev);
break;
default:
return -ENOTSUP;
}
return 0;
}
// 注册设备,绑定PM回调
PM_DEVICE_DT_DEFINE(uart0, uart_pm_control);
嗯,这里要注意:PM_DEVICE_DT_DEFINE 宏会自动从设备树中读取 power-domain 等信息。如果你的设备树里没配这些,那这个宏就退化成普通设备注册,PM功能就没了。
我刚开始做低功耗设计时,犯过一个低级错误:把所有设备的PM回调都写成了空函数。结果系统进入睡眠后,UART还在耗电。你想想看,这跟没做电源管理有什么区别?
个人经验:调试PM驱动注册时,可以打开 CONFIG_PM_DEVICE_RUNTIME 和 CONFIG_PM_DEVICE_DEBUG。这样系统会在控制台打印每个设备的PM状态变化。我第一次看到打印信息时,才发现有个SPI设备根本没注册PM回调。
最后,我整理了一个表格,方便你对照不同注册方式的适用场景:
| 注册方式 | 适用场景 | 设备树依赖 | 备注 |
|---|---|---|---|
PM_DEVICE_DT_DEFINE |
有设备树节点的设备 | 强依赖 | 推荐方式,自动解析属性 |
PM_DEVICE_DEFINE |
无设备树节点的设备 | 无依赖 | 需要手动指定电源域 |
pm_device_init_simple |
简单设备,不需要回调 | 可选 | 仅用于标记设备存在 |
说白了,PM子系统初始化就是三件事:框架搭好、配置读对、驱动注册全。这三步走扎实了,后面的低功耗设计才有根基。我见过不少项目,一上来就调睡眠策略,结果发现设备根本就没注册PM回调——那功耗能降下来才怪。
好,这一节就到这里。下一节咱们聊聊具体的电源状态切换流程,那才是真正「动刀子」的地方。