2011年3月17日,Linus Torvalds 在 ARM 架构维护者的邮件列表里,留下了一句让整个社区炸锅的话。
原话是:
This whole ARM thing is a f*cking pain in the ass.
— Linus Torvalds, 2011年3月
翻译过来很直接:这东西就是个“该死的麻烦”。
Linux 之父为什么会发这么大火?这得从当时 ARM 内核代码的真实状态说起。
01 ARM内核代码,乱成了一锅粥
在设备树出现之前,Linux内核怎么描述硬件?
答案简单粗暴:硬编码。
每出一块新的开发板,就在内核源码里写一个 mach-xxx.c 文件,把板子上有什么设备、寄存器地址多少、中断号多少,全部写死在 C 代码里。
不同厂商、不同型号的板子各写各的。哪怕用的是同一颗 SoC,只要板子设计不同,就得重新写一套。
结果就是:
- 3000+ 个
mach-* 板级文件
- 1000+ 个
plat-* 平台文件
- 50万+ 行代码,大部分只是描述硬件
这是 Linux 3.0 之前 arch/arm 目录下的真实情况。五十万行代码里,大部分不是驱动逻辑,只是硬件配置信息——地址、中断号、时钟、电源参数。
换块板子,改内核源码,重新编译。同一个 SoC 的不同板子之间,代码几乎无法复用。
Linus 看不下去了。
02 蠢在哪?硬件信息焊死在内核里
传统方式用 platform_device 结构体,在内核代码里逐个注册硬件设备。设备名、物理地址、中断号、时钟——全部写死在 C 文件里。
问题出在哪?
驱动代码和硬件信息混在一起,焊死了。
打个比方:你住进一栋公寓,物业本来只需要给你一份入住手册,告诉你房间号在哪、水电开关在哪。但传统方式的思路,相当于物业把每栋楼的管线图、承重墙位置、施工记录全部写进物业管理制度里。改一栋楼,就得重印一整本制度。
内核镜像(zImage)本来应该只装驱动逻辑,现在却塞满了板级配置。每换一块板子,改的是内核源码,编的是整个内核。
效率低到什么程度?内核越来越胖,维护越来越难,ARM 分支一度是内核社区最头疼的角落。
传统方式 vs 设备树:
| 维度 |
传统硬编码 |
设备树 |
| 硬件信息 |
写在内核源码里 |
独立 DTB 文件 |
| 换板子 |
改内核源码、重编译 |
换 DTB 文件即可 |
| 同 SoC 不同板子 |
几乎无法复用 |
dtsi 共享,dts 覆盖 |
| 内核镜像 |
每块板子一个镜像 |
一个镜像通吃 |
03 设备树——一块板子的“硬件简历”
Linus 发火之后,ARM 社区意识到:硬编码的硬件描述方式不能再继续了。必须设计一套通用、可移植、可复用的硬件描述标准。
设备树(Device Tree)就是这么来的。
本质很简单:把硬件信息从内核代码里拎出来,写成一个独立的文本文件。内核启动时读取这个文件,就知道板子上有什么硬件、该怎么配置。

你可以把它理解成一块板子的“硬件简历”。

板子出厂时带着这份简历,上面写着:我叫什么名字(compatible)、我住在哪里(reg 地址)、中断号是多少(interrupts)、当前是否启用(status)。
内核不需要提前认识每块板子。它只需要看简历,按图索骥。
同一个内核镜像,换不同的 DTB 文件,就能适配不同的硬件。驱动代码和硬件信息彻底解耦。
设备树的语法是统一的标准格式:
device@address { compatible = "vendor,model"; reg = ; interrupts = ; status = "okay"; };
所有平台用同一套语法,同一个工具链。不同板子之间可以通过引用公共文件复用配置。比如同一颗 SoC 的通用配置写一份 .dtsi 文件,每块板子只写自己的差异部分:
// rk3506.dtsi — SoC通用定义(所有板子共用) &uart0 { compatible = "rockchip,rk3506-uart"; status = "disabled"; // 默认禁用 }; // board1.dts — 板子1启用UART0 &uart0 { status = "okay"; }; // board2.dts — 板子2禁用UART0 &uart0 { status = "disabled"; };
SoC 层面的配置写一次,所有板子共用。板子层面只写“我跟默认配置不一样的地方”。代码量直接砍掉一大截。
04 四个零件,各司其职
设备树整套体系由四部分组成,分工明确:
- DTS:板级源文件(
.dts),描述当前开发板的独有硬件配置,是设备树的入口。
- DTSI:公共头文件(
.dtsi),SoC 级通用配置,类似 C 语言的 .h 文件,可被多个 DTS 引用。
- DTC:设备树编译器,把文本设备树编译成二进制,类似 C 语言的 GCC。
- DTB:二进制文件(
.dtb),编译产物,内核启动时实际解析的就是它。
如果你写过 C 程序,这个分工很好理解:DTS 是 .c 源码,DTSI 是 .h 头文件,DTC 是编译器,DTB 是编译出来的可执行文件。
完整流程如下:
DTS(板级配置) + DTSI(公共配置) → DTC(编译器) → DTB(二进制产物)
更直白一点:DTS + DTSI → DTC 编译 → DTB → 内核解析。
开发者写 DTS 描述自己的板子,引用 DTSI 复用 SoC 通用配置,用 DTC 编译出 DTB 二进制文件。设备上电后,U-Boot 加载 DTB,内核解析它,完成硬件适配。
05 开机的时候发生了什么
设备树最终被内核读取,是在开机启动阶段。整个过程可以拆成四步:
- U-Boot 加载 DTB:上电启动,U-Boot 把 DTB 文件加载到内存,把 DTB 地址存入
r2 寄存器,传给内核。
- 内核展开设备树:内核调用
unflatten_device_tree(),把紧凑的二进制 DTB 展开成一棵 device_node 树,在内存里形成硬件拓扑结构。
- 匹配创建设备:内核根据每个节点的
compatible 属性,找到对应驱动,创建 platform_device 等设备实例。
- 驱动 probe:驱动从
device_node 读取具体配置参数(地址、中断、时钟),完成硬件初始化,板子就跑起来了。
注意第 3 步——内核并不认识硬件,它只是读 DTB 里的 compatible 属性,拿着这个字符串去驱动列表里匹配。compatible 就是设备树和驱动之间的“接头暗号”。
驱动说“我认识 rockchip,rk3506-uart”,设备树里某个节点的 compatible 也是这个值,两边对上了,probe 触发,硬件就活了。
06 一句话记住设备树
从 2011 年 Linus 的一句暴怒,到设备树成为 ARM 架构的硬件描述标准,核心思路其实就一个:
设备树就是一块板子的“硬件简历”——内核看简历就知道该用什么、怎么用,不用再把每块板子的信息写进自己的脑子里。
这篇讲的是设备树的来龙去脉和基本概念。如果你要实际写设备树文件,还需要看具体 SoC 的 binding 文档、设备树语法规范,以及内核源码里 arch/arm/boot/dts/ 或 arch/arm64/boot/dts/ 目录下的现有范例。
从硬编码到设备树,本质上是一次“解耦”——把该分开的东西分开,让每个部分各管各的事。
Linus 那句暴怒,催生的不只是设备树,更是 Linux 内核硬件适配方式的一次彻底重构。