找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3450

积分

0

好友

454

主题
发表于 3 小时前 | 查看: 3| 回复: 0

有些东西吧,第一次接触的时候,你会觉得它设计得特别优雅。比如 Linux 设备树,再比如 Kubernetes,还有前几年很火的微服务。
后来你会发现,优雅这俩字,往往和“折磨”是绑定销售的。设备树看似简单,但调试起来就像在迷宫里找出口:节点配置错一点系统就跪,属性值写错驱动就挂,日志满天飞却找不到关键线索……

今天这篇文章,我会把我所知道的 8个调试技巧 全部分享出来。作为一个在 云栈社区 摸爬滚打多年的老兵,可以说这些都是在实际项目中踩坑踩出来的经验。

一、为什么设备树调试让人头大?

先讲一个真实场景。你在一个 i.MX6 的板子上写 LCD 驱动,设备树里加了一堆节点:

&lcdif {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_lcd>;
    display = <&display0>;
    status = "okay";
};

编译,烧录,启动。内核 log 里啥也没有,连个错误都没有,就像你根本没写过这段代码。纳尼~ 你开始怀疑人生了?是不是编译没生效?是不是烧错了镜像?是不是内核配置有问题?折腾了半天,最后发现 —— 节点里的 compatible 属性拼错了。驱动在找 "fsl,imx6q-lcdif",你写成了 "fsl,imx6q-lcd"。少了一个 if。就这么一个字母,烧了你两个小时。

设备树调试最大的痛点就是它不报错。 或者说,报错的方式特别“优雅” —— 它只是默默地不匹配,然后把你的驱动当作不存在。所以我们需要一套 主动的、系统性的调试方法。下面这些技巧,就是用来解决这个问题的。


二、基础:设备树长什么样?

在讲调试技巧之前,花 30 秒回顾一下设备树的基本结构。懂的老铁可以跳过这节。

嵌入式Linux设备树系统架构示意图

设备树是一个 描述硬件 的树形结构。根节点下面挂着 CPU、内存、外设等。一个典型的节点长这样:

uart1: serial@02020000 {
    compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
    reg = <0x02020000 0x1000>;
    interrupts = <0 26 4>;
    clocks = <&clks IMX6QDL_CLK_UART_IPG>;
    status = "disabled";
};
  • compatible:驱动匹配用的“身份证”
  • reg:寄存器地址和大小
  • interrupts:中断号
  • status:是否启用

注意那个 & 符号 —— 它引用的是另一个节点。这在 overlay 调试里特别重要。


三、技巧一:反编译 dtb

3.1 这个动作应该被做成肌肉记忆

很多人的误区是:只敢看设备树源码。但实际跑在系统里的设备树,是 .dtb 二进制文件。这个文件可能跟你源码里的 .dts 不一致。有几种情况:

  1. 你忘了重新编译 —— 烧进去的还是旧的。
  2. 编译过程有警告被忽略了,dtc 帮你“修正”了一些写法。
  3. 其他 overlay 动态加载了片段。

所以第一件事:从正在运行的系统里,把 dtb 拽出来反编译。 命令非常非常简单:

# 方法一:如果 /sys/firmware/fdt 存在
dtc -I dtb -O dts /sys/firmware/fdt > current.dts

# 方法二:没有上面那个文件?找找 /proc/device-tree
# 先看看系统用的 dtb 在哪儿
ls -l /proc/device-tree

/sys/firmware/fdt 这个文件,是被内核直接暴露出来的 原始 dtb。只要你的内核编译时打开了 CONFIG_PROC_DEVICETREE(大多数发行版都开了),它就一直在那儿等你。

你可能会问:/proc/device-tree 不是也能看吗?为什么要反编译?因为 /proc/device-tree被内核解析后的视图。它已经丢失了一些原始信息,比如 phandle 的数值、某些属性的原始顺序等。反编译出来的 .dts 才是最原汁原味的。

3.2 反编译后怎么查?

