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 从板级文件到设备树的演进路线图
为了让你更直观地理解这个演进过程,我画了一张图。它展示了从「硬件信息硬编码」到「设备树统一描述」的完整路径。
1.6 为什么设备树能解决问题?
说白了,设备树做了一件很聪明的事:把「硬件是什么」和「硬件怎么用」分开了。
- 设备树负责「是什么」:描述硬件资源、地址、中断、时钟等静态信息
- 驱动代码负责「怎么用」:读取设备树信息,初始化硬件,提供操作接口
这样一来,换板子只需要改 .dts 文件,驱动代码完全不用动。我后来做过一个项目,同一颗 SoC 用在 5 款产品上,内核镜像完全一样,只换了不同的 .dtb 文件。这在板级文件时代想都不敢想。
一句话总结:设备树让嵌入式 Linux 的硬件描述从「硬编码」走向「数据驱动」。它是 ARM 生态走向成熟的关键一步,也是现代嵌入式 Linux 开发的基石。
1.7 本章小结
这一章我们聊了设备树的起源。从板级文件的痛点,到 PowerPC 的先行探索,再到 ARM 社区的全面转向。我结合自己踩过的坑,帮你理清了为什么需要设备树,以及它解决了什么问题。