1. 设备树基础:设备树(DTS)的起源、作用与基本语法结构

1.1 设备树的起源与背景

在早期的 Linux 内核中,板级硬件信息(如内存基址、中断号、GPIO 映射)被硬编码在 arch/arm/mach-xxx 目录下的 C 语言板文件中。这种“代码即配置”的方式导致内核源码充斥着大量重复的、与平台相关的硬件描述代码,每增加一款新硬件就需要修改内核源码并重新编译。

痛点总结: 板文件模式导致内核源码臃肿、维护困难、厂商无法独立发布硬件描述,严重阻碍了 ARM 生态的快速迭代。

为解决上述问题,PowerPC 社区率先提出了 设备树(Device Tree) 概念,随后被 ARM、RISC-V、MIPS 等架构广泛采纳。设备树是一种描述硬件资源的、与操作系统无关的数据结构,它将硬件配置从内核代码中剥离出来,以 .dts(Device Tree Source)文本文件的形式独立存在,编译后生成 .dtb(Device Tree Blob)二进制文件,由 bootloader 传递给内核。

对比维度 传统板文件(Board File) 设备树(Device Tree)
硬件描述位置 内核源码 arch/arm/mach-xxx 独立 .dts 文件
修改硬件配置 修改 C 代码,重新编译内核 修改 .dts,重新编译 DTB
内核与硬件耦合度 高耦合,每板一份内核镜像 低耦合,同一内核镜像支持多板
厂商参与门槛 需提交内核补丁 只需提供 DTS 文件
可维护性 差,冗余代码多 好,结构清晰

1.2 设备树的核心作用

设备树在 Linux 内核启动过程中扮演着“硬件翻译官”的角色,其核心作用可归纳为以下四点:

  1. 硬件资源描述: 向内核传递 CPU 数量、内存布局、外设寄存器地址、中断号、DMA 通道、时钟频率等硬件参数。
  2. 驱动匹配: 通过 compatible 属性将设备节点与对应的驱动程序绑定,实现“设备-驱动”的自动匹配。
  3. 配置灵活性: 允许在不修改内核源码的前提下,通过修改 DTS 文件来调整引脚复用、中断触发方式、电源管理等板级参数。
  4. 多平台统一: 同一份内核镜像可以加载不同的 DTB 文件,从而支持多种硬件平台,实现“一次编译,到处运行”。
关键概念: 设备树是“数据”而非“代码”。内核通过解析 DTB 来获取硬件信息,而不是通过执行函数来初始化硬件。这种“数据驱动”的设计是设备树架构的精髓。

1.3 设备树的基本语法结构

设备树源文件(.dts)采用树形层次结构,以节点(node)和属性(property)为核心元素。以下是一个典型的设备树片段:

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

    chosen {
        stdout-path = &uart0;
    };

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

    leds {
        compatible = "gpio-leds";
        led0 {
            label = "beaglebone:green:usr0";
            gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        clocks = <&uart0_clk>;
    };
};

1.3.1 节点(Node)

节点是设备树的基本组成单元,每个节点代表一个硬件设备或一个逻辑功能块。节点名称格式为 node-name@unit-address,其中 unit-address 通常为设备寄存器基址,用于区分同一层级下的同名节点。

  • 根节点: 用 / 表示,是整个设备树的起点,包含所有其他节点。
  • 子节点: 在根节点内部通过缩进定义,可以嵌套多层。
  • 标签(Label): 如 uart0:,用于在 DTS 文件中引用节点,编译时会被替换为节点的路径。

1.3.2 属性(Property)

属性用于描述节点的特征,采用 key = value; 的形式。常见的属性类型包括:

属性类型 语法格式 示例 说明
字符串 "string" model = "TI AM335x BeagleBone Black"; 描述设备型号
字符串列表 "str1", "str2" compatible = "ti,am3352-uart", "ti,omap3-uart"; 驱动兼容性匹配
32 位整数 <value> interrupts = <72>; 中断号、时钟频率等
64 位整数 <0x00000000 0xvalue> reg = <0x00000000 0x80000000>; 大地址空间(如 64 位内存基址)
字节数组 [hex bytes] local-mac-address = [00 11 22 33 44 55]; MAC 地址、UUID 等
布尔值 仅属性名,无值 status = "disabled"; 控制设备启用/禁用
引用(phandle) &label clocks = <&uart0_clk>; 引用其他节点