反编译出来的文件很长。但你只需要看重点。

第一招:grep 你的节点名

dtc -I dtb -O dts /sys/firmware/fdt | grep -A 20 "uart1"

第二招:看属性值是否被篡改

有时候 dtc 编译器 会自动帮你“优化”。比如:

// 你写的
interrupts = <1 2 3>;

// 编译后可能变成
interrupts = <0x01 0x02 0x03>;

数值上没区别,但如果你用字符串比较工具,就会出问题。

第三招:检查 phandle 引用

看这段:

clocks = <&clks 42>;

反编译后,&clks 会被转成一个数字,比如 &clksphandle = <0x88>。如果你看到某个引用变成了 <0xffffffff>,那就是 phandle 解析失败了 —— 这是非常隐蔽的错误。

一个真实案例

之前调试一个 I2C 触摸屏,驱动死活读不到触摸数据。反编译后发现了这个:

touch@38 {
    compatible = "goodix,gt911";
    reg = <0x38>;
    interrupt-parent = <0xffffffff>;  // 注意这里!
    interrupts = <13 2>;
};

interrupt-parent 变成了 0xffffffff —— 无效的 phandle。原因是设备树里引用的中断控制器节点被 status = "disabled" 了,但引用时没有做条件判断。编译阶段没报错,链接阶段也没报错。它就这样静默地失败了。

所以我的建议是,每次遇到设备树相关的问题,先反编译现场 dtb 看一眼。这个动作应该被做成肌肉记忆。


四、技巧二:procfs

4.1 /proc/device-tree 里有什么?

如果你不想反编译,直接进 /proc/device-tree 也能看。这是内核帮你“展开”的设备树视图。表现形式是 文件系统

cd /proc/device-tree
ls -l

你会看到一堆像目录一样的节点。每个节点对应一个目录。每个属性对应一个文件。举个例子,查看根节点的 compatible 属性:

cat /proc/device-tree/compatible

输出可能是:

fsl,imx6q-sabresd
fsl,imx6q

这实际上是一个字符串数组,用 \0 分隔。cat 出来时会把第一个 \0 当作结束符,所以你只能看到第一段。想看全的用 od

od -c /proc/device-tree/compatible

4.2 怎么利用这个“上帝视角”?

场景一:确认你的节点是否真的被加载了

ls /proc/device-tree/   # 看看根下有哪些节点
ls /proc/device-tree/soc@0/   # 看看某个子节点

如果你的节点路径是 /soc@0/uart@02020000,但你在 /proc/device-tree 里找不到 uart@02020000 目录 —— 说明节点没有被内核解析。可能的原因:

  • status = "disabled"(最常见)
  • 编译时被 dtc 剔除了(有条件编译指令 #ifdef 之类的)
  • 父节点被 disabled 了

场景二:检查属性值类型

属性有三种类型:字符串、数字数组、字节数组。怎么判断?看文件是普通文件还是目录。

  • 普通文件 = 属性
  • 目录 = 子节点

一个非常有用的命令:

find /proc/device-tree -name "compatible" -exec cat {} \; -print

这会递归所有 compatible 属性,并打印出路径。可以快速看到哪些驱动被匹配上了。

场景三:检查 phandle 是否有效

每个有 phandle 属性的节点,在 /proc/device-tree/ 下会有一个同名文件。例如:

ls /proc/device-tree/__symbols__/

这个 __symbols__ 目录(如果存在)记录了所有的标签(label)到 phandle 的映射。你可以对比一下你的引用是否在这个表里。

吐槽时间:我知道有老铁会说“我直接用 dtc 反编译不好吗?”好,当然好。但是 /proc/device-tree 有一个反编译做不到的优势 —— 它是活的。如果你动态加载了设备树 overlay,/proc/device-tree 会实时更新。而 /sys/firmware/fdt 不会。所以这两个方法,一个静态(dtb),一个动态(procfs),建议配合使用


五、技巧三:debugfs

5.1 打开 debugfs 开关

说实话,这个技巧知道的人不多。真的有点东西。debugfs 里有一个非常宝藏的接口:/sys/kernel/debug/devicetree/。但很多内核编译时默认 不打开 debugfs。你得先检查一下:

mount | grep debugfs

如果没有挂载,手动挂:

mount -t debugfs none /sys/kernel/debug

或者确认内核配置:

CONFIG_DEBUG_FS=y

5.2 有了 debugfs,你可以做这些事

先看一眼:

ls /sys/kernel/debug/devicetree/

我敢打赌你会看到一个叫 unflattendtb 的文件。不同内核版本略有差异。最常用的是 unflatten 这个接口。你能想象吗 —— 你可以把一段设备树文本字符串直接写进去,内核会 实时解析并合并到现有的设备树中。不需要重新编译 dtb,不需要重启!这个在调试期间简直神了。

用法:

# 创建一个测试片段
echo " / {
    test-node {
        compatible = \"test,device\";
        status = \"okay\";
    };
};" > /sys/kernel/debug/devicetree/unflatten

