2. 设备树(Device Tree)起源

2.1 为什么引入设备树:从“硬编码”到“数据驱动”

在早期的嵌入式 Linux 系统中(如 ARM、PowerPC 平台),内核通过大量 “板级文件”(Board File)来描述硬件资源。每个开发板都需要在 arch/arm/mach-xxx 目录下编写一个包含 GPIO、中断、时钟、内存映射等信息的 C 结构体数组。这种方式的弊端随着 ARM 生态的碎片化而急剧放大:

  • 代码冗余:同一款 SoC 被数十种开发板使用,每个板子都要复制一份几乎相同的板级文件。
  • 内核与硬件耦合:更换外设或修改引脚分配必须重新编译内核,无法实现“一次编译,多处运行”。
  • 社区维护灾难:Linux 3.x 时代,ARM 架构的板级文件数量超过 3000 个,成为内核维护者的噩梦。
  • 启动参数传递混乱:ATAGS(传统启动参数列表)只能传递少量字符串,无法描述复杂的设备拓扑和资源依赖。
核心痛点:嵌入式系统需要一种 与内核源码分离的硬件描述语言,让 Bootloader 负责探测硬件,内核只负责解析数据。

设备树(Device Tree)正是为解决上述问题而生。它将硬件描述从 C 代码中剥离,形成独立的 .dts 源文件,编译为 .dtb 二进制 blob,由 Bootloader 传递给内核。内核通过 Device Tree Blob (DTB) 动态构建设备模型,实现“硬件描述数据化、驱动匹配自动化”。

从板级文件到设备树的演进 传统板级文件 vs 设备树模式 传统板级文件模式 • 每个板子一个 .c 文件 • 硬编码 GPIO/IRQ/时钟 • 修改硬件 → 重新编译内核 • 内核体积膨胀,维护困难 • 启动参数通过 ATAGS 传递 • 代表:arch/arm/mach-omap2/board-*.c 设备树模式 • 硬件描述在 .dts 文件中 • 资源通过属性声明(reg/interrupts) • 修改硬件 → 只改 .dts,无需重编内核 • 内核通过 DTB 动态创建设备 • 启动参数通过 chosen 节点传递 • 代表:arch/arm/boot/dts/xxx.dts

2.2 扁平设备树(FDT)概念

扁平设备树(Flattened Device Tree, FDT) 是设备树在内存中的二进制表示形式,即 .dtb 文件。它由 Bootloader 加载到内存后,以 大端字节序 传递给内核。FDT 的核心设计理念是:自描述、可扩展、无依赖。

2.2.1 FDT 的二进制结构

一个标准的 FDT 由以下四部分组成:

段名称 偏移 说明
FDT Header 0x00 魔数(0xd00dfeed)、总大小、结构块偏移、字符串块偏移等元信息
Memory Reservation Block Header 后 保留内存区域列表,防止内核使用特定物理内存
Structure Block 由 header 指定 节点(node)和属性(property)的线性序列,使用 token 标记(FDT_BEGIN_NODE, FDT_PROP 等)
Strings Block 由 header 指定 属性名的字符串池,结构块中的属性通过偏移引用此处的字符串
关键设计:FDT 的字符串块与结构块分离,使得多个节点可以共享同一个属性名字符串,节省空间。内核通过 libfdt 库提供的 API(如 fdt_getprop())遍历和查询 FDT。
FDT 二进制布局 FDT(Flattened Device Tree)内存布局 FDT Header (40 字节) magic=0xd00dfeed | totalsize | off_dt_struct | off_dt_strings | version Memory Reservation Block Structure Block (节点 + 属性 token 序列) FDT_BEGIN_NODE → FDT_PROP → FDT_END_NODE → ... Strings Block (属性名池: "compatible\0reg\0interrupts\0...") 内核通过 libfdt 库解析,无需任何外部依赖

2.2.2 FDT 的启动流程

  1. Bootloader 加载 DTB:U-Boot 从存储介质(eMMC、NAND、网络)读取 .dtb 文件到内存。
  2. Bootloader 调整 DTB:U-Boot 根据实际硬件(如内存大小、MAC 地址)修改 DTB 中的属性(通过 fdt_setprop())。
  3. 传递 DTB 地址:U-Boot 将 DTB 的物理地址写入寄存器 r2(ARM 架构约定),然后跳转到内核入口。
  4. 内核早期解析:内核启动汇编代码保存 r2,在 start_kernel() 中调用 unflatten_device_tree() 将 FDT 展开为树状结构。
  5. 驱动匹配:内核遍历设备树节点,根据 compatible 属性匹配驱动,调用 probe() 函数。
