3. 设备树编译、反编译与调试

设备树从源码到内核可用,中间要经过“编译—打包—烧录—运行时解析”四个阶段。很多适配问题并不是DTS写错,而是编译产物不对、烧录位置不对,或者运行时根本没走到你写的那段节点。本节围绕一条可复现的验证链路展开:用dtc把DTS编译成DTB,用fdtdump和dtc -I dtb反查产物,用/proc/device-tree确认内核实际看到的树,再用of_*接口在驱动里读取并判断结果。

一、DTS到DTB:dtc的编译与打包

设备树编译器dtc(Device Tree Compiler)负责把人类可读的DTS翻译成扁平化二进制DTB。内核源码树里自带一份,位于scripts/dtc/,也可以单独安装。最基础的编译命令是:

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

其中-I指定输入格式,-O指定输出格式。实际工程中更常用的是内核构建系统,因为它会自动处理头文件包含、#include、/include/以及cpp预处理:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
# 产物通常位于 arch/arm/boot/dts/xxx.dtb

如果DTS里引用了.dtsi公共文件,直接调用dtc会因找不到宏而失败,此时应先用C预处理器展开,再交给dtc:

cpp -nostdinc -I include -undef -x assembler-with-cpp board.dts board.pre.dts
dtc -I dts -O dtb -o board.dtb board.pre.dts

编译阶段最常见的报错是语法错误和引用错误。语法错误会直接指出行号,例如缺少分号、括号不匹配;引用错误则表现为ERROR (phandle_references): Reference to non-existent node or label "xxx",说明某个&label在合并后不存在。这类问题必须在编译期解决,不能带到运行时。

二、反编译与产物核对:dtc -I dtb 与 fdtdump

拿到DTB后,第一件事是确认它是否真的包含你写的节点。有两种互补的手段。第一种是dtc反向编译,把DTB还原成DTS文本:

dtc -I dtb -O dts -o board.decompiled.dts board.dtb

反编译结果会显示所有节点、属性、phandle和status,适合逐项核对。第二种是fdtdump,它以更接近二进制布局的方式打印DTB头部和结构块,适合快速确认魔数、版本和节点偏移:

fdtdump board.dtb | head -40

正常输出开头应包含d00dfeed魔数、totalsize、off_dt_struct等字段。如果魔数不对,说明文件不是DTB,或者烧录时被截断。反编译和fdtdump解决的是“产物里有什么”,而运行时验证解决的是“内核用了什么”。

三、运行时验证:/proc/device-tree

内核启动后会把展开的设备树以文件系统形式暴露在/proc/device-tree(部分内核为/sys/firmware/devicetree/base)。这是判断“内核实际看到什么”的最直接入口。每个节点是一个目录,每个属性是一个文件:

ls /proc/device-tree/
ls /proc/device-tree/soc/
cat /proc/device-tree/soc/uart@10000000/status
hexdump -C /proc/device-tree/soc/uart@10000000/reg

字符串属性可以直接cat,数值属性(如reg、clock-frequency)是二进制大端,需要用hexdump或od查看。例如reg = <0x10000000 0x1000>会显示为10 00 00 00 00 00 10 00。如果某个节点在DTS里写了但/proc/device-tree里没有,通常说明该节点被status = "disabled"过滤,或者被后续.dtsi覆盖删除。

调试时优先看status、compatible、reg三个属性。它们分别决定驱动是否匹配、匹配哪个驱动、以及寄存器基地址是否正确。

四、驱动侧读取:of_*接口

驱动通过of_*系列接口从设备树读取参数。常用接口包括of_find_node_by_path、of_get_child_by_name、of_property_read_u32、of_property_read_string、of_iomap等。一个典型的读取片段如下:

struct device_node *np = pdev->dev.of_node;
u32 freq;
int ret;

ret = of_property_read_u32(np, "clock-frequency", &freq);
if (ret) {
    dev_err(&pdev->dev, "clock-frequency missing: %d\n", ret);
    return ret;
}
base = of_iomap(np, 0);
if (!base) {
    dev_err(&pdev->dev, "of_iomap failed\n");
    return -ENOMEM;
}

这里of_property_read_u32返回0表示成功,返回-EINVAL表示属性不存在或长度不符。很多“驱动没反应”的问题,本质是属性名拼写不一致,例如DTS写clock-frequency而驱动读clock_frequency。用of_property_read_u32的返回值做日志,可以快速定位。

五、一条完整的验证链路

把上述工具串起来,形成可复现的验证顺序:先编译得到DTB,再反编译核对节点,然后烧录并重启,最后在目标板上检查/proc/device-tree并观察驱动probe日志。下面这张图展示了从DTS到运行时验证的信号与数据流向,以及每一步的核对点。

DTS → DTB → 运行时验证 数据流与核对点 board.dts / .dtsi 源码:节点、属性、label #include / /include/ dtc 编译 -I dts -O dtb 或 make dtbs board.dtb 魔数 d00dfeed 结构块 + 字符串块 烧录 / 加载 bootloader 传参 或内核内建 DTB 内核展开设备树 → /proc/device-tree 节点目录:soc/uart@10000000/ 属性文件:status、compatible、reg 驱动 of_* 读取 of_property_read_u32 of_iomap / of_get_child probe 结果判断 成功:dmesg 出现 probe success,寄存器可读写 失败:-EINVAL / -ENODEV / of_iomap 返回 NULL 反查工具 dtc -I dtb -O dts fdtdump board.dtb 常见断点 属性名拼写不一致 status 被覆盖为 disabled

按这条链路排查,可以把问题定位到具体阶段:编译期报错说明DTS语法或引用有问题;反编译看不到节点说明合并或覆盖有误;/proc/device-tree里没有说明内核没采用这份DTB;有节点但驱动probe失败,则回到of_*读取的返回值和属性名上核对。每一步都有明确的观察对象和判断依据,而不是反复重启碰运气。

修改DTS后必须重新编译并确认烧录的是新DTB。很多“改了没生效”的情况,是bootloader仍加载旧文件,或内核内建DTB未更新。可用fdtdump对比时间戳与节点内容确认。

六、可直接使用的核对清单

阶段命令/接口观察对象通过判据
编译dtc -I dts -O dtb编译输出无 ERROR,生成 .dtb
反查dtc -I dtb -O dts反编译文本目标节点与属性存在
二进制核对fdtdump头部魔数显示 d00dfeed
运行时/proc/device-tree节点目录与属性文件status 为 okay
驱动读取of_property_read_u32返回值返回 0,参数正确
结果dmesgprobe 日志probe success,无 -EINVAL