然后去 /proc/device-tree/ 下看,test-node 已经在了!但是注意:这个操作不会写入持久化存储,重启就没了。而且有可能把系统搞挂,不建议在产品设备上用。但在开发板上,这是调试神器。

另一个宝藏文件:/sys/kernel/debug/devicetree/dt_regions。这个文件显示了所有动态加载的 overlay 的内存区域(reserved-memory 相关)。如果你发现某个 overlay 加载失败,去这个文件看一眼,能发现是不是内存冲突了。

真实案例:之前有一个项目,加载第二个 overlay 时内核报错:“Failed to allocate memory for overlay”。所有人都以为是内存不够。看 dt_regions 才发现 —— 第一个 overlay 申请的内存区域跟第二个 overlay 的基址重叠了。但是设备树里明明写的 reg = <0x40000000 0x1000>,为什么会重叠?因为第一个 overlay 在释放时没有正确清理 reserved-memory 节点。这是内核的一个已知 bug(某些 4.x 版本)。解决方案是手动在 overlay 的 remove 函数里释放。这个 bug 是 debugfs 帮我发现的。你说神不神?


六、技巧四:设备树 overlay 调试

overlay(以下简称 OVL,我也不知道这么说对不对)是现代嵌入式 Linux 的一个热门话题。它允许你在 运行时 动态增加、修改设备树节点。听起来很美好对不对?但调试 overlay 的难度,比静态设备树 高出一个数量级

6.1 先确认 overlay 是否真的被加载了

overlay 加载通常用 configfs 接口:

# 挂载 configfs
mount -t configfs configfs /config

# 创建 overlay
mkdir /config/device-tree/overlays/panel

# 把 dtbo 文件写进去
cat panel.dtbo > /config/device-tree/overlays/panel/dtbo

加载成功后,你会在 /proc/device-tree/ 下看到新节点。如果没有出现,说明加载失败。

查看加载状态

cat /config/device-tree/overlays/panel/status

可能的输出:loadedunloadederror。如果是 error,看内核日志:

dmesg | tail -20

最坑的错误:overlay 里用到的符号(phandle)在基础设备树里不存在。举个例子,你在 overlay 里写:

&i2c1 {
    touch@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;
    };
};

基础设备树里必须有 i2c1 这个标签,并且它已经被 status = "okay" 了。如果基础设备树里没有 i2c1,或者它被 disabled 了,overlay 加载时会报错:

OF: overlay: failed to resolve symbol i2c1

这个错误很容易被忽略,因为 dmesg 里可能只有一行,而且不会导致内核崩溃。

6.2 overlay 碎片化调试

overlay 加载后,它的节点跟基础设备树是 物理上分在两个内存区域 的。遍历设备树时,of_find_node_by_path 这类函数可以跨区域找到节点。但有些 API(比如老旧版本的 of_get_child_by_name)可能只查基础区域。这就导致了“明明节点存在,但驱动 find 不到”的玄学问题。

