2. 设备树文件结构:DTS、DTSI、DTB、DTC工具的关系与编译流程
2.1 设备树文件家族:四种核心文件类型
设备树(Device Tree)是嵌入式Linux系统中用于描述硬件拓扑结构的数据结构,它由一系列文本源文件和二进制目标文件组成。理解这四种文件类型及其关系,是掌握设备树配置的第一步。
| 文件类型 | 扩展名 | 格式 | 作用 | 是否可读 |
|---|---|---|---|---|
| 设备树源文件 | .dts |
文本(C风格) | 描述单板级硬件,是编译入口 | 是 |
| 设备树头文件 | .dtsi |
文本(C风格) | 描述SoC级公共硬件,供.dts包含 |
是 |
| 设备树二进制 | .dtb |
二进制(FDT格式) | Bootloader传递给内核的硬件描述 | 否 |
| 设备树编译工具 | dtc |
可执行程序 | 将.dts/.dtsi编译为.dtb |
— |
💡 关键概念:
.dtsi 类似于 C 语言中的 .h 头文件,用于定义 SoC 级别的公共节点(如 CPU、中断控制器、时钟控制器、GPIO 控制器等),而 .dts 则专注于单板级别的差异化配置(如外设引脚、时钟频率、内存大小等)。
2.2 编译流程:从DTS到DTB的完整链路
设备树的编译流程是一个典型的“预处理 → 编译 → 输出”过程,与C语言的编译过程高度相似。DTC(Device Tree Compiler)是这一流程的核心工具。
🔧 编译流程四步骤:
- 预处理(CPP):C预处理器处理
#include、#define、#ifdef等指令,将.dtsi文件展开到.dts中,生成展开后的临时文件.dts.pre。 - 词法/语法分析(dtc):DTC 对预处理后的文件进行词法分析和语法解析,构建内部节点树。
- 节点合并与覆盖(dtc):DTC 根据节点路径和标签(label)执行节点覆盖规则,合并同名节点属性。
- 二进制输出(dtc):DTC 将最终的设备树结构序列化为扁平设备树(Flattened Device Tree, FDT)格式,生成
.dtb文件。
2.3 Include机制:如何组织多层级设备树
设备树通过 #include 指令实现模块化组织,这与C语言的预处理机制完全一致。合理的 #include 层次结构可以大幅提高代码复用率,减少重复定义。
2.3.1 典型的三层包含结构
// 第一层:SoC公共定义(芯片厂商提供)
#include "soc.dtsi" // CPU、中断控制器、时钟控制器等
// 第二层:SoC系列公共定义(芯片厂商提供)
#include "soc-pinctrl.dtsi" // pinmux复用配置
// 第三层:单板级定义(板级开发者编写)
#include "myboard-common.dtsi" // 板级公共外设(如UART、I2C)
/ {
model = "MyBoard v2";
compatible = "vendor,myboard-v2", "vendor,myboard";
&uart1 {
status = "okay";
pinctrl-0 = &uart1_pins_a;
};
};
⚠️ 注意事项:
#include路径相对于 DTC 的-i参数指定的搜索路径,或相对于源文件所在目录。- 内核构建系统(Kbuild)会自动将
arch/<arch>/boot/dts/加入搜索路径。 - 不要使用
/include/旧语法(DTC v1.4.0 之前),统一使用#include以获得CPP宏支持。
2.4 节点覆盖规则:同名节点的合并与覆盖
设备树最强大的特性之一就是节点覆盖(Node Overlay)机制。当多个文件或同一个文件中出现同名节点时,DTC 会按照特定规则进行合并或覆盖,而不是简单报错。
2.4.1 覆盖规则详解
| 场景 | 规则 | 示例 |
|---|---|---|
| 同名节点,不同属性 | 属性合并:两个节点的属性集合取并集,同名属性以后者为准 | node { a=1; } + node { b=2; } → node { a=1; b=2; } |
| 同名节点,相同属性 | 后者覆盖前者:后出现的属性值覆盖先出现的 | node { speed=<100>; } + node { speed=<200>; } → node { speed=<200>; } |
| 同名子节点 | 递归合并:子节点也遵循相同的合并规则 | 父节点合并时,子节点也逐层合并 |
使用 /delete-node/ |
删除节点:显式删除之前定义的节点 | /delete-node/ &uart2; |
使用 /delete-property/ |
删除属性:显式删除之前定义的属性 | /delete-property/ unused-flag; |
2.4.2 覆盖顺序优先级
覆盖顺序由 #include 的顺序和节点在文件中的出现顺序共同决定:
- 先包含的先定义:
#include "soc.dtsi"中的节点先被解析。 - 后出现的覆盖先出现的:在同一个文件中,后面的节点定义覆盖前面的。
- 根节点下的覆盖优先级最高:
.dts文件根节点下的定义覆盖.dtsi中的定义。 - 标签引用(&label)覆盖路径引用:使用
&label { }语法可以精确覆盖指定节点。
2.5 实战:使用标签(Label)进行精确覆盖
在实际开发中,最推荐的覆盖方式是使用 标签引用(Label Reference) 语法 &label { }。这种方式比使用完整路径更简洁、更不易出错。
// soc.dtsi 中定义标签
soc {
uart1: serial@1000 {
compatible = "vendor,uart";
reg = <0x1000 0x100>;
clocks = <&clkc 16>;
pinctrl-names = "default";
pinctrl-0 = <&uart1_pins_a>;
status = "disabled";
};
uart1_pins_a: uart1-pins {
pins = "PA0", "PA1";
function = "uart1";
};
};
// myboard.dts 中通过标签覆盖
#include "soc.dtsi"
&uart1 {
status = "okay"; // 覆盖 status 属性
clock-frequency = <115200>; // 新增属性
};
&uart1_pins_a {
bias-pull-up; // 新增属性:使能上拉
};
📋 标签覆盖的优势:
- 路径无关:即使父节点路径发生变化,标签引用仍然有效。
- 可读性强:
&uart1 { }比&soc { serial@1000 { } }更直观。 - 支持增量修改:只覆盖需要修改的属性,其余保持原样。
- 编译时检查:如果标签未定义,DTC 会报错,避免静默错误。
2.6 反编译与调试:DTB → DTS 的逆向过程
在实际调试中,经常需要将二进制 .dtb 反编译为可读的 .dts 格式,以验证编译结果是否正确,或者分析其他板卡的设备树配置。
# 反编译 DTB 为 DTS
dtc -I dtb -O dts -o decompiled.dts myboard.dtb
# 查看 DTB 的详细信息(包括大小、版本、占用空间)
dtc -I dtb -O dts -V myboard.dtb
# 使用 fdtdump 工具直接查看 DTB 的原始结构
fdtdump myboard.dtb
| 工具 | 功能 | 适用场景 |
|---|---|---|
dtc -I dtb -O dts |
标准反编译,输出格式化的DTS | 日常调试、代码审查 |
dtc -I dtb -O dts -V |
显示DTB版本信息 | 检查DTB兼容性 |
fdtdump |
以十六进制+ASCII方式显示DTB原始数据 | 底层分析、损坏检查 |
dtc -O asm |
输出汇编格式的设备树 | Bootloader集成调试 |
⚠️ 反编译注意事项:
- 反编译后的
.dts会丢失所有注释和宏定义(因为预处理阶段已展开)。 - 反编译结果中的节点顺序可能与源文件不同,但语义等价。
- 如果 DTB 使用了
/plugin/语法(设备树覆盖),反编译时需要加-@参数保留符号信息。
2.7 本章小结
设备树的文件结构设计体现了嵌入式系统“分层抽象、增量覆盖”的核心思想:
- DTSI 负责 SoC 级公共描述,由芯片厂商维护,板级开发者不应修改。
- DTS 负责单板级差异化配置,通过
#include引入 DTSI,并通过标签覆盖机制实现精确修改。 - DTC 作为编译工具,将文本源文件转换为二进制 FDT 格式,供 Bootloader 和内核使用。
- 节点覆盖规则 是设备树最灵活的特性,理解“属性合并、后者覆盖、标签优先”三大原则,是编写正确设备树的基础。