Skip to content

04. 设备树历史演进——从混乱到秩序的变革之路

前言:为什么要了解历史

说实话,我刚开始学设备树的时候,压根没想过去研究它的历史。那时候的想法很简单:这东西能用就行了,管它从哪来的呢?但后来在实际开发中踩了足够多的坑之后,我意识到一个道理:理解技术的来龙去脉,能帮你更好地理解它为什么要这样设计

你知道设备树为什么是树状结构吗?为什么要有 .dts.dtsi 的区分?为什么有些属性叫 compatible 而不是别的名字?这些问题的答案,都藏在设备树的演进历史里。

而且,当你了解了那段混乱的历史,你会对今天能使用设备树这种优雅的解决方案感到庆幸。这一章,我们就来聊聊设备树的前世今生——从 PowerPC 上的诞生,到 ARM 社区的混乱时代,再到 Linus 的那次著名"爆发",最后演变成今天多架构通用的标准机制。

这不仅仅是历史课,更是一次理解设计思想的机会。你会发现,技术演进的过程,本质上就是不断解决实际问题的过程。


PowerPC 时代:设备树的诞生

设备树并不是 ARM 的原创,它的起源要追溯到更早的 PowerPC 时代。

上世纪 90 年代,IBM 和苹果在合作开发 PowerPC 架构的电脑。那时候他们面临一个问题:如何让操作系统在没有人工干预的情况下,自动识别板子上的硬件设备?要知道,PowerMac 系列有好几代产品,每代的硬件配置都不一样,但系统软件希望能做到"一张光盘装遍所有机型"。

解决方案来自一个叫做 Open Firmware 的标准。这是 IEEE 制定的一个固件接口规范,它的核心思想是:固件应该向操作系统提供一份完整的硬件描述,这样操作系统就不需要为每块板子写专门的初始化代码了。

Open Firmware 里定义了一种叫做"设备树"的数据结构,用树状层次来描述系统中的所有设备。每个设备是一个节点,节点里包含设备的属性(比如寄存器地址、中断号、时钟频率等)。这种结构非常直观,和硬件的实际连接方式一一对应。

早期的 PowerPC 机器在启动时,固件会把设备树构建好,放在内存里,然后告诉操作系统:"嘿,硬件信息都在这儿了,你自己看着办。"操作系统只需要遍历这棵树,就能知道该加载哪些驱动、怎么访问这些设备。

这个设计在当时非常先进。它把硬件描述从操作系统代码里剥离出来,大大提高了代码的可移植性。如果你换了一块板子,只需要更新固件里的设备树,操作系统代码完全不用改。

但那时候这个技术还局限在 PowerPC 和 SPARC 这些"高端"架构上。x86 世界里走的是另一条路——BIOS/ACPI,而 ARM 呢?那时候 ARM Linux 还在混乱的"蛮荒时代",根本没意识到这个问题的严重性。


ARM 的混乱时代:代码膨胀的噩梦

时间来到 2000 年代中后期,ARM 芯片开始爆发式增长。智能手机、平板电脑、嵌入式设备遍地开花,每家芯片厂商都在推自己的 SOC,每块板子都有自己的硬件配置。

但当时的 ARM Linux 社区处理硬件描述的方式非常原始——直接写在 C 代码里

内核源码树里塞满了 arch/arm/mach-xxxarch/arm/plat-xxx 目录。mach 是 machine 的缩写,plat 是 platform 的缩写。每个目录对应一种板子或一个芯片平台,里面全是描述板级信息的 .c.h 文件。

我们来看一个真实的例子。这是早期 Linux 内核中针对 S3C2440 开发板的板级初始化代码:

c
/* arch/arm/mach-s3c2440/mach-smdk2440.c (片段) */
static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {
    .lcdcon5 = S3C2410_LCDCON5_FRM565 |
              S3C2410_LCDCON5_INVVLINE |
              S3C2410_LCDCON5_INVVFRAME |
              S3C2410_LCDCON5_PWREN |
              S3C2410_LCDCON5_HWSWP,
    .type = S3C2410_LCDCON1_TFT,
    .width = 240,
    .height = 320,
    .pixclock = 166667,
    .xres = 240,
    .yres = 320,
    .bpp = 16,
    /* ... 还有一堆配置 ... */
};