调试方法:在驱动里加打印,遍历所有子节点:

struct device_node *np = NULL;
for_each_child_of_node(root, np) {
    pr_info("node: %s, full_name: %s\n", np->name, np->full_name);
}

如果发现 overlay 节点的 full_name 带了一个奇怪的后缀(如 @0 变成了 @0/something),说明是重叠区域的路径解析出了问题。

解决方案:使用 of_find_node_by_phandle 替代路径查找,或者在内核配置中打开 CONFIG_OF_OVERLAYCONFIG_OF_RESOLVE(很多 BSP 忘了开后者)。

6.3 一个让老手都翻车的例子

有一个 overlay 用来动态加载一个 FPGA 位流。FPGA 的寄存器地址是 0x100000000x10000fff。overlay 里这么写的:

&amba {
    fpga@10000000 {
        compatible = "foo,my-fpga";
        reg = <0x10000000 0x1000>;
        interrupts = <0 42 4>;
    };
};

加载后,内核报了一个匪夷所思的错误:

OF: overlay: apply error -22

-22-EINVAL。完全没头绪。后来用 debugfs 的 unflatten 功能,一段一段注释掉,发现是 interrupts 属性里的中断号超出了范围。这个 SoC 的中断控制器只支持 0~31 号 SPI 中断,42 是不合法的。但是编译 dtbo 时不会报错,因为 dtc 不知道你的中断控制器硬件限制。

总之,overlay 调试时,尽量简化属性,先加载一个空节点,再加属性,逐步逼近问题点。


七、技巧五:dtc 编译器

7.1 打开所有警告

很多人编译设备树用的是:

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

这就相当于用 gcc 编译 C 代码不打开任何警告。太浪费了。正确的打开方式:

dtc -I dts -O dtb -o foo.dtb foo.dts -W all

如果还要检查过时语法:

dtc -I dts -O dtb -o foo.dtb foo.dts -W all -W obsolete

常见的警告及其含义

  • Warning (reg_format)reg 属性格式不对,比如长度不是 addr-cells 和 size-cells 的倍数。
  • Warning (avoid_default_addr_size): 缺少 #address-cells#size-cells,dtc 会使用默认值 2,但可能不是你想要的。
  • Warning (interrupt_provider): 中断控制器节点缺少 #interrupt-cells
  • Warning (gpios_property)gpios 属性应该用 -gpios 后缀(如 reset-gpios 而不是 reset)。

这些警告 必须全部消除。不要觉得“只是警告,没事的”。设备树里的警告几乎都意味着运行时行为异常。

7.2 利用 dtc 的“语法检查”找错别字

你有一个节点:

touch@38 {
    coupled-gpios = <&gpio1 13 GPIO_ACTIVE_LOW>;
};

但是内核驱动里读的是 coupled-gpio(少了 s)。这种错别字用普通文本编辑器很难发现。但你可以这样做:

dtc -I dts -O dts foo.dts -s > expanded.dts

-s 参数会把所有宏、包含文件展开成一个单一 dts 文件。然后用 grep 结合你的驱动源码里的属性名做交叉检查。更高级的玩法:写一个脚本,提取驱动中用到的所有 of_property_read_* 的属性名,跟 dts 中的属性名做 diff。

我一般用这个命令:

grep -r "of_property_read" drivers/your_driver/ | sed 's/.*"\([^"]*\)".*/\1/' | sort -u > props_in_driver.txt
grep "=" expanded.dts | grep -o "[a-z-]* =" | sed 's/ *=//' | sort -u > props_in_dts.txt
diff props_in_driver.txt props_in_dts.txt

虽然不能百分百准确(因为驱动里可能读的是字符串解析出来的名字),但起码能帮你发现 80% 的拼写错误。

7.3 反编译时的“反优化”

有时候你用 dtc -I dtb -O dts 反编译出来的文件,跟你原始 dts 长得不一样。这是 dtc 在做“优化”。比如它会合并连续的 <...> 数组,或者把字符串连接起来。为了得到更接近原始写法的结果,可以加 -s(sort)和 -H epapr(使用标准格式):

