1. 设备树基础:什么是设备树、为什么需要设备树、设备树在Linux内核中的角色
各位同学,咱们今天聊聊设备树。说实话,我刚入行那会儿,Linux内核里还没有设备树这玩意儿。那时候板级文件满天飞,每次换个Flash型号都得改arch/arm/mach-xxx下的代码,改完还得重新编译内核。嗯,那日子,想想都头疼。
设备树(Device Tree)的出现,说白了就是把硬件描述从内核代码里剥离出来。它用一套独立的数据结构来描述硬件资源,内核启动时解析这个结构,就知道板子上接了哪些外设、内存多大、中断怎么连的。你想想看,这就像给硬件画了一张“配置清单”,内核照着清单干活就行。
1.1 什么是设备树
设备树是一个树形数据结构,每个节点代表一个硬件设备或总线。节点里包含属性(property),属性就是键值对,用来描述设备的寄存器地址、中断号、时钟频率等信息。
我习惯把设备树比作硬件的“身份证”。比如一个UART控制器,它的身份证上写着:我叫uart0,我住在0x10000000这个地址,我用中断号33,我跑115200波特率。内核拿到这张身份证,就知道怎么初始化它。
设备树的源文件是.dts(Device Tree Source),编译后生成.dtb(Device Tree Blob)。uboot或bootloader把.dtb传给内核,内核解析后构建出设备树的内存表示——我们叫它“展开的设备树”(Flattened Device Tree)。
核心概念速览
- .dts:设备树源文件,文本格式,可读性强
- .dtsi:设备树头文件,通常放SoC公共部分
- .dtb:编译后的二进制文件,内核直接使用
- DTB:Device Tree Blob,就是.dtb文件
- FDT:Flattened Device Tree,内核内存中的展开表示
一个简单的设备树节点长这样:
/ {
soc {
uart0: serial@10000000 {
compatible = "vendor,uart-ip-v1";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <115200>;
status = "okay";
};
};
};
你看,compatible属性告诉内核用哪个驱动,reg描述地址范围,interrupts描述中断信息。这些信息以前都硬编码在C代码里,现在全挪到设备树里了。
1.2 为什么需要设备树
这个问题我当年也问过。为什么非要搞这么一套东西?直接在内核代码里写死不行吗?
行,但代价太大。我给你列几个痛点:
| 痛点 | 说明 |
|---|---|
| 代码膨胀 | 每个板子都要在arch/arm/mach-xxx下加一堆板级文件,内核越来越臃肿 |
| 维护困难 | 换个Flash型号、改个GPIO复用,就得改内核代码,重新编译 |
| 碎片化严重 | 厂商各自维护自己的板级补丁,主线内核很难统一支持 |
| 启动流程复杂 | uboot和内核之间传递硬件信息全靠ATAG,扩展性差 |
| 多平台支持差 | 同一份内核镜像很难同时支持多个硬件平台 |
我曾经在一个项目里维护过三块不同的板子,每块板子的Flash分区、网卡型号、LCD参数都不一样。每次改硬件,都得改arch/arm/mach-xxx下的代码,然后重新编译内核。更崩溃的是,有时候只是换了个Flash型号,结果启动时内核panic了——因为分区表写死了。
设备树解决了这些问题。硬件描述和内核代码解耦了。换硬件?改.dts文件,重新编译.dtb,内核不用动。同一份内核镜像,配不同的.dtb,就能跑在不同的板子上。这就是设备树最大的价值。
我的经验:如果你在做BSP开发,建议把设备树当成“硬件配置单”来管理。每个硬件变更,先改设备树,再改驱动。这个习惯能帮你省掉很多调试时间。
1.3 设备树在Linux内核中的角色
设备树在内核启动过程中扮演着“硬件信息传递者”的角色。它的工作流程是这样的:
- Bootloader加载DTB:uboot从Flash或网络加载.dtb文件到内存
- Bootloader传递DTB地址:通过寄存器r2(ARM架构)把DTB内存地址传给内核
- 内核早期启动解析DTB:内核在start_kernel之前,通过unflatten_device_tree()把DTB展开成设备树结构
- 驱动匹配:驱动通过of_match_table匹配设备树中的compatible属性
- 资源获取:驱动通过of_xxx系列API读取设备树中的reg、interrupts等属性
说白了,设备树就是内核和硬件之间的“翻译官”。内核不直接操作硬件寄存器,而是通过设备树知道硬件长什么样,然后让对应的驱动去干活。
注意:设备树不是万能的。它只描述硬件“有什么”,不描述硬件“怎么用”。怎么用是驱动的事。别把驱动逻辑写进设备树,那是典型的反模式。
我画了一张图,帮你理解设备树在内核中的位置:
这张图展示了数据流向。硬件是客观存在的,设备树是它的“描述”,内核根据描述做资源管理,驱动根据描述做硬件操作。每一层各司其职,互不干扰。
1.4 设备树的核心机制
设备树有几个关键机制,我简单说一下:
- 节点(Node):树形结构的基本单元,每个节点代表一个设备或总线
- 属性(Property):节点的配置信息,键值对形式
- 标签(Label):给节点起别名,方便引用,比如上面的uart0
- phandle:节点引用机制,通过&标签实现节点间关联
- 覆盖(Overlay):运行时动态修改设备树,常用于FPGA或可插拔模块
我记得有一次调试一个I2C设备,死活读不到设备ID。查了半天,发现设备树里i2c节点的时钟频率写错了,写成了400kHz,但实际硬件只支持100kHz。改回100kHz,问题解决。嗯,这种坑我踩过不止一次。
避坑指南:设备树里的reg属性,地址和长度都是32位或64位整数,注意大小端。ARM默认小端,PowerPC默认大端。我曾经因为大小端问题,把一个UART的地址配错了,结果读写全乱套。
1.5 小结
设备树不是什么高深的技术,它就是一套硬件描述规范。把硬件信息从内核代码里抽出来,放到一个独立的数据结构里。好处是:内核更干净、硬件变更更灵活、多平台支持更容易。
学设备树,你不需要背语法,也不需要记API。你只需要理解它的设计思想:描述硬件,而不是控制硬件。控制是驱动的事,描述是设备树的事。分清楚这个边界,后面学起来就顺了。