1. 设备树基础概念
各位同学好,我是老李。做嵌入式Linux驱动开发十几年了,今天咱们来聊聊设备树。说实话,我刚入行那会儿还没有设备树这玩意儿,那时候写驱动,板级信息全是硬编码在C文件里的。后来换了平台、换了板子,代码改得那叫一个痛苦。设备树的出现,说白了就是要把硬件描述和驱动代码彻底解耦。
什么是设备树
设备树(Device Tree),是一套描述硬件资源的数据结构。它用树形结构把CPU、内存、外设、中断、GPIO、时钟这些硬件信息组织起来。内核启动时,Bootloader把设备树二进制文件(DTB)传给内核,内核解析后就知道板子上有什么硬件、怎么访问它们。
我习惯把设备树比作一张硬件“配置单”。你想想看,驱动代码是通用的,但板子上的硬件资源是千差万别的。设备树就是告诉驱动:“我这块板子的UART挂在哪个地址上,中断号是多少,时钟频率是多少。”驱动拿到这些信息,就能正确初始化硬件。
设备树的核心组成:
- DTS(Device Tree Source):文本格式的源文件,我们工程师直接编写
- DTC(Device Tree Compiler):编译器,把DTS转成DTB
- DTB(Device Tree Blob):二进制格式,内核实际使用的数据
- DTBO(Device Tree Blob Overlay):叠加层,用于动态修改设备树
举个例子,一个简单的设备树节点长这样:
/ {
model = "MyBoard v1.0";
compatible = "vendor,myboard";
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
status = "okay";
};
};
这段代码描述了一个UART控制器,地址在0x10000000,中断号33,时钟24MHz。驱动通过of_match_table匹配compatible属性,就能找到对应的硬件资源。
为什么需要设备树
在设备树出现之前,Linux内核是怎么处理板级信息的?答案是板级文件(Board File)。每个板子都要在arch/arm/mach-xxx目录下写一个C文件,里面硬编码了所有硬件资源。比如:
static struct resource uart_resources[] = {
{
.start = 0x10000000,
.end = 0x10000fff,
.flags = IORESOURCE_MEM,
},
{
.start = IRQ_UART0,
.end = IRQ_UART0,
.flags = IORESOURCE_IRQ,
},
};
这种方式有什么问题?我遇到过最头疼的情况:同一个SoC,客户换了颗DDR颗粒,内存地址变了。我得改板级文件、重新编译内核、烧录验证。如果同时维护五六块板子,那简直是噩梦。
设备树解决了三个核心痛点:
- 硬件描述与驱动解耦:驱动代码不再包含板级信息,换板子只需换DTB
- 减少内核分支:同一份内核镜像可以支持多种硬件配置
- 社区统一标准:ARM、RISC-V、x86都在用,生态统一
我的经验:曾经有个项目,同一款SoC用在三款不同产品上,外设配置差异很大。如果没有设备树,我得维护三份内核代码。用了设备树之后,只需要一份内核镜像,启动时加载不同的DTB文件就行。开发效率提升不止一个量级。
设备树与ACPI的区别
ACPI(高级配置与电源管理接口)是x86平台的标准硬件描述方式。很多从x86转过来的同学会问:设备树和ACPI到底有什么区别?我整理了一张对比表:
| 对比项 | 设备树(DT) | ACPI |
|---|---|---|
| 起源平台 | PowerPC / ARM | x86 / IA64 |
| 描述方式 | 文本(DTS)→ 二进制(DTB) | ASL → AML(字节码) |
| 运行时修改 | 支持Overlay动态叠加 | 支持DSDT动态修改 |
| 电源管理 | 需要驱动自行实现 | 内置电源管理方法 |
| 热插拔支持 | 较弱,需额外机制 | 原生支持 |
| 复杂度 | 轻量,学习曲线平缓 | 庞大,规范上千页 |
| 适用场景 | 嵌入式、IoT、FPGA | 服务器、PC、笔记本 |
说白了,ACPI更像一个“虚拟机”,它有一套自己的语言(ASL)和解释器(AML),能执行复杂的电源管理策略。设备树则简单得多,它只描述硬件“长什么样”,不描述硬件“怎么用”。
避坑指南:我曾经见过有人试图在ARM服务器上用ACPI替代设备树。理论上可行,但实际坑很多。ACPI的AML解释器在ARM上性能不佳,而且很多嵌入式外设(比如I2C传感器、SPI设备)在ACPI规范里根本没有标准描述方式。我的建议是:ARM平台老老实实用设备树,别折腾ACPI。
设备树在Linux内核中的角色
设备树在内核启动流程中扮演着“硬件信息传递者”的角色。从Bootloader到内核初始化,再到驱动加载,设备树贯穿始终。
我画了一张流程图,展示设备树在内核中的生命周期:
从这张图可以看出,设备树在内核中经历了加载→解析→展开→匹配→使用→释放的完整生命周期。其中几个关键环节:
- unflatten_device_tree():把扁平化的DTB展开成树形结构,每个节点对应一个
struct device_node - of_platform_populate():遍历设备树,为每个
compatible节点创建platform_device - 驱动匹配:内核根据
compatible、type、name等属性,找到对应的驱动 - 资源获取:驱动通过
of_iomap()映射寄存器,of_get_property()读取属性
我的习惯:调试设备树问题时,我经常在drivers/of/目录下加打印,或者用of_dump内核参数查看设备树展开后的完整结构。另外,/proc/device-tree目录在运行时可以直接查看设备树内容,非常方便。
嗯,这里要注意一点:设备树不是万能的。它只描述硬件“有什么”,不描述硬件“怎么用”。比如一个I2C设备,设备树告诉驱动它的地址和中断号,但具体的通信协议、寄存器操作,还是得驱动代码自己实现。设备树是“数据”,驱动是“逻辑”,两者配合才能正常工作。
好了,设备树的基础概念就讲到这里。记住一句话:设备树是硬件和驱动之间的“翻译官”,它让硬件描述标准化、可复用。后面的章节,我们会深入设备树的语法、绑定规则、以及如何编写高质量的设备树文件。