1. 设备树起源:为什么需要设备树?从板级文件到设备树的演进历史

1.1 嵌入式Linux早期的“板级文件”时代

在Linux内核引入设备树(Device Tree)之前,嵌入式Linux开发者依赖一种被称为“板级文件”(Board File)的机制来描述硬件。每个开发板或平台都需要在Linux内核源码的 arch/arm/ 目录下编写一个或多个 .c 文件,其中硬编码了该板卡的所有硬件资源信息:内存基址、中断号、GPIO映射、时钟频率、外设寄存器地址等。

这种模式在早期嵌入式设备种类较少、硬件相对简单时尚可接受。但随着ARM架构的爆发式增长,板级文件模式逐渐暴露出严重问题。

板级文件的核心痛点: 内核源码与硬件描述高度耦合。每增加一个新板卡,就必须修改内核源码、重新编译内核。Linux内核主线中充斥着大量重复、冗余的板级文件,导致内核体积膨胀、维护成本急剧上升。

1.2 板级文件 vs 设备树:核心差异对比

对比维度 板级文件(Board File) 设备树(Device Tree)
描述方式 C语言结构体 + 宏定义,硬编码在内核源码中 文本文件(.dts),使用节点/属性描述硬件
与内核关系 必须编译进内核,修改需重新编译内核 独立于内核二进制,通过Bootloader传递
硬件变更响应 修改内核源码 → 重新编译 → 烧录新内核 修改.dts → 编译.dtb → 替换设备树二进制
多板卡支持 每个板卡一个文件,内核镜像膨胀 同一内核镜像 + 不同.dtb,灵活切换
社区维护成本 极高,大量重复代码难以合并 低,硬件描述与驱动逻辑分离
标准化程度 厂商自定义,无统一规范 遵循ePAPR规范,跨架构通用

1.3 演进历史:从混乱到统一

设备树的引入并非一蹴而就,而是经历了多个关键里程碑。以下时间线展示了嵌入式Linux硬件描述方式的演进路径:

  1. 2005年以前: PowerPC架构率先使用设备树(源于Open Firmware),但ARM社区仍坚持板级文件模式。
  2. 2005-2010年: ARM设备数量激增,板级文件数量突破数百个,内核维护者开始呼吁改革。
  3. 2011年: Linux 3.0 发布,ARM架构开始实验性引入设备树支持,但尚未全面推广。
  4. 2012年: Linux 3.4-3.7 期间,ARM社区加速设备树迁移,大量板级文件被标记为“deprecated”。
  5. 2014年: Linux 3.16 发布,ARM架构中设备树成为默认硬件描述方式,板级文件基本退出历史舞台。
  6. 2015年至今: 设备树成为RISC-V、ARM64、x86(部分场景)等架构的通用硬件描述标准。
关键转折点: 2011年,Linus Torvalds 在邮件列表中公开批评ARM社区“充斥着大量垃圾代码”,直接推动了设备树在ARM架构的全面落地。这一事件被社区称为“ARM的觉醒时刻”。

1.4 设备树的核心设计思想

设备树的设计哲学可以概括为:“硬件描述与驱动代码分离”。它将硬件资源信息从内核源码中剥离出来,形成一份独立的数据结构,由Bootloader在启动时传递给内核。

设备树文件(.dts)经过编译生成二进制设备树(.dtb),内核在启动时解析.dtb,动态构建硬件设备模型,并匹配对应的驱动程序。这一机制带来了以下关键优势:

  • 二进制兼容性: 同一内核镜像可以运行在不同硬件平台上,只需更换.dtb文件。
  • 驱动复用: 驱动代码不再依赖具体板卡,而是通过设备树中的“compatible”属性进行匹配。
  • 社区协作: 硬件厂商只需提供.dts文件,无需修改内核主线,降低了贡献门槛。
  • 启动灵活性: Bootloader(如U-Boot)可以在启动时动态修改设备树(如调整内存大小、禁用外设)。

1.5 设备树工作流程概览

下图展示了从设备树源文件到内核驱动匹配的完整工作流程:

设备树工作流程概览 设备树工作流程:从 .dts 到驱动匹配 .dts 源文件 文本格式,可读可编辑 DTC 编译器 设备树编译器 .dtb 二进制 扁平化设备树 Bootloader U-Boot 传递 .dtb 内核解析 .dtb 构建设备模型 驱动匹配 compatible 匹配 💡 关键说明: • .dts 源文件由开发者编写,描述硬件拓扑和资源。 • .dtb 是二进制格式,由 Bootloader 加载到内存并传递给内核。 • 内核通过 compatible 属性匹配驱动,实现硬件与驱动的解耦。

1.6 设备树带来的变革:一个实际案例

假设我们需要为一个新的开发板添加UART串口支持。在板级文件时代,开发者需要:

  1. 在 arch/arm/mach-xxx/ 下创建板级文件,定义UART的寄存器基址、中断号、时钟ID。
  2. 调用 platform_device_register() 注册设备。
  3. 修改 Kconfig 和 Makefile,确保新文件被编译进内核。
  4. 重新编译整个内核,烧录到开发板。

而在设备树时代,只需在.dts文件中添加以下节点:

/* 设备树节点:描述UART外设 */
uart0: serial@10000000 {
    compatible = "vendor,uart-ip";
    reg = <0x10000000 0x1000>;
    interrupts = <0 33 4>;
    clocks = <&uart_clk>;
    status = "okay";
};

内核驱动通过 of_match_table 匹配 compatible 属性,自动获取寄存器地址、中断号和时钟信息,无需任何内核源码修改。

⚠️ 常见误区: 设备树不是万能解药。它只描述“硬件有什么”,不描述“硬件怎么用”。驱动逻辑、数据传输、中断处理等仍然需要C代码实现。设备树是硬件信息的载体,而非驱动的替代品。

1.7 本章小结

设备树的诞生是嵌入式Linux发展史上的重要转折点。它解决了板级文件时代硬件描述与内核代码高度耦合的顽疾,使得Linux内核能够以更优雅、更可维护的方式支持海量嵌入式设备。理解设备树的起源和设计思想,是掌握后续.dts编写和驱动适配的基础。

演进阶段 核心特征 代表架构
板级文件时代(2005-2011) 硬件描述硬编码在C文件中,修改需重新编译内核 ARM32
过渡期(2011-2014) 设备树与板级文件并存,逐步迁移 ARM32
设备树时代(2014-至今) 设备树成为标准硬件描述方式,跨架构通用 ARM64、RISC-V、ARM32