dtc -I dtb -O dts -s -H epapr /sys/firmware/fdt > current.dts

但是,不管你用什么参数,丢失的注释永远回不来了。所以强烈建议把注释写在属性名或节点名旁边,别单独一行。

不好的写法:

/* 这个是触摸屏的中断引脚 */
int-gpios = <&gpio1 12 0>;

好的写法:

int-gpios = <&gpio1 12 0>; /* 触摸屏中断引脚,GPIO1_12,低电平有效 */

这样即使反编译后注释没了,属性本身仍然有意义。


八、技巧六:内核里的 of API

8.1 驱动探测时打印设备树节点信息

写驱动时,很多人只在 probe 函数里放一个 printk

static int my_probe(struct platform_device *pdev)
{
    printk("probe called\n");
    // ...
}

这能告诉你 probe 被调用了,但 为什么 probe 被调用?因为 compatible 匹配上了?还是因为设备树里匹配到了?更有用的做法是:打印出这个设备的完整节点路径和 compatible。

static int my_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;

    pr_info("Probing device: %pOF\n", np);  // %pOF 是内核专门为设备节点设计的格式符
    pr_info("compatible: %s\n", of_get_property(np, "compatible", NULL));

    // 也可以遍历所有属性
    struct property *prop;
    for_each_property_of_node(np, prop) {
        pr_info("  property: %s\n", prop->name);
    }
    // ...
}

%pOF 这个格式符是真的绝了。它会把节点的 full_name 打印出来,而且如果这个节点来自 overlay,它还会标注 (fragment)

8.2 检查某个属性是否存在

有时候属性是存在的,但值不对。你可以用 of_property_read_u32 这类函数,如果返回错误,打出来:

u32 val;
int ret = of_property_read_u32(np, "my-val", &val);
if (ret) {
    pr_err("Failed to read my-val: %d\n", ret);
    // 进一步打印这个属性在设备树里的原始值(如果有)
    const __be32 *prop = of_get_property(np, "my-val", NULL);
    if (prop) {
        pr_err("  Raw prop value: 0x%08x\n", be32_to_cpup(prop));
    }
}

of_get_property 返回的是原始字节序的指针,记得用 be32_to_cpup 转成 CPU 字节序。

8.3 跟踪 of 核心代码的调试信息

如果你打开了内核的 CONFIG_OF_DYNAMICCONFIG_DEBUG_DRIVER,可以在挂载 debugfs 后看到 of 核心的详细日志:

echo 1 > /sys/kernel/debug/dynamic_debug/control

但这会打印海量信息。更好的方法是只打开 of 相关文件的调试:

echo "file drivers/of/* +p" > /sys/kernel/debug/dynamic_debug/control

然后重新加载你的驱动模块(或者触发 probe)。你会看到 of_match_deviceof_find_node_by_phandle 等函数的内部调用过程。

有一次我用这个方法发现,驱动里的 of_match_table 指向了一个已经被编译进内核但被 __initdata 标记的表。probe 调用时这个表已经被释放了,内容全是垃圾。内核不崩溃已经算仁慈了,它只是匹配不上。你说这谁能想到?


九、技巧七:devicetree schema

9.1 schema 是什么?

近年来,Linux 内核引入了设备树 schema 验证机制(基于 JSON Schema)。你可以理解成 “设备树的编译器 + 静态检查器”。它不仅能检查语法错误,还能检查 语义错误。比如:

  • 某个节点的 compatible 是否在官方列表中?
  • reg 的长度是否匹配 #address-cells#size-cells
  • 必需的属性是否缺失?

9.2 怎么用?

首先安装工具:

# Ubuntu/Debian
apt-get install device-tree-compiler python3-dt-schema

然后对 dts 文件运行 schema 验证:

make dt_binding_check   # 在内核源码目录下运行

或者单独验证一个 dts:

