4、调试基础:devicetree调试,检查设备树节点与属性匹配

设备树(DeviceTree)调试,说白了就是检查硬件描述和驱动代码之间的“对暗号”过程。我刚开始接触Linux驱动时,最头疼的就是设备树写对了没?驱动能不能找到节点?属性值读出来对不对?这些问题几乎每个驱动开发者都会遇到。

今天我就把设备树调试的常用方法和工具,系统地讲一遍。嗯,这里要注意,很多问题其实不是代码逻辑错了,而是设备树和驱动没对上号。

4.1 设备树调试的核心思路

设备树调试,本质上就三件事:

  • 节点是否存在——驱动能不能找到这个设备节点
  • 属性是否正确——读到的属性值是不是你想要的
  • 匹配是否成功——compatible属性有没有和驱动对上

我个人习惯,拿到一个新板子,第一件事就是先看设备树。不看设备树就写驱动,十有八九要翻车。

核心原则:设备树是硬件和驱动之间的“契约”。契约不对,驱动跑得再欢也是白搭。

4.2 查看设备树的常用方法

4.2.1 从内核中导出设备树

系统启动后,内核会把设备树展开成扁平设备树(FDT)。我们可以从文件系统中把它导出来看看。

# 查看设备树源文件(如果内核配置了CONFIG_PROC_DEVICETREE)
ls /proc/device-tree/

# 导出完整的设备树二进制文件
dtc -I fs -O dts /proc/device-tree/ -o myboard.dts

# 或者从/sys/firmware/devicetree/base导出
dtc -I fs -O dts /sys/firmware/devicetree/base -o myboard.dts

我在项目中遇到过,有个同事死活找不到设备节点,结果一查,是内核配置里没开CONFIG_PROC_DEVICETREE。嗯,这个坑我踩过一次就记住了。

4.2.2 使用dtc工具反编译

如果你手头有dtb文件,可以直接反编译成dts来看。

# 反编译dtb为dts
dtc -I dtb -O dts -o myboard.dts myboard.dtb

# 只查看特定节点
dtc -I dtb -O dts myboard.dtb | grep -A 20 "i2c"

你想想看,有时候设备树编译时没报错,但实际生成的dtb和预期不一样。反编译一下,一目了然。

4.3 检查节点是否存在

驱动里最常用的API就是of_find_node_by_path()of_find_compatible_node()。但很多时候,节点路径写错了,或者节点根本没被编译进去。

4.3.1 在驱动中打印节点信息

#include <linux/of.h>

static int my_probe(struct platform_device *pdev)
{
    struct device_node *np;
    
    // 通过路径查找节点
    np = of_find_node_by_path("/soc/i2c@12340000");
    if (!np) {
        dev_err(&pdev->dev, "找不到i2c节点!\n");
        return -ENODEV;
    }
    
    // 打印节点完整路径
    pr_info("找到节点: %s\n", np->full_name);
    
    // 检查节点状态
    if (!of_device_is_available(np)) {
        dev_warn(&pdev->dev, "节点被禁用(status = disabled)\n");
    }
    
    of_node_put(np);
    return 0;
}

小技巧:在驱动里加dev_dbg()pr_debug()打印节点路径,配合dynamic_debug动态开启,不用重新编译内核。

4.3.2 通过sysfs查看节点

内核把设备树节点暴露在/sys/firmware/devicetree/base下。你可以直接看目录结构。

# 查看所有节点
find /sys/firmware/devicetree/base -type d | head -20

# 查看特定节点是否存在
ls /sys/firmware/devicetree/base/soc/i2c@12340000/

# 查看节点属性
cat /sys/firmware/devicetree/base/soc/i2c@12340000/compatible
cat /sys/firmware/devicetree/base/soc/i2c@12340000/reg

我曾经调试一个I2C控制器驱动,驱动里死活找不到节点。结果用find一查,发现节点路径是/soc/i2c@12340000,但驱动里写的是/soc/i2c@1234000(少了个0)。这种低级错误,查半天才找到。

4.4 检查属性匹配

属性匹配是设备树调试的重头戏。compatible属性、reg属性、中断属性,这些最容易出问题。

4.4.1 compatible属性匹配

驱动和设备树的匹配,核心就是compatible属性。驱动里写的是of_match_table,设备树里写的是compatible,两者必须完全一致。

