2. 设备树基础:设备树(DTS)语法、节点、属性、值的表示方法

设备树(Device Tree)是一种描述硬件资源的数据结构,它通过树形节点和属性将非可枚举的硬件细节传递给操作系统。在嵌入式 Linux 中,设备树源文件(.dts)经过编译生成设备树二进制(.dtb),由 Bootloader 加载并传递给内核。本章将系统讲解 DTS 的语法规则、节点结构、属性定义以及值的编码方式。

学习目标:掌握 DTS 文件的语法规范,能够独立编写或修改设备树节点;理解 reg、interrupts、compatible 等核心属性的含义;熟悉 #address-cells 和 #size-cells 对地址映射的影响。

2.1 设备树文件结构

一个典型的设备树源文件由以下部分组成:

  • 根节点:/,整个设备树的入口。
  • 节点:表示一个硬件设备或总线,格式为 node-name@unit-address。
  • 属性:键值对,描述设备的特性,如地址、中断、时钟等。
  • 值:可以是空、32 位整数、64 位整数、字符串、字节数组或混合类型。
/dts-v1/;

/ {
    compatible = "acme,coyote's-revenge";

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a9";
            reg = <0>;
        };
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB at 0x80000000 */
    };

    serial@101f1000 {
        compatible = "ns16550";
        reg = <0x101f1000 0x1000>;
        interrupts = <0 4>;
    };
};
关键点:每个节点必须有一个 compatible 属性,用于内核匹配驱动;reg 属性描述设备地址和大小,其格式由父节点的 #address-cells 和 #size-cells 决定。

2.2 节点命名与路径

节点名称由两部分组成:节点名和可选的 单元地址,中间用 @ 分隔。单元地址通常是对应设备的起始物理地址,用于区分同一层级下的多个相同类型节点。

节点名 单元地址 完整名称 说明
cpu 0 cpu@0 第一个 CPU 核心
serial 101f1000 serial@101f1000 UART 控制器,基址 0x101f1000
i2c 3000 i2c@3000 I2C 总线控制器

节点路径使用 / 分隔,例如 /cpus/cpu@0 表示根节点下的 cpus 节点下的第一个 CPU。内核通过 of_find_node_by_path() 等 API 根据路径查找节点。

2.3 属性与值的表示

属性是设备树的核心信息载体,每个属性都有一个名称和对应的值。值的类型由编码方式决定,常见类型如下:

值类型 语法示例 说明
空 interrupt-controller; 仅表示存在该属性,无值
32 位整数 clock-frequency = <50000000>; 尖括号包裹,单个或多个整数
64 位整数 reg = <0x00000000 0x80000000>; 用两个 32 位整数拼接表示
字符串 compatible = "ns16550"; 双引号包裹
字符串列表 compatible = "ti,omap3-uart", "ns16550"; 逗号分隔多个字符串
字节数组 local-mac-address = [00 11 22 33 44 55]; 方括号包裹,十六进制字节
混合类型 reg = <0x101f1000 0x1000>, [00 11 22]; 可组合使用,用逗号分隔
常见错误:忘记在 reg 属性中匹配父节点的 #address-cells 和 #size-cells 会导致地址解析错误。例如,若父节点 #address-cells = <2>,则 reg 的第一个地址必须写两个 32 位整数。

2.4 地址映射与 cells 规则

设备树使用 #address-cells 和 #size-cells 定义地址和长度的编码宽度。这两个属性在总线节点(如 /soc、/amba)中声明,影响其子节点的 reg 属性解析。

/ {
    #address-cells = <1>;
    #size-cells = <1>;

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        serial@101f1000 {
            compatible = "ns16550";
            reg = <0x101f1000 0x1000>;
            interrupts = <0 4>;
        };
    };
};

上例中,#address-cells = <1> 表示地址用 1 个 32 位整数表示,#size-cells = <1> 表示长度也用 1 个 32 位整数。因此 reg = <0x101f1000 0x1000> 表示基址 0x101f1000,长度 0x1000 字节。

ranges 属性:用于将子节点的地址空间映射到父节点的地址空间。若 ranges; 为空,表示直接映射(1:1);若 ranges = <0 0x10000000 0x1000>; 则表示子地址 0 映射到父地址 0x10000000,长度 0x1000。

2.5 中断属性与连接

