1. 设备树基础:DTS 的起源、作用与基本语法结构
1.1 设备树的起源:从“硬编码”到“数据驱动”
在早期的 Linux 内核(2.6 及更早版本)中,板级硬件信息(如内存基址、外设寄存器地址、中断号、GPIO 映射)被直接硬编码在 arch/arm/mach-xxx 下的 C 语言板文件中。这种模式导致内核源码充斥着大量重复的、与具体开发板绑定的代码,每更换一款 SoC 或开发板,就需要重新编译内核,严重阻碍了 ARM 生态的软件复用。
为了解决这一痛点,PowerPC 社区率先引入了 设备树(Device Tree) 概念,随后被 ARM、RISC-V、X86(ACPI 替代场景)等架构广泛采纳。设备树的核心思想是:将硬件描述从内核源码中剥离,以结构化数据的形式传递给内核,使同一份内核镜像能够适配多种硬件平台。
关键转折: Linux 主线内核自 3.x 起强制要求 ARM 平台必须提供设备树文件,否则无法启动。这一决策彻底终结了“板文件泛滥”的时代。
1.2 设备树的作用:硬件描述的“桥梁”
设备树在嵌入式 Linux 系统中扮演着 硬件描述语言(HDL for Hardware Description) 的角色,其核心作用可归纳为以下四点:
| 作用维度 | 具体说明 | 典型场景 |
|---|---|---|
| 硬件拓扑描述 | 描述 CPU、内存、总线、外设的层级连接关系 | SoC 内部总线结构、外设挂载位置 |
| 资源静态配置 | 定义寄存器地址、中断号、时钟频率、DMA 通道等 | UART 基址 0x1c090000,中断号 49 |
| 驱动匹配标识 | 通过 compatible 属性将设备节点与驱动绑定 |
compatible = "fsl,imx6q-uart" |
| 运行时参数传递 | Bootloader 可修改设备树,传递内存大小、MAC 地址等 | U-Boot 中 fdt set 命令 |
实践要点: 设备树不是万能药。它只描述“硬件有什么”,不描述“硬件怎么用”。驱动逻辑仍然由内核驱动代码实现,设备树仅提供数据。
1.3 基本语法结构:DTS 文件解剖
设备树源文件(.dts)采用树形层级结构,以 节点(node) 和 属性(property) 为核心元素。下面是一个典型的 UART 外设节点示例:
/dts-v1/;
/ {
model = "Freescale i.MX6Q SabreSD Board";
compatible = "fsl,imx6q-sabresd", "fsl,imx6q";
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges;
uart1: serial@1c090000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x1c090000 0x4000>;
interrupts = <0 49 4>;
clocks = <&clks 0x1c>;
status = "okay";
};
};
};
1.3.1 节点(Node)
- 根节点:
/表示设备树的根,所有设备节点都挂载在根下。 - 子节点:
soc、uart1等,通过层级缩进体现父子关系。 - 标签(Label):
uart1:是节点标签,用于在其他地方引用该节点(如&uart1)。 - 节点名称:
serial@1c090000,格式为名称@地址,地址通常为寄存器基址。
1.3.2 属性(Property)
| 属性名 | 含义 | 示例值 |
|---|---|---|
compatible |
驱动匹配字符串,优先级从高到低 | "fsl,imx6q-uart", "fsl,imx21-uart" |
reg |
寄存器地址范围,格式为 <地址 长度> |
<0x1c090000 0x4000> |
interrupts |
中断描述,格式依赖 #interrupt-cells |
<0 49 4>(GIC 格式) |
clocks |
时钟引用,通过 phandle 指向时钟节点 |
<&clks 0x1c> |
status |
设备启用状态:okay / disabled |
"okay" |
#address-cells |
子节点 reg 中地址字段占用的 32 位单元数 |
<1> |
#size-cells |
子节点 reg 中长度字段占用的 32 位单元数 |
<1> |
常见陷阱:
#address-cells 和 #size-cells 必须在父节点中定义,子节点 reg 的解析依赖这两个属性。忘记设置或设置错误会导致内核无法正确读取寄存器地址。
1.4 设备树在内核启动时的解析流程
从 Bootloader 加载 DTB 到内核驱动完成绑定,设备树经历了以下关键阶段:
- Bootloader 加载 DTB:U-Boot 从存储介质(eMMC、NAND、网络)读取
.dtb文件到内存,并将物理地址传递给内核(通过r2寄存器或bootm命令)。 - 早期未压缩内核解压:内核解压代码保存 DTB 地址,进入架构相关的
setup_arch()函数。 - DTB 线性映射:调用
early_init_dt_scan(),将 DTB 从物理内存映射到内核虚拟地址空间,并验证魔数(0xd00dfeed)。 - 内存与 CPU 初始化:扫描
/memory节点获取内存大小,扫描/cpus节点获取 CPU 拓扑。 - 设备节点展开:调用
unflatten_device_tree(),将扁平 DTB 展开为内核内部的device_node树形结构。 - 驱动与设备绑定:总线驱动(如 platform bus)遍历设备树节点,匹配
compatible属性,触发probe()回调。
1.5 设备树文件组织:DTS、DTSI 与 DTB
实际工程中,设备树文件分为三种形态:
| 文件类型 | 扩展名 | 作用 | 编译工具 |
|---|---|---|---|
| 源文件 | .dts |
板级设备树源文件,通常包含 #include 引用公共头文件 |
dtc(Device Tree Compiler) |
| 包含文件 | .dtsi |
SoC 级公共定义,被多个 .dts 共享(如 imx6q.dtsi) |
dtc(通过 #include 展开) |
| 二进制文件 | .dtb |
编译后的二进制设备树,Bootloader 直接加载 | dtc -I dts -O dtb |
工程最佳实践: 永远不要直接修改
.dtb 文件。正确的做法是修改 .dts 源文件,然后使用 dtc 重新编译。内核源码树中执行 make dtbs 可一键编译所有设备树。
1.6 本章小结
- 设备树解决了 ARM 平台“板文件泛滥”的历史问题,实现了硬件描述与内核代码的解耦。
- 其核心语法包括节点(node)和属性(property),
compatible是驱动匹配的关键。 - 内核启动时,DTB 经过验证、映射、展开、匹配四个阶段,最终驱动
probe()被调用。 - 掌握
dtc编译工具和of_*API 是后续中断调试的基础。