4. DTS到DTB的编译流程:从DTS源文件到DTB二进制文件的完整编译过程

设备树从可读的文本格式(DTS)转换为二进制格式(DTB)的过程,由设备树编译器(Device Tree Compiler,简称 dtc)完成。这个编译过程并非简单的文本到二进制的转换,而是包含了预处理、语法解析、符号展开、二进制打包等多个阶段。理解这一流程,是定位设备树相关编译错误的基础。

4.1 编译流程全景图

下图展示了从 .dts 源文件到 .dtb 二进制文件的完整编译流水线,以及中间生成的关键文件类型。

DTS → DTB 编译流水线 .dts 源文件 (设备树源文本) .dtsi 包含文件 (SoC/板级头文件) 阶段1:C 预处理器 .dts.pre 中间文件 阶段2:词法 & 语法分析 阶段3:符号 & 引用展开 阶段4:DTB 二进制打包 .dtb 二进制文件 (扁平设备树 Blob) ⚠ 常见中间产物 .dts_compiled .dtb.dep.tmp (依赖文件) 💡 最终产物 由 Bootloader 加载到内存

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 的编译是一个严谨的多阶段过程:

  1. 预处理:展开 #include 和宏定义,生成单一文本流。
  2. 解析:将文本转换为 AST,检查语法正确性。
  3. 展开:解析标签引用,合并节点,消除所有符号依赖。
  4. 打包:将 AST 序列化为 FDT 二进制格式,包含 Header、保留块、结构块和字符串块。
💡 生产环境建议: 在 Linux 内核源码树中,使用 make dtbs 命令编译所有设备树。内核的 scripts/dtc/ 目录下提供了与内核版本匹配的 dtc 工具,避免因 dtc 版本差异导致的编译结果不一致。