4. 设备树语法详解

设备树(Device Tree)在 Android 内核适配中承担着“硬件描述与内核解耦”的关键角色。它把 SoC 内部外设、板级连接、引脚复用、时钟与中断等硬件信息,从 C 代码中抽离为可读、可复用、可覆盖的文本结构。要真正掌握平台适配,必须先吃透 DTS 的语法骨架:节点如何定义、属性如何赋值、数据类型如何区分、标签如何引用、文件如何包含,以及条件编译如何按目标板裁剪配置。

一、节点定义:树形结构与命名规则

设备树本质上是一棵以根节点 / 为起点的树。每个节点用大括号包裹,节点名通常采用 名称@地址 的形式,其中地址部分称为 unit-address,用于区分同一总线上地址不同的同类设备。节点内部可以继续嵌套子节点,形成层级关系。

/ {
    model = "My Android Board";
    compatible = "vendor,board", "vendor,soc";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu0: cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a55";
            reg = <0x0>;
        };
    };

    uart0: serial@10000000 {
        compatible = "vendor,uart";
        reg = <0x10000000 0x1000>;
        interrupts = <10>;
        status = "okay";
    };
};

节点名应体现设备类型,unit-address 必须与 reg 属性中的首地址一致,否则内核在解析时可能无法正确匹配。根节点下的 #address-cells 与 #size-cells 决定了子节点 reg 属性中地址和长度各占几个 32 位单元,这是初学者最容易忽略却影响全局的规则。

二、属性赋值与数据类型

属性是节点的“键值对”,值可以是空、字符串、32 位整数数组、字节序列或混合形式。DTS 编译器 dtc 会根据写法推断类型,因此必须严格区分。

写法 数据类型 典型用途
status = "okay"; 字符串 设备使能/禁用
reg = <0x10000000 0x1000>; 32 位整数数组 地址与长度
mac-address = [00 11 22 33 44 55]; 字节序列 MAC 地址等原始数据
clock-names = "core", "bus"; 字符串列表 多时钟命名
interrupts = <0 10 4>; 整数数组 中断号与触发类型
vendor,flag; 空属性(布尔) 开关式标记

需要特别注意:< > 内是 32 位单元,[ ] 内是字节,字符串必须加双引号。若把地址写成字符串,内核读取 reg 时会得到错误结果。自定义属性建议加厂商前缀,如 vendor,,避免与标准属性冲突。

三、标签引用与 phandle

标签(label)写在节点名之前,形如 uart0: serial@10000000。标签本身不进入最终 DTB 的节点名,但会被 dtc 记录,供其他节点通过 &uart0 引用。引用后生成 phandle,实现节点之间的关联,例如引脚控制器、时钟、电源域和中断控制器的连接。

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

&i2c1 {
    touchscreen@38 {
        compatible = "vendor,ts";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 2>;
    };
};

这种“先定义、后引用、再覆盖”的写法,是设备树支持多板级复用的核心机制。板级 DTS 可以只写差异部分,通过 &label 追加或修改属性,而不必复制整个 SoC 节点。

四、包含机制与条件编译

DTS 使用 #include 包含头文件,使用 /include/ 包含其他 DTS/DTI 文件。典型工程会分层组织:SoC 级 .dtsi 描述片内外设,板级 .dts 描述外设连接与使能,最终由 Makefile 编译为 DTB。

#include <dt-bindings/gpio/gpio.h>
#include "soc-vendor.dtsi"
#include "board-common.dtsi"

/ {
    model = "Vendor Android Board";
    compatible = "vendor,board", "vendor,soc";
};

&uart0 {
    status = "okay";
};

条件编译则通过 C 预处理器宏实现,常用于同一份 DTS 适配多个硬件版本或 Android 变体:

#ifdef CONFIG_BOARD_REV_B
&uart0 {
    status = "disabled";
};
#endif

#ifdef CONFIG_ANDROID_VENDOR_BOOT
&firmware {
    android {
        compatible = "android,firmware";
    };
};
#endif
建议把“共性的 SoC 定义”放在 .dtsi,把“板级差异”放在 .dts,把“版本差异”交给宏或 overlay。这样既能复用,又能避免一份文件膨胀到难以维护。

五、综合关系图

下图把节点、属性、标签、包含与条件编译放在同一张关系视图中,帮助建立整体认知:

设备树语法核心关系图 节点 · 属性 · 标签 · 包含 · 条件编译 根节点 / model · compatible · #address-cells 节点定义 名称@地址 子节点嵌套 unit-address 与 reg 对应 #address-cells / #size-cells 属性赋值 字符串 "okay" 整数数组 <0x1000 0x100> 字节序列 [00 11 22] 空属性 / 字符串列表 标签引用 uart0: serial@10000000 &uart0 { ... } 生成 phandle 跨节点关联时钟/引脚/中断 包含机制 #include <dt-bindings/...> /include/ "soc-vendor.dtsi" SoC 级 .dtsi + 板级 .dts 分层复用,差异覆盖 条件编译 #ifdef CONFIG_BOARD_REV_B #ifdef CONFIG_ANDROID_... 按目标板裁剪节点 同一 DTS 适配多硬件版本 dtc 编译:DTS → DTB,内核启动时解析为 device_node 与 property 结构

六、常见误区与复盘

  • 地址与 unit-address 不一致:节点名写 serial@10000000,但 reg 写成 <0x20000000 0x1000>,会导致资源映射错误。
  • 数据类型混用:把 reg 写成字符串,或把布尔属性写成 "true",内核解析结果与预期不符。
  • 标签未定义就引用:&uart0 在包含链中找不到对应 label,dtc 直接报错。
  • 覆盖顺序错误:板级 DTS 中 &uart0 的覆盖必须出现在包含 SoC dtsi 之后,否则会被后续定义覆盖。
  • 条件编译宏未生效:宏需在编译 DTB 的 Makefile 中通过 -D 或内核配置传入,否则 #ifdef 分支不会参与编译。
调试阶段建议使用 dtc -I dtb -O dts 反编译生成的 DTB,确认最终合并结果是否符合预期。很多“驱动 probe 失败”的问题,根源都在 DTS 合并后的属性值或 status 状态上。

掌握节点、属性、标签、包含与条件编译这五个语法要素后,就能读懂绝大多数 Android 平台 DTS,并能按板级差异进行安全覆盖。后续无论是添加外设、调整引脚,还是适配 Android 特有的 firmware 与 vendor 节点,都可以在这套语法框架内完成。