3. of核心API概览:of_find_node_by_path、of_get_property、of_parse_phandle等函数族

设备树解析的核心工作由一组以 of_ 前缀命名的内核API完成,它们定义在 drivers/of/ 目录下,头文件位于 include/linux/of.h。本章聚焦于最常用的三个函数族:节点查找、属性读取和phandle解析。理解这些API的调用链和返回值,是使用printk进行调试的前提。

调试切入点: 所有 of API 在失败时通常返回 NULL 或负错误码。在调用后立即添加 printk 打印返回值,是定位设备树匹配失败的第一道防线。

3.1 节点查找:of_find_node_by_path 与 of_find_node_by_name

节点查找是设备树解析的入口。驱动通常通过设备路径或节点名获取 struct device_node 指针,后续所有属性读取都依赖该指针。

函数原型 功能说明 典型返回值
of_find_node_by_path(const char *path) 通过绝对路径(如 "/soc/i2c@7e804000")或别名(如 "/aliases/i2c0")查找节点 成功返回 struct device_node *,失败返回 NULL
of_find_node_by_name(struct device_node *from, const char *name) 通过节点 name 属性(不含 unit-address)遍历查找 成功返回匹配节点,遍历完返回 NULL
of_find_node_by_type(struct device_node *from, const char *type) 通过 device_type 属性查找(旧式设备树兼容) 成功返回匹配节点,失败返回 NULL
of_find_compatible_node(struct device_node *from, const char *type, const char *compatible) 通过 compatible 字符串查找,最常用 成功返回匹配节点,失败返回 NULL
引用计数陷阱: of_find_node_by_path 等函数会递增节点的引用计数。使用完毕后必须调用 of_node_put() 释放,否则会造成内存泄漏。调试时可通过 /sys/kernel/debug/of_node 观察引用计数异常。
// 典型调试代码片段:查找 /soc/i2c@7e804000 节点
struct device_node *np;
np = of_find_node_by_path("/soc/i2c@7e804000");
if (!np) {
    printk(KERN_ERR "OF_DEBUG: Failed to find node /soc/i2c@7e804000\n");
    return -ENODEV;
}
printk(KERN_INFO "OF_DEBUG: Found node %pOF\n", np);  // %pOF 是内核专用打印格式
// ... 使用 np 读取属性 ...
of_node_put(np);  // 释放引用
节点查找API调用流程 设备路径字符串 of_find_node_by_path() device_node * 返回 NULL 调试建议:在调用后立即 printk 打印 np 指针值 使用 %pOF 格式说明符可自动打印节点全路径

3.2 属性读取:of_get_property 与 of_property_read_* 系列

获取节点指针后,下一步是读取属性值。of_get_property 是底层函数,返回原始字节流;而 of_property_read_* 系列封装了类型转换和错误检查,推荐在驱动中使用。

函数 读取类型 返回格式 典型错误
of_get_property(np, name, &len) 原始字节 const void *,长度通过 len 返回 属性不存在返回 NULL
of_property_read_u32(np, name, &val) 32位整数 成功返回0,失败返回负错误码 -EINVAL 属性不存在或类型不匹配
of_property_read_u32_array(np, name, out, sz) 32位整数数组 成功返回0,失败返回负错误码 -EOVERFLOW 数组长度不足
of_property_read_string(np, name, &str) 字符串 成功返回0,str 指向属性值 -ENODATA 属性为空
of_property_read_bool(np, name) 布尔值 属性存在返回 true,否则 false 无错误码
// 属性读取调试示例
struct device_node *np;
u32 clock_freq;
const char *compat_str;
int ret;

np = of_find_node_by_path("/soc/i2c@7e804000");
if (!np) {
    printk(KERN_ERR "OF_DEBUG: node not found\n");
    return -ENODEV;
}

// 读取 u32 属性
ret = of_property_read_u32(np, "clock-frequency", &clock_freq);
if (ret) {
    printk(KERN_WARNING "OF_DEBUG: clock-frequency read failed, ret=%d\n", ret);
} else {
    printk(KERN_INFO "OF_DEBUG: clock-frequency = %u Hz\n", clock_freq);
}

// 读取字符串属性
ret = of_property_read_string(np, "compatible", &compat_str);
if (!ret) {
    printk(KERN_INFO "OF_DEBUG: compatible = %s\n", compat_str);
}

// 使用底层 of_get_property 调试原始数据
int proplen;
const u32 *raw = of_get_property(np, "reg", &proplen);
if (raw) {
    printk(KERN_INFO "OF_DEBUG: reg property length = %d bytes\n", proplen);
    printk(KERN_INFO "OF_DEBUG: reg[0] = 0x%08x\n", be32_to_cpu(raw[0]));
}

of_node_put(np);
字节序注意: 设备树中所有多字节整数以大端(big-endian)存储。使用 of_property_read_u32 系列函数时,内核会自动转换为主机字节序;但使用 of_get_property 获取原始数据时,必须手动调用 be32_to_cpu() 转换。

3.3 phandle解析:of_parse_phandle 与 of_parse_phandle_with_args

设备树通过 phandle(指针句柄)实现节点间的引用,例如中断控制器、时钟源、GPIO控制器等。解析phandle是驱动编写中最容易出错、也最需要printk调试的环节。

