3、设备树编译器:dtc工具的使用、编译与反编译、设备树二进制格式(.dtb)

设备树源文件(.dts)写好了,怎么变成内核能用的东西?

答案就是 dtc —— Device Tree Compiler。

这玩意儿其实没那么神秘。说白了,它就是个翻译官,把人类可读的文本格式转成机器能快速解析的二进制格式。我在几个项目里都吃过没搞懂 dtc 的亏,今天咱们一次性把它聊透。

核心一句话:dtc 是设备树生态的基石。你写的 .dts,最终都要靠它变成 .dtb,才能被 Bootloader 加载、被内核解析。

3.1 dtc 工具从哪来?

dtc 的源码在 scripts/dtc/ 目录下。Buildroot 在编译内核时,会自动编译出 dtc 工具。

你也可以单独编译它:

make -C scripts/dtc

编译完成后,可执行文件就在 scripts/dtc/dtc。我个人习惯把它复制到 /usr/local/bin/,方便全局调用。

小技巧:Buildroot 生成的交叉编译工具链里,通常也带了 dtc。路径在 output/host/bin/dtc。直接用就行,不用自己再编译一遍。

3.2 编译:从 .dts 到 .dtb

最基本的用法就一行命令:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

参数解释:

  • -I dts:输入格式是 .dts(文本)
  • -O dtb:输出格式是 .dtb(二进制)
  • -o myboard.dtb:指定输出文件名
  • 最后一个参数:输入文件

我在项目中遇到过一个问题:直接编译带 #include 的 .dts 会报错。因为 dtc 本身不支持 C 预处理器。解决办法是先用 cpp 预处理:

cpp -nostdinc -I include -undef -x assembler-with-cpp myboard.dts myboard_preprocessed.dts
dtc -I dts -O dtb -o myboard.dtb myboard_preprocessed.dts

或者更省事,直接用内核的 Makefile 编译。Buildroot 里也是这么干的。

注意:如果你在 .dts 里用了 /include/ 而不是 #include,那 dtc 自己就能处理。但 /include/ 功能有限,不支持宏定义。我建议统一用 #include + 预处理的方式。

3.3 反编译:从 .dtb 回到 .dts

有时候你拿到一个 .dtb 文件,想看看里面到底定义了啥。反编译就派上用场了:

dtc -I dtb -O dts -o myboard_reverse.dts myboard.dtb

参数基本一样,只是输入输出格式互换。

反编译出来的 .dts 文件,跟原始文件相比,会有一些差异:

  • 所有标签(labels)会丢失,变成路径引用
  • 注释全部消失
  • 节点顺序可能调整
  • 属性值会以最简形式展示

我曾经用反编译来调试一个奇怪的问题:内核启动时某个外设没识别。反编译 .dtb 后发现,status = "disabled" 被写成了 status = "disabeld"。拼写错误!这种问题在源文件里很难一眼看出来,但反编译后的 .dts 里,属性值一目了然。

调试利器:把内核实际加载的 .dtb 反编译出来,跟你的源文件对比,能快速定位很多问题。比如确认 chosen 节点里的 bootargs 是不是你想要的。

3.4 设备树二进制格式(.dtb)长什么样?

.dtb 文件不是随便堆的二进制数据。它有严格的格式。我画了一张图帮你理解:

.dtb 文件结构示意图 .dtb 文件结构 文件头 (Header) — 40 字节 内存预留块 (Memory Reservation Block) — 16 字节/块 结构块 (Structure Block) — 节点和属性的实际数据 包含:FDT_BEGIN_NODE, FDT_PROP, FDT_END_NODE 等令牌 字符串块 (Strings Block) — 所有属性名的字符串 对齐填充 (Padding) — 保证 8 字节对齐 所有块之间通过偏移量(offset)定位,内核通过文件头中的指针找到各块起始位置

每个部分的作用:

