2. 设备树基础

设备树(Device Tree)是 Linux 内核用于描述硬件拓扑与资源分配的核心机制。它把“板级硬件信息”从内核代码中剥离出来,使同一份内核镜像可以适配多种硬件平台。理解设备树,首先要厘清四个关键角色:DTS、DTSI、DTC、DTB,它们分别对应“源文件、公共头文件、编译器、二进制产物”,构成一条清晰的编译链路。

一、四个核心对象的关系

DTS(Device Tree Source)是设备树源文件,通常以 .dts 结尾,描述某一块具体板子的硬件。DTSI(Device Tree Source Include)是设备树包含文件,通常以 .dtsi 结尾,用于存放 SoC 级或系列板卡共用的节点定义,类似 C 语言的 .h 头文件。DTC(Device Tree Compiler)是设备树编译器,负责把人类可读的 DTS 编译成内核可解析的 DTB。DTB(Device Tree Blob)是编译产物,以二进制形式被 bootloader 加载并传递给内核。

它们之间的转换关系可以概括为:DTS + DTSI →(DTC 编译)→ DTB →(bootloader 传递)→ 内核解析。DTSI 通过 #include 被 DTS 引入,DTC 在编译时先做预处理展开包含关系,再生成扁平化的 DTB。因此,调试设备树时,我们面对的是源码,而内核真正读取的是 DTB,二者之间隔着一次编译。

设备树编译链路与对象关系 DTSI SoC/系列公共定义 .dtsi 头文件 DTS 具体板级描述 .dts 源文件 #include DTC 设备树编译器 预处理 + 编译 DTB 二进制设备树 .dtb 产物 编译 Linux 内核 解析 DTB 并匹配驱动 bootloader 传递 调试反查 dtc -I dtb -O dts

二、设备树语法:节点、属性与标签

设备树采用一种类似 JSON 的层级结构,最外层是根节点 /,内部由若干子节点组成。每个节点用大括号包裹,节点名后可以跟一个“单元地址”,例如 uart0: serial@10000000。其中 uart0 是标签(label),serial 是节点名,@10000000 是寄存器基地址,用于区分同类型的不同实例。

属性(property)是节点的键值对,描述该硬件的具体参数。常见属性包括:compatible(驱动匹配字符串)、reg(寄存器地址范围)、interrupts(中断号)、status(使能状态)、clocks(时钟引用)等。属性值可以是字符串、32 位整数数组、字节序列或空值。

/ {
    model = "MyBoard";
    compatible = "vendor,myboard";

    uart0: serial@10000000 {
        compatible = "snps,dw-apb-uart";
        reg = <0x10000000 0x1000>;
        interrupts = <10>;
        clocks = <&clk_uart0>;
        status = "okay";
    };
};

标签与引用机制是设备树最实用的设计之一。标签写在节点名前,形如 uart0:,之后可以在其他节点中用 &uart0 引用该节点。引用可以用于追加属性、覆盖原有配置,或作为 phandle 被其他属性引用。例如 clocks = <&clk_uart0>; 就是把时钟节点的句柄赋给 uart0 的 clocks 属性。

三、DTSI 与 DTS 的协作方式

实际项目中,SoC 厂商会提供一份 soc.dtsi,里面定义 CPU、中断控制器、GPIO、I2C、SPI 等所有片上外设节点,但多数外设默认 status = "disabled"。板级 DTS 通过 #include "soc.dtsi" 引入这些定义,然后只针对本板实际使用的外设进行覆盖:

#include "soc.dtsi"

&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
};

&i2c1 {
    status = "okay";
    clock-frequency = <400000>;
};

这种“公共定义 + 板级覆盖”的模式,使同一 SoC 的多个板卡可以共享大量节点定义,只维护差异部分。调试时如果发现某个外设不工作,首先要确认它在最终 DTB 中的 status 是否被正确覆盖为 okay。

四、编译、反编译与调试命令

DTC 既可以把 DTS 编译为 DTB,也可以把 DTB 反编译回 DTS,这是排查设备树问题的关键手段。常用命令如下:

操作 命令 说明
编译 DTS → DTB dtc -I dts -O dtb -o board.dtb board.dts 最基础的编译方式
反编译 DTB → DTS dtc -I dtb -O dts -o board.dts board.dtb 查看内核实际使用的设备树
查看当前运行设备树 ls /proc/device-tree/ 内核解析后的节点目录
查看节点属性 hexdump -C /proc/device-tree/serial@10000000/reg 确认 reg 实际值
检查 status cat /proc/device-tree/serial@10000000/status 确认外设是否使能
在调试阶段,建议同时保留 DTS 源码和反编译出的 DTS,对比二者差异。很多“节点写了但没生效”的问题,根源在于 DTSI 中的默认值覆盖了板级修改,或者 #include 顺序导致后定义覆盖先定义。

五、常见误区与复盘

  • 误区一:改了 DTS 但没重新编译 DTB。内核读取的是 DTB,不是 DTS。修改后必须重新编译并更新 bootloader 加载的 DTB 文件。
  • 误区二:标签名与节点名混淆。标签用于引用,节点名用于层级展示,二者可以不同。引用时用 &label,不是 &node-name。
  • 误区三:忽略 status 默认值。SoC dtsi 中大量外设默认 disabled,板级 DTS 必须显式覆盖为 okay。
  • 误区四:reg 地址与手册不符。reg 的第一个值通常是基地址,第二个是长度,单位是字节,不是寄存器个数。
  • 误区五:引用节点不存在。使用 &xxx 时,若标签未定义,DTC 会报错或生成无效 phandle,导致驱动无法匹配。
当驱动 probe 失败时,优先检查三件事:compatible 是否与驱动 of_match_table 一致;status 是否为 okay;reg、interrupts、clocks 等资源属性是否完整且地址正确。这三项覆盖了大多数设备树层面的故障。

掌握 DTS、DTSI、DTC、DTB 的关系,理解节点、属性、标签与引用的语法规则,是后续进行设备树调试与故障定位的基础。只有清楚“源码如何变成内核读取的二进制”,才能在出现问题时准确判断是编译环节、覆盖环节还是解析环节出了偏差。