函数 解析内容 典型使用场景
of_parse_phandle(np, name, index) 返回第 index 个phandle指向的节点指针 单个引用,如 interrupt-parent
of_parse_phandle_with_args(np, name, cells, index) 返回节点指针 + 参数数组(如 #clock-cells 定义的参数) 带参数的引用,如 clocks
of_parse_phandle_with_fixed_args(np, name, cells, index, out_args) 固定参数个数的phandle解析 GPIO、DMA等固定参数引用
// phandle 解析调试示例:解析 clocks 属性
struct device_node *np, *clk_np;
struct of_phandle_args clk_spec;
int ret, i;

np = of_find_node_by_path("/soc/spi@7e204000");
if (!np) return -ENODEV;

// 解析第0个clock引用,带1个参数(#clock-cells = 1)
ret = of_parse_phandle_with_args(np, "clocks", "#clock-cells", 0, &clk_spec);
if (ret) {
    printk(KERN_ERR "OF_DEBUG: Failed to parse clocks[0], ret=%d\n", ret);
    of_node_put(np);
    return ret;
}

printk(KERN_INFO "OF_DEBUG: clocks[0] phandle node = %pOF\n", clk_spec.np);
printk(KERN_INFO "OF_DEBUG: clocks[0] args count = %d\n", clk_spec.args_count);
for (i = 0; i < clk_spec.args_count; i++) {
    printk(KERN_INFO "OF_DEBUG: clocks[0] args[%d] = %u\n", i, clk_spec.args[i]);
}

// 使用完后必须释放 phandle 节点引用
of_node_put(clk_spec.np);
of_node_put(np);
phandle 解析关系图 消费者节点 spi@7e204000 clocks = <&clk_osc 0> interrupts = <0 15 4> phandle 时钟控制器 clk_osc: clock@3f701000 #clock-cells = <1> 中断控制器 intc: interrupt-controller@7e00b000 #interrupt-cells = <3> interrupt-parent of_parse_phandle 返回节点指针 of_parse_phandle _with_args 返回节点指针 + args数组 调试时打印 args 确认参数个数

3.4 常用辅助函数与调试技巧

除了上述核心API,以下辅助函数在调试中同样重要:

  • of_match_node(matches, np):检查节点是否匹配 of_device_id 表,常用于 probe 入口调试。
  • of_alias_get_id(np, name):从 /aliases 节点获取别名序号,如获取 i2c0 中的 0。
  • of_count_phandle_with_args(np, name, cells):统计phandle引用个数,用于循环解析前的边界检查。
  • of_node_full_name(np):返回节点的完整路径名,比直接打印 np->name 更准确。
printk 调试黄金法则:
1. 每次调用 of API 后立即打印返回值。
2. 使用 %pOF 打印节点全路径,使用 %pOFp 打印 phandle 值。
3. 在 of_parse_phandle_with_args 后打印 args_count 和 args 数组,确认参数个数和值正确。
4. 在 of_node_put 前打印节点引用计数(通过 kref_read(&np->kref)),排查引用泄漏。
// 综合调试示例:完整解析一个带 phandle 和属性的节点
static int debug_of_parse(struct device_node *np)
{
    struct of_phandle_args gpio_spec;
    u32 reg_base;
    int ret, i;

    // 1. 打印节点基本信息
    printk(KERN_INFO "OF_DEBUG: Parsing node %pOF\n", np);

    // 2. 读取 reg 属性
    ret = of_property_read_u32(np, "reg", ®_base);
    printk(KERN_INFO "OF_DEBUG: reg read ret=%d, value=0x%x\n", ret, reg_base);

    // 3. 解析 GPIO phandle
    ret = of_parse_phandle_with_fixed_args(np, "gpios", "#gpio-cells", 0, &gpio_spec);
    if (ret) {
        printk(KERN_ERR "OF_DEBUG: gpios parse failed, ret=%d\n", ret);
        return ret;
    }
    printk(KERN_INFO "OF_DEBUG: gpio controller = %pOF\n", gpio_spec.np);
    printk(KERN_INFO "OF_DEBUG: gpio args = ");
    for (i = 0; i < gpio_spec.args_count; i++)
        printk("%u ", gpio_spec.args[i]);
    printk("\n");

    of_node_put(gpio_spec.np);
    return 0;
}

3.5 API 调用链与错误传播

在实际驱动中,of API 通常形成调用链:of_find_node_by_path → of_property_read_u32 → of_parse_phandle。任何一个环节失败都会导致后续操作无法进行。下表总结了常见错误及其调试方向:

错误场景 典型返回值 调试方向
节点路径错误 NULL 检查设备树源文件中的路径拼写,使用 dtc -I dtb -O dts 反编译确认
属性名拼写错误 -EINVAL 或 NULL 对比设备树绑定文档,注意大小写和下划线
phandle 索引越界 -EINVAL 使用 of_count_phandle_with_args 确认总数
#*-cells 属性缺失 -EINVAL 检查被引用节点是否定义了 #clock-cells、#gpio-cells 等
引用计数泄漏 内核内存泄漏告警 确保每个 of_find_* 和 of_parse_phandle 都有对应的 of_node_put
本章小结: of_find_node_by_path、of_get_property 和 of_parse_phandle 构成了设备树解析的三大支柱。掌握它们的返回值含义和错误处理模式,是后续使用 printk 进行逐层调试的基础。下一章将深入 of_find_node_by_path 的内核源码,在关键路径插入 printk 观察节点查找的完整过程。