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

1.1 设备树的起源与设计动机

在早期的 Linux 内核中,板级硬件描述信息(如内存基地址、中断号、时钟频率、GPIO 引脚分配等)被直接硬编码在 arch/arm/mach-xxx 目录下的 C 语言板文件中。这种方式的弊端随着 ARM 架构的碎片化迅速暴露:

  • 代码冗余:每款开发板都需要独立的 board-xxx.c 文件,大量重复的寄存器地址和资源定义散落在不同文件中。
  • 维护困难:硬件引脚变更需要重新编译内核,无法做到内核与硬件描述分离。
  • 社区分裂:厂商提交的板级代码质量参差不齐,导致主线内核维护成本急剧上升。

为了解决上述问题,PowerPC 架构率先引入了 Open Firmware 设备树概念,随后被 ARM、RISC-V、X86(ACPI 之外)等架构广泛采纳。设备树的核心思想是:将硬件描述从内核源码中剥离,以结构化文本形式独立存在,由 bootloader 传递给内核,内核在启动时解析并动态创建平台设备。

关键转变: 从“编译时绑定”到“运行时描述”——硬件变化只需修改 DTS 文件并重新编译设备树二进制(DTB),无需重新编译内核。
设备树演进时间线 板级硬编码 Linux 2.6 ~ 3.0 Open Firmware PowerPC 先行 ARM 强制 DTS Linux 3.10+ RISC-V / X86 当前 设备树将硬件描述与内核源码解耦,成为嵌入式 Linux 硬件描述的事实标准

1.2 设备树在 Linux 内核启动流程中的位置

设备树文件(.dts)经过 dtc 编译生成二进制 .dtb 文件,由 bootloader(如 U-Boot)加载到内存中,并将起始地址传递给内核。内核在启动早期完成以下关键步骤:

启动阶段 执行主体 设备树相关操作
1. Bootloader 加载 U-Boot / Barebox 从存储介质读取 .dtb 到内存,设置 r2 寄存器(ARM)或 a1 寄存器(RISC-V)指向 DTB 地址
2. 内核入口汇编 head.S / head_64.S 保存 DTB 地址到全局变量 __fdt_pointer,进入 C 语言初始化
3. 早期解析 setup_arch() 调用 early_init_dt_scan() 扫描 DTB 中的 chosen 节点获取启动参数、内存大小
4. 设备树展开 unflatten_device_tree() 将扁平 DTB 展开为 struct device_node 树状结构,挂入全局链表
5. 平台设备创建 of_platform_populate() 遍历设备树节点,为 compatible 匹配的节点创建 platform_device
6. 驱动绑定 总线驱动模型 驱动通过 of_match_table 匹配设备树节点,执行 probe() 回调
Linux 内核启动流程与设备树交互 Bootloader 加载 DTB 到内存 内核入口汇编 保存 DTB 地址 setup_arch() 早期扫描 chosen unflatten 展开为 device_node of_platform_populate 创建 platform_device 驱动 probe() 匹配 compatible 设备树在内核启动中的角色 从硬件描述 → 内存结构 → 设备模型 → 驱动绑定,设备树贯穿内核启动全程 任何时钟/GPIO/pinmux 配置都源自设备树节点的属性解析

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;
    };

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
            reg = <0x0>;
            clock-frequency = <1000000000>;
        };
    };

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

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
        clock-frequency = <48000000>;
        status = "okay";
    };
};

1.4 节点与属性的核心规则

语法元素 格式 说明 示例
根节点 / { } 设备树的顶层容器,所有硬件节点都是其子节点 / { model = "xxx"; };
节点名称 node-name@unit-address @ 后跟寄存器基地址或单元编号,用于区分同名节点 uart0: serial@44e09000
标签(label) label: node-name 为节点起别名,方便在 .dtsi 中通过 &label 引用 uart0: serial@44e09000
属性 key = value; 描述节点特性,值可以是字符串、32位整数数组、字节数组等 compatible = "ti,am3352-uart";
reg 属性 reg = <addr size>; 描述设备占用的地址空间,#address-cells 和 #size-cells 决定格式 reg = <0x44e09000 0x2000>;
interrupts interrupts = <num>; 指定设备使用的中断号,依赖 interrupt-parent 解析 interrupts = <72>;
status "okay" / "disabled" 控制设备是否启用,常用于 .dtsi 中默认禁用,板级 .dts 中开启 status = "okay";
实战提示: compatible 属性是驱动匹配的核心,格式为 "厂商,设备型号",内核通过 of_match_table 进行字符串精确匹配。建议始终将最具体的型号放在最前面,通用型号放在后面,如 "ti,am3352-uart", "ti,omap3-uart"。

1.5 设备树包含机制:.dtsi 与 .dts

实际项目中,设备树文件分为两类:

  • .dtsi(设备树包含文件):存放 SoC 公共硬件描述,如 CPU 集群、通用外设(UART、I2C、SPI)、中断控制器、时钟控制器等。由芯片厂商提供,多个板级文件共享。
  • .dts(设备树源文件):存放板级特定硬件描述,如内存大小、板载外设型号、引脚复用配置、时钟频率调整等。通过 #include 引入 .dtsi,并通过 &label 覆盖或追加属性。
/* am335x-bone-common.dtsi (SoC 公共部分) */
#include "am33xx.dtsi"

&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = &uart0_pins;
    status = "okay";
};

/* am335x-boneblack.dts (板级特定部分) */
#include "am335x-bone-common.dtsi"

&uart0 {
    clock-frequency = <48000000>;  /* 覆盖默认值 */
};
设备树 .dtsi 与 .dts 包含关系 .dtsi (SoC 公共) CPU 集群描述 通用外设节点 时钟/中断控制器 #include .dts (板级特定) 内存大小配置 引脚复用覆盖 时钟频率调整 通过 &label 引用机制,.dts 可以覆盖或追加 .dtsi 中的属性,实现分层描述

1.6 设备树编译与反编译

设备树编译器 dtc 是操作设备树的核心工具,常用命令如下:

操作 命令 说明
编译 DTS → DTB dtc -I dts -O dtb -o output.dtb input.dts 将可读的文本格式编译为内核解析的二进制格式
反编译 DTB → DTS dtc -I dtb -O dts -o output.dts input.dtb 从二进制还原为文本,用于调试或逆向分析
查看 DTB 内容 fdtdump input.dtb 以十六进制+ASCII 方式打印 DTB 原始内容
内核编译时自动生成 make dtbs 在内核源码目录执行,编译所有 .dts 为 .dtb
常见陷阱: 反编译得到的 .dts 会丢失原始文件中的注释、标签名(label)和宏定义(如 #include 展开后的内容)。反编译文件仅用于参考,不应直接作为修改基础,应始终维护原始 .dts 源文件。

1.7 本章小结

  • 设备树解决了板级硬编码带来的维护灾难,实现了硬件描述与内核源码的解耦。
  • 在启动流程中,设备树从 bootloader 传递到内核,经过解析、展开、设备创建三个阶段,最终驱动绑定。
  • 基本语法以树形节点 + 属性为核心,compatible 是驱动匹配的关键,reg 描述地址空间。
  • .dtsi 与 .dts 的分层包含机制,使得 SoC 公共描述与板级特定描述分离,便于复用和维护。
  • dtc 工具链支持编译、反编译和调试,是设备树开发的必备技能。