4. DTS到DTB的编译流程:从DTS源文件到DTB二进制文件的完整编译过程
设备树从可读的文本格式(DTS)转换为二进制格式(DTB)的过程,由设备树编译器(Device Tree Compiler,简称 dtc)完成。这个编译过程并非简单的文本到二进制的转换,而是包含了预处理、语法解析、符号展开、二进制打包等多个阶段。理解这一流程,是定位设备树相关编译错误的基础。
4.1 编译流程全景图
下图展示了从 .dts 源文件到 .dtb 二进制文件的完整编译流水线,以及中间生成的关键文件类型。
4.2 各阶段详细解析
4.2.1 阶段一:C 预处理器(CPP)展开
设备树源文件通常使用 #include 指令引用 SoC 级 .dtsi 文件或头文件。dtc 在编译前会调用 C 预处理器(cpp)对这些宏和包含文件进行展开。
- 输入:
.dts源文件 + 所有被#include的.dtsi文件 - 处理内容:
#include、#define、#ifdef、#if等预处理指令 - 输出:一个完全展开的
.dts.pre中间文件(或直接通过管道传递给下一阶段)
🔧 常见错误: 如果
.dtsi 文件路径错误或循环包含,预处理阶段会报 fatal error: xxx.dtsi: No such file or directory。此时应检查 -I 包含路径参数。
4.2.2 阶段二:词法分析与语法分析
dtc 对预处理后的文本进行词法扫描(lex)和语法解析(yacc)。此阶段将文本流转换为抽象语法树(AST)。
- 词法分析:识别关键字(
/dts-v1/、/memreserve/)、节点名、属性名、字符串、数字、标点符号等 token。 - 语法分析:根据设备树语法规则(节点嵌套、属性赋值、标签定义)构建 AST 节点。
- 输出:内存中的 AST 结构,此时所有节点和属性已被识别,但引用(
&label)尚未解析。
⚠ 语法错误高发区: 缺少分号
;、花括号不匹配 { }、属性值格式错误(如字符串缺少引号)、节点名包含非法字符。dtc 会给出行号提示,例如 syntax error before '...'。
4.2.3 阶段三:符号与引用展开
设备树支持通过标签(label)进行节点引用,例如 &uart1 { status = "okay"; };。此阶段负责将引用解析为实际节点的路径。
- 标签注册:遍历 AST,将所有
label: node-name形式的标签注册到符号表。 - 引用替换:将
&label替换为对应节点的绝对路径(如/soc/serial@.../)。 - 合并操作:将引用节点中的属性与原始节点进行合并(覆盖或追加)。
- 输出:完全展开且无引用的 AST。
💡 调试技巧: 使用
dtc -O dts -o output.dts input.dtb 可以将编译后的 DTB 反编译为 DTS,检查引用是否被正确解析为完整路径。
4.2.4 阶段四:DTB 二进制打包
最后阶段将展开后的 AST 序列化为扁平设备树(Flattened Device Tree, FDT)二进制格式。这是最终被 Bootloader 和内核解析的格式。
| DTB 结构块 | 说明 | 大小(典型值) |
|---|---|---|
| FDT Header | 魔数(0xd00dfeed)、总大小、结构块偏移、字符串块偏移、版本号等 | 40 字节 |
| Memory Reservation Block | 保留内存区域列表(由 /memreserve/ 指定) |
16 字节 × 保留区数量 |
| Structure Block | 节点和属性的 token 序列(FDT_BEGIN_NODE、FDT_PROP 等) |
取决于设备树复杂度 |
| Strings Block | 所有属性名的字符串表,供 Structure Block 中的偏移量引用 | 取决于属性名长度 |
打包过程中,dtc 会进行以下关键操作:
- 对齐填充:每个节点和属性数据按 4 字节对齐,不足补零。
- 字符串去重:相同的属性名(如
compatible、reg)在 Strings Block 中只存储一次。 - 大小计算:计算总大小并写入 Header,确保 Bootloader 可以一次性加载完整 DTB。
4.3 编译命令与参数
标准编译命令格式如下:
# 基本编译:将 .dts 编译为 .dtb
dtc -I dts -O dtb -o output.dtb input.dts
# 常用参数组合(带预处理和路径指定)
cpp -nostdinc -I include -undef -x assembler-with-cpp input.dts \
| dtc -I dts -O dtb -o output.dtb -
# 生成依赖文件(用于 Makefile 增量编译)
dtc -I dts -O dtb -o output.dtb -d output.dtb.dep input.dts
# 反编译:将 .dtb 还原为 .dts(调试用)
dtc -I dtb -O dts -o output.dts input.dtb
| 参数 | 含义 | 典型值 |
|---|---|---|
-I |
输入格式 | dts(源文件)、dtb(二进制)、fs(文件系统) |
-O |
输出格式 | dtb、dts、asm(汇编) |
-o |
输出文件路径 | arch/arm/boot/dts/xxx.dtb |
-d |
生成依赖文件 | xxx.dtb.dep |
-@ |
生成符号节点(支持插件覆盖) | 用于设备树叠加层(DTO) |
-H |
指定 DTB 格式变体 | epapr(默认) |
4.4 编译过程中的常见错误与排查
📋 错误排查清单:
| 错误类型 | 典型错误信息 | 根因 | 解决方案 |
|---|---|---|---|
| 预处理错误 | fatal error: xxx.dtsi: No such file or directory |
#include 路径不在搜索范围内 |
添加 -I <path> 参数指定包含目录 |
| 语法错误 | syntax error before '...' token |
缺少分号、括号不匹配、属性值格式错误 | 根据行号检查 DTS 文件,确保每个属性以 ; 结尾 |
| 引用错误 | Reference to non-existent node or label "xxx" |
&label 引用的标签未定义 |
检查标签拼写,确认 .dtsi 中是否定义了该标签 |
| 属性重复 | Duplicate property name "xxx" in node "yyy" |
同一节点内定义了两次相同属性 | 删除重复定义,或使用 /delete-property/ 删除继承的属性 |
| 节点名冲突 | Duplicate node name "xxx" with same unit-address |
两个节点具有相同的名称和 reg 地址 |
确保节点名 + @ + 地址唯一 |
| 大小溢出 | DTB blob too large: > 65536 bytes |
设备树过于复杂,超出 Bootloader 限制 | 精简设备树,或增大 Bootloader 的 DTB 加载大小限制 |
4.5 编译流程总结
从 DTS 到 DTB 的编译是一个严谨的多阶段过程:
- 预处理:展开
#include和宏定义,生成单一文本流。 - 解析:将文本转换为 AST,检查语法正确性。
- 展开:解析标签引用,合并节点,消除所有符号依赖。
- 打包:将 AST 序列化为 FDT 二进制格式,包含 Header、保留块、结构块和字符串块。
💡 生产环境建议: 在 Linux 内核源码树中,使用
make dtbs 命令编译所有设备树。内核的 scripts/dtc/ 目录下提供了与内核版本匹配的 dtc 工具,避免因 dtc 版本差异导致的编译结果不一致。