4、设备树绑定:PMU设备树节点定义、regulator节点、power-domain节点、clock节点

设备树绑定,说白了就是告诉内核:你的PMU长什么样、接在哪个总线上、控制哪些电源轨。我刚开始接触PMU驱动时,总觉得设备树就是写几个属性嘛,没什么技术含量。直到有一次,一个regulator节点少了个regulator-always-on,系统休眠后就再也唤不醒了……嗯,从那以后我再也不敢小看设备树绑定了。

4.1 PMU设备树节点定义

一个典型的PMU节点,通常挂载在I2C或SPI总线下。我个人习惯把PMU节点放在i2cspi总线的子节点中,这样内核可以自动完成设备枚举。

// 一个典型的PMU设备树节点
&i2c3 {
    status = "okay";
    clock-frequency = <400000>;

    pmic: pmic@3c {
        compatible = "myvendor,pmu-axp313";
        reg = <0x3c>;
        interrupt-parent = <&gpio1>;
        interrupts = <20 IRQ_TYPE_LEVEL_LOW>;
        interrupt-controller;
        #interrupt-cells = <1>;

        // 内部regulator子节点
        regulators {
            // ... 见下文
        };

        // 电源域子节点
        power-domains {
            // ... 见下文
        };
    };
};

这里有几个关键点:

  • compatible:驱动匹配的关键字符串,必须与驱动中of_device_id表一致。我见过有人把大小写写错,结果驱动死活加载不上。
  • reg:I2C设备地址或SPI片选号。注意不要和总线上其他设备冲突。
  • interrupts:PMU的中断引脚。PMU通常通过中断通知系统:电压异常、温度过高、电池电量低等。
  • interrupt-controller#interrupt-cells:如果PMU内部有多个中断源(比如每个regulator都有自己的过流保护中断),就需要声明这个PMU本身是一个中断控制器。
注意: 我曾经遇到一个坑——PMU的I2C地址是0x3c,但另一个设备也用了0x3c。结果两个设备都工作异常。所以一定要确认地址唯一性,尤其是多路复用器后面的设备。

4.2 regulator节点

regulator节点是PMU设备树中最核心的部分。每个regulator代表一个电压输出通道,比如DCDC1、LDO2等。我建议把所有的regulator放在一个regulators子节点下,这样结构清晰。

regulators {
    // DCDC1: 核心供电,1.1V
    dcdc1: dcdc1 {
        regulator-name = "vdd_core";
        regulator-min-microvolt = <1000000>;
        regulator-max-microvolt = <1300000>;
        regulator-always-on;
        regulator-boot-on;
        regulator-ramp-delay = <2500>;  // 单位: uV/us
    };

    // LDO2: IO口供电,3.3V
    ldo2: ldo2 {
        regulator-name = "vdd_io";
        regulator-min-microvolt = <3300000>;
        regulator-max-microvolt = <3300000>;
        regulator-always-on;
    };

    // LDO3: 摄像头供电,可关断
    ldo3: ldo3 {
        regulator-name = "vdd_cam";
        regulator-min-microvolt = <1800000>;
        regulator-max-microvolt = <2800000>;
        regulator-state-mem {
            regulator-off-in-suspend;
        };
    };
};

这里我重点说几个容易被忽略的属性:

  • regulator-always-on:表示这个regulator永远不能关。比如DDR供电、核心供电,一旦关掉系统就挂了。我调试过一个板子,DDR供电的regulator没加这个属性,结果系统休眠后DDR掉电,醒来时内存数据全丢了。
  • regulator-boot-on:表示bootloader已经打开了这个regulator,内核不需要再操作。这可以避免启动时电压跳变。
  • regulator-ramp-delay:电压爬升速率。如果设置得太快,可能会引起过冲;太慢,系统启动时间变长。我一般参考PMU数据手册的典型值。
  • regulator-state-mem:系统进入suspend-to-mem时的状态。可以指定关断或保持电压。这个在低功耗设计中非常关键。
小技巧: 如果你不确定某个regulator是否需要always-on,可以先用regulator-boot-on,然后在内核启动后通过/sys/class/regulator/查看状态。如果发现某个regulator被意外关掉了,就加上always-on。

4.3 power-domain节点

