【Linux驱动开发】第11天:设备树(Device Tree)超详细全解:从诞生背景到工作原理
一、设备树的诞生背景:传统驱动的致命痛点
在设备树出现之前(Linux 3.0之前),Linux内核采用硬编码的方式描述所有硬件信息。这意味着:
- 每一个开发板的寄存器地址、中断号、GPIO号,都直接写死在驱动代码里
- 换一个开发板,哪怕只是GPIO号变了,也要修改驱动代码重新编译
- 为了支持不同的开发板,内核里充斥着大量重复的、针对特定硬件的代码
最直观的例子:ARM内核的"灾难"
ARM架构有上百种不同的芯片、上千种不同的开发板。在设备树出现之前:
- 每一个开发板都需要在内核里添加一份自己的硬件描述代码
- 内核代码量爆炸式增长,变得无比臃肿
- 维护成本极高,每一个新开发板都需要内核开发者手动添加支持
- 一个内核镜像只能支持一个开发板,无法做到"一个镜像跑遍所有开发板"
设备树的诞生
2011年,Linux 3.1内核正式引入设备树机制,彻底解决了这个问题。
设备树的核心思想非常简单:
把硬件描述信息从内核代码中完全抽离出来,用一个独立的、通用的文件来描述硬件。内核只需要一份通用的驱动代码,通过读取这个文件来识别不同的硬件。
二、设备树的核心定义
设备树(Device Tree,简称DT)是一种描述硬件资源的数据结构,它用树形结构来描述一个计算机系统的所有硬件设备,包括:
- CPU
- 内存
- 总线控制器(I2C、SPI、USB等)
- 外设(LED、按键、传感器、显示屏等)
- 中断、时钟、电源等硬件资源
设备树的本质
设备树本质上是一个硬件的"清单",它告诉内核:
这个板子上有哪些硬件?它们的地址在哪里?它们用了哪个中断?它们的工作频率是多少?
注意:设备树不是驱动!它只描述"硬件是什么",不描述"怎么操作硬件"。操作硬件的逻辑仍然在驱动代码里。
三、设备树的三大核心组件:DTS、DTB、DTC
设备树体系由三个核心部分组成,三者分工明确,缺一不可。
1. DTS:设备树源文件(Device Tree Source)
- 文件后缀:
.dts(主文件)、.dtsi(头文件) - 文件类型:纯文本文件,人类可读可编辑
- 作用:用C语言风格的语法,描述硬件的所有信息
- 编辑方式:任何文本编辑器都可以编辑
什么是.dtsi文件?
.dtsi是设备树头文件,相当于C语言的.h头文件,用于存放多个DTS文件共享的公共硬件信息。
- 比如同一个芯片的所有开发板,芯片内部的外设(CPU、内存、UART、I2C控制器等)是完全相同的
- 这些公共信息可以放在一个
.dtsi文件中(如stm32mp157.dtsi) - 每个开发板自己的
.dts文件只需要#include这个.dtsi,然后添加自己独有的硬件信息(如LED、按键、外接传感器等) - 大大提高了代码复用性,减少了重复代码
2. DTC:设备树编译器(Device Tree Compiler)
- 文件类型:可执行程序
- 作用:将人类可读的DTS文本文件,编译成内核能够识别和解析的二进制DTB文件
- 同时也可以将DTB二进制文件反编译成DTS文本文件,用于调试
3. DTB:设备树二进制文件(Device Tree Blob)
- 文件后缀:
.dtb - 文件类型:二进制文件,人类不可读
- 作用:内核启动时加载并解析的最终文件
- 特点:体积小、解析速度快,适合嵌入式系统使用
三者的关系和编译流程
四、设备树的树形结构
设备树采用树形结构来描述硬件,和计算机的文件系统结构非常相似。
4.1 基本结构
/ (根节点) ├── cpu@0 (CPU节点) ├── memory@80000000 (内存节点) ├── soc@0 (片上系统节点) │ ├── uart@12340000 (串口节点) │ ├── i2c@12341000 (I2C控制器节点) │ │ ├── sensor@48 (I2C传感器节点) │ │ └── eeprom@50 (I2C EEPROM节点) │ └── gpio@12342000 (GPIO控制器节点) └── led@12343000 (LED节点)4.2 核心概念:节点(Node)
- 每个硬件设备对应设备树中的一个节点
- 节点格式:
节点名@地址 { ... };- 节点名:描述设备的类型,如
cpu、memory、uart、led - @地址:设备的寄存器基地址,用于区分同类型的不同设备(如
uart@12340000和uart@12341000是两个不同的串口)
- 节点名:描述设备的类型,如
4.3 核心概念:属性(Property)
- 每个节点包含多个属性,用于描述设备的具体信息
- 属性格式:
属性名 = 属性值; - 属性值可以是字符串、32位无符号整数、整数数组、字节数组等
4.4 最常用的核心属性(驱动开发每天都会用到)
| 属性名 | 作用 | 示例 |
|---|---|---|
compatible | 最重要的属性,用于和驱动匹配,格式为"厂商,设备名" | compatible = "st,stm32-uart"; |
reg | 描述设备的寄存器地址范围,格式为<基地址 长度> | reg = <0x12340000 0x1000>; |
interrupts | 描述设备使用的中断号 | interrupts = <5>; |
status | 描述设备状态,okay表示启用,disabled表示禁用 | status = "okay"; |
model | 设备的人类可读名称 | model = "STM32MP157 Development Board"; |
五、设备树的完整工作流程
设备树从编写到最终驱动硬件,会经历以下6个完整阶段。
阶段1:编写设备树源文件
开发者根据开发板的硬件原理图,编写DTS文件,描述所有硬件信息。
阶段2:编译生成DTB文件
使用DTC编译器将DTS文件编译成DTB二进制文件。
阶段3:bootloader加载DTB
系统启动时,bootloader(如U-Boot)会:
- 将内核镜像(zImage)加载到内存
- 将DTB文件加载到内存的另一个位置
- 启动内核,并将DTB的内存地址传递给内核
阶段4:内核解析DTB
内核启动时,根据bootloader传递的地址,解析DTB文件:
- 遍历DTB中的所有节点
- 为每个节点创建一个对应的
platform_device结构体 - 将
platform_device注册到platform总线
阶段5:总线匹配设备和驱动
platform总线会:
- 遍历所有已注册的
platform_driver - 比较设备的
compatible属性和驱动的of_device_id表 - 如果字符串完全一致,则匹配成功
阶段6:调用驱动的probe函数
匹配成功后,总线自动调用驱动的probe函数:
- 驱动从
platform_device中获取硬件信息(地址、中断号等) - 初始化硬件
- 注册字符设备/块设备/网络设备
- 创建设备文件,对外提供服务
六、设备树的核心优势
对比传统硬编码驱动,设备树具有以下不可替代的优势:
1. 硬件描述与驱动代码完全分离
- 驱动代码通用,不需要针对特定开发板修改
- 一个驱动可以支持所有符合
compatible属性的设备 - 新增硬件只需要修改设备树,不需要修改驱动代码
2. 一个内核镜像支持多个开发板
- 内核镜像不再包含任何特定硬件的信息
- 同一个内核镜像可以在所有支持设备树的开发板上运行
- 只需要更换不同的DTB文件即可
3. 大大降低内核维护成本
- 内核不再需要包含大量针对特定开发板的硬编码代码
- 内核代码量大幅减少,更加简洁和通用
- 新开发板的支持变得非常简单,只需要添加一个DTS文件
4. 支持热插拔
- 设备树可以动态描述热插拔设备的信息
- 设备插入时,内核动态解析设备树节点,创建设备并匹配驱动
- 设备拔出时,自动调用驱动的
remove函数释放资源
七、常见误区澄清
❌ 误区1:设备树可以代替驱动
错误。设备树只描述"硬件是什么",不描述"怎么操作硬件"。操作硬件的逻辑仍然在驱动代码里。没有驱动,设备树只是一个没用的文本文件。
❌ 误区2:设备树是ARM架构特有的
错误。设备树最早用于PowerPC架构,现在已经被所有主流架构采用,包括ARM、x86、RISC-V、MIPS等。
❌ 误区3:设备树只能描述片上外设
错误。设备树可以描述系统中的所有硬件,包括CPU、内存、总线控制器、外接设备、甚至电源和时钟。
❌ 误区4:设备树的compatible属性可以随便写
错误。compatible属性必须严格按照"厂商,设备名"的格式编写,并且必须和驱动中的of_device_id表完全一致,否则无法匹配。
八、总结
设备树是现代Linux驱动开发的基础,它的核心价值在于实现了硬件描述与驱动代码的彻底分离,解决了传统硬编码驱动的所有致命痛点。
对于驱动开发者来说,你只需要记住:
- 设备树是硬件的清单,告诉内核有什么硬件
- 驱动是硬件的操作手册,告诉内核怎么操作硬件
- compatible属性是两者之间的配对暗号,暗号对上了,驱动才能工作
面试必背考点
- 什么是设备树?它解决了什么问题?
- DTS、DTB、DTC分别是什么?三者的关系是什么?
- 什么是.dtsi文件?它的作用是什么?
- 设备树的基本结构是什么?什么是节点和属性?
compatible属性的作用是什么?格式是什么?- 内核解析设备树的完整流程是什么?
- bootloader在设备树启动过程中起到什么作用?
- 设备树和platform驱动是怎么配合工作的?
