第1章 设备树起源:为什么需要设备树?
做嵌入式Linux开发的朋友,一定都跟设备树打过交道。但说实话,我刚入行那会儿,设备树对我来说就是个黑盒子——照着模板改,能跑就行,出了问题全靠蒙。
直到有一次,我在一个项目里被板级文件折腾得够呛。那是个基于ARM Cortex-A9的老平台,每次换一个外设,就得改一堆C代码,重新编译整个内核。改一次编译半小时,调试一次烧录五分钟。嗯,那滋味,谁干谁知道。
后来我认真研究了设备树的来龙去脉,才明白这东西到底解决了什么问题。今天咱们就从根源上聊聊:设备树到底是怎么来的?它凭什么能取代板级文件?
1.1 板级文件的时代
在设备树出现之前,Linux内核是怎么描述硬件平台的?
答案是:硬编码。
每个开发板,都在内核源码里有一堆C文件和头文件。这些文件里写死了内存地址、中断号、GPIO引脚、时钟频率……所有硬件信息,全写在代码里。
板级文件的核心问题:
- 硬件信息与内核代码深度耦合
- 换一个板子就要改内核源码
- 不同厂商的板级文件风格各异,维护成本极高
- 同一个SoC的不同开发板,代码大量重复
举个例子。当年我维护一个项目,用的是某厂商的ARM芯片。厂商给了个BSP包,里面光是板级文件就有十几个。每个文件里都有一段类似的代码:
static struct resource uart_resources[] = {
{
.start = 0x10009000,
.end = 0x10009fff,
.flags = IORESOURCE_MEM,
},
{
.start = IRQ_UART0,
.end = IRQ_UART0,
.flags = IORESOURCE_IRQ,
},
};
static struct platform_device uart_device = {
.name = "my_uart",
.id = 0,
.resource = uart_resources,
.num_resources = ARRAY_SIZE(uart_resources),
};
你看,每个外设都要手动定义资源结构体,注册platform_device。一个板子几十个外设,代码量可想而知。更麻烦的是,如果两个板子用的同一个SoC,只是外设引脚不同,你得复制一份代码,改几个数字。内核里充斥着这种「复制-修改」的板级文件。
1.2 为什么ARM社区最先受不了?
你可能会问:x86平台怎么没有设备树?
原因很简单。x86平台有ACPI(高级配置与电源管理接口),BIOS/UEFI固件已经帮你把硬件信息描述好了,内核启动时直接读取就行。但嵌入式平台,尤其是ARM平台,没有统一的固件标准。
ARM架构的碎片化是出了名的。同一个ARMv7指令集,有高通、TI、Freescale、NXP、三星……每家都有自己的外设布局。Linux内核为了支持这些五花八门的板子,板级文件数量呈爆炸式增长。
| Linux内核版本 | ARM板级文件数量(估算) | 主要问题 |
|---|---|---|
| 2.6.x | 数百个 | 每个板子独立文件,代码重复严重 |
| 3.0 ~ 3.8 | 超过1000个 | 维护成本急剧上升,社区不堪重负 |
| 3.9+ | 逐步淘汰 | 引入设备树,板级文件开始迁移 |
我记得在2011年左右,Linus Torvalds公开吐槽过ARM的板级文件乱象。他说:「每次看到ARM的代码提交,都是一堆新的板级文件,这简直是一场灾难。」
说白了,ARM社区需要一个统一的硬件描述方式。设备树,就是在这个背景下被引入的。
1.3 设备树的诞生
设备树(Device Tree)的概念其实不是Linux原创的。它最早来自Open Firmware(开放固件),用于SPARC和PowerPC平台。Linux内核在PowerPC上已经用了很多年,效果不错。
2011年,ARM Linux社区决定引入设备树。从Linux 3.9开始,设备树成为ARM平台的标准硬件描述方式。到2014年左右,大部分主流ARM SoC都完成了设备树迁移。
设备树的核心思想:把硬件信息从内核代码中剥离出来,放到一个独立的数据结构中。内核启动时解析这个结构,动态创建platform_device,而不是在代码里硬编码。
设备树文件通常以.dts为后缀(Device Tree Source),经过编译生成.dtb(Device Tree Blob),由bootloader传递给内核。
一个简单的设备树节点长这样:
/ {
model = "MyBoard";
compatible = "vendor,myboard", "vendor,soc";
uart@10009000 {
compatible = "vendor,uart";
reg = <0x10009000 0x1000>;
interrupts = <0 33 4>;
clocks = <&uart_clk>;
};
};
你看,同样的UART外设,现在用文本描述,而不是C代码。内核驱动通过compatible属性匹配设备,通过reg获取地址,通过interrupts获取中断号。硬件变了,改.dts文件就行,不用重新编译内核。
1.4 设备树在Linux内核中的角色
设备树到底在内核里扮演什么角色?我用一张图来说明:
这张图展示了设备树的完整生命周期:
- 编写:开发者编写
.dts源文件,描述硬件平台 - 编译:DTC(Device Tree Compiler)将
.dts编译为.dtb二进制 - 传递:Bootloader(如U-Boot)将
.dtb加载到内存,传递给内核 - 解析:内核启动时解析
.dtb,动态创建设备,驱动通过compatible匹配
注意:设备树不是万能的。它只描述硬件「有什么」,不描述「怎么用」。驱动的行为逻辑,仍然由C代码实现。设备树只是把硬件配置信息从代码里抽出来了。
1.5 设备树带来的改变
设备树引入后,嵌入式Linux开发流程发生了根本变化:
| 对比项 | 板级文件时代 | 设备树时代 |
|---|---|---|
| 硬件描述方式 | C代码硬编码 | 文本文件描述 |
| 更换板子 | 修改内核源码,重新编译 | 修改.dts,重新编译dtb |
| 内核镜像通用性 | 一个内核只支持一个板子 | 一个内核支持多个板子 |
| 驱动开发 | 需要了解板级注册细节 | 只需关注compatible匹配 |
| 社区维护 | 板级文件泛滥,维护困难 | 设备树统一管理,易于维护 |
我个人最直观的感受是:以前给客户做方案,换一个Flash芯片,得改内核代码、重新编译、验证。现在改一下.dts里的reg属性,重新编译.dtb,重启就行。效率提升不是一星半点。
1.6 避坑指南
最后,分享几个我踩过的坑:
我曾经犯过的错:
- 修改了
.dts但忘记重新编译.dtb,调试了半天发现内核用的还是旧设备树 - 设备树里
reg属性写错了地址,导致驱动访问了错误的内存区域,系统随机崩溃 interrupts属性里的中断类型写错,导致中断无法触发
嗯,这些坑其实都很基础,但越基础的东西越容易忽略。后面章节我会详细讲每个属性的含义和用法,帮你从一开始就避开这些雷区。
设备树的出现,本质上是为了解决硬件描述与内核代码解耦的问题。它不是什么高深的技术,但它的设计思想——把配置从代码中分离出来——值得每个嵌入式开发者深入理解。