1. 设备树概述:DTS 的起源、作用与基本概念
1.1 为什么 Linux 内核需要设备树?
在早期的 Linux 内核(2.6 及之前版本)中,嵌入式平台的硬件描述信息被直接硬编码在 arch/arm/ 或 arch/powerpc/ 等目录下的 C 语言源文件中。这种“硬编码”方式带来了严重的维护问题:
- 代码膨胀:每增加一款开发板,就需要新增或修改大量 board-*.c 文件。
- 内核与硬件耦合:更换外设或调整内存映射必须重新编译内核。
- 碎片化:不同厂商的板级代码风格各异,社区难以统一维护。
为了解决上述痛点,Linux 社区从 PowerPC 架构引入并推广了 设备树(Device Tree) 机制。设备树将硬件描述从内核源码中剥离,形成一份独立的数据结构,由 bootloader 传递给内核。内核在启动时解析设备树,动态识别硬件拓扑、寄存器地址、中断号、时钟频率等关键信息。
核心思想:“描述硬件,而非编写硬件代码”。设备树让同一份内核镜像可以支持多种硬件平台,只需更换不同的 .dts 文件即可。
1.2 设备树的起源与发展
| 时间 | 里程碑 | 说明 |
|---|---|---|
| 2005 年 | PowerPC 率先采用 | Open Firmware 的 FDT(Flattened Device Tree)概念被引入 Linux PowerPC 架构。 |
| 2011 年 | ARM 架构开始迁移 | Linux 3.x 系列内核中,ARM 社区启动“设备树化”改造,逐步淘汰 board 文件。 |
| 2014 年 | ARM64 强制要求 | 从 Linux 3.19 开始,ARM64(AArch64)平台必须使用设备树启动。 |
| 至今 | 全架构普及 | RISC-V、MIPS、X86(ACPI 为主)等架构也支持或兼容设备树。 |
1.3 设备树的基本概念
设备树由三个层次的文件组成,它们共同描述硬件:
- .dts(Device Tree Source):设备树源文件,人类可读的文本格式,使用 C 风格的预处理器宏。
- .dtsi(Device Tree Source Include):设备树头文件/包含文件,通常存放 SoC 公共硬件描述,可被多个 .dts 引用。
- .dtb(Device Tree Blob):设备树二进制文件,由 dtc(Device Tree Compiler)编译 .dts 生成,是内核实际解析的格式。
编译流程:
.dts + .dtsi → dtc 编译器 → .dtb → bootloader 加载 → 内核解析
1.4 设备树的核心数据结构
设备树在内存中以“扁平化”的树形结构存在,每个节点代表一个硬件设备或总线。关键数据结构包括:
| 概念 | 语法表示 | 作用 |
|---|---|---|
| 节点(Node) | device_name@address { }; |
描述一个硬件设备或总线控制器。 |
| 属性(Property) | reg = <0x10000000 0x1000>; |
描述设备的寄存器地址、中断号、时钟等参数。 |
| 兼容性(compatible) | compatible = "vendor,device"; |
用于内核匹配驱动,是设备树与驱动绑定的关键。 |
| 中断(interrupts) | interrupts = <0 29 4>; |
指定设备使用的中断控制器、中断号和触发类型。 |
1.5 设备树与 Linux 驱动的绑定关系
设备树是驱动与硬件之间的“桥梁”。驱动通过 of_match_table 匹配设备树中的 compatible 属性,从而获得硬件资源:
// 驱动侧:定义匹配表
static const struct of_device_id my_driver_of_match[] = {
{ .compatible = "vendor,my-device", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);
// 驱动侧:在 probe 中读取设备树资源
static int my_probe(struct platform_device *pdev) {
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
// ... 初始化硬件
return 0;
}
注意:设备树只描述硬件“有什么”和“在哪里”,不描述硬件“怎么用”。驱动仍然需要实现具体的寄存器操作逻辑。
1.6 设备树整体架构图
1.7 设备树带来的核心价值
| 维度 | 传统硬编码方式 | 设备树方式 |
|---|---|---|
| 硬件变更 | 需修改 C 代码,重新编译内核 | 只需修改 .dts,重新编译 DTB |
| 内核镜像 | 每个板子一个专属镜像 | 同一镜像支持多板,通过 DTB 区分 |
| 社区维护 | board 文件大量重复,难以合并 | 设备树文件独立,易于审查和复用 |
| 驱动开发 | 驱动需硬编码地址和中断号 | 驱动通过 API 读取设备树,自动适配 |
1.8 一个简单的设备树示例
// 文件:myboard.dts
/dts-v1/;
#include "soc-common.dtsi"
/ {
model = "MyBoard v1.0";
compatible = "vendor,myboard", "vendor,soc";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB RAM
};
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
};
gpio0: gpio@10001000 {
compatible = "vendor,gpio";
reg = <0x10001000 0x100>;
interrupts = <0 34 4>;
gpio-controller;
#gpio-cells = <2>;
};
};
关键点解析:
/dts-v1/;声明设备树版本,必须位于文件开头。#include "soc-common.dtsi"引入 SoC 公共描述,避免重复。compatible属性是驱动匹配的核心标识,格式通常为"厂商,设备型号"。reg属性描述地址空间,格式为<起始地址 长度>。interrupts属性描述中断控制器、中断号和触发类型。
1.9 设备树在 Linux 启动流程中的位置
1.10 本章小结
- 设备树解决了 Linux 内核硬件描述与代码耦合的历史问题,是嵌入式 Linux 开发的基石。
- 设备树由 .dts / .dtsi 源文件通过 dtc 编译为 .dtb 二进制,由 bootloader 加载并传递给内核。
- 设备树的核心是“节点 + 属性”的树形结构,
compatible属性是驱动匹配的关键。 - 掌握设备树语法和绑定规则,是编写高质量 Linux 驱动的必备技能。