块名称 大小 内容说明
文件头 40 字节 魔数(0xd00dfeed)、大小、版本号、各块偏移量
内存预留块 16 字节/块 记录哪些内存区域不能被内核使用(比如给 GPU 预留的)
结构块 可变 节点树结构,用令牌(token)表示开始、结束、属性
字符串块 可变 所有属性名的字符串,结构块里只存偏移量
对齐填充 0-7 字节 保证整个 .dtb 文件 8 字节对齐

3.5 常用 dtc 选项

除了基本的编译反编译,dtc 还有一些实用选项。我整理了一份清单:

选项 作用 使用场景
-@ 生成插件(overlay)所需的符号信息 制作 .dtbo 文件时必加
-H 指定生成的 .dtb 版本(epapr、fdt 等) 兼容老旧 Bootloader
-V 指定 .dtb 格式版本(16、17 等) 一般用默认值就行
-S 指定 .dtb 最小大小 给 Bootloader 预留空间
-W 启用特定警告 检查 .dts 中的潜在问题
-E 启用特定错误检查 严格模式编译

我的习惯:编译时加上 -W no-unit_address_vs_reg,可以屏蔽一些不必要的警告。但如果是正式发布,建议不加,让所有警告都暴露出来。

3.6 避坑指南

这些年用 dtc,我踩过不少坑。挑几个典型的说说:

  • 版本不匹配:我曾经用新版本 dtc 编译的 .dtb,加载到旧版本内核上,直接 panic。原因是新格式里某些字段的语义变了。解决办法:用内核自带的 dtc 编译。
  • 大小限制:.dtb 文件不能超过 2MB。我在一个项目里塞了太多节点,编译出来的 .dtb 超过 2MB,Bootloader 加载失败。后来不得不精简设备树。
  • 字节序:dtc 默认生成大端(big-endian)的 .dtb。如果你的 CPU 是小端(比如 ARM),dtc 会自动处理。但如果你在 x86 机器上交叉编译,记得检查 -b 选项。
  • 插件编译:编译设备树插件(.dtbo)时,必须加 -@ 选项。我忘了加,结果插件加载时内核报 symbols not found,查了半天才发现。

重要提醒:永远不要手动修改 .dtb 文件。它是个二进制格式,改错一个字节就全废了。要改就改 .dts,然后重新编译。这是最安全、最可控的方式。

3.7 在 Buildroot 中集成 dtc

Buildroot 里其实不用你手动调用 dtc。它通过内核的 make dtbs 目标自动完成编译。

你只需要在 board/yourboard/ 目录下放好 .dts 文件,然后在 Config.in 或 yourboard_defconfig 里指定:

BR2_LINUX_KERNEL_DTS_SUPPORT=y
BR2_LINUX_KERNEL_INTREE_DTS_NAME="yourboard"

Buildroot 会自动找到 arch/arm/boot/dts/yourboard.dts,调用 dtc 编译成 yourboard.dtb,然后打包到镜像里。

如果你想用自己写的 .dts,不在内核源码树里,可以设置:

BR2_LINUX_KERNEL_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_YOURBOARD_PATH)/board/yourboard/yourboard.dts"

这样 Buildroot 会先把你的 .dts 复制到内核源码目录,再编译。我在项目中一直用这种方式,方便管理。

3.8 验证 .dtb 文件

编译完成后,怎么确认 .dtb 没问题?

我一般用两个方法:

  1. 用 fdtdump 查看:这个工具能把 .dtb 的原始结构打印出来。用法:fdtdump myboard.dtb | less。你会看到每个节点的偏移量、大小、属性值。
  2. 用 dtc -I dtb -O dts 反编译:对比反编译结果和源文件,确认没有信息丢失。

终极验证:在 QEMU 或实际硬件上启动内核,看启动日志里有没有 OF: fdt: Machine model: ... 这一行。如果能看到你的板子名字,说明 .dtb 被正确加载了。

嗯,关于 dtc 和 .dtb 格式,核心内容就这些。工具本身不复杂,但细节决定成败。下次你遇到设备树相关的问题,记得先从反编译 .dtb 开始排查——这招帮我省了不少时间。