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 与层级
statusokay / 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 板使用不同的中断控制器。迁移步骤可归纳为:

  1. 保留 SoC 级 dtsi 中的节点结构与 label,确认目标 SoC 是否已有对应 dtsi。
  2. 修改板级 dts 中的 reg 地址与长度,核对父节点 #address-cells/#size-cells。
  3. 更新 interrupts 与 interrupt-parent,指向 B 板中断控制器 label。
  4. 检查 compatible 是否被目标内核驱动支持,不支持则补充兼容字符串或调整驱动。
  5. 用 dtc -I dtb -O dts 反编译验证最终 DTB,确认无语法错误与引用悬空。
迁移时优先使用 label 引用而非硬编码 phandle,这样在地址变化时只需修改节点定义,引用处无需改动,可显著降低出错概率。

六、误区与复盘

常见误区一:把 label 当成 phandle 使用。label 只在源码中有效,编译后不存在;如果 overlay 中引用了基础树不存在的 label,合并会失败。误区二:忽略 #address-cells 与 #size-cells 的继承关系,直接复制 reg 数值,导致地址解析错误。误区三:overlay 中覆盖属性时未考虑原有属性是否应保留,直接整体替换可能丢失必要配置。误区四:compatible 写成与目标驱动不匹配的字符串,设备树语法正确但驱动不 probe。

复盘方法:每次迁移后,用 fdtdump 或 dtc 反编译目标 DTB,逐节点核对 reg、interrupts、compatible、status 四项;再结合内核启动日志中的 of_ 相关打印,确认节点是否被正确解析与匹配。只有语法正确、语义匹配、驱动可用三者同时满足,设备树迁移才算真正完成。

设备树语法结构:节点、属性、引用与覆盖 / 根节点 model / compatible / #address-cells uart0: serial@10000000 compatible = "acme,uart" reg = <0x10000000 0x1000> gic: interrupt-controller@... interrupt-controller; #interrupt-cells = <2> clk_uart: clock@... clock-frequency = <48000000> #clock-cells = <0> interrupt-parent clocks 核心属性语义 compatible 驱动匹配字符串列表 reg 地址 + 长度,受父节点约束 interrupts 中断号 + 触发类型 status okay / disabled #include 复用 soc.dtsi → SoC 级外设定义 board.dts → 板级差异 组合编译为完整 DTB overlay 增量覆盖 fragment@0 → 目标节点 __overlay__ → 新增/覆盖属性 运行时与基础树合并 label 用于源码引用,phandle 用于编译后引用;迁移时优先保留 label,按目标平台重写 reg / interrupts / compatible