// 驱动中的匹配表
static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-device-v1", },
    { .compatible = "vendor,my-device-v2", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

// 设备树中的节点
my_device: my-device@12340000 {
    compatible = "vendor,my-device-v1";
    reg = <0x12340000 0x1000>;
    interrupts = <0 42 4>;
};

注意:compatible字符串必须完全匹配,包括大小写、连字符、下划线。我曾经因为把"my-device"写成了"my_device"(下划线代替连字符),折腾了一下午。

4.4.2 使用of_match_node手动匹配

有时候你想在驱动里手动检查匹配情况,可以用of_match_node()

static int my_probe(struct platform_device *pdev)
{
    const struct of_device_id *match;
    struct device_node *np = pdev->dev.of_node;
    
    // 手动匹配
    match = of_match_node(my_of_match, np);
    if (!match) {
        dev_err(&pdev->dev, "没有匹配的compatible\n");
        return -ENODEV;
    }
    
    dev_info(&pdev->dev, "匹配成功: %s\n", match->compatible);
    return 0;
}

4.5 读取属性值的调试技巧

属性值读出来不对,是另一个常见问题。我总结了几种调试方法。

4.5.1 使用of_property_read_*系列函数

u32 val;
int ret;

// 读取32位整数
ret = of_property_read_u32(np, "clock-frequency", &val);
if (ret) {
    dev_err(dev, "读取clock-frequency失败: %d\n", ret);
} else {
    dev_info(dev, "clock-frequency = %d Hz\n", val);
}

// 读取字符串
const char *str;
ret = of_property_read_string(np, "status", &str);
if (!ret) {
    dev_info(dev, "status = %s\n", str);
}

// 读取数组
u32 reg[2];
ret = of_property_read_u32_array(np, "reg", reg, 2);
if (!ret) {
    dev_info(dev, "reg = [0x%08x, 0x%08x]\n", reg[0], reg[1]);
}

4.5.2 使用devicetree-unittest

内核自带了一个设备树单元测试框架,叫devicetree-unittest。虽然不常用,但关键时刻能救命。

# 配置内核时开启
CONFIG_OF_UNITTEST=y
CONFIG_OF_OVERLAY_TEST=y

# 启动后查看测试结果
dmesg | grep "OF: unittest"

我个人习惯,在开发板上先跑一遍unittest,确认设备树解析没问题,再开始写驱动。这样可以排除设备树本身的问题。

4.6 设备树调试的避坑指南

做设备树调试这么多年,我踩过的坑不少。下面这几个,你一定要注意。

避坑1:设备树编译时不会检查属性值的合法性。比如reg属性写了个负数,编译器不会报错,但驱动读出来就是错的。

避坑2:中断号在设备树里是<中断控制器 中断号 触发类型>,但不同中断控制器的编码方式不同。GIC和GPIO的中断号编码完全不一样。

避坑3:status属性默认是"okay",但如果你写成了"ok"或者"enable",内核会认为节点被禁用了。我曾经因为这个,一个串口驱动死活不工作。

4.7 实战:调试一个I2C设备节点

假设你有一个I2C温度传感器,设备树节点如下:

&i2c1 {
    status = "okay";
    
    temperature_sensor: tmp102@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
    };
};

调试步骤:

  1. 检查节点是否存在ls /sys/firmware/devicetree/base/soc/i2c@.../tmp102@48/
  2. 检查compatiblecat /sys/firmware/devicetree/base/.../tmp102@48/compatible
  3. 检查regcat /sys/firmware/devicetree/base/.../tmp102@48/reg | xxd
  4. 检查中断cat /sys/firmware/devicetree/base/.../tmp102@48/interrupts | xxd
  5. 在驱动中打印:用of_property_read_u32读取reg,和xxd的结果对比

嗯,这套流程走下来,90%的设备树问题都能定位到。

4.8 总结

设备树调试,说白了就是“查、对、读”三个字。查节点是否存在,对compatible是否匹配,读属性值是否正确。掌握了这些方法,设备树问题就不再是玄学。

我个人建议,每个驱动开发者都应该养成一个习惯:写驱动前先看设备树,写驱动时加调试打印,驱动跑不动先查设备树。这样能省下大量调试时间。

下一章,我们会讲更高级的调试技巧——使用ftrace跟踪设备树解析过程。到时候你会发现,设备树解析的每一步,都能看得清清楚楚。