1. 设备树起源:为什么需要设备树?从板级文件到设备树的演进历史
1.1 那个让人头疼的板级文件时代
做嵌入式Linux开发的老司机们,应该都记得那个被 arch/arm/ 目录支配的恐惧。
大概在2010年以前,每当我们拿到一块新的ARM开发板,第一件事就是往内核里塞一堆 board-xxx.c 文件。这些文件里密密麻麻地写着:
- GPIO几号接了LED
- I2C总线上挂了哪个传感器
- SPI设备的片选信号是哪个引脚
- 内存映射的起始地址是多少
说白了,每个板子都有自己的“身份证”——一个几百行的C文件。内核启动时,这些代码会一股脑地执行,把硬件信息硬编码进去。
核心问题:硬件描述和内核代码完全耦合。换一个板子,就得改内核代码,重新编译。
我记得有一次,客户临时换了一颗Flash芯片,只是引脚重映射了一下。结果呢?我得从头翻那个 board-xxx.c,找到对应的GPIO初始化代码,改完再重新编译整个内核。折腾了大半天,就为了改几个数字。你说烦不烦?
1.2 为什么板级文件模式走不下去了?
你想想看,ARM架构不像x86那样有固定的硬件枚举机制。x86有ACPI、有PCIe的配置空间,操作系统可以自动发现硬件。但ARM呢?每个SoC厂商、每个板子厂商,硬件连接方式千奇百怪。
这就导致了一个现象:内核代码里,ARM架构的板级文件数量呈指数级增长。到了Linux 3.x时代,arch/arm/ 目录下已经有上千个板级文件了。每次内核发布,光维护这些文件就够受的。
更糟糕的是,这些文件里充斥着大量重复代码。比如两个板子用了同一颗SoC,只是外设引脚不同,但它们的板级文件可能有80%的内容是重复的。内核社区的大佬们终于坐不住了——这哪是写驱动,这分明是在做体力活。
1.3 设备树:把硬件描述从内核代码中剥离出来
所以,设备树(Device Tree)的核心理念就一句话:把硬件描述和内核代码解耦。
设备树本质上是一个描述硬件的数据结构。它用树形结构来描述:
- CPU有几个核心
- 内存的地址范围
- 总线上挂了哪些设备
- 每个设备的中断号、寄存器地址、时钟频率
- 引脚复用关系
这些信息不再写在C代码里,而是写在一个 .dts 文件中。内核启动时,会解析这个文件,动态构建硬件信息。换板子?换一个 .dts 文件就行,内核代码不用动。
我的个人习惯:拿到一块新板子,我第一件事就是看它的 .dts 文件。这比看原理图还快,因为设备树已经把硬件拓扑结构清晰地表达出来了。
1.4 设备树的演进时间线
| 时间 | 事件 | 说明 |
|---|---|---|
| 2005年 | PowerPC架构率先引入设备树 | IBM和Freescale为了解决PowerPC板级文件泛滥问题,从Open Firmware中借鉴了设备树概念 |
| 2011年 | ARM架构开始支持设备树 | Linux 3.0开始,ARM社区正式推动设备树化,但初期支持有限 |
| 2012年 | 设备树成为ARM架构的标准配置 | Linux 3.7之后,新加入的ARM平台必须使用设备树,板级文件被标记为“deprecated” |
| 2014年 | 设备树覆盖大部分ARM平台 | Linux 3.16时,ARM架构下设备树已经全面取代板级文件 |
| 至今 | RISC-V、MIPS等架构也采用设备树 | 设备树已经成为嵌入式Linux的事实标准 |
1.5 设备树到底解决了哪些痛点?
我总结了一下,设备树主要解决了三个核心问题:
- 硬件描述与内核代码分离:改硬件配置不用重新编译内核,只需要更新
.dtb文件(编译后的设备树二进制文件)。 - 减少内核代码膨胀:ARM架构的板级文件从上千个减少到几十个核心文件,大部分硬件差异由设备树处理。
- 统一硬件描述语言:不管你是ARM、RISC-V还是MIPS,都用同一套语法描述硬件,驱动代码可以跨架构复用。
我曾经踩过的坑:刚开始用设备树时,我犯过一个低级错误——在 .dts 里把GPIO的极性写反了。结果LED灯怎么点都不亮。查了两天才发现,原来设备树里 GPIO_ACTIVE_LOW 和 GPIO_ACTIVE_HIGH 的定义和我想的正好相反。嗯,从那以后,我每次写GPIO节点都会再三确认极性。
1.6 设备树的工作流程
设备树在系统中的流转过程,其实很简单:
编写 .dts 文件
↓
dtc 编译器编译
↓
生成 .dtb 二进制文件
↓
Bootloader(如U-Boot)加载 .dtb 到内存
↓
内核启动时解析 .dtb,构建 device_node 树
↓
驱动通过 of_xxx 系列API读取硬件信息
↓
驱动完成设备初始化
这里有个细节要注意:.dts 是给人看的文本文件,.dtb 是给机器读的二进制文件。中间那个 dtc 编译器,就是设备树的“翻译官”。
1.7 一个简单的设备树长什么样?
为了让你有个直观感受,我贴一个最简单的设备树示例:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "mycompany,myboard";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 5 4>;
clock-frequency = <1843200>;
};
gpio0: gpio@20000000 {
compatible = "mycompany,gpio";
reg = <0x20000000 0x1000>;
gpio-controller;
#gpio-cells = <2>;
};
};
你看,这个文件清晰地描述了:
- 板子型号是
MyBoard - 内存从
0x80000000开始,大小512MB - 有一个串口,地址
0x10000000,中断号5 - 有一个GPIO控制器,地址
0x20000000
这些信息,以前都得写在C代码里。现在?一个文本文件搞定。
1.8 小结
设备树的出现,说白了就是嵌入式Linux开发的一次“工业化革命”。它把硬件描述从内核代码中解放出来,让驱动开发变得更灵活、更可维护。
我个人觉得,理解设备树的起源,比学会它的语法更重要。因为只有知道了它要解决什么问题,你才能理解为什么设备树要这样设计——为什么要有 compatible 属性?为什么要有 reg 属性?为什么要有中断映射?
这些问题的答案,都藏在板级文件时代的痛苦里。下一章,我会带你深入设备树的语法细节,看看那些 .dts 文件里的每一行到底在说什么。