4、设备树中的电源管理:电源域、regulator、operating-points-v2、power-domains 属性详解
设备树,说白了就是一块“硬件描述板”。你告诉内核,我这颗芯片上哪个模块用多少电、谁给谁供电、电压怎么调。电源管理相关的属性,就是这块板子上最关键的几根“电线”。
我个人习惯把设备树里的电源描述分成三层:供电来源(regulator)、电压频率表(operating-points-v2)、电源域归属(power-domains)。这三层搞清楚了,移植电源管理框架就成功了一半。
4.1 regulator:谁在供电?
regulator 就是“稳压器”。它可以是芯片内部的 LDO,也可以是板子上的 PMIC。设备树里用 regulator-* 属性来描述它的行为。
举个例子,我做过一个项目,CPU 核心供电来自一个叫 vdd_core 的 regulator:
vdd_core: regulator-vdd-core {
compatible = "regulator-fixed";
regulator-name = "vdd_core";
regulator-min-microvolt = <800000>;
regulator-max-microvolt = <1200000>;
regulator-always-on;
regulator-boot-on;
};
这里要注意几个点:
regulator-min-microvolt和regulator-max-microvolt定义了可调范围。如果硬件只支持固定电压,这两个值设成一样就行。regulator-always-on表示这个 regulator 永远不掉电。我建议只在关键电源上用它,比如 DDR 供电。普通外设别乱加,否则系统永远进不了深度睡眠。regulator-boot-on表示 bootloader 阶段已经打开了,内核不需要再操作硬件。这个属性能省掉一次 I2C 通信,对启动时间敏感的项目很有用。
duplicate regulator name。建议用 regulator-xxx 的命名格式,一目了然。
4.2 operating-points-v2:电压频率表
operating-points-v2 描述的是“在某个频率下,需要多少电压”。这是 DVFS(动态电压频率调整)的核心数据。
我习惯把它理解成一张二维表:
cpu_opp_table: opp-table {
compatible = "operating-points-v2";
opp-shared;
opp-200000000 {
opp-hz = /bits/ 64 <200000000>;
opp-microvolt = <900000>;
};
opp-400000000 {
opp-hz = /bits/ 64 <400000000>;
opp-microvolt = <1000000>;
};
opp-800000000 {
opp-hz = /bits/ 64 <800000000>;
opp-microvolt = <1100000>;
};
};
这里有几个关键点:
opp-shared表示这些 OPP 是共享的。比如双核 CPU,两个核共用一个电压域,那这个标志必须加。否则内核会以为每个核可以独立调压,结果就是电压打架。opp-hz的单位是 Hz,但用/bits/ 64强制 64 位。为什么?因为 32 位最大只能表示 4.29GHz,现在很多芯片的 turbo 频率已经超过这个值了。嗯,提前留好余量。opp-microvolt可以是一个值,也可以是三个值(最小、典型、最大)。我一般只填典型值,因为芯片出厂时已经做了 trim,偏差不大。
opp-xxx 节点里加一个 opp-suspend 属性,表示这个 OPP 是系统挂起时用的。这样内核在进入 suspend 时会自动切到这个频率,避免醒来时电压不够。
4.3 power-domains:谁管谁?
power-domains 描述的是“电源域”的层级关系。一个电源域可以控制一组模块的开关,比如 GPU 域、ISP 域、DDR 域。
设备树里用 power-domains 属性来绑定:
gpu: gpu@13000000 {
compatible = "arm,mali-g72";
reg = <0x13000000 0x10000>;
power-domains = <&pd_gpu>;
operating-points-v2 = <&gpu_opp_table>;
};
这里 &pd_gpu 指向一个电源域控制器节点:
pd_gpu: power-domain@10002000 {
compatible = "mediatek,power-controller";
reg = <0x10002000 0x1000>;
#power-domain-cells = <0>;
clocks = <&topckgen CLK_TOP_GPU>;
};
我个人觉得,power-domains 最难理解的是“嵌套”。比如一个 SoC 里,整个芯片有一个顶层电源域,下面分 CPU 域、GPU 域、多媒体域。多媒体域里又分 ISP 域、VPU 域。这种层级关系在设备树里用 power-domains 属性链起来:
pd_multimedia: power-domain@10003000 {
compatible = "mediatek,power-controller";
reg = <0x10003000 0x1000>;
#power-domain-cells = <0>;
power-domains = <&pd_top>; // 父域是顶层电源域
};
pd_isp: power-domain@10004000 {
compatible = "mediatek,power-controller";
reg = <0x10004000 0x1000>;
#power-domain-cells = <0>;
power-domains = <&pd_multimedia>; // 父域是多媒体域
};
为什么要嵌套?你想想看,如果 ISP 要上电,必须先保证多媒体域已经上电。内核在操作电源域时,会先递归打开父域,再打开子域。关电时顺序反过来。这个逻辑在 pm_domain 框架里已经实现了,你只需要在设备树里把关系描述清楚。
4.4 三者如何配合?
我画了一张图,帮你理清这三者的关系:
实际工作中,这三者是这样配合的:
- 内核在启动时,解析设备树,建立 regulator、OPP 表、power-domain 的拓扑结构。
- 当某个设备要改变频率时(比如 CPU 从 200MHz 升到 800MHz),内核先查 OPP 表,找到对应的电压值。
- 然后通过 regulator 接口设置电压。
- 最后通过 power-domain 接口确保该域已经上电。
嗯,这里要注意:如果 regulator 和 power-domain 之间有冲突(比如 regulator 要求先上电再调压,但 power-domain 要求先调压再上电),那就要在驱动里做时序控制。我遇到过这种问题,最后是在 struct dev_pm_opp 里加了一个 set_voltage_first 标志才搞定。
/sys/kernel/debug/regulator/ 确认电压对不对,再看 /sys/kernel/debug/opp/ 确认 OPP 表有没有加载成功。
好了,这一章的内容就到这里。设备树里的电源管理属性,说白了就是告诉内核三件事:谁供电、供多少电、谁管谁。把这三点理清楚,移植电源管理框架就不会抓瞎。