第3章:设备树编译器(DTC):dtc工具的使用、反编译与编译、设备树覆盖层(overlay)

设备树写好了,怎么变成二进制文件给内核用?

答案就是 DTC——设备树编译器。

说实话,我刚入行那会儿,觉得这工具没啥好学的,不就是 dtc -I dts -O dtb 嘛。直到有一次,我在一个项目里因为编译选项没加对,导致整块板子起不来,排查了整整两天……嗯,从那以后,我对 DTC 的态度就认真多了。

3.1 dtc 工具的基本用法

DTC 的全称是 Device Tree Compiler。它负责把 .dts 文本文件编译成 .dtb 二进制文件,也能反向操作。

最基本的编译命令长这样:

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

参数说明:

  • -I:指定输入格式,dts 表示设备树源文件
  • -O:指定输出格式,dtb 是二进制格式
  • -o:输出文件名

反编译呢?就是把 .dtb 变回 .dts:

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

我个人习惯在调试时加一个 -V 17 参数,指定设备树版本。版本 17 是目前最通用的,兼容性最好。

小技巧: 反编译出来的 .dts 文件,节点名会带地址后缀,比如 uart@fe010000。这是正常的,不是 bug。

3.2 编译过程中的那些坑

我在项目中遇到过最典型的问题,是 头文件路径找不到

很多 .dts 文件会包含 .dtsi 文件,比如:

#include "soc.dtsi"
#include "board-common.dtsi"

如果这些文件不在当前目录,编译就会报错。解决办法是加 -i 参数指定搜索路径:

dtc -I dts -O dtb -o my_board.dtb my_board.dts -i /path/to/include/dir

另一个常见问题是 语法错误。比如少了个分号,或者标签名重复。DTC 的错误信息有时候不太友好,只会告诉你「语法错误在第 123 行附近」。这时候我一般会先用 dtc -I dts -O dts 做一次「格式化输出」,看看 DTC 自己能不能解析通过。

注意: 不要直接用文本编辑器修改 .dtb 文件。它是二进制格式,改坏了连错误提示都不会有。我曾经试过用 hexdump 改一个 GPIO 的配置值,结果板子直接变砖。

3.3 设备树覆盖层(Overlay)

覆盖层是什么?说白了,就是「运行时打补丁」。

你想想看,一个硬件平台可能有多种外设扩展板。如果每种组合都写一个完整的 .dts,那得累死人。覆盖层允许你写一个基础 .dtb,再动态加载额外的片段。

覆盖层的语法和普通 .dts 几乎一样,但有一个关键区别:必须用 /plugin/; 声明

举个例子:

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &i2c1;
        __overlay__ {
            status = "okay";
            temp_sensor: tmp102@48 {
                compatible = "ti,tmp102";
                reg = <0x48>;
            };
        };
    };
};

编译覆盖层时,要加 -@ 参数:

dtc -I dts -O dtb -@ -o i2c_sensor.dtbo i2c_sensor.dts

注意输出后缀是 .dtbo,这是覆盖层二进制文件的约定命名。

核心要点: 覆盖层通过 fragmenttarget 指定要修改的节点。每个 fragment 就是一个独立的补丁块。

3.4 覆盖层的加载方式

覆盖层怎么用?有两种主流方式:

方式 说明 适用场景
configfs 接口 通过 /sys/kernel/config/device-tree/overlays/ 加载 运行时动态加载,适合热插拔外设
U-Boot 加载 在 bootloader 阶段合并覆盖层 启动时固定配置,适合扩展板

configfs 的方式我比较常用。命令大致是:

mkdir /sys/kernel/config/device-tree/overlays/my_overlay
cat my_overlay.dtbo > /sys/kernel/config/device-tree/overlays/my_overlay/dtbo

加载成功后,对应的设备节点就会出现在 /sys/firmware/devicetree/base/ 下。

注意: 覆盖层卸载时,要确保没有驱动在使用对应的设备。否则内核可能会 panic。我遇到过一回,卸载顺序搞反了,直接黑屏重启。

3.5 反编译覆盖层

有时候拿到一个 .dtbo 文件,想看看里面到底改了啥。反编译命令和普通 .dtb 一样:

dtc -I dtb -O dts -o dump.dts my_overlay.dtbo

但要注意,反编译出来的内容会包含 __symbols____fixups__ 这些元数据节点。它们是覆盖层机制内部用的,不用太在意。

我个人习惯在反编译时加 -f 参数,强制输出,即使遇到一些格式问题也能继续:

dtc -I dtb -O dts -f -o dump.dts my_overlay.dtbo

3.6 实战建议

最后,分享几个我踩过的坑:

  • 覆盖层的 target 必须存在:如果基础 .dtb 里没有 i2c1 节点,覆盖层加载会失败。不会自动创建。
  • 标签名不能冲突:覆盖层里的标签(比如 temp_sensor)不能和基础 .dtb 里的标签重名。
  • 编译选项要一致:基础 .dtb 和覆盖层 .dtbo 最好用同一版本的 DTC 编译。版本差异可能导致兼容性问题。

嗯,设备树编译器这块,说白了就是「写对语法、选对参数、注意路径」。但真正用起来,细节还是挺多的。尤其是覆盖层,用好了能省不少事,用不好就是给自己挖坑。

下一章,我们会深入设备树的绑定文档和 GPIO 控制器的具体实现。到时候再聊。