很多人第一次接触设备树里的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, param1);如果想把整个数组完整取出来则优先用of_property_read_variable_u32_arrayu32 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, 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, 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, reg)) continue; dev_info(dev, child %s reg0x%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 编译、加载与日志验证模块编译通常用内核树外的Makefileobj-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, version3 labellogical-test test-device test-device: child child-a reg0x1000 test-device test-device: child child-b reg0x2000如果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都会顺畅很多。