一、设备树的前世今生:为什么需要设备树?从板级文件到设备树的演进
1.1 那个没有设备树的年代
说实话,我刚入行那会儿,Linux内核里还没有设备树这东西。那时候我们做嵌入式开发,最头疼的事情之一就是——改板子。
你想想看,一个芯片方案,今天客户要换颗传感器,明天要改个GPIO复用,后天又想把LCD接口从RGB换成LVDS。每次硬件改版,内核代码就得跟着改。改什么?改那个叫board-xxx.c的板级文件。
我记得2010年左右,我在一家做安防摄像头的公司。当时我们用的是一颗ARM9的芯片,每次硬件改版,我都要在arch/arm/mach-xxx目录下翻半天,找到对应的板级文件,然后手动修改寄存器地址、中断号、GPIO配置。改完还得重新编译整个内核。那感觉,就像每次搬家都要重新装修房子一样痛苦。
- 硬件改动 → 内核代码改动 → 重新编译 → 烧录验证
- 每个板子一个
board-xxx.c,代码冗余严重 - 驱动代码和硬件信息高度耦合,可移植性极差
- 不同厂商的板级文件风格各异,维护成本高
说白了,那时候的Linux内核,就像一个把所有家具都焊死在墙上的房子。你想换个沙发?对不起,得把墙拆了重砌。
3.2 设备树的诞生:把硬件信息从代码中剥离
为什么会这样?因为内核里同时包含了「驱动逻辑」和「硬件配置信息」。这两者本该是分开的。
设备树(Device Tree)的出现,就是为了解决这个问题。它的核心思想很简单:把硬件描述从内核代码中抽离出来,放到一个独立的数据结构中。
这个数据结构,就是设备树源文件(.dts)。它用树形结构描述硬件:CPU、内存、外设、中断、GPIO、时钟……所有硬件资源,都在这里定义。内核启动时,解析这个文件,动态构建硬件信息,然后驱动去匹配、去使用。
嗯,这里要注意:设备树不是Linux独有的。它最早源于PowerPC架构,后来被ARM社区采纳,再后来几乎成了嵌入式Linux的标准做法。
3.3 从板级文件到设备树的演进路线
这个演进过程,我把它总结为三个阶段:
| 阶段 | 代表方式 | 特点 | 典型问题 |
|---|---|---|---|
| 第一阶段 | 板级文件(board-xxx.c) | 硬件信息硬编码在C代码中 | 改硬件=改内核,维护噩梦 |
| 第二阶段 | 平台设备(platform_device) | 用结构体描述硬件资源 | 代码量依然大,冗余多 |
| 第三阶段 | 设备树(Device Tree) | 独立描述文件,内核动态解析 | 学习曲线陡,但一劳永逸 |
我曾经在一个项目里,从板级文件迁移到设备树。那是一个基于i.MX6Q的工控主板,原来有6个板级文件,每个文件2000多行。迁移后,变成了1个.dts文件和几个.dtsi头文件,总共不到800行。而且,后续硬件改版,只需要改.dts,内核代码完全不用动。
你说爽不爽?
3.4 设备树的核心结构:一个简单的例子
来看一个最简单的设备树片段:
/dts-v1/;
/ {
model = "MyBoard v1.0";
compatible = "vendor,myboard", "vendor,myboard-v1";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB内存
};
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 31 4>;
clock-frequency = <24000000>;
};
gpio0: gpio@20000000 {
compatible = "vendor,gpio";
reg = <0x20000000 0x1000>;
gpio-controller;
#gpio-cells = <2>;
};
};
这个例子展示了设备树的几个核心要素:
- 根节点:
/,代表整个板子 - 属性:
compatible、reg、interrupts等,描述硬件特性 - 节点:
memory、uart0、gpio0,每个外设一个节点 - 标签:
uart0:,方便其他节点引用
reg属性里的地址写错了——少了一个0。设备树里地址写错,驱动是不会有任何提示的,它只会默默地读到错误的数据。所以,写设备树一定要对着原理图逐字核对。
3.5 设备树如何映射到硬件寄存器?
这个问题,说白了就是:设备树里写的那些地址,怎么变成驱动里能用的寄存器?
流程是这样的:
- 内核启动时,解析
.dtb(编译后的设备树),构建device_node树 - 驱动通过
of_match_table匹配compatible属性 - 匹配成功后,驱动调用
of_iomap(),把reg里的物理地址映射到虚拟地址 - 然后驱动就可以通过这个虚拟地址,直接读写硬件寄存器了
我画了一张图,帮你理解这个流程:
这个流程里,最关键的一步就是of_iomap()。它把设备树里写的物理地址,映射到内核能用的虚拟地址。驱动拿到这个虚拟地址后,就可以用readl()/writel()直接操作寄存器了。
.dts。先拿一个简单的板子,对着原理图,把UART、GPIO、I2C这几个基本外设的设备树写出来。然后写一个简单的驱动,用of_iomap()映射寄存器,看看能不能点亮一个LED。这一步走通了,后面就顺了。
3.6 设备树的真正价值
说了这么多,设备树到底解决了什么问题?我总结三点:
- 硬件与驱动解耦:同一个内核镜像,可以支持不同硬件配置的板子。只需要换
.dtb文件就行。 - 减少内核代码量:ARM架构从板级文件迁移到设备树后,内核代码量减少了约30%。
- 硬件描述标准化:所有厂商都使用同一套语法描述硬件,降低了学习成本。
嗯,当然设备树也不是完美的。它的语法有点啰嗦,调试起来也不方便。但相比板级文件时代,已经是天壤之别了。
我记得有一次,客户临时要改一个GPIO的复用功能。放在以前,我得改板级文件、重新编译内核、烧录、重启。现在呢?改一行.dts,重新编译.dtb,替换文件,重启。前后不到5分钟。
这就是设备树的价值。
本章小结:
- 设备树的核心目的:将硬件描述从内核代码中分离
- 演进路径:板级文件 → 平台设备 → 设备树
- 映射流程:.dts → .dtb → device_node → of_iomap → 寄存器访问
- 设备树不是银弹,但它解决了嵌入式Linux开发中最头疼的硬件耦合问题
下一章,我们会深入设备树的语法细节,看看.dts文件到底怎么写,那些compatible、reg、interrupts属性到底是什么意思。到时候,我会拿一个真实的板子来拆解。
公众号:蓝海资料掘金营,微信deep3321