1.3.3 标准属性详解

设备树规范定义了一系列标准属性,用于统一硬件描述方式:

  • compatible: 最重要的属性,格式为 "厂商,设备型号",内核通过该属性查找匹配的驱动程序。匹配顺序为从最具体到最通用。
  • reg: 描述设备地址空间,格式为 <基址 长度>,可包含多组地址。
  • interrupts: 描述设备使用的中断号,具体含义由父节点的 #interrupt-cells 决定。
  • status: 设备状态,常用值包括 "okay"(启用)、"disabled"(禁用)、"fail"(故障)。
  • device_type: 早期规范遗留属性,主要用于内存节点(device_type = "memory"),新设备建议避免使用。

1.4 设备树层次结构与寻址

设备树的树形结构通过父子节点关系来体现硬件的总线拓扑。每个节点可以包含 #address-cells 和 #size-cells 属性,用于定义子节点 reg 属性中地址和长度的编码位数。

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

    gpio0: gpio@44e07000 {
        compatible = "ti,omap4-gpio";
        reg = <0x44e07000 0x1000>;
        interrupts = <96>;
        #gpio-cells = <2>;
        gpio-controller;
    };

    i2c0: i2c@44e0b000 {
        compatible = "ti,omap4-i2c";
        reg = <0x44e0b000 0x1000>;
        interrupts = <70>;
        #address-cells = <1>;
        #size-cells = <0>;

        eeprom@50 {
            compatible = "atmel,24c32";
            reg = <0x50>;
        };
    };
};

在上例中:

  • soc 节点定义 #address-cells = <1> 和 #size-cells = <1>,因此其子节点(如 gpio0)的 reg 包含两个 32 位整数:基址和长度。
  • i2c0 节点定义 #size-cells = <0>,因此其子节点(如 eeprom@50)的 reg 只包含一个 32 位整数:I2C 从设备地址。
常见错误: 忘记在总线节点中设置 #address-cells 和 #size-cells,或者子节点 reg 的整数个数与父节点定义不匹配,会导致内核解析地址失败,设备无法被正确识别。

1.5 设备树编译与使用流程

设备树的完整工作流程包括编写、编译、加载和解析四个阶段:

  1. 编写 DTS: 开发者根据硬件原理图编写 .dts 源文件,或通过 #include 包含 .dtsi(设备树包含文件)来复用 SoC 公共定义。
  2. 编译 DTB: 使用 dtc(Device Tree Compiler)工具将 .dts 编译为二进制 .dtb 文件。
  3. 加载 DTB: Bootloader(如 U-Boot)从存储介质(eMMC、SD 卡、网络)读取 .dtb 文件,并将其加载到内存中,将地址传递给内核。
  4. 内核解析: 内核启动早期调用 unflatten_device_tree() 函数,将 DTB 展开为 device_node 树结构,供驱动子系统使用。
# 编译设备树
dtc -I dts -O dtb -o am335x-boneblack.dtb am335x-boneblack.dts

# 反编译设备树(调试用)
dtc -I dtb -O dts -o am335x-boneblack.dts am335x-boneblack.dtb

1.6 设备树结构总览图

下图展示了设备树从源文件到内核解析的完整结构关系:

设备树结构总览:从 DTS 到内核驱动 DTS 源文件 /dts-v1/; / { model = "Board"; compatible = "vendor"; uart0: serial@... { reg = <...>; }; dtc 编译 DTB 二进制 扁平化二进制数据 节点/属性/字符串表 大小端无关 加载 Bootloader 从存储读取 DTB 传递 DTB 地址给内核 启动 内核解析 unflatten_device_tree() 展开为 device_node 树 驱动匹配与 probe 设备驱动 通过 of_match_table 匹配 compatible,读取 reg/interrupts 等属性 驱动匹配

1.7 本章小结

设备树是嵌入式 Linux 开发中不可或缺的硬件描述语言,它将硬件配置从内核代码中解耦出来,极大地提高了代码的可移植性和可维护性。掌握 DTS 的基本语法——节点、属性、标准属性以及地址编码规则——是后续学习中断控制器、GPIO 配置、时钟管理等高级主题的基础。

学习建议: 初学者可以从反编译现有 DTB 文件入手,对照硬件原理图逐行理解每个节点和属性的含义,再尝试修改并验证效果。实践是掌握设备树最快的方式。