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语法或引用有问题;反编译看不到节点说明合并或覆盖有误;/proc/device-tree里没有说明内核没采用这份DTB;有节点但驱动probe失败,则回到of_*读取的返回值和属性名上核对。每一步都有明确的观察对象和判断依据,而不是反复重启碰运气。
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,参数正确 |
| 结果 | dmesg | probe 日志 | probe success,无 -EINVAL |