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 文件不是随便堆的二进制数据。它有严格的格式。我画了一张图帮你理解:
每个部分的作用:
| 块名称 | 大小 | 内容说明 |
|---|---|---|
| 文件头 | 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 没问题?
我一般用两个方法:
- 用
fdtdump查看:这个工具能把 .dtb 的原始结构打印出来。用法:fdtdump myboard.dtb | less。你会看到每个节点的偏移量、大小、属性值。 - 用
dtc -I dtb -O dts反编译:对比反编译结果和源文件,确认没有信息丢失。
终极验证:在 QEMU 或实际硬件上启动内核,看启动日志里有没有 OF: fdt: Machine model: ... 这一行。如果能看到你的板子名字,说明 .dtb 被正确加载了。
嗯,关于 dtc 和 .dtb 格式,核心内容就这些。工具本身不复杂,但细节决定成败。下次你遇到设备树相关的问题,记得先从反编译 .dtb 开始排查——这招帮我省了不少时间。