dt-validate -s dtschema/schema.json your.dts

输出示例(故意写错):

your.dts:34.21-37.5: ERROR: 'interrupts' is a required property for node '/soc/uart@20000000'

这种错误,如果没有 schema,只能在运行到驱动 probe 时才发现 —— 而且大概率只在某个特定的调用路径上触发(比如当你真正去读中断号时)。有了 schema,编译阶段就能发现

9.3 schema 的一个“槽点”

“我也不知道这么说对不对”——schema 验证非常 。一个中等规模的 dts 文件,验证一次可能需要十几秒。而且它依赖的 dt-schema 包更新频繁,内核版本和 schema 版本不匹配时会报一堆奇怪的错误。

解决方案:在 CI/CD 里跑 schema,本地开发时只跑 dtc 警告。或者给 make 加一个环境变量跳过 schema:

make SKIP_DT_SCHEMA=1 dtbs

但千万不要完全放弃 schema。它曾经帮我发现过一个极隐蔽的 bug —— 我把 #interrupt-cells 错误地写在了父节点而不是中断控制器节点上。dtc 没报错,因为语法正确。但所有引用这个中断控制器的子节点,内核都解析不到中断号。这个 bug 藏了两个星期,最后是 schema 揪出来的。


十、技巧八:常见 bug 速查手册

这节我整理了自己踩过的 7 个经典坑。每个坑都配上了“症状”和“解药”。

坑 1:节点明明存在,驱动 probe 不调用

症状/proc/device-tree/ 下能看到节点,但驱动的 probe 函数从未执行。

原因compatible 字符串写错了(大小写、逗号、空格)。或者驱动中的 of_match_table 没有被正确引用。

解药