注意:如果 Bootloader 传递的 DTB 地址错误或 DTB 损坏,内核会在 setup_arch() 阶段 panic,无法启动。调试时可使用 fdt dump 命令检查 DTB 内容。

2.3 设备树与 ACPI 对比

ACPI(Advanced Configuration and Power Interface)是 x86 平台的标准硬件描述规范,而设备树是嵌入式/ARM 平台的事实标准。两者虽然目标相似(将硬件信息从内核代码中分离),但设计哲学和实现方式截然不同。

对比维度 设备树 (DT) ACPI
起源平台 嵌入式(ARM、PowerPC、RISC-V) x86(PC、服务器)
描述语言 DTS 源文件(文本),编译为 DTB(二进制) ASL(ACPI Source Language),编译为 AML(ACPI Machine Language)
运行时解释 内核直接解析 DTB 结构,无解释器 内核包含 AML 解释器,执行 ACPI 方法(Method)
动态性 静态描述,Bootloader 可修改属性 支持动态命名空间、热插拔、电源管理方法
电源管理 弱,依赖驱动自身实现 强,内置 _PS0/_PS3/_PRx 等标准对象
中断模型 简单,通过 interrupts 属性描述 复杂,支持 APIC、MSI、GSI 等多种中断控制器
资源描述 reg、clocks、resets 等属性 _CRS(Current Resource Settings)返回资源模板
驱动匹配 compatible 字符串匹配 HID(Hardware ID) + CID(Compatible ID)匹配
内核支持 drivers/of/ 目录,libfdt 库 drivers/acpi/ 目录,ACPICA 子系统
适用场景 嵌入式设备、IoT、单板计算机 PC、服务器、笔记本、工作站
核心差异总结:设备树是 静态数据,内核只读不执行;ACPI 是 可执行固件接口,内核通过 AML 解释器与固件交互。嵌入式系统不需要 ACPI 的复杂电源管理和热插拔能力,设备树的简洁性更适合资源受限的环境。
设备树 vs ACPI 架构对比 设备树 vs ACPI:架构对比 设备树 (DT) DTS 源文件 → DTC 编译 → DTB Bootloader 加载 DTB 到内存 内核 unflatten_device_tree() 驱动匹配 compatible → probe() 静态数据,无解释器,轻量 ACPI ASL 源文件 → iASL 编译 → AML BIOS/UEFI 加载 AML 到内存 内核 ACPICA 解释执行 AML 驱动匹配 HID/CID → probe() 可执行固件接口,支持热插拔/电源管理

2.4 为什么嵌入式选择设备树而非 ACPI

  • 资源开销:ACPI 的 AML 解释器需要数 MB 的 RAM 和 ROM,而设备树的 libfdt 库仅需几十 KB。
  • 启动速度:设备树解析是线性遍历,O(n) 复杂度;ACPI 需要解释执行 AML 方法,启动延迟更高。
  • 硬件确定性:嵌入式设备的硬件拓扑在制造时已固定,不需要 ACPI 的动态命名空间和热插拔能力。
  • 工具链简单:DTC(Device Tree Compiler)是一个轻量级工具,而 ACPI 的 iASL 编译器 + 调试工具链复杂得多。
  • 社区生态:ARM 生态已全面拥抱设备树,Linux 主线内核的 ARM 平台几乎全部使用 DTS。
例外情况:部分 ARM 服务器(如 Ampere Altra)同时支持设备树和 ACPI,在 UEFI 环境下使用 ACPI 以兼容企业级操作系统(如 Windows Server、RHEL)。嵌入式开发者通常只需关注设备树。

2.5 本章小结

设备树的引入解决了嵌入式 Linux 长期存在的“板级文件泛滥”问题,通过 FDT(扁平设备树) 实现了硬件描述与内核源码的解耦。与 ACPI 相比,设备树更轻量、更简单、更适合资源受限的嵌入式场景。理解设备树的起源和设计哲学,是后续学习 DTS 语法、中断映射、时钟绑定等高级内容的基础。