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

1.1 一个让我头疼的问题

我刚入行那会儿,做嵌入式 Linux 开发最怕什么?不是内核崩溃,不是驱动 bug,而是——换了一块板子,内核就起不来了。

你想想看,那时候每块开发板都有一堆 .c 和 .h 文件,专门描述板子上有什么硬件。CPU 型号、内存地址、GPIO 怎么接、I2C 挂在哪……全写死在代码里。我印象特别深,有一次项目要换一颗 Flash 芯片,就改了个分区表,结果编译出来的内核镜像死活不认新分区。查了两天,最后发现是板级文件里有个宏定义没同步更新。

说白了,这种「硬件描述硬编码」的方式,在早期嵌入式设备少、变化慢的时候还能凑合。但到了 ARM 架构大爆发、SoC 厂商遍地开花的年代,问题就藏不住了。

1.2 板级文件时代的痛点

在设备树出现之前,Linux 内核用的是什么?板级文件(Board File)。每个板子对应一个 arch/arm/mach-xxx/board-xxx.c,里面塞满了硬件初始化代码。

我列几个当时最让人抓狂的问题:

  • 代码冗余严重:同一颗 SoC 用在 10 块板子上,板级文件里 80% 的代码是重复的。我见过一个同事,从别的板子复制粘贴了一份文件,改了改名字就提交了——结果漏改了一个 GPIO 号,硬件点不亮。
  • 内核镜像与硬件绑定:每块板子都要单独编译一个内核。你拿 A 板的内核去跑 B 板,大概率启动到一半就 panic。我记得有一次出差,带错了 SD 卡,现场重新编译内核,客户在旁边等着,那叫一个尴尬。
  • 社区维护成本爆炸:Linux 主线内核里,ARM 架构的板级文件一度超过 2000 个。每次内核版本升级,这些文件都要跟着改。Linus 本人就发过火,说 ARM 代码是「一团乱麻」。
  • 设备枚举不灵活:硬件信息写死在 C 代码里,想改个中断号?重新编译。想加个外设?重新编译。想换个内存大小?还是重新编译。

核心矛盾:内核代码应该描述「行为」,而不是描述「硬件」。硬件信息是数据,不是程序。

3. 设备树的诞生:把硬件描述从代码里解放出来

设备树(Device Tree)最早来自 PowerPC 架构。IBM 和 Motorola 当年也遇到过类似的问题——PowerPC 的板级文件同样又臭又长。所以他们搞了一套 Open Firmware 规范,用设备树来描述硬件拓扑。

2011 年左右,ARM 社区终于坐不住了。Linus 在邮件列表里直接开喷:「ARM 的代码简直是一坨屎,你们能不能学学 PowerPC 的做法?」这话虽然难听,但确实推动了 ARM 架构全面转向设备树。

设备树的核心思想其实很简单:

  • 把硬件描述从 C 代码里抽出来,放到一个 文本文件 里
  • 这个文本文件用 树形结构 描述 CPU、内存、总线、外设等所有硬件资源
  • 内核启动时解析这个文件,动态构建硬件信息

我举个例子,以前板级文件里描述一个 UART 设备,大概是这样:

static struct platform_device uart_device = {
    .name = "my_uart",
    .id = 0,
    .resource = (struct resource[]) {
        {
            .start = 0x10000000,
            .end   = 0x10000FFF,
            .flags = IORESOURCE_MEM,
        },
        {
            .start = 42,
            .end   = 42,
            .flags = IORESOURCE_IRQ,
        },
    },
};

换成设备树,就变成了这样:

uart0: serial@10000000 {
    compatible = "my_uart";
    reg = <0x10000000 0x1000>;
    interrupts = <42>;
};

你看,同样的信息,后者清晰得多。而且最关键的是——改硬件描述不需要重新编译内核。换 Flash 分区?改 .dts 文件,重新编译设备树就行,内核镜像不用动。

我个人习惯:把设备树看作「硬件的 JSON 配置文件」。它不包含任何逻辑,只描述「有什么、在哪、怎么用」。驱动代码负责读这个配置,然后做该做的事。

1.4 设备树的核心概念速览

在深入后面的章节之前,我们先快速过一下设备树的基本骨架。这样你心里有个底。

概念 说明 我踩过的坑
.dts 文件 设备树源文件,文本格式,人类可读写 缩进不对,编译报错,找了半天
.dtsi 文件 设备树头文件,用于 SoC 级公共描述 忘了 include,重复定义了节点
.dtb 文件 编译后的二进制设备树,内核直接使用 版本不匹配,内核解析失败
节点(node) 描述一个硬件设备或总线 节点名格式写错,驱动匹配不上
属性(property) 节点的具体参数,如地址、中断号 reg 属性大小端搞反了
compatible 驱动匹配的关键字符串 拼写错误,驱动死活不 probe

我曾经犯过的错:刚接触设备树时,我以为 .dts 文件可以随便写,结果编译出来的 .dtb 内核根本不认。后来才发现,设备树有严格的语法规范,节点名、属性名、地址格式都不能乱来。后面我会专门讲这些规则。

1.5 从板级文件到设备树的演进路线图

为了让你更直观地理解这个演进过程,我画了一张图。它展示了从「硬件信息硬编码」到「设备树统一描述」的完整路径。

从板级文件到设备树的演进路线 板级文件时代 board-xxx.c / h 设备树初期 PowerPC 率先采用 ARM 全面转向 2011-2014 年 痛点 • 代码冗余严重 • 内核与硬件绑定 • 维护成本爆炸 • 设备枚举不灵活 解决方案 • 硬件描述文本化 • 树形结构组织 • 内核动态解析 • 驱动与硬件解耦 现状 • ARM/ARM64 标配 • RISC-V 全面采用 • x86 也开始支持 • U-Boot 直接加载 核心转变:硬件信息从「代码」变成「数据」 一次编译,到处运行(同一 SoC 的不同板子)

1.6 为什么设备树能解决问题?

说白了,设备树做了一件很聪明的事:把「硬件是什么」和「硬件怎么用」分开了。

  • 设备树负责「是什么」:描述硬件资源、地址、中断、时钟等静态信息
  • 驱动代码负责「怎么用」:读取设备树信息,初始化硬件,提供操作接口

这样一来,换板子只需要改 .dts 文件,驱动代码完全不用动。我后来做过一个项目,同一颗 SoC 用在 5 款产品上,内核镜像完全一样,只换了不同的 .dtb 文件。这在板级文件时代想都不敢想。

一句话总结:设备树让嵌入式 Linux 的硬件描述从「硬编码」走向「数据驱动」。它是 ARM 生态走向成熟的关键一步,也是现代嵌入式 Linux 开发的基石。

1.7 本章小结

这一章我们聊了设备树的起源。从板级文件的痛点,到 PowerPC 的先行探索,再到 ARM 社区的全面转向。我结合自己踩过的坑,帮你理清了为什么需要设备树,以及它解决了什么问题。