1. 设备树起源:为什么需要设备树?从板级文件到设备树的演进历史
大家好,我是你们的老朋友。今天咱们聊聊设备树的起源。
说实话,我刚入行那会儿,Linux内核里还没有设备树这玩意儿。那时候写驱动,最头疼的就是处理那些板级信息。你想想看,一个SoC要适配几十种开发板,每种板子的GPIO、中断、时钟配置都不一样,这代码怎么写?
1.1 板级文件时代的痛点
在设备树出现之前,Linux内核用的是什么?板级文件(Board File)。
每个开发板都有一个对应的 .c 文件,比如 arch/arm/mach-xxx/board-yyy.c。这个文件里写满了硬件信息:
// 伪代码示例:板级文件中的硬件描述
static struct platform_device my_device = {
.name = "my_driver",
.id = -1,
.dev = {
.platform_data = &my_platform_data,
},
};
static struct i2c_board_info __initdata my_i2c_devices[] = {
{
I2C_BOARD_INFO("temperature_sensor", 0x48),
.irq = IRQ_GPIO_A_START + 17,
},
};
static void __init my_board_init(void)
{
platform_device_register(&my_device);
i2c_register_board_info(0, my_i2c_devices, ARRAY_SIZE(my_i2c_devices));
}
这种写法有什么问题?我给大家列几条:
- 代码冗余:每个板子都要写一套类似的初始化代码
- 维护困难:换一个板子就要改内核源码,重新编译
- 碎片化严重:Linux内核里充斥着各种板级文件,社区维护成本极高
- 二进制不兼容:同一个内核镜像无法在不同板子上运行
我记得有一次,客户要求我们支持一款新的开发板。我花了整整两天时间,就为了改那几个板级文件里的GPIO号和中断号。改完之后还要重新编译整个内核,烧录到板子上测试。效率低得让人抓狂。
1.2 设备树的诞生
为什么会想到用设备树?说白了,就是要把硬件描述和驱动代码彻底分开。
设备树(Device Tree)最早源于PowerPC架构。2005年左右,PowerPC社区开始使用一种叫Open Firmware的技术,用类似Forth的语言来描述硬件。后来ARM社区也遇到了同样的问题——板级文件太多了,多到让人崩溃。
2011年,Linux 3.0内核开始正式支持ARM架构的设备树。到了2012年,ARM社区明确表态:新板子必须使用设备树,不再接受新的板级文件。
设备树的核心思想很简单:
- 用树形结构描述硬件
- 每个硬件节点包含属性(reg、interrupts、clocks等)
- 驱动通过匹配机制找到对应的设备节点
- 硬件变化时,只需修改设备树文件,无需改动内核
设备树的本质:它是一份硬件描述文件,告诉内核"你的板子上有什么硬件,它们怎么连接"。驱动代码只关心"怎么操作这个硬件",不关心"这个硬件在哪个板子上"。
1.3 从板级文件到设备树的演进
咱们用一张图来看看这个演进过程:
这张图很清楚地展示了三个阶段的演进。我个人觉得,过渡期是最痛苦的——既要维护旧的板级文件,又要学习新的设备树语法。那段时间我经常搞混,在板级文件里写设备树语法,或者在设备树里用板级文件的写法。
1.4 设备树解决了哪些问题?
咱们用表格对比一下,设备树到底带来了哪些改变:
| 对比项 | 板级文件时代 | 设备树时代 |
|---|---|---|
| 硬件描述方式 | C语言代码硬编码 | 文本文件描述(.dts) |
| 修改硬件 | 改内核源码,重新编译 | 改设备树文件,重新编译dtb |
| 多板支持 | 每个板子一个内核镜像 | 一个内核镜像+多个dtb |
| 驱动复用 | 驱动与板级信息耦合 | 驱动与硬件描述解耦 |
| 社区维护 | 碎片化严重,维护成本高 | 统一规范,维护成本低 |
| 学习曲线 | 需要了解内核架构 | 只需学习设备树语法 |
1.5 设备树的基本概念
说了这么多,设备树到底是什么?我给大家拆解一下:
- .dts文件:设备树源文件,文本格式,人类可读
- .dtsi文件:设备树头文件,通常包含SoC公共部分
- .dtb文件:设备树二进制文件,由dts编译生成,内核使用
- 节点(node):描述一个硬件设备,用
{}包裹 - 属性(property):描述设备的特性,如地址、中断号等
一个简单的设备树示例:
// my-board.dts
/dts-v1/;
#include "soc-common.dtsi"
/ {
model = "My Custom Board";
compatible = "mycompany,myboard", "generic-board";
chosen {
bootargs = "console=ttyS0,115200";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB内存
};
i2c@1000 {
compatible = "mycompany,i2c";
reg = <0x1000 0x100>;
interrupts = <17>;
clock-frequency = <100000>;
temperature@48 {
compatible = "mycompany,temp-sensor";
reg = <0x48>;
};
};
};
这段代码描述了一个简单的开发板:
- 板子型号是"My Custom Board"
- 内存起始地址0x80000000,大小256MB
- 有一个I2C控制器,地址0x1000,中断号17
- I2C总线上挂了一个温度传感器,地址0x48
关键点:设备树描述的是硬件长什么样,而不是驱动怎么操作硬件。驱动代码通过"compatible"属性来匹配设备树中的节点。这个匹配机制,咱们下一节会详细讲。
1.6 我的经验总结
做了这么多年嵌入式开发,我最大的感受是:设备树让硬件描述变得标准化了。
以前写驱动,你得先搞清楚这个板子上的硬件是怎么连接的,然后去板级文件里找对应的初始化代码。现在呢?打开设备树文件,一目了然。哪个外设在哪个地址,用哪个中断,时钟怎么配,全都在一个文件里。
我记得有一次,客户要求把一块板子上的UART从2号改成1号。在板级文件时代,我得改好几个文件:板级初始化、时钟配置、中断映射...改完还要重新编译。用设备树之后,只需要改一行 reg = <0x10010000 0x1000>;,重新编译dtb,搞定。
嗯,这就是设备树的魅力。它把硬件描述从内核代码中剥离出来,让驱动开发变得更纯粹、更高效。
好了,这一节就到这里。下一节咱们聊聊设备树的语法细节,包括节点命名、属性定义、引用机制这些实战中经常用到的东西。
公众号:蓝海资料掘金营,微信deep3321