2. 设备树(Device Tree)起源:为什么引入设备树、扁平设备树(FDT)概念、设备树与ACPI对比
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) 动态构建设备模型,实现“硬件描述数据化、驱动匹配自动化”。
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。
2.2.2 FDT 的启动流程
- Bootloader 加载 DTB:U-Boot 从存储介质(eMMC、NAND、网络)读取
.dtb文件到内存。 - Bootloader 调整 DTB:U-Boot 根据实际硬件(如内存大小、MAC 地址)修改 DTB 中的属性(通过
fdt_setprop())。 - 传递 DTB 地址:U-Boot 将 DTB 的物理地址写入寄存器
r2(ARM 架构约定),然后跳转到内核入口。 - 内核早期解析:内核启动汇编代码保存
r2,在start_kernel()中调用unflatten_device_tree()将 FDT 展开为树状结构。 - 驱动匹配:内核遍历设备树节点,根据
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 的复杂电源管理和热插拔能力,设备树的简洁性更适合资源受限的环境。
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 语法、中断映射、时钟绑定等高级内容的基础。