4、设备树编译器:dtc工具的使用、编译与反编译

设备树写好了,怎么变成内核能吃的二进制文件?

答案就是 dtc——Device Tree Compiler。

这玩意儿说白了就是设备树的「翻译官」。你把 .dts 文本文件丢给它,它给你吐出 .dtb 二进制。反过来也行,.dtb 反编译成 .dts,方便你调试。

我在项目里见过不少新人,拿着 .dts 直接往内核里塞,结果编译报错一脸懵。其实很多时候,就是没搞懂 dtc 的脾气。

4.1 dtc 工具从哪来?

dtc 的源码在 Linux 内核的 scripts/dtc/ 目录下。你编译内核的时候,它会被自动编译出来。

当然,你也可以单独编译它:

# 进入内核源码目录
make scripts/dtc
# 或者直接编译整个内核,dtc 也会生成
make defconfig
make -j4

编译完之后,工具在 scripts/dtc/dtc。我个人习惯把它拷到 /usr/local/bin/ 下,方便全局调用。

小提示: 如果你用的是 Ubuntu,可以直接 apt install device-tree-compiler。但版本可能比较老,我建议还是用内核自带的,版本匹配,坑少。

4.2 基本用法:编译 .dts → .dtb

这是最常用的操作。你写好了设备树源文件,想生成内核能识别的二进制:

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

参数解释:

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

就这么简单?嗯,大部分情况是的。但如果你遇到编译错误,dtc 会告诉你哪一行有问题。比如:

myboard.dts:45.3-11 syntax error
FATAL ERROR: Unable to parse input tree

这时候别慌。去第 45 行看看,多半是少了个分号,或者花括号没配对。我刚开始写设备树时,这种错误犯过不下十次。

4.3 反编译:.dtb → .dts

有时候你拿到一块开发板,厂商只给了 .dtb 文件。你想看看里面到底定义了哪些设备?反编译一下就行:

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

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

反编译出来的 .dts 文件,可读性还不错。但要注意,它会把所有节点都展开,包括那些被 /delete-node//delete-property/ 删掉的内容。嗯,这里有个坑,我后面会讲。

注意: 反编译出来的 .dts 不能直接拿回去编译。因为 dtc 在反编译时会丢失一些注释和格式信息。我曾经试过「反编译→修改→再编译」,结果板子起不来了。排查了半天,发现是反编译时把某个 status = "disabled" 给丢了。

4.4 常用选项与调试技巧

dtc 还有一些选项,调试时特别有用。我列个表:

选项 作用 我的使用场景
-@ 生成符号节点(支持插件) 做设备树插件(overlay)时必须加
-H 指定 phandle 格式 兼容旧版内核时用,默认就好
-W 启用警告 比如 -W unit_address_vs_reg 检查地址格式
-E 启用错误检查 比如 -E no_unique_unit_address 检查地址唯一性
-v 显示版本信息 确认 dtc 版本,避免踩坑

举个例子,我想检查设备树里有没有地址冲突:

dtc -I dts -O dtb -o test.dtb test.dts -W unit_address_vs_reg -E no_unique_unit_address

如果输出是空的,说明没问题。如果有警告,dtc 会告诉你哪个节点有问题。

4.5 设备树插件(Overlay)的编译

这个稍微进阶一点。设备树插件允许你在不修改主设备树的情况下,动态添加或修改节点。比如你想在运行时加载一个 FPGA 的配置,就可以用 overlay。

编译插件时,必须加 -@ 选项:

dtc -@ -I dts -O dtb -o my-overlay.dtbo my-overlay.dts

注意输出后缀是 .dtbo,不是 .dtb。这是内核用来区分普通设备树和插件设备树的约定。

插件文件的内容长这样:

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &i2c1;
        __overlay__ {
            status = "okay";
            my-sensor@48 {
                compatible = "my,pressure-sensor";
                reg = <0x48>;
            };
        };
    };
};

这里 /plugin/; 告诉 dtc 这是一个插件。fragment@0 定义了一个片段,target 指向要修改的节点,__overlay__ 里面放你要添加或修改的内容。

核心要点: 主设备树编译时也要加 -@,否则插件找不到符号节点。我见过有人主设备树没加 -@,结果插件加载时内核报 symbols not found,折腾了一下午。

4.6 反编译的坑与避坑指南

前面提到反编译有坑,这里展开说说。

坑一:丢失 /delete-node//delete-property/

如果你在 .dts 里用 /delete-node/ 删除了某个节点,反编译出来的 .dts 里这个节点又出现了。因为 dtc 在反编译时,只输出最终的内存表示,不会保留删除操作。

坑二:phandle 变成数字

原 .dts 里你写的 &gpio1,反编译后可能变成 &phandle 0x...<数字>。可读性差了很多。

坑三:标签丢失

你在节点前加的标签(label),比如 uart0: serial@... ,反编译后可能变成 serial@... ,标签没了。

我曾经在调试一个音频驱动时,反编译了厂商的 .dtb,想看看它的 codec 节点是怎么配的。结果反编译出来的文件里,所有标签都丢了,我对着原理图一个一个对地址,差点没崩溃。

避坑指南:

  • 反编译只用来「看」,不要用来「改」
  • 如果非要改,保留原始 .dts 源文件
  • dtc -O dts -o /dev/stdout myboard.dtb 直接输出到终端,快速查看

4.7 实战:一个完整的编译流程

假设你有一个项目,设备树文件结构如下:

project/
├── myboard.dts          # 主设备树
├── myboard.dtsi         # 公共头文件(被 include)
└── my-overlay.dts       # 插件

编译主设备树:

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

编译插件:

dtc -@ -I dts -O dtb -o my-overlay.dtbo my-overlay.dts

检查生成的二进制:

dtc -I dtb -O dts myboard.dtb | head -50

这样你就能快速确认编译结果对不对。

我的习惯: 在 Makefile 里加一个 dtc-check 目标,每次修改 .dts 后自动编译并检查警告。比如:
dtc-check:
    dtc -@ -I dts -O dtb -o /dev/null myboard.dts -W unit_address_vs_reg
这样能提前发现很多低级错误。

4.8 本章小结

dtc 是设备树开发中最常用的工具。你不需要记住所有选项,但下面这几个一定要烂熟于心:

  • 编译: dtc -I dts -O dtb -o xxx.dtb xxx.dts
  • 反编译: dtc -I dtb -O dts -o xxx.dts xxx.dtb
  • 插件编译:-@,输出后缀 .dtbo
  • 调试:-W-E 检查潜在问题

嗯,工具本身不难。难的是你拿到一个 .dtb 后,能不能快速读懂它、修改它、验证它。下一节我们会深入设备树的语法和节点定义,到时候你会觉得,dtc 只是开胃菜。


公众号:蓝海资料掘金营,微信 deep3321