Device Tree 调试:外设不工作,先别急着改驱动
Device Tree 调试:外设不工作,先别急着改驱动
一、深度引言:Device Tree 是硬件和内核之间的合同,错一个字就失效
嵌入式 Linux 开发中,外设不工作是最常见的故障场景。传感器没有数据、I2C 通信超时、SPI Flash 无法识别、GPIO 不受控——很多人第一反应是在驱动里加 printk、改寄存器读写逻辑、甚至重构 probe 函数。但大量外设问题的真正根因不在驱动代码里,而在 Device Tree(设备树)中。
Device Tree 是 Linux 内核用来描述硬件拓扑的数据结构。它定义了 SoC 内部外设的寄存器基地址、中断号、时钟、DMA 通道、pinmux 配置,以及板级外设的挂载关系和属性。驱动代码通过标准 API(of_*、devm_*)读取这些信息来配置硬件。如果 Device Tree 的信息是错的,驱动写得再漂亮也是在错误的地址上做正确的操作。
常见的 Device Tree 问题包括:compatible 字符串写错导致驱动不匹配、reg 地址写错导致访问了错误寄存器、clock 漏配导致外设没有时钟源、pinctrl 缺失导致 GPIO 功能未启用、status = "disabled" 忘记改、reset GPIO 极性反转……这些错误有一个共同特征:dmesg 里没有惊天动地的报错,只有 silence——驱动静默 probe 失败,或者外设静默不工作。
本文从 DTS 编译流程、compatible 匹配机制、寄存器地址映射解析、到 regmap 调试代码,系统性地还原 Device Tree 调试的完整方法论。
二、原理剖析:DTS→DTB 编译流程与 compatible 匹配机制
2.1 DTS 编译流程
Device Tree 源码(.dts和.dtsi)需要编译成二进制格式(.dtb)才能被内核使用。编译工具是 DTC(Device Tree Compiler)。
flowchart TD A["SoC 厂商 .dtsi\n(芯片级定义)"] --> D["预处理\n(cpp)"] B["板级 .dts\n(板卡级定义)"] --> D C["Overlay .dts\n(运行时覆盖)"] --> D D --> E[".dts 合并文件"] E --> F["DTC 编译\n(dtc -I dts -O dtb)"] F --> G[".dtb 二进制"] G --> H["U-Boot 加载"] H --> I["内核解析\n(flattened DT)"] I --> J["/proc/device-tree\n(sysfs 暴露)"] F --> K["fdtdump\n(fdtdump board.dtb)"] F --> L["反编译验证\n(dtc -I dtb -O dts)"]编译命令:
# 编译 dtc -I dts -O dtb -o board.dtb board.dts # 反编译(从运行的系统中导出并反编译) dtc -I fs -O dts /proc/device-tree > running.dts # 反编译 dtb 文件 dtc -I dtb -O dts board.dtb > board_decompiled.dts # 查看 dtb 二进制内容 fdtdump board.dtb | less关键点:源码里的.dts正确不代表运行时.dtb正确。U-Boot 可能加载了旧的 dtb、overlay 可能修改了字段、FIT image 可能内含不同版本的 dtb。运行时反查/proc/device-tree或fdtdump才是最终真相。
2.2 compatible 字符串匹配机制
驱动能否 probe,第一条检查就是 compatible 字符串。内核维护了一个匹配表(of_match_table),每个驱动的 compatible 字符串作为一个条目。设备树节点中的 compatible 属性与驱动匹配表中的条目做前缀匹配——如果一个设备节点写compatible = "rockchip,rk3399-i2c",驱动声明{ .compatible = "rockchip,rk3399-i2c" },则匹配成功。
匹配不是模糊搜索。多一个空格、大小写错误、版本号不匹配,都会导致驱动静默跳过这个节点。常见的坑:
- 设备树写
"vendor,device-v2",驱动匹配表只有"vendor,device"→ 不匹配。 - 设备树写
"vendor,device",驱动匹配表写"vendor,device-v2"→也不匹配。内核匹配是精确字符串比对,不是子串搜索。 - 多个 compatible 值(fallback 机制):
compatible = "vendor,soc-device", "vendor,base-device",内核会先尝试第一个,失败再尝试第二个。
2.3 寄存器地址映射(reg 属性解析)
reg 属性定义了外设的寄存器基地址和长度。格式为:
reg = <address_cells address ... length_cells length ...>;在 64 位系统中,#address-cells = <2>,#size-cells = <2>:
i2c@ff110000 { reg = <0x0 0xff110000 /* 高32位地址, 低32位地址 */ 0x0 0x1000>; /* 高32位长度, 低32位长度 */ };内核驱动通过platform_get_resource()或of_address_to_resource()解析 reg 属性,然后用devm_ioremap_resource()映射到虚拟地址空间。后者的一个关键行为是:它会在映射前自动调用devm_request_mem_region(),如果该区域已被其他驱动占用,会直接返回-EBUSY。这就是为什么两个外设的 reg 重叠时,第二个驱动会静默 probe 失败——不报 panic,不打印调用栈,只是不工作。
常见的 reg 错误:
- 地址写错:
reg = <0xff110000 0x1000>— 在 64 位系统上,这表示地址=0xff110000, 大小=0x1000,缺少高 32 位地址。 - 长度不足:
reg = <0x0 0xff110000 0x0 0x100>— 只映射了 256 字节,而外设寄存器空间可能有 4KB。 - 地址冲突:两个外设的 reg 范围重叠,后加载的
devm_ioremap_resource会失败。
三、代码实现:regmap 调试工具和 DTS 验证脚本
/** * Device Tree 调试辅助模块 * 功能:运行时检查 compatible 匹配状态、解析 reg 属性、dump 寄存器 * * 编译:作为内核模块加载 * obj-m += dt_debug.o * make -C /lib/modules/$(uname -r)/build M=$(pwd) modules */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/regmap.h> #include <linux/io.h> #include <linux/device.h> #include <linux/platform_device.h> MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Device Tree Debug Helper"); #define DRIVER_NAME "dt_debug" /* ---- 辅助函数:遍历并打印设备树中所有 compatible 节点 ---- */ static void dump_compatible_nodes(struct device *dev) { struct device_node *np; int count = 0; pr_info("=== 设备树 compatible 节点扫描 ===\n"); for_each_compatible_node(np, NULL, NULL) { const char *compat; int ret = of_property_read_string(np, "compatible", &compat); if (ret == 0) { /* 检查是否有驱动绑定 */ bool bound = (np->fwnode.flags & 0x1) || of_device_is_available(np); pr_info(" [%s] node=%s compatible=%s bound=%d\n", bound ? "BOUND" : "---- ", np->full_name, compat, bound); count++; } } pr_info("共扫描 %d 个 compatible 节点\n", count); } /* ---- 辅助函数:解析 reg 属性并验证地址映射 ---- */ static int check_reg_property(struct device_node *np, int index) { struct resource res; void __iomem *base; int ret; ret = of_address_to_resource(np, index, &res); if (ret != 0) { pr_err("[%s] 无法解析 reg[%d]: ret=%d\n", np->full_name, index, ret); return -EINVAL; } pr_info("[%s] reg[%d]: start=0x%llx end=0x%llx size=0x%llx\n", np->full_name, index, res.start, res.end, (unsigned long long)resource_size(&res)); /* 尝试 ioremap 验证地址是否可用 */ if (!request_mem_region(res.start, resource_size(&res), DRIVER_NAME)) { pr_warn("[%s] 内存区域已被占用: 0x%llx\n", np->full_name, res.start); } base = ioremap(res.start, resource_size(&res)); if (!base) { pr_err("[%s] ioremap 失败: 0x%llx\n", np->full_name, res.start); return -ENOMEM; } /* 读出第一个寄存器的值作为验证 */ uint32_t val = readl(base); pr_info("[%s] 基地址寄存器值: 0x%08x\n", np->full_name, val); iounmap(base); release_mem_region(res.start, resource_size(&res)); return 0; } /* ---- regmap 调试:安全读取寄存器组 ---- */ struct regmap_debug_ctx { struct regmap *map; struct device *dev; uint32_t base_reg; int num_regs; }; static struct regmap_debug_ctx *regmap_debug_init(struct device *dev, void __iomem *base, uint32_t start_reg, int num_regs) { struct regmap_debug_ctx *ctx; struct regmap_config config = { .reg_bits = 32, .val_bits = 32, .reg_stride = 4, .max_register = start_reg + num_regs * 4, .fast_io = true, }; ctx = kzalloc(sizeof(*ctx), GFP_KERNEL); if (!ctx) { dev_err(dev, "无法分配 regmap_debug_ctx\n"); return ERR_PTR(-ENOMEM); } ctx->map = devm_regmap_init_mmio(dev, base, &config); if (IS_ERR(ctx->map)) { dev_err(dev, "regmap_init_mmio 失败: %ld\n", PTR_ERR(ctx->map)); kfree(ctx); return ERR_PTR(PTR_ERR(ctx->map)); } ctx->dev = dev; ctx->base_reg = start_reg; ctx->num_regs = num_regs; return ctx; } /** * 批量读取并打印寄存器值 * 输出格式与芯片手册对齐,便于逐位比对 */ static void regmap_bulk_dump(struct regmap_debug_ctx *ctx, const char *const *reg_names) { if (!ctx || !reg_names) return; unsigned int val; dev_info(ctx->dev, "=== 寄存器 dump (base=0x%08X) ===\n", ctx->base_reg); for (int i = 0; i < ctx->num_regs; i++) { int ret = regmap_read(ctx->map, ctx->base_reg + i * 4, &val); if (ret != 0) { dev_err(ctx->dev, "regmap_read 失败 @ 0x%08X: ret=%d\n", ctx->base_reg + i * 4, ret); continue; } dev_info(ctx->dev, " 0x%04X (%s) = 0x%08X\n", ctx->base_reg + i * 4, reg_names[i] ? reg_names[i] : "UNKNOWN", val); } } static void regmap_debug_cleanup(struct regmap_debug_ctx *ctx) { kfree(ctx); } /* ---- 内核模块入口 ---- */ static int __init dt_debug_init(void) { pr_info("Device Tree 调试模块加载\n"); /* 1. 扫描 compatible 节点 */ /* 注意:在模块中无 device 上下文时用 pr_* 系列 */ /* 2. 示例:查找特定 compatible 的节点 */ struct device_node *np = of_find_compatible_node(NULL, NULL, "rockchip,rk3399-i2c"); if (np) { pr_info("找到 i2c 节点: %s, status=%s\n", np->full_name, of_device_is_available(np) ? "okay" : "disabled"); /* 解析 reg 属性 */ check_reg_property(np, 0); /* 检查 clock */ struct clk *clk = of_clk_get(np, 0); if (!IS_ERR(clk)) { pr_info(" 时钟: %lu Hz\n", clk_get_rate(clk)); clk_put(clk); } else { pr_warn(" 时钟获取失败: %ld\n", PTR_ERR(clk)); } /* 检查 pinctrl */ struct device_node *pinctrl = of_parse_phandle(np, "pinctrl-0", 0); if (pinctrl) { pr_info(" pinctrl: %s\n", pinctrl->full_name); } else { pr_warn(" 未找到 pinctrl-0 配置\n"); } of_node_put(np); } else { pr_info("未找到 rockchip,rk3399-i2c 节点\n"); } return 0; } static void __exit dt_debug_exit(void) { pr_info("Device Tree 调试模块卸载\n"); } module_init(dt_debug_init); module_exit(dt_debug_exit);调试脚本:运行时校验设备树与驱动的绑定关系
#!/bin/bash # dt_audit.sh — 设备树审计脚本 # 用法: ./dt_audit.sh [device_name] DEVICE=${1:-"i2c"} echo "=== 设备树审计: $DEVICE ===" # 1. 查看 sysfs 中设备是否被驱动绑定 echo "--- sysfs 绑定状态 ---" find /sys/devices -name "*${DEVICE}*" -type d 2>/dev/null | while read d; do driver=$(readlink "$d/driver" 2>/dev/null | xargs basename) [ -n "$driver" ] && echo " $d → driver=$driver" || echo " $d → 未绑定驱动" done # 2. 反编译当前设备树并查找目标节点 echo "--- 运行时 Device Tree ---" if [ -d /proc/device-tree ]; then dtc -I fs -O dts /proc/device-tree 2>/dev/null | grep -A 20 -B 2 "$DEVICE" | head -40 else echo " /proc/device-tree 不可用" fi # 3. 检查 pinmux 状态 echo "--- Pinmux 状态 ---" if [ -d /sys/kernel/debug/pinctrl ]; then for p in /sys/kernel/debug/pinctrl/*/pinmux-pins; do [ -f "$p" ] && echo " $(dirname "$p" | xargs basename):" && grep -i "$DEVICE" "$p" 2>/dev/null done fi # 4. 检查 clock 信息 echo "--- 时钟摘要 ---" if [ -f /sys/kernel/debug/clk/clk_summary ]; then grep -i "$DEVICE" /sys/kernel/debug/clk/clk_summary fi四、边界分析:Device Tree 调试中最容易忽略的七种模式
模式一:Overlay 覆盖导致的字段丢失。基础 dtb 定义了status = "okay",一个 overlay 将status = "disabled"并添加了某个属性。运行时的最终结果以最后合并的 overlay 为准。只读源码里的基础.dts会误判。必须fdtdump运行时 dtb。
模式二:reg 属性中 address-cells 和 size-cells 不匹配。子节点的 reg 格式取决于父节点的#address-cells和#size-cells。如果父节点定义了#address-cells = <2>,子节点却只写了一对<addr size>,解析结果完全错乱。DTC 编译时不会报错,只在运行时表现为寄存器无法映射。
模式三:中断号在不同中断控制器下的转换。SPI 中断号在 GIC 中从 32 开始编号。设备树中写interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>对应 GIC 中断号 32+89=121。如果驱动里用platform_get_irq拿到的编号直接查 GIC 手册,中间差一个 32 的偏移,这个问题排查难度很大。
模式四:deferred probe 的静默等待。外设依赖的时钟控制器或 GPIO 控制器尚未加载时,devm_clk_get()返回-EPROBE_DEFER。这是正常的延迟 probe 机制,但如果没有用dev_err_probe()包装,日志中看不到任何线索。外设就是静默不加载。
模式五:reg-names与reg顺序不一致。reg-names = "data", "ctrl"对应reg中的第一个和第二个地址区域。如果顺序写反,"ctrl" 被映射到 data 区域——驱动在数据区域写控制寄存器,硬件完全不响应。
模式六:dma-ranges 的地址转换陷阱。当外设挂在有 IOMMU 或地址转换的 bus 上时,设备树的dma-ranges会改变设备视角的地址和 CPU 视角的地址之间的映射。DMA 地址计算错误导致数据写到错误的内存位置,现象非常随机。
模式七:status = "reserved" 但驱动仍尝试 probe。status = "reserved"在原意中表示该设备存在但被其他软件(如协处理器)管理,不应对 Linux 可见。但某些内核版本对 "reserved" 的处理行为不一致,可能仍然触发 probe 并导致资源冲突。在产品设备树中,不被 Linux 管理的外设应该设为status = "disabled"或完全删除节点。
五、总结
Device Tree 是嵌入式 Linux 内核的硬件"合同文本"。外设不工作时,合同先审一遍:compatible 是否匹配、reg 地址是否正确、clock 和 pinctrl 是否完整、运行时 dtb 是不是你以为的那个文件。
调试方法论上,按层级来:先fdtdump确认运行时 dtb → 查/sys/devices确认驱动绑定 → 查 pinmux 确认管脚功能 → 查 clock_summary 确认时钟使能 → 最后才是读驱动代码。驱动是按设备树描述的信息做事的——信息错,驱动只会按错误信息认真地失败。
