1. 设备树基础概念:起源、作用与基本语法结构
1.1 设备树的起源与设计动机
在早期的 Linux 内核中,板级硬件描述信息(如内存基地址、中断号、时钟频率、GPIO 引脚分配等)被直接硬编码在 arch/arm/mach-xxx 目录下的 C 语言板文件中。这种方式的弊端随着 ARM 架构的碎片化迅速暴露:
- 代码冗余:每款开发板都需要独立的
board-xxx.c文件,大量重复的寄存器地址和资源定义散落在不同文件中。 - 维护困难:硬件引脚变更需要重新编译内核,无法做到内核与硬件描述分离。
- 社区分裂:厂商提交的板级代码质量参差不齐,导致主线内核维护成本急剧上升。
为了解决上述问题,PowerPC 架构率先引入了 Open Firmware 设备树概念,随后被 ARM、RISC-V、X86(ACPI 之外)等架构广泛采纳。设备树的核心思想是:将硬件描述从内核源码中剥离,以结构化文本形式独立存在,由 bootloader 传递给内核,内核在启动时解析并动态创建平台设备。
关键转变: 从“编译时绑定”到“运行时描述”——硬件变化只需修改 DTS 文件并重新编译设备树二进制(DTB),无需重新编译内核。
1.2 设备树在 Linux 内核启动流程中的位置
设备树文件(.dts)经过 dtc 编译生成二进制 .dtb 文件,由 bootloader(如 U-Boot)加载到内存中,并将起始地址传递给内核。内核在启动早期完成以下关键步骤:
| 启动阶段 | 执行主体 | 设备树相关操作 |
|---|---|---|
| 1. Bootloader 加载 | U-Boot / Barebox | 从存储介质读取 .dtb 到内存,设置 r2 寄存器(ARM)或 a1 寄存器(RISC-V)指向 DTB 地址 |
| 2. 内核入口汇编 | head.S / head_64.S |
保存 DTB 地址到全局变量 __fdt_pointer,进入 C 语言初始化 |
| 3. 早期解析 | setup_arch() |
调用 early_init_dt_scan() 扫描 DTB 中的 chosen 节点获取启动参数、内存大小 |
| 4. 设备树展开 | unflatten_device_tree() |
将扁平 DTB 展开为 struct device_node 树状结构,挂入全局链表 |
| 5. 平台设备创建 | of_platform_populate() |
遍历设备树节点,为 compatible 匹配的节点创建 platform_device |
| 6. 驱动绑定 | 总线驱动模型 | 驱动通过 of_match_table 匹配设备树节点,执行 probe() 回调 |
1.3 设备树基本语法结构
设备树源文件(.dts)采用树形层级结构描述硬件,每个硬件单元对应一个节点(node),节点内包含若干属性(property)。以下是一个典型的设备树片段:
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
chosen {
stdout-path = &uart0;
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a8";
reg = <0x0>;
clock-frequency = <1000000000>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MB */
};
uart0: serial@44e09000 {
compatible = "ti,am3352-uart", "ti,omap3-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
clock-frequency = <48000000>;
status = "okay";
};
};
1.4 节点与属性的核心规则
| 语法元素 | 格式 | 说明 | 示例 |
|---|---|---|---|
| 根节点 | / { } |
设备树的顶层容器,所有硬件节点都是其子节点 | / { model = "xxx"; }; |
| 节点名称 | node-name@unit-address |
@ 后跟寄存器基地址或单元编号,用于区分同名节点 |
uart0: serial@44e09000 |
| 标签(label) | label: node-name |
为节点起别名,方便在 .dtsi 中通过 &label 引用 |
uart0: serial@44e09000 |
| 属性 | key = value; |
描述节点特性,值可以是字符串、32位整数数组、字节数组等 | compatible = "ti,am3352-uart"; |
| reg 属性 | reg = <addr size>; |
描述设备占用的地址空间,#address-cells 和 #size-cells 决定格式 |
reg = <0x44e09000 0x2000>; |
| interrupts | interrupts = <num>; |
指定设备使用的中断号,依赖 interrupt-parent 解析 |
interrupts = <72>; |
| status | "okay" / "disabled" |
控制设备是否启用,常用于 .dtsi 中默认禁用,板级 .dts 中开启 |
status = "okay"; |
实战提示:
compatible 属性是驱动匹配的核心,格式为 "厂商,设备型号",内核通过 of_match_table 进行字符串精确匹配。建议始终将最具体的型号放在最前面,通用型号放在后面,如 "ti,am3352-uart", "ti,omap3-uart"。
1.5 设备树包含机制:.dtsi 与 .dts
实际项目中,设备树文件分为两类:
.dtsi(设备树包含文件):存放 SoC 公共硬件描述,如 CPU 集群、通用外设(UART、I2C、SPI)、中断控制器、时钟控制器等。由芯片厂商提供,多个板级文件共享。.dts(设备树源文件):存放板级特定硬件描述,如内存大小、板载外设型号、引脚复用配置、时钟频率调整等。通过#include引入.dtsi,并通过&label覆盖或追加属性。
/* am335x-bone-common.dtsi (SoC 公共部分) */
#include "am33xx.dtsi"
&uart0 {
pinctrl-names = "default";
pinctrl-0 = &uart0_pins;
status = "okay";
};
/* am335x-boneblack.dts (板级特定部分) */
#include "am335x-bone-common.dtsi"
&uart0 {
clock-frequency = <48000000>; /* 覆盖默认值 */
};
1.6 设备树编译与反编译
设备树编译器 dtc 是操作设备树的核心工具,常用命令如下:
| 操作 | 命令 | 说明 |
|---|---|---|
| 编译 DTS → DTB | dtc -I dts -O dtb -o output.dtb input.dts |
将可读的文本格式编译为内核解析的二进制格式 |
| 反编译 DTB → DTS | dtc -I dtb -O dts -o output.dts input.dtb |
从二进制还原为文本,用于调试或逆向分析 |
| 查看 DTB 内容 | fdtdump input.dtb |
以十六进制+ASCII 方式打印 DTB 原始内容 |
| 内核编译时自动生成 | make dtbs |
在内核源码目录执行,编译所有 .dts 为 .dtb |
常见陷阱: 反编译得到的
.dts 会丢失原始文件中的注释、标签名(label)和宏定义(如 #include 展开后的内容)。反编译文件仅用于参考,不应直接作为修改基础,应始终维护原始 .dts 源文件。
1.7 本章小结
- 设备树解决了板级硬编码带来的维护灾难,实现了硬件描述与内核源码的解耦。
- 在启动流程中,设备树从 bootloader 传递到内核,经过解析、展开、设备创建三个阶段,最终驱动绑定。
- 基本语法以树形节点 + 属性为核心,
compatible是驱动匹配的关键,reg描述地址空间。 .dtsi与.dts的分层包含机制,使得 SoC 公共描述与板级特定描述分离,便于复用和维护。dtc工具链支持编译、反编译和调试,是设备树开发的必备技能。