1. 设备树基础概念:什么是设备树、为什么需要设备树、设备树在Linux内核中的角色
1.1 什么是设备树?
设备树,英文叫 Device Tree,简称 DT。说白了,它就是一个描述板级硬件信息的数据结构。你想想看,以前我们做嵌入式 Linux 移植,每换一块板子,内核代码就得改一堆——GPIO 映射、中断号、时钟频率、内存地址……全写在 C 代码里。这活儿干多了,真的会让人崩溃。
设备树就是为了解决这个问题而生的。它用一套树形结构,把硬件信息从内核源码里剥离出来,放到一个独立的 .dts 文件中。内核启动时,通过解析这个文件,动态地知道“我跑在什么硬件上”。
我个人习惯把设备树比作一张“硬件清单”。这张清单上写着:CPU 是什么、内存多大、外设挂在哪条总线上、中断连到哪个引脚。内核拿到清单,按图索骥,就能把驱动加载起来。
核心定义:设备树是一种描述硬件资源的树形数据结构,由一系列节点(node)和属性(property)组成。每个节点代表一个硬件设备或总线,属性则描述该设备的配置参数。
来看一个最简单的设备树片段:
/dts-v1/;
/ {
model = "MyBoard v1.0";
compatible = "vendor,myboard";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>;
};
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 5 4>;
};
};
这段代码描述了一块板子:内存从 0x80000000 开始,大小 256MB;UART0 挂在地址 0x10000000,中断号是 5。嗯,这里要注意,reg 属性里的两个值,第一个是基地址,第二个是地址范围长度。
1.2 为什么需要设备树?
在设备树出现之前,Linux 内核是怎么处理板级差异的?答案是——硬编码。每个板子都在 arch/arm/mach-xxx 目录下有一堆 C 文件,里面写满了 .map_io、.init_irq、.timer 等回调函数。我记得早期做 ARM 移植时,光是 mach-s3c2440 目录下就有几十个文件,每个文件对应一款开发板。
这种做法的痛点很明显:
- 代码膨胀:每加一块板子,内核源码就多一堆文件。Linux 3.x 时代,ARM 架构的板级文件数量已经超过 3000 个。
- 维护噩梦:改一个 SoC 的公共逻辑,得把所有板子文件都翻一遍。漏改一个,板子就起不来。
- 二进制不兼容:同一个内核镜像,换块板子就得重新编译。这对商业产品来说,简直是灾难。
设备树把这些问题一锅端了。它带来的好处,我总结成三点:
| 痛点 | 设备树方案 | 实际效果 |
|---|---|---|
| 板级文件膨胀 | 硬件信息写入 .dts,内核只留通用驱动 | 内核源码体积大幅缩减 |
| 维护成本高 | 修改硬件只需改 .dts,无需动内核 | 板级适配与内核开发解耦 |
| 二进制不兼容 | 同一内核镜像 + 不同 .dtb 即可适配多板 | 一次编译,到处运行 |
我曾经在一个项目里遇到过这种情况:客户临时换了颗 Flash 芯片,地址映射变了。如果没有设备树,我得重新编译内核、烧录、验证,折腾一整天。有了设备树,改两行 .dts,重新编译 dtb,5 分钟搞定。这就是设备树的价值。
1.3 设备树在 Linux 内核中的角色
设备树在 Linux 内核里到底扮演什么角色?我用一句话概括:它是内核与硬件之间的“翻译官”。
内核启动时,Bootloader(比如 U-Boot)会把设备树二进制文件(.dtb)加载到内存,然后把它的地址传给内核。内核启动后,第一时间解析这个 dtb,构建出一棵设备树结构体(struct device_node)。之后,所有驱动模块都通过这棵树来查找硬件资源。
具体流程是这样的:
- Bootloader 加载 dtb:U-Boot 从 Flash 或文件系统中读取 .dtb 文件,放到内存指定位置。
- 内核解析 dtb:启动早期,内核调用
unflatten_device_tree(),把扁平化的 dtb 展开成树形结构。 - 驱动匹配:总线驱动(如 platform bus)遍历设备树,找到与驱动
compatible属性匹配的节点,然后调用驱动的 probe 函数。 - 资源获取:驱动通过
of_property_read_*系列 API,从设备树节点中读取寄存器地址、中断号、DMA 通道等资源。
避坑指南:我曾经在调试一块板子时,发现 UART 驱动死活不 probe。查了半天,原来是 .dts 里 compatible 字符串写成了 "vendor,uart1",而驱动里匹配的是 "vendor,uart"。少了个数字,内核就不认。所以,写 compatible 时一定要跟驱动源码里的 of_match_table 完全一致,一个字符都不能差。
下面这张图,展示了设备树在内核启动过程中的位置和作用:
从这张图可以看得很清楚:设备树不是内核启动的“主角”,但它是连接 Bootloader 和驱动的“桥梁”。没有它,内核就不知道硬件长什么样;有了它,驱动才能找到自己的“饭碗”。
注意事项:设备树只描述硬件,不描述策略。比如,你可以告诉内核“这里有一个 UART”,但你不能通过设备树告诉内核“这个 UART 的波特率设为 115200”。波特率是驱动运行时配置的,属于策略范畴。把策略写进设备树,是新手常犯的错误。
1.4 小结
设备树不是什么高深莫测的东西。它就是一张硬件清单,让内核知道“我在什么板子上跑”。它的出现,把板级适配从内核源码中解放出来,让嵌入式 Linux 开发变得更灵活、更高效。
我个人觉得,理解设备树的关键就三点:
- 它是树形结构,节点对应设备,属性对应参数。
- 它让内核与板级硬件解耦,实现“一次编译,到处运行”。
- 它是驱动获取硬件资源的唯一入口,所有
of_开头的 API 都是它的“传话筒”。
嗯,基础概念就讲到这里。后面我们会深入设备树的语法、编译、绑定规则,以及实际调优中的各种坑。准备好了吗?