4. 内核OF API入门:of_find_node_by_path、of_get_property等核心API的使用方法

设备树(Device Tree)是嵌入式Linux开发中绕不开的话题。很多刚接触内核驱动开发的朋友,一看到of_开头的函数就头大。其实没那么复杂。

我个人习惯把OF API看作是一套“查字典”的工具。设备树就是那本字典,而OF API就是帮你翻页、查词、看解释的方法。今天我们就从最常用的几个API入手,把这件事说透。

核心观点:OF API的本质,就是让驱动代码能够读取设备树中描述的硬件信息。你不需要背下所有API,但必须理解查找路径、读取属性、解析资源这三板斧。

4.1 从路径到节点:of_find_node_by_path

先问一个问题:驱动怎么找到设备树里描述自己的那个节点?

答案就是 of_find_node_by_path。这个函数接受一个字符串路径,返回对应的节点指针。路径怎么写?就是设备树里从根节点开始的完整路径。

struct device_node *np;
np = of_find_node_by_path("/soc/spi@ff110000");
if (!np) {
    pr_err("找不到SPI控制器节点\n");
    return -ENODEV;
}

嗯,这里要注意:路径必须和设备树里的node-name@unit-address完全一致。大小写、斜杠、@符号,一个都不能错。

我在项目中遇到过一个问题:某次调试时,路径写成了/soc/spi@ff110000/,末尾多了一个斜杠。结果内核直接返回NULL,查了半天才发现。这种小坑,你想想看,是不是很冤?

小技巧:如果你不确定路径对不对,可以在内核启动时打开设备树调试信息(CONFIG_OF_UNITTEST),或者直接在板子上用ls /proc/device-tree/查看实际路径结构。

4.2 读取属性值:of_get_property

找到节点之后,下一步就是读属性。比如你想知道某个外设的中断号、时钟频率、或者一些自定义的配置参数。

of_get_property 就是干这个的。它的原型很简单:

const void *of_get_property(const struct device_node *np,
                            const char *name,
                            int *lenp);

第一个参数是节点指针,第二个是属性名,第三个返回属性值的长度(字节数)。返回值是属性值的首地址。

举个例子,读取一个32位整型属性:

const u32 *val;
int len;

val = of_get_property(np, "clock-frequency", &len);
if (val) {
    u32 freq = be32_to_cpup(val);
    pr_info("时钟频率: %u Hz\n", freq);
}

这里有个细节:设备树里存储的数据是大端格式(Big Endian)。而ARM、RISC-V这些处理器通常是小端。所以读取多字节数值时,必须用be32_to_cpup做转换。我刚开始写驱动时忘了这茬,读出来的频率值完全不对,折腾了一下午。

避坑指南:我曾经在读取字符串属性时犯过错误。of_get_property返回的字符串末尾不保证有'\0'。如果你直接把它当C字符串用,可能会读到乱码。安全做法是:

const char *str;
int len;
str = of_get_property(np, "compatible", &len);
if (str) {
    char buf[64];
    strlcpy(buf, str, min(len + 1, (int)sizeof(buf)));
    pr_info("compatible: %s\n", buf);
}

4.3 更便捷的读取方式:of_property_read_* 系列

说实话,of_get_property 用起来有点麻烦。每次都要手动处理字节序、长度检查。内核开发者们也意识到了这一点,所以提供了更友好的封装函数。

我个人最常用的是 of_property_read_u32of_property_read_string

u32 freq;
int ret;

ret = of_property_read_u32(np, "clock-frequency", &freq);
if (ret) {
    pr_err("读取clock-frequency失败\n");
    return ret;
}
pr_info("时钟频率: %u Hz\n", freq);

看到了吗?不需要手动处理字节序,不需要关心长度。函数内部已经帮你做好了。返回值0表示成功,负数表示出错。

常用的读取函数还有这些:

函数 用途 返回值类型
of_property_read_u32 读取单个32位整数 int(0成功)
of_property_read_u32_array 读取整数数组 int
of_property_read_string 读取字符串 int
of_property_read_bool 检查布尔属性是否存在 bool
of_property_count_elems_of_size 统计数组元素个数 int

我的建议:新写的驱动尽量用of_property_read_*系列函数。代码更简洁,可读性更好。只有在需要获取属性原始长度等特殊场景下,才用of_get_property

4.4 实战:完整的使用流程

说了这么多,我们来看一个完整的例子。假设设备树里有一个LED节点:

myled: led@0 {
    compatible = "mycompany,myled";
    reg = <0x01 0x100>;
    label = "status-led";
    max-brightness = <255>;
};

驱动中读取这些信息的代码:

static int myled_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    const char *label;
    u32 max_brightness;
    u32 reg_addr, reg_size;
    int ret;

    // 读取字符串属性
    ret = of_property_read_string(np, "label", &label);
    if (ret) {
        dev_err(dev, "缺少label属性\n");
        return ret;
    }

    // 读取整数属性
    ret = of_property_read_u32(np, "max-brightness", &max_brightness);
    if (ret) {
        dev_warn(dev, "使用默认亮度值\n");
        max_brightness = 255;
    }

    // 读取reg属性(地址和大小)
    ret = of_property_read_u32_index(np, "reg", 0, ®_addr);
    if (ret) {
        dev_err(dev, "读取reg地址失败\n");
        return ret;
    }
    ret = of_property_read_u32_index(np, "reg", 1, ®_size);

    dev_info(dev, "LED: %s, 亮度: %u, 地址: 0x%x, 大小: %u\n",
             label, max_brightness, reg_addr, reg_size);

    return 0;
}

你看,整个流程很清晰:先拿到节点,然后按需读取各个属性。出错时及时返回,并打印错误信息。这就是驱动开发的标准做法。

4.5 知识体系总览

为了帮你理清思路,我画了一张图,把本章的核心内容串起来:

OF API 核心知识体系 设备树(Device Tree Source) 编译生成 dtb / dtbo 驱动中通过 OF API 操作设备树节点 查找节点 of_find_node_by_path 读取属性 of_get_property / of_property_read_* 解析资源 of_iomap / of_irq_get 驱动获取硬件信息,完成初始化

这张图把整个流程串起来了。从设备树源文件开始,经过编译生成二进制dtb,然后驱动通过OF API完成查找节点、读取属性、解析资源这三步,最终拿到硬件信息完成初始化。

说白了,OF API就是驱动和设备树之间的桥梁。你不需要记住每个API的每个参数,但一定要理解这个“查字典”的思维模型。遇到具体需求时,查内核文档或者include/linux/of.h头文件,很快就能找到对应的函数。

总结一下本章要点:

  • of_find_node_by_path:通过路径字符串查找节点,路径必须精确匹配
  • of_get_property:原始读取方式,需要手动处理字节序和长度
  • of_property_read_*系列:更安全的封装,推荐优先使用
  • 读取字符串时注意末尾可能没有'\0',需要自己处理
  • 设备树数据是大端格式,多字节数值需要转换

公众号:蓝海资料掘金营,微信deep3321