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_u32 和 of_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 知识体系总览
为了帮你理清思路,我画了一张图,把本章的核心内容串起来:
这张图把整个流程串起来了。从设备树源文件开始,经过编译生成二进制dtb,然后驱动通过OF API完成查找节点、读取属性、解析资源这三步,最终拿到硬件信息完成初始化。
说白了,OF API就是驱动和设备树之间的桥梁。你不需要记住每个API的每个参数,但一定要理解这个“查字典”的思维模型。遇到具体需求时,查内核文档或者include/linux/of.h头文件,很快就能找到对应的函数。
总结一下本章要点:
of_find_node_by_path:通过路径字符串查找节点,路径必须精确匹配of_get_property:原始读取方式,需要手动处理字节序和长度of_property_read_*系列:更安全的封装,推荐优先使用- 读取字符串时注意末尾可能没有'\0',需要自己处理
- 设备树数据是大端格式,多字节数值需要转换
公众号:蓝海资料掘金营,微信deep3321