// 在驱动里加上这段,确认 match table 被用了
static const struct of_device_id my_of_match[] = {
    { .compatible = "foo,bar" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_match);

// 并且在 probe 函数里打印 match 的 compatible
static int my_probe(struct platform_device *pdev)
{
    const struct of_device_id *match;
    match = of_match_device(my_of_match, &pdev->dev);
    if (match)
        pr_info("Matched compatible: %s\n", match->compatible);
}

坑 2:reg 属性读出来全是 0

症状of_iomapplatform_get_resource 返回的地址是 0。

原因:父节点的 #address-cells#size-cells 错了。比如 64 位地址的 SOC,父节点应该是 #address-cells = <2>,你写成了 1。

解药:检查设备树里的父节点。并且用 of_address_to_resource 替代 platform_get_resource,它会更严格地检查 cells。

struct resource res;
if (of_address_to_resource(np, 0, &res))
    pr_err("Failed to get resource\n");
else
    pr_info("Resource: %pap\n", &res.start);

坑 3:中断号不生效

症状request_irq 返回 -EINVAL,或者中断处理函数永远不触发。

原因:中断号被映射错了。或者 interrupt-parent 指向了一个无效的节点。

解药:打印出解析后的中断号:

int irq = of_irq_get(np, 0);
pr_info("IRQ number: %d\n", irq);

如果 irq 是负数,说明根本没解析到。用 irq_of_parse_and_map 看能不能映射到虚拟中断号。另一个常见原因:interrupts 里的 cell 个数跟中断控制器的 #interrupt-cells 不匹配。比如中断控制器是 3 个 cell(<domain irq flags>),你只写了两个。

坑 4:gpiod_get 返回 -ENOENT

症状:用 GPIO 子系统获取 gpio 描述符失败。

原因:设备树里 gpio 属性的后缀写错了。内核期望的是 foo-gpios(复数)或 foo-gpio(单数,已废弃)。

解药:检查 gpio 属性的名字。标准的写法是 reset-gpios = <&gpio1 2 GPIO_ACTIVE_LOW>;。然后驱动里用 gpiod_get(dev, "reset", ...),注意这里是去掉 -gpios 后的名字。如果你不想看文档,直接打印设备树里的所有属性名,看看实际的名字是什么。

坑 5:pinctrl 配置不生效

症状:引脚电平不对,或者功能错误。

原因:pinctrl 节点里的 functionpins 写错了。或者 pinctrl 驱动没有加载。

解药

// 在驱动 probe 里强制应用 pinctrl
struct pinctrl *p = devm_pinctrl_get(dev);
if (IS_ERR(p))
    pr_err("Failed to get pinctrl\n");
else
    devm_pinctrl_put(p);

更直接的方法:进 debugfs 看 pinctrl 状态:

cat /sys/kernel/debug/pinctrl/<pinctrl-device>/pinmux-pins

看看你的引脚当前被 mux 成了什么功能。不是预期的话,就是设备树或 pinctrl 驱动的问题。

坑 6:status 属性被忽略

症状:节点明明写的是 status = "okay",但内核却当成 disabled

原因:你写在了错误的位置。status 属性可以出现在 任何 节点上,但如果父节点被 disabled,子节点即使 okay 也不会被初始化。

解药:从根节点向下,检查每一级的 status

find /proc/device-tree -name "status" -exec cat {} \; -print

如果某一级输出是 disabled,那么这一级以下的所有节点都不可用。

坑 7:内存地址重叠导致的诡异崩溃

症状:系统跑几个小时突然崩溃,或者在加载某个驱动时 SIGSEGV。

原因:两个设备树节点映射到了同一段物理地址。一个驱动写数据,另一个驱动读到脏数据。

解药:检查所有的 reg 地址区域是否重叠。可以用这个脚本(粗糙但有用):

dtc -I dtb -O dts /sys/firmware/fdt | grep -E "reg = <" | sed 's/.*<\(.*\)>.*/\1/' | awk '{print "0x"$1, "0x"$2}' | while read start size; do echo $start $size; done | sort -n

然后手动看有没有重叠区间。


十一、实战案例:从报错到修复全过程

光说理论有点干。咱们来一个 真人真事

背景

一块 RK3568 的板子,外接了一个 MIPI CSI 摄像头。驱动是供应商给的。设备树片段:

&i2c2 {
    camera@36 {
        compatible = "ovti,ov13850";
        reg = <0x36>;
        clocks = <&cru CLK_CAM0>;
        clock-names = "xvclk";
        reset-gpios = <&gpio1 21 GPIO_ACTIVE_LOW>;
        pwdn-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
        port {
            csi_out: endpoint {
                remote-endpoint = <&csi_in>;
                data-lanes = <1 2>;
            };
        };
    };
};

&csi_dphy {
    status = "okay";
    ports {
        port@1 {
            reg = <1>;
            csi_in: endpoint {
                remote-endpoint = <&csi_out>;
                data-lanes = <1 2>;
            };
        };
    };
};

症状

启动后 dmesg 看到:

ov13850 2-0036: failed to get clock
ov13850: probe of 2-0036 failed with error -2

-2-ENOENT,找不到时钟。

调试过程

第一步:反编译现场 dtb

dtc -I dtb -O dts /sys/firmware/fdt > current.dts
grep -A 10 "camera@36" current.dts

发现 clocks 属性变成了:

clocks = <0x22 0x1a>;

0x22 是 cru 节点的 phandle,0x1a 是时钟 ID。看起来没问题。

第二步:检查时钟驱动是否加载

ls /proc/device-tree/clocks/

只有 clock@0 ... 但 cru 节点应该在 /cru 路径下。果然,/proc/device-tree/cru 不存在!这意味着 cru 节点被内核跳过了。为什么会跳过?看 current.dts 里 cru 的 statusdisabled。但是源码里写的是 status = "okay" 啊?被谁改了?

第三步:检查 include 路径

发现上游的 rk3568.dtsi 里 cru 节点是 disabled,然后板级的 dts 里用 &cru 重新 status = "okay"。但板级 dts 里 同时 存在另一行:

&cru {
    status = "disabled";  // 这行是谁加的?
};

原来是一个同事在合并代码时误操作,加了一行 disabled,覆盖了 okay

第四步:修复后,时钟有了,但又出现了新错误

ov13850 2-0036: failed to get reset gpio

第五步:用 gpio 测试

进入 gpio 子系统查看:

cat /sys/kernel/debug/gpio

发现 gpio1-21 被另一个驱动占用了。那个驱动是 SPI 的 CS 脚,但在设备树里没有显式声明。原来是因为 pinctrl 配置把同一个引脚分配给了两个功能。

第六步:修改 pinctrl

在设备树里禁用 SPI 的 CS 引脚 pinctrl,或者重新分配摄像头的 reset 到另一个 GPIO(刚好有一个备用)。

最终:摄像头正常工作。

这个案例花了 4 个小时。如果一开始就反编译 + 检查 /proc/device-tree + debugfs gpio,可能 40 分钟就搞定了。


十二、一些“不传之秘”

12.1 利用 ftrace 跟踪 of 函数调用

# 开启 ftrace
echo function > /sys/kernel/debug/tracing/current_tracer
echo of_* > /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 重新加载驱动或操作设备树
cat /sys/kernel/debug/tracing/trace

你会看到 of_find_node_by_nameof_match_device 等函数的调用栈。有一次我用它发现,of_find_node_by_path 被调用了 200 多次,每次都在遍历整个设备树 —— 性能问题就是这么暴露的。

12.2 设备树的内存布局分析

编译 dtb 时加上 -S 参数,可以输出一个“符号表”:

dtc -S -O dtb -o foo.dtb foo.dts > foo.sym

这个符号表里记录了每个节点在 dtb 中的偏移量。配合 hexdump,你可以直接看二进制布局。

12.3 使用 device-tree-compiler 的最新版本

有些发行版自带的 dtc 版本很老(比如 1.4.7)。老版本的 dtc 对 overlay 的支持有 bug。从内核源码里编译最新的 dtc:

make scripts/dtc/  # 在内核源码目录

然后把 scripts/dtc/dtc 加到 PATH 里。新版本的 dtc 对警告信息的输出更友好,而且支持 -@(生成 symbols,overlay 必需)。


十三、总结

写了这么多,键盘都冒烟了。设备树调试,说到底就是 一件事 —— 确认“写进去”的和“跑起来”的是否一致。因为设备树不像 C 代码,编译错误会告诉你哪行错了。它更像一个“沉默的配置”。只有你主动去检查,它才会告诉你真相。

这 8 个技巧,分享出来供大家参考:

  1. 反编译 dtb —— 看现场真相
  2. /proc/device-tree —— 动态上帝视角
  3. debugfs —— 内核后门,动态加载测试片段
  4. overlay 专用调试 —— 注意符号解析和碎片化
  5. dtc 警告全开 —— 第一道防线
  6. of API 打印 —— 驱动里埋“传感器”
  7. devicetree schema —— 语义检查
  8. 常见 bug 清单 —— 踩坑索引

很多人在设备树上每次都要捣鼓半天,不是因为他们水平不够。而是因为 没有建立起“验证”的意识。他们总觉得自己写的 dts 一定是对的。编译通过了,就烧进去跑。出了问题就改驱动。

下次再遇到设备树相关的怪问题,请先反问自己三句话:

  • 反编译现场 dtb 看了吗?
  • 确认过节点真的被内核解析了吗?
  • 在 probe 里打印出设备树属性了吗?

这三件事做完了,90% 的问题都已经水落石出。剩下的 10%,可能需要你打开 debugfs 或者 ftrace。但相信我,那是值得的。在 云栈社区计算机基础 板块里,我们经常讨论这些底层调试的心得,欢迎来一起交流那些年让人头大的驱动适配经历。




上一篇:2025 年 Node.js 自带了哪些功能替代旧依赖?15 项特性盘点
下一篇:GRPO训练数据真能照搬SFT吗?面试官追问奖励信号怎么造
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-13 23:15 , Processed in 0.870314 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表