设备树通过 interrupts、interrupt-parent 和 interrupt-controller 描述中断拓扑。每个中断控制器节点需声明 interrupt-controller; 和 #interrupt-cells 属性。

/ {
    interrupt-parent = <&gic>;

    gic: interrupt-controller@10480000 {
        compatible = "arm,cortex-a9-gic";
        reg = <0x10480000 0x1000>;
        interrupt-controller;
        #interrupt-cells = <3>;
    };

    serial@101f1000 {
        compatible = "ns16550";
        reg = <0x101f1000 0x1000>;
        interrupts = <0 4 1>;  /* 类型 0,中断号 4,触发方式 1 */
    };
};

#interrupt-cells = <3> 表示每个中断描述符由 3 个 32 位整数组成:中断类型(SPI/P PI)、中断号、触发标志。子节点通过 interrupts 属性引用中断,interrupt-parent 指定所属的中断控制器。

2.6 设备树编译与反编译

设备树源文件(.dts)通过 Device Tree Compiler(dtc)编译为二进制文件(.dtb),也可从 .dtb 反编译为 .dts 用于调试。

命令 说明
dtc -I dts -O dtb -o output.dtb input.dts 将 DTS 编译为 DTB
dtc -I dtb -O dts -o output.dts input.dtb 将 DTB 反编译为 DTS
dtc -I dts -O dtb -@ -o output.dtb input.dts 编译时保留符号(用于插件)
fdtdump input.dtb 以可读格式打印 DTB 内容
调试技巧:在开发阶段,使用 dtc -I dts -O dtb -o test.dtb test.dts && dtc -I dtb -O dts test.dtb 可以验证 DTS 语法是否正确,并查看编译器优化后的结果。

2.7 设备树节点关系 SVG 图

下图展示了一个典型 ARM SoC 设备树的节点层级关系,包含根节点、CPU、内存、中断控制器和 UART 设备,并标注了关键属性。

设备树节点层级关系图 根节点 / compatible = "acme,coyote's-revenge" #address-cells = <1>; #size-cells = <1> cpus cpu@0 compatible = "arm,cortex-a9"; reg = <0> memory@80000000 reg = <0x80000000 0x20000000> gic@10480000 interrupt-controller; #interrupt-cells = <3> soc compatible = "simple-bus"; ranges; serial@101f1000 compatible = "ns16550"; reg = <0x101f1000 0x1000> interrupts = <0 4 1>; interrupt-parent = <&gic>

2.8 设备树包含与宏

设备树支持 #include 预处理指令,可以引入头文件(.dtsi)或宏定义。这使得 SoC 厂商可以定义公用的设备树包含文件,板级文件只需包含并覆盖特定节点。

// imx6ul.dtsi (SoC 级)
#include "skeleton.dtsi"

/ {
    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";

        uart1: serial@02020000 {
            compatible = "fsl,imx6ul-uart", "fsl,imx21-uart";
            reg = <0x02020000 0x4000>;
            interrupts = <0 26 4>;
            status = "disabled";
        };
    };
};
// myboard.dts (板级)
#include "imx6ul.dtsi"

/ {
    model = "My i.MX6UL Board";
    compatible = "my,imx6ul-board", "fsl,imx6ul";

    &uart1 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_uart1>;
    };
};
覆盖规则:板级文件通过 &uart1 引用 SoC 级节点,并修改 status 属性为 "okay" 以启用设备。未覆盖的属性继承自 .dtsi 文件。这种分层设计极大减少了重复代码。

2.9 本章小结

设备树是嵌入式 Linux 驱动开发的基础设施,掌握 DTS 语法、节点命名、属性编码和地址映射规则是编写正确设备树的前提。本章重点包括:

  • 节点由 名称 和可选的 单元地址 组成,路径用 / 分隔。
  • 属性值支持 整数、字符串、字节数组 和 混合类型,语法符号需严格匹配。
  • #address-cells 和 #size-cells 决定 reg 属性的解析方式,是地址映射的核心。
  • 中断拓扑通过 interrupt-controller、#interrupt-cells 和 interrupts 属性描述。
  • 使用 #include 和节点引用实现 SoC 级与板级设备树的分层复用。
实践建议:在开发板上执行 dtc -I dtb -O dts /sys/firmware/fdt 可以导出当前内核使用的设备树,是逆向学习和调试的利器。