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

1.1 设备树的起源:从“硬编码”到“数据驱动”

在早期的 Linux 内核(2.6 及更早版本)中,板级硬件信息(如内存基址、外设寄存器地址、中断号、GPIO 映射)被直接硬编码在 arch/arm/mach-xxx 下的 C 语言板文件中。这种模式导致内核源码充斥着大量重复的、与具体开发板绑定的代码,每更换一款 SoC 或开发板,就需要重新编译内核,严重阻碍了 ARM 生态的软件复用。

为了解决这一痛点,PowerPC 社区率先引入了 设备树(Device Tree) 概念,随后被 ARM、RISC-V、X86(ACPI 替代场景)等架构广泛采纳。设备树的核心思想是:将硬件描述从内核源码中剥离,以结构化数据的形式传递给内核,使同一份内核镜像能够适配多种硬件平台。

关键转折: Linux 主线内核自 3.x 起强制要求 ARM 平台必须提供设备树文件,否则无法启动。这一决策彻底终结了“板文件泛滥”的时代。
设备树起源:从硬编码到数据驱动 旧模式:板文件硬编码 arch/arm/mach-xxx/*.c 新模式:设备树描述 *.dts / *.dtsi → dtb 痛点 • 每板一份 C 代码 • 内核镜像与板绑定 • 社区维护成本爆炸 • 无法通用启动 优势 • 硬件描述与内核解耦 • 同一内核适配多板 • 社区统一维护规范 • 支持设备树覆盖

1.2 设备树的作用:硬件描述的“桥梁”

设备树在嵌入式 Linux 系统中扮演着 硬件描述语言(HDL for Hardware Description) 的角色,其核心作用可归纳为以下四点:

作用维度 具体说明 典型场景
硬件拓扑描述 描述 CPU、内存、总线、外设的层级连接关系 SoC 内部总线结构、外设挂载位置
资源静态配置 定义寄存器地址、中断号、时钟频率、DMA 通道等 UART 基址 0x1c090000,中断号 49
驱动匹配标识 通过 compatible 属性将设备节点与驱动绑定 compatible = "fsl,imx6q-uart"
运行时参数传递 Bootloader 可修改设备树,传递内存大小、MAC 地址等 U-Boot 中 fdt set 命令
实践要点: 设备树不是万能药。它只描述“硬件有什么”,不描述“硬件怎么用”。驱动逻辑仍然由内核驱动代码实现,设备树仅提供数据。

1.3 基本语法结构:DTS 文件解剖

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

/dts-v1/;

/ {
    model = "Freescale i.MX6Q SabreSD Board";
    compatible = "fsl,imx6q-sabresd", "fsl,imx6q";

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

        uart1: serial@1c090000 {
            compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
            reg = <0x1c090000 0x4000>;
            interrupts = <0 49 4>;
            clocks = <&clks 0x1c>;
            status = "okay";
        };
    };
};

1.3.1 节点(Node)

  • 根节点:/ 表示设备树的根,所有设备节点都挂载在根下。
  • 子节点:soc、uart1 等,通过层级缩进体现父子关系。
  • 标签(Label):uart1: 是节点标签,用于在其他地方引用该节点(如 &uart1)。
  • 节点名称:serial@1c090000,格式为 名称@地址,地址通常为寄存器基址。

1.3.2 属性(Property)

属性名 含义 示例值
compatible 驱动匹配字符串,优先级从高到低 "fsl,imx6q-uart", "fsl,imx21-uart"
reg 寄存器地址范围,格式为 <地址 长度> <0x1c090000 0x4000>
interrupts 中断描述,格式依赖 #interrupt-cells <0 49 4>(GIC 格式)
clocks 时钟引用,通过 phandle 指向时钟节点 <&clks 0x1c>
status 设备启用状态:okay / disabled "okay"
#address-cells 子节点 reg 中地址字段占用的 32 位单元数 <1>
#size-cells 子节点 reg 中长度字段占用的 32 位单元数 <1>
常见陷阱: #address-cells 和 #size-cells 必须在父节点中定义,子节点 reg 的解析依赖这两个属性。忘记设置或设置错误会导致内核无法正确读取寄存器地址。

1.4 设备树在内核启动时的解析流程

从 Bootloader 加载 DTB 到内核驱动完成绑定,设备树经历了以下关键阶段:

  1. Bootloader 加载 DTB:U-Boot 从存储介质(eMMC、NAND、网络)读取 .dtb 文件到内存,并将物理地址传递给内核(通过 r2 寄存器或 bootm 命令)。
  2. 早期未压缩内核解压:内核解压代码保存 DTB 地址,进入架构相关的 setup_arch() 函数。
  3. DTB 线性映射:调用 early_init_dt_scan(),将 DTB 从物理内存映射到内核虚拟地址空间,并验证魔数(0xd00dfeed)。
  4. 内存与 CPU 初始化:扫描 /memory 节点获取内存大小,扫描 /cpus 节点获取 CPU 拓扑。
  5. 设备节点展开:调用 unflatten_device_tree(),将扁平 DTB 展开为内核内部的 device_node 树形结构。
  6. 驱动与设备绑定:总线驱动(如 platform bus)遍历设备树节点,匹配 compatible 属性,触发 probe() 回调。
设备树内核启动解析流程 ① Bootloader 加载 DTB U-Boot → 内存地址 ② setup_arch() 保存 DTB 地址 ③ early_init_dt_scan() 验证魔数 + 线性映射 ④ 内存 & CPU 扫描 /memory /cpus 节点 ⑤ unflatten_device_tree() 扁平 DTB → device_node 树 ⑥ 驱动匹配 & probe() platform bus 匹配 关键数据结构 • struct device_node:内核中表示设备树节点的核心结构体,包含 name、type、phandle、parent、children 等字段。 • struct property:表示节点属性,包含 name、value、length 和链表指针。 • of_* API:如 of_find_node_by_path()、of_property_read_u32(),驱动通过它们读取设备树数据。

1.5 设备树文件组织:DTS、DTSI 与 DTB

实际工程中,设备树文件分为三种形态:

文件类型 扩展名 作用 编译工具
源文件 .dts 板级设备树源文件,通常包含 #include 引用公共头文件 dtc(Device Tree Compiler)
包含文件 .dtsi SoC 级公共定义,被多个 .dts 共享(如 imx6q.dtsi) dtc(通过 #include 展开)
二进制文件 .dtb 编译后的二进制设备树,Bootloader 直接加载 dtc -I dts -O dtb
工程最佳实践: 永远不要直接修改 .dtb 文件。正确的做法是修改 .dts 源文件,然后使用 dtc 重新编译。内核源码树中执行 make dtbs 可一键编译所有设备树。

1.6 本章小结

  • 设备树解决了 ARM 平台“板文件泛滥”的历史问题,实现了硬件描述与内核代码的解耦。
  • 其核心语法包括节点(node)和属性(property),compatible 是驱动匹配的关键。
  • 内核启动时,DTB 经过验证、映射、展开、匹配四个阶段,最终驱动 probe() 被调用。
  • 掌握 dtc 编译工具和 of_* API 是后续中断调试的基础。