static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
    .displays        = &smdk2440_lcd_cfg,
    .num_displays    = 1,
    .default_display = 0,
    .lpcsel          = ((0x01 << 1) | (0x01 << 7)),
};

static struct platform_device *smdk2440_devices[] __initdata = {
    &s3c_device_ohci,
    &s3c_device_lcd,
    &s3c_device_wdt,
    &s3c_device_i2c0,
    &s3c_device_iis,
    &s3c_device_usbgadget,
};

static void __init smdk2440_init(void)
{
    s3c24xx_fb_set_platdata(&smdk2440_fb_info);
    platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
    /* ... 更多初始化代码 ... */
}

你看,这仅仅是描述一块板子上的 LCD 配置和设备列表。而使用 S3C2440 芯片的板子有成百上千种,理论上每种板子都需要一个类似的文件。

更糟糕的是,这些文件的重复率高达 90% 以上。为什么?因为大部分代码都在描述芯片本身的信息——这个芯片有几个 UART、每个 UART 的寄存器地址是多少、GPIO 控制器在哪里。这些是芯片固有的属性,不应该因为板子不同而改变。但在旧的做法里,每块板子都要复制一遍。

到了 2010 年左右,ARM Linux 内核源码已经膨胀到一个可怕的程度。arch/arm 目录下的代码量比其他所有架构加起来还要多。每次内核发布,ARM 社区都会向主线提交海量的板级代码,大部分都是换汤不换药的重复劳动。

维护成本也高得离谱。假设某个驱动的板级初始化代码写错了,你需要修复所有使用这个驱动的板子代码。如果有上百块板子,你就得改上百个文件。而且这些文件散落在不同的 mach-xxx 目录下,很难保证都改对了。

这种混乱状态让 ARM Linux 的发展陷入了瓶颈。内核开发者们意识到:必须改变,否则 ARM Linux 会因为自己的重量而崩溃


Linus 的怒火:历史的转折点

2011 年,事情终于到了爆发的临界点。

当年 6 月,在澳大利亚墨尔本举行的 Linux Kernel Summit 上,ARM 架构的维护者 Russell King 做了一个关于 ARM Linux 当前状态的报告。他详细描述了 ARM 代码膨胀的问题,以及维护工作的巨大困难。

Linus Torvalds 坐在台下,越听脸色越难看。当看到幻灯片上展示的那些重复的板级代码时,他终于忍不住了。

在随后的讨论环节中,Linus 说了一句后来被无数人引用的名言:

"This whole ARM thing is a f*cking pain in the ass, it's a pain in the ass to merge these trees because each board does things differently."

(这该死的 ARM 事儿真是一团糟,合并这些代码树简直是痛苦,因为每块板子都用不同的方式做同样的事情。)

Linus 的愤怒不是没有道理的。作为 Linux 之父,他每天要处理来自世界各地的代码提交。ARM 社区每次都提交海量的、重复的板级代码,让合并工作变成了一场噩梦。更让他无法接受的是,这些代码大部分都在做同一件事——描述硬件,只是方式各不相同。

在邮件列表的后续讨论中,Linus 更加直白地表达了他的不满:

"Guys, this whole ARM thing is a f*cking pain in the ass. We have 'arch/arm/mach-foo' and 'arch/arm/plat-bar' directories that contain tons of random board files, and it's all garbage."

(伙计们,ARM 这事儿真是一团糟。我们有一堆 'arch/arm/mach-foo' 和 'arch/arm/plat-bar' 目录,里面全是随机的板级文件,全都是垃圾。)

这次"爆发"成为了 ARM Linux 历史上的一个转折点。Linus 的态度很明确:ARM 社区必须解决代码膨胀问题,否则他拒绝继续合并这些垃圾代码


ARM 社区的妥协:引入设备树

面对 Linus 的最后通牒,ARM 社区不得不做出改变。但他们没有从零开始设计新方案,而是把目光投向了 PowerPC 等架构已经使用了多年的技术——设备树

其实早在 2005 年左右,就已经有人在 ARM 架构上尝试过设备树。当时 Silicon Graphics(SGI)的工程师在开发基于 MIPS 的系统时,把 PowerPC 的设备树机制移植了过来。后来这个机制也被引入到 ARM 架构中,但一直作为可选功能,没人真正重视。

