1. 设备树基础概念

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

在早期的嵌入式 Linux 内核(如 Linux 2.4 / 2.6)中,板级硬件信息(内存基址、外设寄存器地址、中断号、GPIO 映射等)被直接硬编码在 arch/arm/mach-xxx 下的 C 语言板级文件中。每增加一款新硬件平台,就需要新增或修改大量 C 代码,导致内核源码树急剧膨胀,且不同厂商的代码难以统一维护。

为了解决这一痛点,PowerPC 社区率先引入了 Open Firmware 中的设备树概念,随后被 ARM、RISC-V、x86 等架构广泛采纳。设备树的核心思想是:将硬件描述从内核代码中剥离出来,形成一个独立的数据结构,由 Bootloader(如 U-Boot)传递给内核,内核解析后动态创建平台设备。

关键转折点: Linux 3.x 之后,ARM 架构强制要求使用设备树,传统的板级文件(board-*.c)被逐步废弃。这一决策使得同一份内核镜像可以支持多个硬件平台,只需更换不同的 .dtb 文件即可。
设备树起源:从硬编码到描述性架构 传统板级文件 arch/arm/mach-xxx/board.c 硬编码寄存器地址、中断号 每款硬件需修改内核源码 内核镜像与硬件强耦合 设备树描述 .dts → .dtb → 内核解析 硬件信息与内核代码分离 同一内核支持多款硬件 Bootloader 动态传递 .dtb

1.2 设备树的作用与核心价值

设备树(Device Tree)是一种描述硬件资源的树形数据结构,其核心作用可归纳为以下四点:

作用维度 具体说明 对驱动开发的影响
硬件描述标准化 使用统一的节点/属性语法描述 CPU、内存、外设、中断、GPIO、时钟等 驱动通过 of_* API 读取硬件参数,无需硬编码
平台解耦 同一份内核镜像通过更换 .dtb 文件适配不同硬件平台 驱动代码无需为每款硬件单独维护板级文件
动态配置 Bootloader 可根据硬件版本、外设选型动态修改 .dtb 驱动在 probe 时获取运行时配置,灵活性大幅提升
社区生态统一 Linux 主线内核要求所有新平台必须提供设备树支持 驱动开发者只需关注设备树绑定文档,降低协作成本
驱动开发者视角: 设备树本质上是一个“硬件参数数据库”。你的以太网驱动不再需要关心“PHY 地址是 0x01 还是 0x02”,而是通过设备树节点中的 reg 属性动态获取。这使得同一份驱动二进制可以适配不同硬件设计。

1.3 设备树的基本语法结构

设备树源文件(.dts)采用树形层级结构,由 节点(node) 和 属性(property) 组成。每个节点代表一个硬件设备或总线,属性描述该设备的配置参数。

1.3.1 节点定义

/dts-v1/;

/ {
    model = "Embedded Linux Board";
    compatible = "vendor,board-rev1";

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

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

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

        uart0: serial@10000000 {
            compatible = "vendor,uart-ip";
            reg = <0x10000000 0x1000>;
            interrupts = <0 33 4>;
            clock-frequency = <24000000>;
        };

        eth0: ethernet@20000000 {
            compatible = "vendor,gmac";
            reg = <0x20000000 0x4000>;
            interrupts = <0 45 4>;
            phy-mode = "rgmii";
            phy-handle = <&phy0>;

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

                phy0: ethernet-phy@1 {
                    reg = <0x1>;
                    compatible = "ethernet-phy-ieee802.3-c22";
                };
            };
        };
    };
};

1.3.2 语法要素速查表

语法元素 示例 说明
根节点 / { }; 设备树的顶层容器,所有设备节点都位于其下
节点名称 uart0: serial@10000000 格式:[标签:]名称[@单元地址],标签用于引用
属性 compatible = "vendor,gmac"; 键值对,值类型包括字符串、整数、数组、布尔等
reg 属性 reg = <0x20000000 0x4000>; 描述设备地址空间:<基址 长度>
interrupts interrupts = <0 45 4>; 中断描述:<中断控制器类型 中断号 触发方式>
phandle phy-handle = <&phy0>; 引用其他节点,&phy0 指向标签为 phy0 的节点
包含文件 #include "soc.dtsi" .dtsi 文件用于定义 SoC 公共部分,.dts 文件引用并覆盖

1.3.3 设备树层级结构示意

设备树层级结构(以太网 MAC + PHY 示例) / (根节点) memory@80000000 soc uart0: serial@10000000 eth0: ethernet@20000000 mdio phy0: ethernet-phy@1 phy-handle = <&phy0> 引用 PHY 节点,实现 MAC 与 PHY 的绑定

1.4 设备树编译与使用流程

  1. 编写 .dts 源文件:开发者根据硬件原理图编写设备树源文件,通常包含一个 .dtsi(SoC 公共定义)和多个 .dts(板级具体配置)。
  2. 编译为 .dtb 二进制:使用 dtc(Device Tree Compiler)工具将 .dts 编译为 .dtb 文件。
  3. Bootloader 加载 .dtb:U-Boot 等 Bootloader 从存储介质(eMMC、NAND、网络)读取 .dtb 文件到内存,并在启动内核时传递其地址。
  4. 内核解析并创建设备:内核启动早期调用 unflatten_device_tree() 将 .dtb 解析为 device_node 树,随后平台总线根据 compatible 属性匹配驱动并调用 probe。
常见误区: 设备树不是编程语言,不支持循环、条件判断或函数调用。它纯粹是静态的硬件描述数据。所有“逻辑”行为(如根据某个 GPIO 电平选择 PHY 地址)必须在驱动代码中实现,不能依赖设备树。

1.5 设备树与驱动适配的关联

对于以太网 MAC 驱动而言,设备树提供了以下关键信息:

  • MAC 控制器基址与中断:通过 reg 和 interrupts 属性获取。
  • PHY 绑定方式:phy-handle 引用 PHY 节点,phy-mode 指定接口模式(RGMII、RMII、MII 等)。
  • PHY 地址与类型:PHY 节点的 reg 属性定义 MDIO 总线上的 PHY 地址,compatible 属性标识 PHY 标准或厂商。
  • 时钟与复位:clocks、resets 属性提供时钟源和复位控制。

驱动开发者在编写以太网驱动时,应优先使用 of_* 系列 API(如 of_property_read_u32()、of_parse_phandle())从设备树中读取配置,而不是在驱动代码中硬编码任何硬件参数。