3. 设备树语法与结构
设备树(Device Tree)本质上是一种用文本描述硬件拓扑与资源分配的数据结构。它由 DTC(Device Tree Compiler)编译为 DTB 二进制,再由内核在启动阶段展开为设备节点树。理解设备树语法,是跨平台迁移工程的基本功:只有看懂节点、属性、引用与覆盖规则,才能判断哪些内容可以原样搬运,哪些必须按目标平台重写。
一、核心原理:节点与属性构成树
设备树以根节点 / 为起点,向下形成父子层级。每个节点用大括号包裹,节点名通常写成 name@address 形式,其中 @ 后的地址称为 unit-address,用于区分同一类控制器的多个实例。节点内部由若干属性组成,属性采用 key = value; 的键值形式,值可以是字符串、32 位整数数组、字节序列或空值(布尔属性)。
节点名与属性名都有字符集约束:节点名建议使用小写字母、数字和短横线,属性名同样以小写字母、数字、短横线、下划线为主。属性值若为字符串,必须用双引号;若为整数,默认按大端 32 位单元解析,多个单元用尖括号包裹并以空格分隔。
/ {
model = "Acme Board V2";
compatible = "acme,board-v2", "acme,board";
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@10000000 {
compatible = "acme,uart";
reg = <0x10000000 0x1000>;
interrupts = <5 0>;
status = "okay";
};
};
上面这段代码已经包含了本章最关键的几个语法元素:根节点、子节点、label(uart0:)、compatible、reg、interrupts、status,以及 #address-cells 与 #size-cells 这类描述子节点地址格式的元属性。
二、phandle、label 与引用机制
设备树中节点之间经常需要互相引用,例如中断控制器、时钟源、GPIO 控制器、电源域等。引用有两种常见方式:phandle 与 label。
- phandle:编译后每个被引用的节点会获得一个全局唯一的 32 位整数句柄,属性值中直接写
<&uart0>这类引用,DTC 会将其转换为 phandle 数值。 - label:写在节点名前的冒号标识,如
uart0: serial@10000000,它只是源码层面的别名,便于在其它位置用&uart0引用,编译后 label 本身不进入 DTB。
引用在属性中通常表现为 phandle 数组,例如 clocks = <&clk_uart>;、interrupt-parent = <&gic>;。跨平台迁移时,label 名称可以保留,但被引用节点的实际地址、中断号、时钟 ID 往往需要按目标 SoC 修改,这是迁移中最容易出错的地方。
三、include 与 overlay:复用与增量修改
设备树源文件支持 #include 预处理指令,用于引入公共头文件或 SoC 级 dtsi。典型工程会把 SoC 内部外设定义放在 soc.dtsi,把板级差异放在 board.dts,通过 include 组合成完整设备树。这样迁移到新板卡时,只需替换板级 dts,SoC 级定义可复用。
Overlay(叠加层)则是在不重新编译基础 DTB 的前提下,对已有设备树做增量修改。它常用于 FPGA 动态加载、扩展板热插拔等场景。Overlay 通过 fragment@0 指定目标节点,再用 __overlay__ 写入新增或覆盖的属性。基础树与 overlay 的合并由内核在运行时完成,因此 overlay 中的引用必须指向基础树中已存在的 label 或 phandle。
/dts-v1/;
/plugin/;
&uart0 {
status = "okay";
pinctrl-0 = <&uart0_pins>;
};
四、常见属性语义:compatible、reg、interrupts
设备树属性众多,但迁移工作中最常打交道的核心属性集中在以下几类:
| 属性 | 语义 | 迁移关注点 |
|---|---|---|
| compatible | 驱动匹配字符串列表,按从具体到通用排列 | 目标平台驱动是否支持该 compatible,必要时追加兼容项 |
| reg | 寄存器基地址与长度,格式由父节点 #address-cells/#size-cells 决定 | 地址映射、总线宽度、地址单元数是否变化 |
| interrupts | 中断号与触发类型,具体编码由中断控制器决定 | 中断号重映射、触发方式(电平/边沿)差异 |
| interrupt-parent | 指向中断控制器节点的 phandle | 目标 SoC 中断控制器 label 与层级 |
| status | okay / disabled,控制设备是否启用 | 迁移后默认状态是否与硬件实际一致 |
| clocks / clock-names | 时钟源引用与名称 | 时钟树结构、时钟 ID 与门控关系 |
其中 compatible 是驱动匹配的第一入口。内核会用 compatible 列表逐项与驱动 of_match_table 比对,因此迁移时若目标平台没有对应驱动,即使寄存器地址写对,设备也无法工作。reg 的解析依赖父节点的 #address-cells 与 #size-cells,如果父节点这两个属性变化,子节点 reg 的单元数必须同步调整,否则会出现地址错位。
五、案例场景:从 A 板迁移到 B 板
假设原设备树中 UART0 定义为 serial@10000000,compatible 为 "acme,uart",中断号为 5。迁移到 B 板后,UART0 基地址变为 0x12000000,中断号变为 12,且 B 板使用不同的中断控制器。迁移步骤可归纳为:
- 保留 SoC 级 dtsi 中的节点结构与 label,确认目标 SoC 是否已有对应 dtsi。
- 修改板级 dts 中的 reg 地址与长度,核对父节点 #address-cells/#size-cells。
- 更新 interrupts 与 interrupt-parent,指向 B 板中断控制器 label。
- 检查 compatible 是否被目标内核驱动支持,不支持则补充兼容字符串或调整驱动。
- 用
dtc -I dtb -O dts反编译验证最终 DTB,确认无语法错误与引用悬空。
六、误区与复盘
常见误区一:把 label 当成 phandle 使用。label 只在源码中有效,编译后不存在;如果 overlay 中引用了基础树不存在的 label,合并会失败。误区二:忽略 #address-cells 与 #size-cells 的继承关系,直接复制 reg 数值,导致地址解析错误。误区三:overlay 中覆盖属性时未考虑原有属性是否应保留,直接整体替换可能丢失必要配置。误区四:compatible 写成与目标驱动不匹配的字符串,设备树语法正确但驱动不 probe。
复盘方法:每次迁移后,用 fdtdump 或 dtc 反编译目标 DTB,逐节点核对 reg、interrupts、compatible、status 四项;再结合内核启动日志中的 of_ 相关打印,确认节点是否被正确解析与匹配。只有语法正确、语义匹配、驱动可用三者同时满足,设备树迁移才算真正完成。