很多人第一次接触设备树里的of函数,是在顺着别人的驱动代码找compatible匹配流程的时候。代码里突然冒出一堆of_property_read_u32、of_find_node_by_path,看着挺唬人,其实本质就是一套帮你从设备树节点上取数据的接口。如果你接下来要做一个test用的驱动模块,想在probe函数里读一读dts里自定义的参数,那这篇文章应该能让你少走不少弯路。
设备树方案在嵌入式Linux里已经用了很多年,它解决的问题很直白:硬件资源描述从“写死在驱动头文件里”变成了“由bootloader或内核启动时传入的dts文件描述”。驱动程序不再关心这块板子的内存基址为什么是0x10000000,而是直接去设备树节点上找reg属性。of函数就是驱动和dts之间的正式沟通方式。
这篇文章围绕“用of函数读取设备树信息”这个test需求,我会从节点查找、属性读取、子节点遍历讲到实际调试中的坑,最后给一个能直接参考的模块骨架。不论你是刚接触内核驱动,还是已经写了一段时间但没系统整理过of函数,按这个思路往下看基本就够用了。
1. of函数到底在解决什么问题:从“改宏重编”到“按图索骥”
1.1 为什么驱动里不直接写死参数
先还原一个很常见的场景。早些年写驱动,板子上的某个外设基地址、中断号,都是直接在驱动源码头部的宏定义里写死的。那时候代码风格就是:
#define TEST_BASE_ADDR 0x10000000 #define TEST_IRQ 31单板没问题,但只要换一个硬件版本,内存布局稍微动一下,就得改宏定义、重新编译整个内核模块,还要保证改的人和硬件配置的人能对得上号。这个过程既慢又容易出错。后来有了module_param,可以把参数在加载时传进来,算是一种缓解手段,但参数一多还是乱。
设备树方案的思路完全不同。dts把硬件拓扑和资源配置描述成节点和属性,驱动通过节点名字或compatible字符串找到自己的那棵节点,再用of_property_read_*这类接口把属性值解析出来。代码里不再有#define版本参数,有的只是“我需要哪个属性、这个属性存的是什么类型”的声明。
1.2 of函数家族的核心成员
of全称是Open Firmware,这套API是从早期的Firmware接口演进过来的,在Linux内核里专门负责device tree节点的查询、属性读取和资源解析。常用的成员可以分成几类:
- 节点查找类:
of_find_node_by_path、of_find_compatible_node、of_find_node_by_name、of_get_child_by_name - 遍历类:
for_each_child_of_node、for_each_node_by_type - 属性读取类:
of_property_read_u32、of_property_read_string、of_property_read_variable_u32_array、of_get_property - 资源解析类:
of_address_to_resource、of_iomap、of_irq_get - 引用管理类:
of_node_put、of_node_get
你在test模块里最先用到的往往是前两类,也就是找到节点、读属性。后面解析寄存器地址和中断的时候,又会接触到of_address_to_resource和of_iomap。这篇文章先把节点和属性的部分讲透,因为这是基础。
2. 锁定目标节点:路径查找、compatible匹配和引用计数
2.1 用of_find_node_by_path按绝对路径取节点
设备树和文件系统很像,每个节点都有一个路径,比如根节点下的某个测试节点就是:
/root-node/child-node/test-device对应的查找函数是:
struct device_node *np = of_find_node_by_path("/root-node/child-node/test-device");这个接口接受一个绝对路径字符串,返回struct device_node *。如果找不到,返回NULL。它特别适合在驱动probe之前,根据一个相对固定的路径去拿节点,比如某些系统级配置节点。
有一类特殊写法是of_find_node_by_path("/test-device"),直接从根下找。实际项目里我一般不建议在路径里写死完整层级,因为dts在不同板子上可能嵌套不一样。路径越短,越容易在不同硬件版本之间保持稳定。
还有一个变体叫of_find_node_opts_by_path,它支持在路径后追加逗号分隔的选项字符串。这个在特殊场景才用得到,日常test模块里很少碰到,知道即可。
2.2 用of_find_compatible_node按compatible匹配
驱动和设备树节点匹配最常见的依据就是compatible属性。设备树里一个节点会写:
test-device { compatible = "vendor,test-device"; };在驱动里,可以用of_find_compatible_node来找节点。这个函数需要你传入起始节点、设备类型字符串和compatible字符串,通常写法是:
struct device_node *np = of_find_compatible_node(NULL, NULL, "vendor,test-device");第一个参数传NULL表示从头开始找,第二个参数传NULL表示不限制节点类型,第三个参数就是要匹配的compatible值。这个函数一个很大的作用是:你手头没有父节点指针,但知道设备叫什么,就可以直接全局搜索。
在驱动的probe函数里反而不太需要这个接口,因为struct platform_device里的dev.of_node通常已经指向正确节点。of_find_compatible_node更多用在辅助函数里,比如某个模块要独立去探测一个固定设备是否存在。
2.3 别忘引用计数:节点查找之后要of_node_put
这是初学者最容易漏的点。设备树节点是动态引用计数的,of_find_node_by_path或of_find_compatible_node在返回节点时,内部会调of_node_get增加引用计数。当你用完这个节点,必须调用of_node_put(np)释放,否则节点永远不会被释放。
你可能会觉得一个test模块用完就退出,不释放好像问题不大。但如果这个函数出现在一个会被反复调用的路径里,比如每次打开设备文件都执行一次查找,那么每次泄漏一点,最终就可能出现节点无法回收、内存碎片化的问题。我在调试某个模拟项目时遇到过类似现象:反复执行几十次之后,系统报告设备树节点分配失败。最后定位到的原因就是某个分支路径上忘记of_node_put。
正确习惯是拿到节点后,立刻想清楚“这个引用在哪个路径上释放”。如果是驱动probe里从pdev->dev.of_node拿到的节点,那通常是借用引用,不需要你释放——那是总线核心管理好的。但凡是自己通过of_find_*系列查到的,就一定要配一个of_node_put。
2.4 遍历全表:for_each_node_by_type等循环宏
有时候你想扫描所有满足条件的节点。内核也提供了遍历宏,比如按类型遍历:
struct device_node *np; for_each_node_by_type(np, "test-device") { ... }不过现代设备树更推荐用compatible匹配而不是type,所以for_each_compatible_node也是常用宏:
struct device_node *np; for_each_compatible_node(np, NULL, "vendor,test-device") { ... }这类遍历宏的循环体仍然要把of_node_put做对。遍历宏语义上持有当前节点的引用,但循环最后一般会由宏在迭代时自动处理一部分,具体要看你是否在循环体内提前break。如果提前退出循环,更要小心是否需要手动释放。我的建议是:宁可多看几遍内核文档里对应宏的注释,也不要凭感觉写。很多死锁或泄漏案例都出现在遍历循环的break分支里。
3. 读取属性解析:整数、字符串、数组与返回值的门道
3.1 用of_property_read_u32读取单值整数
设备树中一个节点最常见的自定义属性就是整数。例如:
test-device { test-version = <3>; };驱动侧读取它的代码是:
u32 version; int ret = of_property_read_u32(np, "test-version", &version); if (ret) { dev_err(dev, "failed to get test-version\n"); return ret; }注意,test-version的<3>写法在dts中是一个无符号32位整数数组,属性里虽然只有一个元素,但在设备树内部仍然以数组形式存储。of_property_read_u32只是帮你取第一个元素并做字节序转换。
这里要理解为什么会牵扯字节序。dts文件编译成dtb后,属性值在内存里是按大端存还是小端存,取决于编译工具和当前平台的约定。设备树规范为了跨平台一致性,规定属性值以big-endian方式存储,而CPU可能是little-endian。of_property_read_u32内部会调用of_property_read_u32_index,并帮你做必要的字节序转换。所以不要试图用指针强转直接读属性原始数据,否则你会拿到一个翻转过的数字。
3.2 多索引整数和数组读取
有时候属性本身存了多个整数,比如坐标值、或者一组寄存器偏移:
test-params = <0x100 0x200 0x300>;取第一个值还是上面的of_property_read_u32,取第二个值就要用:
u32 param1; of_property_read_u32_index(np, "test-params", 1, ¶m1);如果想把整个数组完整取出来,则优先用of_property_read_variable_u32_array:
u32 params[3]; int count = of_property_read_variable_u32_array(np, "test-params", params, 0, ARRAY_SIZE(params)); if (count < 0) { dev_err(dev, "failed to read test-params\n"); return count; }这个函数的签名里,第三个参数是输出缓冲区,第四个参数是固定的最小期望元素个数,第五个是缓冲区容量。返回值是实际读出的元素个数。如果属性不存在,返回负数。你用这个接口时要特别小心:它要求属性元素个数既大于等于指定最小数量,又不能超过缓冲区容量,否则会失败。
3.3 字符串属性不是“读进去”,而是拿到一个指针
设备树里写字符串是这样:
test-label = "logical-dev";驱动侧如果按常规思路,先定义一个char buf[64],然后用of_property_read_string尝试往buf里拷贝,那就误会了这个API的设计。of_property_read_string的正确用法是:
const char *label; int ret = of_property_read_string(np, "test-label", &label); if (ret) { dev_err(dev, "failed to get test-label\n"); return ret; } dev_info(dev, "label: %s\n", label);没错,第二个参数的类型是const char **。它不会把字符串拷贝到你提供的缓冲区,而是把设备树节点内部保存的字符串起始地址直接赋值给你。这意味着你拿到的指针生命周期和设备树节点绑定,节点存活期间都有效;你也不要去kfree它。
这个细节很关键。如果你确实需要一份独立拷贝,应该先用of_property_read_string拿到指针,再用kstrdup复制一份:
const char *label; char *label_copy; if (of_property_read_string(np, "test-label", &label)) return -EINVAL; label_copy = kstrdup(label, GFP_KERNEL); if (!label_copy) return -ENOMEM;3.4 返回值语义:“取不到”和“属性为0”是两回事
初学of_property_read_*时,很容易用“返回值是否为0”判断读取是否成功。这个说法对,但有个易混淆点:一个属性值本身就是0,比如<0>,这时读取函数返回0,*out也是0,这是正常读取,不是失败。
真正的失败是属性不存在或者类型不匹配。比如你用一个u32读取函数去读一个<0x100 0x200>的数组,如果函数实现严格要求属性元素个数,不少变体会直接返回-EINVAL。尤其注意of_property_read_u32只负责取第一个元素,所以元素超过一个它反而不会报错。如果你要校验属性恰好只有一个元素,需要额外用of_property_count_elems_of_size来判断。
以下是我常用的属性存在性快速判断方式:
if (of_find_property(np, "test-version", NULL)) { of_property_read_u32(np, "test-version", &version); } else { dev_warn(dev, "test-version missing, using default\n"); version = 0; }of_find_property用来检查属性本身是否存在,第三个参数返回属性长度,不关心的话传NULL。这个模式在需要兼容多个硬件版本时非常实用。
4. 子节点与层级遍历:遇到组合型配置节点该怎么解析
4.1 先数一数字节点数量:of_get_child_count
设备树节点会嵌套子节点。比如一个test设备下挂了多个逻辑通道,每个通道有自己的属性:
test-device { compatible = "vendor,test-device"; channel-a { reg = <0x100>; enable = <1>; }; channel-b { reg = <0x200>; enable = <0>; }; };读取所有子节点前,先知道子节点总数可以方便分配数组:
int child_count = of_get_child_count(np); struct child_priv *children = kcalloc(child_count, sizeof(*children), GFP_KERNEL); if (!children) return -ENOMEM;of_get_child_count返回的是当前节点直属子节点的数量,不包含孙子节点。它在实现上会遍历子节点链表数一遍,所以如果对性能敏感,且后续本来就要遍历一次,可以考虑先遍历动态收集,而不是单独调用两次。
4.2 逐个遍历子节点:for_each_child_of_node
遍历子节点的标准写法是:
struct device_node *child; int idx = 0; for_each_child_of_node(np, child) { const char *child_name; u32 reg; if (of_property_read_string(child, "compatible", &child_name)) continue; if (of_property_read_u32(child, "reg", ®)) continue; children[idx].name = child_name; children[idx].reg = reg; idx++; }注意,for_each_child_of_node宏展开之后,循环体里拿到的child节点是临时引用。如果你的驱动程序要把某个子节点保存下来长期使用,必须在保存前用of_node_get(child)增加引用,并在释放时对应of_node_put。如果只是临时读取,一般不需要显式释放,因为宏会在迭代过程中处理。但如果你在循环中途break,情况会复杂一些。稳妥起见,循环体内凡是提前退出的地方,自己手动of_node_put(child)。
4.3 按名字直接取子节点:of_get_child_by_name
如果你的子节点名称固定,完全可以不用遍历,直接按名字取:
struct device_node *channel_a = of_get_child_by_name(np, "channel-a"); if (!channel_a) { dev_err(dev, "channel-a node missing\n"); return -ENODEV; }这个函数会遍历当前节点的子节点,查找名字匹配的节点,并返回带引用的节点指针。用完同样要of_node_put(channel_a)。如果你要长期持有,记得先of_node_get或者直接接受这个引用不释放,直到不再使用时再放。
4.4 完整的多子节点解析示例
我把上面的知识点串起来,写一个典型的test驱动解析片段:
struct channel_cfg { const char *name; u32 reg; bool enable; }; static int test_parse_channels(struct device_node *np, struct channel_cfg **out_cfgs, int *out_count) { struct device_node *child; struct channel_cfg *cfgs; int count = of_get_child_count(np); int idx = 0; int ret = 0; if (count == 0) return 0; cfgs = kcalloc(count, sizeof(*cfgs), GFP_KERNEL); if (!cfgs) return -ENOMEM; for_each_child_of_node(np, child) { u32 reg; u32 enable; if (of_property_read_u32(child, "reg", ®)) { ret = -EINVAL; goto out_put; } if (of_property_read_u32(child, "enable", &enable)) enable = 1; cfgs[idx].name = child->name; cfgs[idx].reg = reg; cfgs[idx].enable = !!enable; idx++; } *out_cfgs = cfgs; *out_count = count; return 0; out_put: of_node_put(child); kfree(cfgs); return ret; }这段代码刻意保留了out_put标签,是为了提醒自己在错误路径上把子节点引用释放掉。虽然for_each_child_of_node的宏内部在正常迭代时会管理引用,但异常退出分支往往容易漏。这个习惯养成后,你对of函数的使用才算真正入门。
5. 在test模块里把of函数真正跑起来:一个可编译的参考骨架
5.1 模拟设备树节点的设计与编译前检查
实际操作时,你可能先在一个模拟环境里写test模块。设备树节点可以临时挂在某个存在的总线下,比如平台设备的root节点。假设你的测试设备树节点这样写:
/dts-v1/; / { test-device { compatible = "vendor,test-device"; reg = <0x10000000 0x1000>; test-version = <3>; test-label = "logical-test"; child-a { reg = <0x1000>; enable = <1>; }; child-b { reg = <0x2000>; enable = <0>; }; }; };如果你用的是真实内核和真实dts,修改后需要重新编译dtb,并让bootloader加载新的dtb。但如果你是在模拟平台(比如某种虚拟化的内核调试环境)上做test,可以借助修改dts后重启验证。无论是哪种方式,建议先检查dtb是否正确包含了节点。最直接的方法是启动后查看:
ls /proc/device-tree/test-device如果能看到该目录,说明节点一定加载进了内核。/proc/device-tree其实就是设备树在运行时暴露出来的视图,每个节点对应一个目录,每个属性对应一个文件。这个目录是排查of函数读取问题的第一现场。
5.2 platform驱动里用of_node读取参数的骨架
下面给一个完整的、可以直接参考的模块骨架。这个模块注册成platform驱动,因为compatible匹配成功后,probe函数能拿到pdev->dev.of_node。
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/of_device.h> #include <linux/slab.h> struct test_priv { u32 version; const char *label; }; static int test_drv_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct device_node *np = dev->of_node; struct test_priv *priv; struct device_node *child; int ret; priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; if (!np) { dev_err(dev, "device node not found\n"); return -ENODEV; } ret = of_property_read_u32(np, "test-version", &priv->version); if (ret) { dev_err(dev, "failed to get test-version: %d\n", ret); return ret; } ret = of_property_read_string(np, "test-label", &priv->label); if (ret) { dev_err(dev, "failed to get test-label: %d\n", ret); return ret; } dev_info(dev, "probe ok, version=%u label=%s\n", priv->version, priv->label); for_each_child_of_node(np, child) { u32 reg = 0; if (of_property_read_u32(child, "reg", ®)) continue; dev_info(dev, "child %s reg=0x%x\n", child->name, reg); } platform_set_drvdata(pdev, priv); return 0; } static int test_drv_remove(struct platform_device *pdev) { struct device *dev = &pdev->dev; dev_info(dev, "remove ok\n"); return 0; } static const struct of_device_id test_drv_match[] = { { .compatible = "vendor,test-device" }, { } }; MODULE_DEVICE_TABLE(of, test_drv_match); static struct platform_driver test_drv = { .probe = test_drv_probe, .remove = test_drv_remove, .driver = { .name = "test-device", .of_match_table = test_drv_match, }, }; module_platform_driver(test_drv); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("of function test module");这个骨架把前几节的节点查找、属性读取、子节点遍历全部塞进来了。它没有使用of_find_node_by_path,因为probe里从pdev->dev.of_node拿到的节点是总线层已经匹配好的。这其实是更符合平台驱动常规写法的路径。如果你选择自己用of_find_compatible_node全局搜索,记住要of_node_put,但在这个骨架里不需要。
5.3 编译、加载与日志验证
模块编译通常用内核树外的Makefile:
obj-m += test_of.o然后执行:
make -C /lib/modules/$(uname -r)/build M=$PWD modules加载前先确认dts节点已经在系统里。加载:
sudo insmod test_of.ko之后用dmesg看日志。如果一切正常,你会看到类似输出:
test-device test-device: probe ok, version=3 label=logical-test test-device test-device: child child-a reg=0x1000 test-device test-device: child child-b reg=0x2000如果probe没有跑,先看/sys/bus/platform/devices下有没有对应设备,再看compatible是否完全一致。compatible匹配是字符串精确匹配,多一个空格、大小写不同都会失败。
6. 实际调试中容易踩的坑:这些我都替你试过
6.1 字符串指针的生命周期问题
前面提到of_property_read_string返回的是内部指针。这个特性带来一个隐蔽问题:如果你在模块A里读取了一个字符串,然后节点被覆盖、卸载或重新加载,旧指针就变成悬空指针。设备树节点一般不会频繁变更,但在支持设备树覆盖(DT overlay)的系统里,节点可能在运行时被更新。
我在某个模拟项目X里写了一个test模块,先读取test-label保存指针,然后卸载再加载设备树覆盖层,接着模块重新读取时发现字符串内容变成了另一段内存的随机数据。排查后确认问题不在读取逻辑,而是保存的指针没有在节点更新后重置。所以只要你的驱动需要长期保留字符串,建议立刻kstrdup保存副本,而不是保留原始指针。
6.2 属性值为0时的“假失败”错觉
还有一次调试,同事跟我说of_property_read_u32读出来的值偶尔不对,有时明明是0,日志却显示成一大串数。最后发现他在dts里写的是:
test-version = <0>;然后在驱动里用了if (!of_property_read_u32(np, "test-version", &version))作为“属性不存在”的判断。程序逻辑里把这个判断当成了“没有属性”或“属性无效”,于是走了默认分支。可实际上函数返回0表示成功,version就是0,只是程序误解成了失败。
这个坑看起来简单,但遇到复杂驱动时很容易被忽略。建议养成统一习惯:of_property_read_*返回0就代表成功读取,跟你预期的值是不是“默认值”无关。不要在条件判断上偷懒。
6.3 reg属性解析别想当然:address-cells和size-cells
很多人读reg属性时直接用of_property_read_u32,这在只有单个寄存器地址且父节点#address-cells = <1>、#size-cells = <1>时勉强能凑合。但一旦涉及64位地址或者地址大小组合,直接按u32读就会出错。
更可靠的做法是用设备树标准解析接口:
struct resource res; int ret = of_address_to_resource(np, 0, &res); if (ret) return ret; resource_size_t start = res.start;of_address_to_resource会按照节点父级的#address-cells和#size-cells自动解析reg属性,把它转成标准的struct resource。内部处理了地址宽度和大小端问题。如果你要在驱动里做ioremap,甚至可以直接用of_iomap(np, 0)。
在我自己的test模块里,只要碰到寄存器地址,我不会再用of_property_read_u32去读reg,统一走of_address_to_resource,这样即使以后设备树的address-cells配置变化,驱动代码也不需要改。
6.4 设备树没生效时的排查顺序
最后聊聊最常见的失败场景:驱动代码看着没问题,但probe就是不执行或者读不到属性。这时候不要急着改代码,按下面的顺序排查:
- 启动后先看
/proc/device-tree/下有没有你的节点路径。没有,说明dts没有编进去或没被加载。 - 确认模块的
of_match_table里的compatible和dts节点里的compatible是否完全一致。可以用cat /proc/device-tree/test-device/compatible查看运行时实际值。 - 确认内核是否支持设备树匹配。在platform_driver里,除了
compatible匹配,还要确保设备确实被注册到了platform总线。 - 看
dmesg里有没有驱动加载和匹配失败的日志。很多情况下,of_platform_populate没有遍历到对应总线段落,导致probe根本没触发。
如果以上都没问题,再用我前面给的骨架代码跑一遍,理论上就能在日志里看到读取结果了。如果还是有异常,检查printk级别是否被串口/终端过滤了,我经常在这种地方耗费不少时间。
7. 一个小技巧:把设备树调试做成模块自检
如果你和我一样,经常面对不同的板卡和不同的dts版本,一个实用的做法是给test模块加一个自检模式。加载模块时通过module_param设置一个标志,决定是否对节点下的所有属性做一次“存在性扫描”并打印。
static bool self_check; module_param(self_check, bool, 0644); MODULE_PARM_DESC(self_check, "enable device tree self check mode"); static void test_dump_properties(struct device_node *np) { struct property *prop; dev_info(&pdev->dev, "dump properties of %s", np->full_name); for_each_property_of_node(np, prop) { dev_info(&pdev->dev, "prop %s len=%d", prop->name, prop->length); } }for_each_property_of_node是遍历节点属性的宏,在自检模式下可以把节点下所有属性名和长度打印出来。这样拿到一块新板子时,不需要反复读代码,直接看属性列表就能判断dts里的命名是否和驱动预期一致。这个技巧在快速定位“属性名拼写不一致”问题上特别管用。
我在实际调试中,一般会把self_check默认关闭,只在怀疑dts内容时开一次。它不会影响正常probe流程,成本也不高,却能让设备树相关的问题排查效率提高不少。
最后补充一点个人体会:of函数这套接口,文档和内核源码都写得很清楚,但真正难的部分往往不在API本身,而在你对设备树模型的理解是否足够扎实。先把节点查找、引用释放、属性类型、字符串生命周期这几个基础概念吃透,后面无论是读自定义属性还是解析标准reg、interrupts,都会顺畅很多。