现在情况不同了。Linus 已经下了命令,ARM 社区必须认真对待这个问题。

2011 年下半年,ARM 维护者开始推动设备树的普及工作。他们的策略是渐进式的:

  1. 第一步:让新板子的代码使用设备树,老板子保持现状。
  2. 第二步:逐步把老板子迁移到设备树。
  3. 第三步:最终淘汰所有的板级 C 代码。

这个策略避免了"休克疗法"带来的混乱,给了社区足够的适应时间。

设备树在 ARM 上的引入并非一帆风顺。很多驱动开发者一开始并不买账——他们已经习惯了在 C 代码里写初始化逻辑,突然要改用设备树,需要学习新的语法和 API,还要重构现有代码。这种转变是有成本的。

但随着越来越多的芯片厂商开始支持设备树,越来越多的驱动代码开始适配设备树 API,趋势已经不可逆转。到了 2013 年左右,新提交的 ARM 板级代码几乎都使用了设备树,老代码的迁移工作也在稳步推进。

值得一提的是,ARM 社区并没有照搬 PowerPC 的设备树实现,而是在此基础上做了很多改进。他们定义了一套更加完善的设备树绑定文档,规范了各种设备的属性命名和格式。这些文档后来成为了多架构共享的规范,不仅仅是 ARM 在用。这个文档现在也在咱们的仓库中,笔者也对之做了7篇笔记!可以的话欢迎到document/tutorial/driver/device_tree下转转去!


演进过程:从可选到强制

设备树在 ARM 上的普及经历了一个从"可选机制"到"强制要求"的演进过程。我们来回顾一下这个历程的关键节点。

早期阶段(2011-2012):作为可选辅助机制

在设备树刚引入 ARM 架构时,它和传统的板级 C 代码是并存的。你可以选择用设备树描述硬件,也可以继续写 mach-xxx 文件。

内核启动时会判断:如果 bootloader 传入了设备树,就用设备树;否则就退回到传统的板级初始化代码。这种兼容性设计保证了平滑过渡。

但这个阶段的问题是:很多驱动还没有适配设备树 API。即使你在设备树里描述了硬件,驱动程序也不知道怎么读取这些信息。所以开发者不得不两头兼顾——既写设备树,又在板级代码里手动初始化设备。

这种半吊子状态一度让设备树看起来"很鸡肋"。你花了时间写设备树文件,结果还是要写 C 代码,工作量反而增加了。很多人在这个阶段选择了观望。

中期阶段(2013-2015):逐步淘汰板级代码

随着内核版本的迭代,越来越多的驱动完成了设备树适配。主要的子系统——GPIO、I2C、SPI、UART、中断控制器——都提供了完善的设备树支持 API。

社区开始明确发出信号:不要再提交新的板级 C 代码了。维护者开始拒绝那些没有设备树支持的板级代码补丁。

这个阶段还发生了一件重要的事情:多平台内核的诞生。

在设备树普及之前,每个 SOC 都需要编译一个专门的内核镜像。你想让内核在 I.MX6ULL 和 I.MX6Q 上都能运行?对不起,得编译两个版本。因为板级初始化代码在编译时就固定了,运行时没法切换。

有了设备树之后,情况变了。内核只需要编译一次,运行时根据设备树的内容来决定初始化哪些设备。这就像给内核装上了"变色龙"的能力——同一个内核镜像,配合不同的设备树,就能在不同的板子上运行。

这个变化对产品开发的影响是巨大的。厂商可以维护一个统一的内核镜像,只需要针对不同的板子版本提供对应的设备树文件就行了。更新硬件配置不再需要重新编译整个内核。

现代阶段(2016 至今):多架构的标准机制

如今,设备树已经不再仅仅是 ARM 架构的专属。它被移植到了多个架构中,成为了 Linux 内核中描述硬件的通用机制

使用设备树的主要架构包括:

  • ARM:包括 32 位和 64 位
  • ARM64(AArch64):强制要求,不支持传统的板级代码
  • PowerPC:设备树的老家
  • RISC-V:从第一天起就完全采用设备树
  • MIPS:部分平台使用
  • x86:虽然主要使用 ACPI,但在一些嵌入式场景下也支持设备树