电源域(power domain)是PMU的高级功能。一个电源域可以包含多个regulator,并且支持顺序上下电。我记得在做一个AI芯片项目时,NPU需要先上核心电压,再上IO电压,关断时顺序相反。如果没有电源域,就得在驱动里手动控制,非常麻烦。

power-domains {
    pd_npu: power-domain@0 {
        #power-domain-cells = <0>;
        domain-id = <0>;
        // 上电顺序: 先dcdc1, 再ldo2
        power-on-sequence = <&dcdc1 0>, <&ldo2 10000>;
        // 下电顺序: 先ldo2, 再dcdc1
        power-off-sequence = <&ldo2 0>, <&dcdc1 10000>;
        // 电源域所包含的regulator
        regulators = <&dcdc1>, <&ldo2>;
    };

    pd_gpu: power-domain@1 {
        #power-domain-cells = <0>;
        domain-id = <1>;
        power-on-sequence = <&dcdc2 0>, <&ldo4 5000>;
        power-off-sequence = <&ldo4 0>, <&dcdc2 5000>;
        regulators = <&dcdc2>, <&ldo4>;
    };
};

这里的关键属性:

  • #power-domain-cells:通常为0,表示这个电源域不需要额外的参数。
  • domain-id:PMU内部的电源域编号,用于寄存器操作。
  • power-on-sequencepower-off-sequence:每个条目是一个phandle加延时(微秒)。比如<&dcdc1 0>表示先打开dcdc1,延时0微秒;<&ldo2 10000>表示再打开ldo2,延时10毫秒。
注意: 顺序上下电的延时不能太短。我曾经把延时设成100微秒,结果发现ldo2还没稳定,dcdc1已经开始工作了,导致NPU启动时电压毛刺。后来参考数据手册,把延时改成了5毫秒,问题解决。

4.4 clock节点

PMU通常也提供时钟输出,比如32KHz的RTC时钟、24MHz的晶振缓冲输出等。clock节点用于描述这些时钟源。

clocks {
    // 32KHz RTC时钟
    rtc32k: rtc32k {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <32768>;
        clock-output-names = "pmu_rtc32k";
    };

    // 24MHz主时钟输出
    osc24m: osc24m {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <24000000>;
        clock-output-names = "pmu_osc24m";
    };

    // 可编程时钟输出
    pll_out: pll_out {
        compatible = "myvendor,pmu-pll-clock";
        #clock-cells = <1>;
        clocks = <&osc24m>;
        clock-output-names = "pmu_pll_out";
        // 支持的频率列表
        clock-mult = <1 2 4 8>;
    };
};

clock节点的几个要点:

  • fixed-clock:对于固定频率的时钟(如32KHz),直接用这个compatible,内核会自动注册一个固定频率时钟。
  • #clock-cells:如果为0,表示这个时钟只有一个输出;如果为1,表示有多个输出,需要通过索引选择。
  • clock-output-names:给时钟起个名字,方便其他设备引用。比如RTC驱动可以通过clocks = <&rtc32k>;来获取这个时钟。
小技巧: 如果你需要调试时钟是否正常工作,可以在内核启动后查看/sys/kernel/debug/clk/pmu_rtc32k/clk_rate。如果频率不对,多半是PMU的时钟配置寄存器没初始化。

4.5 设备树绑定的避坑指南

最后,我总结几个实际项目中容易踩的坑:

  • regulator名称冲突:不同PMU的regulator名称不能重复。比如两个PMU都有dcdc1,内核会报错。我建议加上PMU前缀,比如pmu1_dcdc1
  • 中断号错误:PMU的中断号一定要和硬件连接一致。我曾经把GPIO中断号写错了一位,结果PMU的中断一直触发不了,系统无法检测到电池低电量。
  • 电源域循环依赖:如果电源域A依赖电源域B,电源域B又依赖电源域A,内核会死锁。设计时一定要保证电源域是树形结构,不能有环。
  • 时钟频率不匹配:PMU输出的时钟频率必须和下游设备匹配。比如以太网PHY需要25MHz,你给了24MHz,网络就起不来。

嗯,设备树绑定看起来就是写几个节点,但每个属性背后都有它的设计意图。我建议你在写设备树时,手里一定要有PMU的数据手册,对照着寄存器描述来写。这样写出来的设备树,才能让驱动稳定运行。