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,二者之间隔着一次编译。
二、设备树语法:节点、属性与标签
设备树采用一种类似 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 |
确认外设是否使能 |
#include 顺序导致后定义覆盖先定义。
五、常见误区与复盘
- 误区一:改了 DTS 但没重新编译 DTB。内核读取的是 DTB,不是 DTS。修改后必须重新编译并更新 bootloader 加载的 DTB 文件。
- 误区二:标签名与节点名混淆。标签用于引用,节点名用于层级展示,二者可以不同。引用时用
&label,不是&node-name。 - 误区三:忽略 status 默认值。SoC dtsi 中大量外设默认 disabled,板级 DTS 必须显式覆盖为 okay。
- 误区四:reg 地址与手册不符。reg 的第一个值通常是基地址,第二个是长度,单位是字节,不是寄存器个数。
- 误区五:引用节点不存在。使用
&xxx时,若标签未定义,DTC 会报错或生成无效 phandle,导致驱动无法匹配。
compatible 是否与驱动 of_match_table 一致;status 是否为 okay;reg、interrupts、clocks 等资源属性是否完整且地址正确。这三项覆盖了大多数设备树层面的故障。
掌握 DTS、DTSI、DTC、DTB 的关系,理解节点、属性、标签与引用的语法规则,是后续进行设备树调试与故障定位的基础。只有清楚“源码如何变成内核读取的二进制”,才能在出现问题时准确判断是编译环节、覆盖环节还是解析环节出了偏差。