特别值得一提的是 ARM64。这个架构从设计之初就完全抛弃了传统的板级 C 代码,强制使用设备树。如果你在写 ARM64 的板级支持,设备树是唯一的选择,没有退路。

在主线内核中,设备树的代码已经非常成熟和稳定。drivers/of 目录下包含了完整的设备树解析和处理逻辑,内核的驱动模型也深度集成了设备树 API。


当前状态:设备树在今天的地位

经过十多年的发展,设备树已经成为 Linux 嵌入式开发中不可或缺的基础设施。我们来看看它在今天的地位和状态。

在主线内核中的地位

设备树相关的代码分布在内核的多个部分:

  • drivers/of/:设备树核心代码,包括解析、属性查询、设备匹配等
  • scripts/dtc/:设备树编译器 DTC 的源码
  • arch/xxx/boot/dts/:各个架构的设备树源文件
  • Documentation/devicetree/:设备树的规范和绑定文档

设备树 API 已经深深融入了内核的驱动框架。无论是注册平台设备、申请 GPIO、获取中断号,还是请求时钟、电源域,都有对应的设备树辅助函数。写驱动时使用设备树已经成为了默认做法。

哪些架构在使用设备树

截至 Linux 6.x 内核,以下架构主要使用设备树:

架构设备树支持状态备注
ARM完全支持新板子强制使用
ARM64强制要求不支持传统板级代码
RISC-V强制要求从第一天起就采用设备树
PowerPC完全支持设备树的发源地
MIPS部分支持部分平台使用,部分使用其他机制
x86可选支持主要使用 ACPI,嵌入式场景可用设备树
LoongArch部分支持新架构,逐步采用设备树

设备树叠加(Device Tree Overlay)

近年来,设备树生态系统还发展出了一个重要特性:设备树叠加

这个功能允许你在运行时动态地修改设备树,而不是在启动时固定下来。这在某些场景下非常有用,比如:

  • 可扩展硬件:通过扩展槽添加新设备,动态加载对应的设备树片段
  • 模块化设计:主设备树描述基础硬件,模块设备树描述扩展功能
  • 开发调试:不需要重新编译整个设备树,可以叠加测试配置

设备树叠加使用特殊的编译选项(-@)生成符号信息,然后在运行时通过 configfs 接口加载叠加层。这个功能在树莓派等单板计算机上得到了广泛应用。

未来的发展趋势

设备树会继续演进吗?答案是肯定的,但变化更多是在细节和工具层面,而不是核心机制。

当前的发展趋势包括:

  1. 更完善的绑定文档:规范越来越严格,减少模糊地带
  2. 更好的工具支持:验证工具、静态分析、文档生成
  3. 与 ACPI 的共存:在服务器领域,ARM64 正在向 ACPI 靠拢,设备树和 ACPI 将长期并存
  4. 硬件描述的标准化:不仅限于 Linux,其他操作系统(如 FreeBSD、Zephyr)也开始支持设备树

有一点是肯定的:在可预见的未来,设备树仍将是嵌入式 Linux 领域描述硬件的主流方式


小结

这一章我们完整地回顾了设备树的历史演进,从 PowerPC 时代的诞生,到 ARM 的混乱年代,再到 Linus 的怒火和随后的变革,最后到今天多架构通用的标准机制。

我们了解到:

  • 设备树起源于 Open Firmware 标准,最早在 PowerPC 上使用
  • ARM 早期采用硬编码方式,导致代码膨胀和维护噩梦
  • Linus 在 2011 年的著名"爆发"成为了转折点
  • ARM 社区引入设备树,经历了从可选到强制的演进过程
  • 如今设备树已成为多架构通用的硬件描述机制
  • 技术债务会累积利息,早期设计至关重要

了解这些历史,不仅能帮你更好地理解设备树的设计思想,也能在你参与技术选型和架构设计时提供参考。优秀的技术方案不仅能解决当前问题,还要为未来的扩展留出空间。设备树正是这样一个例子——它从解决 PowerPC 的实际问题出发,最终成为了 Linux 嵌入式领域的通用基础设施。

下一章,我们将进入实战环节,讲解如何在驱动程序中读取和使用设备树的信息,把理论知识应用到实际开发中。到时候你会发现,设备树不仅仅是一张"硬件说明书",它更是连接硬件和软件的桥梁,是驱动开发的基石。


下一步

Built with VitePress