1. 设备树基础概念:设备树(DTS)的起源、作用与在Linux内核中的位置

1.1 设备树的起源:从“硬编码”到“描述性”的演进

在早期的 Linux 内核(如 ARM 架构)中,板级硬件信息(内存基址、外设地址、中断号、时钟频率等)被直接硬编码在 arch/arm/mach-xxx 目录下的 C 语言板级文件中。这种方式的弊端随着 SoC 和板卡种类的爆炸式增长而愈发严重:

  • 代码冗余:每增加一块新板卡,就需要新增或修改大量 C 代码。
  • 维护困难:内核版本升级时,板级文件需要同步适配,极易引入回归。
  • 耦合度高:驱动代码与硬件配置信息深度绑定,无法实现“一次编译,多板运行”。

为了解决上述问题,PowerPC 社区率先引入了 Open Firmware 设备树(Device Tree) 的概念,随后被 ARM、RISC-V、x86 等架构广泛采纳。设备树的核心思想是:将硬件描述从内核源码中剥离出来,形成一份独立的数据结构文件,内核在启动时解析该文件,动态构建硬件拓扑。

关键转折点: Linux 3.x 内核开始强制要求 ARM 架构使用设备树,板级文件(board-*.c)被逐步废弃。这一决策使得 Linux 内核的硬件适配方式发生了根本性变革。

1.2 设备树的作用:硬件拓扑的“静态数据库”

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

作用维度 具体说明 典型示例
硬件拓扑描述 定义 CPU、内存、总线、外设的层级连接关系 cpu@0 → soc → serial@ff000000
资源属性配置 提供地址、中断、时钟、DMA、GPIO 等资源编号 reg = <0xff000000 0x1000>
驱动匹配标识 通过 compatible 属性将设备节点与驱动绑定 compatible = "fsl,imx6q-uart", "ns16550"
运行时参数传递 向内核传递启动参数、内存布局、板级定制信息 chosen { bootargs = "console=ttymxc0,115200"; };
理解要点: 设备树是 静态的——它在内核启动时被一次性解析,之后不再动态变化。这与 ACPI(高级配置与电源管理接口)的运行时动态枚举机制有本质区别。

1.3 设备树在 Linux 内核中的位置:从源文件到二进制 Blob

设备树在 Linux 内核源码树中占据明确的物理路径和编译流程位置。下图展示了从 .dts 源文件到内核使用的 .dtb 二进制文件的完整链路:

设备树文件在内核中的编译与使用流程 .dts 源文件 arch/arm/boot/dts/xxx.dts .dtsi 包含文件 SoC 公共定义 / 复用头 DTC 编译器 scripts/dtc/dtc .dtb 二进制 arch/arm/boot/dts/xxx.dtb Bootloader 加载 U-Boot / TF-A 内核解析 of_* API 驱动匹配 说明: DTC 编译器将 .dts 和 .dtsi 合并编译为 .dtb 二进制文件; Bootloader 将 .dtb 加载到内存,并将地址通过寄存器传递给内核。

1.4 设备树相关文件在内核源码树中的分布

Linux 内核源码中,设备树相关文件主要分布在以下目录:

路径 内容说明 典型文件
scripts/dtc/ DTC 编译器源码及 libfdt 库 dtc.c, fdt.c, dtc-lexer.l
arch/arm/boot/dts/ ARM 架构板级 .dts / .dtsi 文件 imx6q-sabresd.dts, sun8i-h3.dtsi
arch/arm64/boot/dts/ ARM64 架构板级 .dts / .dtsi 文件 rk3399.dtsi, mt8183.dtsi
arch/riscv/boot/dts/ RISC-V 架构板级 .dts / .dtsi 文件 sifive-hifive-unleashed.dts
include/dt-bindings/ 设备树绑定头文件(宏定义、中断类型等) interrupt-controller/irq.h, clock/imx6qdl-clock.h
Documentation/devicetree/bindings/ 设备树绑定规范文档(YAML 格式) serial/fsl-imx-uart.yaml

1.5 设备树在内核启动流程中的角色

设备树并非内核启动的“可选配件”,而是现代 ARM/RISC-V 内核启动的 核心输入参数。其在内核启动阶段的参与流程如下:

  1. Bootloader 阶段:U-Boot 从存储介质(eMMC、SD 卡、Flash)读取 .dtb 文件到内存,并在跳转到内核前将 DTB 物理地址写入 r2 寄存器(ARM 约定)。
  2. 早期解析阶段:内核入口函数 start_kernel() 调用 setup_arch(),其中 unflatten_device_tree() 将扁平 DTB 展开为内核可遍历的树形结构。
  3. 驱动匹配阶段:总线驱动(如 platform bus)在注册设备时,通过 of_match_table 匹配设备树节点中的 compatible 属性,完成驱动与设备的绑定。
  4. 资源获取阶段:驱动通过 of_* API(如 of_iomap()、irq_of_parse_and_map())从设备树节点中读取 reg、interrupts 等属性,完成硬件初始化。
常见误区: 设备树不是设备驱动,也不是内核模块。它只是硬件配置信息的“静态快照”。驱动代码仍然需要编写,但驱动不再关心具体板级细节——这些细节全部由设备树提供。

1.6 设备树的核心语法结构速览

一个典型的设备树文件由以下基本元素构成:

/dts-v1/;

/ {
    model = "Freescale i.MX6 Quad SABRE Smart Device Board";
    compatible = "fsl,imx6q-sabresd", "fsl,imx6q";

    memory@10000000 {
        device_type = "memory";
        reg = <0x10000000 0x40000000>;
    };

    chosen {
        bootargs = "console=ttymxc0,115200 root=/dev/mmcblk0p2";
    };

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

        uart1: serial@02020000 {
            compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
            reg = <0x02020000 0x4000>;
            interrupts = <0 26 4>;
            clocks = <&clks 32>;
            status = "okay";
        };
    };
};

上述代码展示了设备树的几个关键语法特征:

  • 根节点 /:整个设备树的起点,包含 model 和 compatible 属性。
  • 节点命名:格式为 node-name@unit-address,如 serial@02020000。
  • 属性:键值对形式,值可以是字符串、32 位整数数组(<>)、字节数组([])等。
  • 标签(Label):如 uart1:,用于在 .dtsi 中引用或覆盖节点。
  • 引用:&clks 表示引用其他节点(通常定义在 .dtsi 中)。