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

1.1 为什么需要设备树?——从“硬编码”到“数据驱动”

在早期 Linux 内核(2.6 及更早版本)中,嵌入式板级支持包(BSP)通常采用 “硬编码” 方式描述硬件:每个板子对应一个 board-xxx.c 文件,其中包含大量 platform_device 结构体、ioremap 地址、中断号、GPIO 号等。这种模式存在严重问题:

  • 代码膨胀:每增加一块新板卡,内核源码树就增加数千行板级代码。
  • 维护灾难:同一 SoC 的不同板卡,硬件资源描述大量重复,修改一处可能影响多板。
  • 内核与硬件耦合:更换外设或调整内存映射必须重新编译内核。
  • 无法通用:一个内核镜像无法同时支持多个硬件平台。
核心痛点: 硬件描述与内核代码深度绑定,导致嵌入式 Linux 的移植成本极高,社区难以维护。

设备树(Device Tree,DT)正是为解决上述问题而生。它将硬件资源描述从内核源码中剥离,形成一份独立的数据结构文件(.dts),由 bootloader 传递给内核。内核在启动时解析该结构,动态注册平台设备、分配资源、匹配驱动。从此,“一份内核镜像,多块板卡共用”成为可能。

1.2 设备树的起源与演进

时间 事件 影响
2005 PowerPC 架构率先引入设备树(源于 Open Firmware) 解决 PowerPC 平台板级代码混乱问题
2011 ARM Linux 社区开始大规模采用设备树(Linux 3.0~3.4) ARM 板级代码从 arch/arm/mach-* 逐步迁移到 .dts 文件
2012 设备树编译器(DTC)成为内核标准工具 .dts → .dtb 编译流程标准化
2014+ 设备树覆盖 ARM64、RISC-V、X86(ACPI 替代)、MIPS 等架构 成为嵌入式 Linux 硬件描述的事实标准
关键转折: 2011~2012 年,ARM 社区提交了“Device Tree for ARM”系列补丁,标志着嵌入式 Linux 从“代码描述硬件”正式进入“数据描述硬件”时代。

1.3 设备树的核心概念

设备树本质上是一种 树形数据结构,用于描述硬件平台中所有可寻址的设备及其连接关系。其核心要素包括:

  • 节点(Node):树中的每个元素,代表一个硬件设备或总线。节点名称通常为 device-name@address 格式。
  • 属性(Property):节点的键值对,描述设备的特性,如 reg(地址/大小)、interrupts(中断号)、compatible(驱动匹配字符串)。
  • 根节点(Root Node):树的起点,通常为 /,包含 model、compatible 等全局属性。
  • 标签(Label):为节点起别名,便于在 .dtsi 文件中引用或覆盖。
  • 包含文件(.dtsi):SoC 级通用描述文件,被具体板级 .dts 文件 #include,实现复用。
// 一个简化的设备树片段示例
/dts-v1/;

/ {
    model = "TI AM335x EVM";
    compatible = "ti,am335x-evm", "ti,am335x";

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

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

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

1.4 设备树的工作流程

从源码到内核识别设备,设备树经历以下阶段:

  1. 编写:开发者编写 .dts 源文件,#include 对应的 .dtsi 头文件。
  2. 编译:设备树编译器(DTC)将 .dts 编译为二进制 .dtb 文件。
  3. 传递:bootloader(如 U-Boot)将 .dtb 加载到内存,并将地址传递给内核。
  4. 解析:内核启动早期,unflatten_device_tree() 将 .dtb 展开为内存中的 device_node 树。
  5. 注册:内核遍历设备树节点,为每个节点创建 platform_device,并尝试匹配驱动。
  6. 绑定:驱动通过 of_match_table 匹配 compatible 属性,完成设备初始化。
常见误区: 设备树不是“配置内核”的工具,而是“描述硬件”的数据结构。内核的编译选项(如 CONFIG_*)仍然控制驱动是否编译进内核,设备树只决定哪些设备被激活。

1.5 设备树 vs 传统板级代码:对比一览

对比维度 传统板级代码(board-*.c) 设备树(.dts/.dtb)
硬件描述位置 内核源码 arch/arm/mach-* 独立 .dts 文件,与内核源码分离
修改硬件配置 需修改 C 代码,重新编译内核 仅修改 .dts,重新编译 .dtb,无需重编内核
多板卡支持 每个板卡一个 board-*.c,内核镜像不通用 同一内核镜像 + 不同 .dtb 即可支持多板
驱动匹配方式 静态注册 platform_device,或通过 arch_initcall 动态解析设备树,通过 compatible 属性匹配驱动
可维护性 差:代码重复多,社区难以协作 好:SoC 级 .dtsi 复用,板级差异最小化
调试手段 依赖内核日志、printk 提供 dtc -I dtb -O dts 反编译、/proc/device-tree 运行时查看

1.6 设备树在嵌入式系统中的位置

下图展示了设备树在嵌入式 Linux 启动流程中的核心位置:

设备树在嵌入式启动流程中的位置 设备树在嵌入式 Linux 启动流程中的位置 Boot ROM Bootloader (U-Boot) 加载 .dtb 到内存 内核启动早期 unflatten_device_tree() 设备树解析 创建 device_node 树 设备注册 platform_device 创建 驱动绑定 of_match_table 匹配 .dts 源文件 设备树编译器 (DTC) ↓ .dtb 二进制文件

1.7 设备树带来的核心价值

  • 硬件描述与内核解耦:更换板卡只需更换 .dtb 文件,内核镜像保持不变。
  • 驱动通用化:驱动通过 compatible 字符串匹配设备,不再依赖板级宏定义。
  • 社区协作效率提升:SoC 厂商提供 .dtsi,板卡厂商只需编写少量板级差异。
  • 调试与验证便利:可通过 /proc/device-tree 实时查看设备树状态,支持 dtc 反编译对比。
  • 启动参数灵活:通过 chosen 节点传递内核启动参数(如 bootargs),无需修改内核。
一句话总结: 设备树让嵌入式 Linux 的硬件描述从“写代码”变成了“写数据”,是嵌入式系统走向标准化、通用化的关键基石。