4. 设备树与STM32MP外设描述

在 STM32MP1 平台上,Linux 内核启动后能否正确识别一个外设,往往不取决于驱动代码本身,而取决于设备树(Device Tree)是否把“硬件长什么样、接在哪个控制器上、用哪些引脚、由哪个时钟和电源域供电”描述清楚。设备树把板级硬件信息从内核源码中剥离出来,同一份 STM32MP1 驱动可以适配不同底板,只要设备树描述不同即可。本节围绕设备树语法、节点与属性、STM32MP 设备树文件组织、pinctrl 配置、时钟与电源域描述,以及如何使能外设并验证这条主线展开。

一、设备树语法:节点、属性与引用

设备树源文件(.dts/.dtsi)本质是一棵有层级的节点树。每个节点用 node-name@unit-address 命名,花括号内是属性。属性由“键 = 值”构成,值可以是字符串、32 位整数数组、字节序列或空(布尔属性)。例如一个 I2C 控制器节点:

i2c1: i2c@40012000 {
    compatible = "st,stm32mp15-i2c";
    reg = <0x40012000 0x400>;
    clocks = <&rcc I2C1_K>;
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&i2c1_pins_a>;
    pinctrl-1 = <&i2c1_pins_sleep_a>;
    status = "disabled";
};

其中 i2c1: 是标签(label),供其它节点用 &i2c1 引用;compatible 是驱动匹配的关键,内核用它找到对应驱动;reg 描述寄存器基地址与长度;status 控制节点是否生效。理解“标签—引用—覆盖”机制是读懂 STM32MP 设备树的前提:.dtsi 定义 SoC 内部控制器,.dts 通过引用标签追加或覆盖属性,从而描述具体底板。

二、STM32MP 设备树文件组织

STM32MP1 的设备树采用分层组织,典型目录为 arch/arm/boot/dts/。核心文件包括:stm32mp151.dtsi 描述 SoC 内部外设控制器;stm32mp157.dtsi 在 151 基础上增加 GPU、DSI 等;stm32mp15-pinctrl.dtsi 集中定义引脚复用组;stm32mp15xx-dkx.dtsi 描述 Discovery 底板公共部分;最终板级文件如 stm32mp157a-dk1.dts 通过 #include 组合并覆盖。这种“SoC 层—封装层—底板层—板级层”的划分,使同一 SoC 的不同产品只需维护少量板级差异。

STM32MP1 设备树分层组织与引用关系 stm32mp151.dtsi SoC 内部控制器定义 stm32mp157.dtsi 增加 GPU / DSI 等 stm32mp15-pinctrl.dtsi 引脚复用组集中定义 stm32mp15xx-dkx.dtsi Discovery 底板公共描述 stm32mp157a-dk1.dts 板级最终组合与覆盖 编译生成 .dtb 内核启动时解析为设备树 驱动匹配 compatible → probe 外设被识别并注册到内核

三、pinctrl 配置:引脚复用的关键

STM32MP1 的每个引脚可复用为多种功能,pinctrl 子系统负责在设备树中描述“哪个引脚、什么功能、什么电气属性”。配置分为两部分:引脚组定义在 stm32mp15-pinctrl.dtsi 中,控制器节点通过 pinctrl-0 引用。例如为 I2C1 定义引脚组:

&pinctrl {
    i2c1_pins_a: i2c1-0 {
        pins {
            pinmux = <STM32_PINMUX('F', 14, AF5)>,
                     <STM32_PINMUX('F', 15, AF5)>;
            bias-disable;
            drive-open-drain;
            slew-rate = <0>;
        };
    };
};

这里 STM32_PINMUX('F',14,AF5) 表示 PF14 引脚选择复用功能 5(I2C1_SCL)。bias-disable 关闭上下拉,drive-open-drain 设为开漏输出,符合 I2C 总线要求。若引脚配置错误,常见现象是总线无波形或通信失败,因此 pinctrl 是外设能否工作的第一道关卡。

四、时钟与电源域描述

STM32MP1 外设要工作,必须有时钟供给和电源域使能。设备树中通过 clocks 属性引用 RCC 时钟,通过 power-domains 引用电源域。例如:

clocks = <&rcc I2C1_K>;
clock-names = "i2c1_k";
power-domains = <&pd_core>;
status = "okay";

其中 I2C1_K 是内核时钟,pd_core 是核心电源域。若时钟未使能,外设寄存器可能读回全 0 或访问超时;若电源域未打开,则外设根本无法访问。因此调试外设时,应同时检查 clocks、power-domains 和 status 三项。

五、使能外设并验证

使能外设的标准做法是在板级 .dts 中引用节点并设置 status = "okay",同时补充底板相关属性。以 I2C1 为例:

&i2c1 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&i2c1_pins_a>;
    pinctrl-1 = <&i2c1_pins_sleep_a>;
    status = "okay";
};

编译设备树后,可通过以下方式验证:查看 /proc/device-tree/ 下对应节点是否存在;使用 dmesg | grep i2c 观察驱动 probe 日志;用 i2cdetect -y 1 扫描总线上的从设备地址。若节点存在但驱动未 probe,应检查 compatible 是否与驱动匹配;若 probe 失败,应检查时钟、电源域和引脚配置。通过“设备树描述—内核解析—驱动匹配—用户空间验证”这条链路,可以系统定位外设不工作的原因。

调试建议:修改设备树后务必重新编译 .dtb 并更新启动分区;使用 fdtdump 或 dtc -I dtb -O dts 反编译确认最终生效的设备树内容,避免“改了源码但没生效”的常见问题。