1. 设备树(DTS)基础概念
1.1 什么是设备树
设备树(Device Tree)是一种描述硬件资源的数据结构,它使用树形节点来记录系统中所有可访问的设备、总线、中断控制器、时钟、GPIO等硬件信息。设备树源文件通常以 .dts 为扩展名,经过编译后生成 .dtb 二进制文件,由Bootloader传递给Linux内核。
设备树的核心思想是将硬件描述从内核代码中剥离出来,形成独立的配置文件。每个节点代表一个硬件设备或总线,节点包含若干属性(property)来描述设备的寄存器地址、中断号、时钟频率等参数。
| 术语 | 全称 | 说明 |
|---|---|---|
| DTS | Device Tree Source | 设备树源文件,人类可读的文本格式 |
| DTB | Device Tree Blob | 设备树二进制文件,由DTC编译生成 |
| DTC | Device Tree Compiler | 设备树编译器,将DTS转换为DTB |
| DTSI | Device Tree Source Include | 设备树头文件,用于公共节点复用 |
| OF | Open Firmware | 设备树的驱动框架,提供API接口 |
.dts 文件通常存放在内核源码的 arch/arm/boot/dts/ 或 arch/arm64/boot/dts/ 目录下。修改设备树后,使用 make dtbs 命令即可重新编译。
1.2 为什么需要设备树
在设备树出现之前,Linux内核使用大量的板级文件(board file)来描述硬件。每个开发板都需要在 arch/arm/mach-xxx 目录下编写C语言结构体来注册平台设备、GPIO、中断等资源。这种方式存在三个严重问题:
- 代码冗余:同一款SoC的不同开发板需要重复编写大量相似的板级代码
- 维护困难:硬件变更时必须修改内核源码,无法做到内核与硬件解耦
- 碎片化严重:ARM架构下大量板级文件导致主线内核难以统一支持
设备树解决了上述问题,带来了以下核心价值:
- 硬件描述与内核分离:修改硬件只需要修改DTS文件,无需重新编译内核
- 统一硬件抽象层:同一款SoC可以共用一套内核镜像,通过不同的DTB适配不同硬件
- 减少主线内核维护负担:ARM架构从Linux 3.x开始全面转向设备树,板级文件被逐步淘汰
- 支持多平台启动:一个内核镜像可以同时支持多种ARM/ARM64开发板
1.3 设备树在Linux内核中的角色
设备树在Linux内核启动和驱动加载过程中扮演着"硬件信息数据库"的角色。内核启动时,Bootloader将DTB加载到内存中,内核通过 unflatten_device_tree() 函数将扁平化的DTB解析为树形结构,供驱动框架使用。
设备树与内核各子系统的交互关系如下图所示:
设备树在内核中的核心作用可以归纳为以下四个层面:
| 层面 | 作用 | 关键API/机制 |
|---|---|---|
| 设备枚举 | 内核启动时扫描DTB,为每个节点创建platform_device | of_platform_populate() |
| 驱动匹配 | 驱动通过compatible属性与设备节点进行匹配绑定 | of_match_table / of_match_node() |
| 资源获取 | 驱动从节点属性中读取寄存器地址、中断号、DMA通道等 | platform_get_resource() / of_iomap() |
| 运行时配置 | 通过设备树属性传递板级差异参数,如时钟频率、电压阈值 | of_property_read_u32() / of_get_property() |
1.4 设备树的基本语法结构
一个典型的设备树节点包含节点名称、节点路径和若干属性。以下是一个简单的设备树片段示例:
/dts-v1/;
/ {
model = "Embedded Development Board v2.0";
compatible = "vendor,emb-board-v2", "vendor,emb-board";
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
};
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
uart0: serial@10000000 {
compatible = "vendor,uart-ip";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
status = "okay";
};
gpio0: gpio@10001000 {
compatible = "vendor,gpio-ctrl";
reg = <0x10001000 0x100>;
gpio-controller;
#gpio-cells = <2>;
interrupts = <0 34 4>;
};
};
};
/dts-v1/;— 设备树版本声明,必须位于文件开头/ { };— 根节点,所有设备节点都挂在根节点下uart0: serial@10000000—uart0是标签(label),serial@10000000是节点名称,@后跟寄存器基地址reg = <0x10000000 0x1000>;— 寄存器地址和长度,地址由#address-cells决定status = "okay";— 设备状态,可选okay、disabled、fail
1.5 设备树与Platform驱动的关联
在Platform驱动模型中,设备树是设备信息的提供者。当内核启动时,OF核心层遍历设备树中的所有节点,为每个 compatible 属性匹配的节点创建一个 platform_device。驱动通过 of_match_table 声明自己支持的设备列表,内核在注册驱动时自动完成匹配。
设备树节点与Platform驱动的匹配流程如下图所示:
of_match_table 必须使用 of_device_id 结构体数组,并以空结构体结尾。驱动加载后,可以通过 dmesg | grep "匹配的设备名" 查看匹配是否成功。
1.6 设备树的调试手段
在实际开发中,设备树配置错误是导致驱动加载失败的常见原因。以下是几种常用的调试方法:
- 查看设备树节点:在系统启动后,通过
/proc/device-tree/目录查看当前生效的设备树结构 - 使用dtc反编译:通过
dtc -I dtb -O dts -o output.dts input.dtb将DTB反编译为DTS,验证配置是否正确 - 内核启动日志:关注
dmesg中与设备树相关的输出,如OF: fdt: ...或platform ...信息 - 驱动匹配验证:在驱动probe函数中添加
dev_info()打印,确认匹配是否触发
reg 属性的地址和长度必须与 #address-cells、#size-cells 严格对应。例如,#address-cells = <1> 表示地址占1个32位单元,#size-cells = <1> 表示长度占1个32位单元。如果配置错误,驱动将无法正确获取寄存器地址,导致系统崩溃。