Linux驱动多设备支持:probe机制与私有数据结构实战
发布时间:2026/9/7 15:23:52 锦皓数字建站

干嵌入式Linux这一行瑞芯微平台是我这几年接触最多的国产SoC之一。从RK3288到RK3399再到现在的RK3568、RK3588做Bring Up时绕不开的一个需求就是“一个驱动怎么同时管住同一类别的多个设备”。很多人觉得这不简单吗同一个驱动文件设备树里挂两个节点不就行了真到调的时候才发现probe到底跑几次、寄存器地址会不会冲突、中断回调里拿哪个设备的数据、日志里分不清是谁在报错这些全是坑。这篇文章就讲两个Linux驱动支持多个设备的技巧全部基于瑞芯微RK3568平台把原理、代码和验证过程一起写出来给正在做驱动开发的人参考。1. 先想清楚驱动的probe机制天然支持多个设备很多初学者会把“一个驱动只能对应一个设备”当成默认前提实际上这是对Linux设备模型最大的误解。在Linux设备模型中驱动不直接创建设备设备树里的每个节点、每条总线上枚举出的每个client都会被内核组织成独立的struct device。驱动注册时内核的bus匹配机制会拿驱动的of_match_table和系统中所有未绑定设备的device节点逐个比对每匹配上一个节点就会调用一次驱动的probe回调。也就是说一个platform_driver如果能和两个设备树节点匹配它就会被调用两次probe每次拿到的struct platform_device指针都不一样。这个机制和具体SoC无关但瑞芯微平台因为大量使用设备树描述板级信息所以这里的体会尤其明显。比如RK3568上接了同型号的两颗MIPI摄像头传感器设备树里写两个i2c client节点compatible都是同一个字符串驱动注册后内核会为每个i2c client各调用一次probe。这就是驱动支持多设备的根基框架层面根本不限制次数限制只可能来自于驱动自身的设计。但为什么很多人做出来之后发现第二个设备不工作、或者两个设备互相干扰问题基本都出在probe内部。如果你在probe里用一个全局变量保存寄存器基地址第二次probe执行时覆盖掉第一次的值那么第一个设备的中断处理、状态查询都会跑到第二个设备的寄存器上。这个方向就是本文第一个技巧的核心。1.1 瑞芯微平台上的多设备典型场景先盘点一下实际项目中常见的多设备需求不是为了列清单而是让你对照自己的场景。同一I2C总线上挂多个同型号sensor比如RK3568的I2C0上同时接两颗OV5645各占一个地址。一个驱动要适配同系列不同型号的PMIC比如RK809和RK817两者寄存器不完全一样但驱动框架一致。主控通过多路SPI接多个同类型外设比如三路SPI各接一个ADC芯片。GPIO控制多个同类型执行器比如点阵屏、电机驱动模块每个实例需要独立的GPIO编号和状态位。这些场景本质上都是“同一种驱动逻辑对应多个物理设备实例”。理解到这个层面你再看设备模型的设计就会明白Linux从一开始就把“驱动”和“设备”分开管理多实例支持不是后加的补丁而是核心机制。1.2 probe调用的幕后机制为什么probe会被调用多次体系结构上内核的bus层维护了一个设备链表和一个驱动链表。新的platform_driver注册时bus会遍历已注册的设备调用driver_match_device做匹配匹配成功就执行really_probe。反过来新设备注册时也会遍历驱动链表。这个双向扫描机制保证了只要device和driver在同一个bus上且compatible匹配总会触发probe。这里有个巴菲特式的比喻设备是钥匙孔驱动是钥匙钥匙插进一个孔就能开一扇门。只要钥匙形状符合有多少个孔就能开多少次。设备树里写多少匹配节点内核就给驱动开多少次门。理解这一点就不会再去查“怎么让驱动支持两个设备”而是把精力放在“probe每次执行时如何不干扰彼此”。2. 技巧一用私有数据结构守住每个实例的地盘第一个技巧也是最重要的一个就是把每个设备的状态、资源、运行数据全部收进一个自定义的私有数据结构在probe里分配一次之后所有操作都基于这个结构体进行。这个结构体在内核驱动里有个约定俗成的名字通常叫xxx_dev或者xxx_priv是整个多设备支持的地基。2.1 为什么不能用全局变量先聊反面教材。我见过不少驱动代码probe里写着static void __iomem *g_reg_base; static int g_irq;第一眼觉得没什么两个设备时问题立刻暴露。第二次probe把g_reg_base覆盖成第二颗芯片的地址第一颗芯片上报中断时中断处理函数用g_reg_base去操作寄存器读到的全是第二颗芯片的状态。如果恰好两个芯片的工作状态不同甚至会读出非法值导致内核崩溃。全局变量的本质问题在于它有且只有一个副本而设备实例有多个。这就像一把钥匙开两扇门时你把钥匙的齿纹改成了第二扇门的形状第一扇门就永远打不开了。线程并发、中断上下文、用户态读写同时进来时全局变量还会引入竞态条件排查起来更加痛苦。所以我的经验是设备实例的数据绝对不允许进全局变量一个都不行。2.2 私有数据结构设计参考在设计私有数据结构时尽量把和硬件实例有关的所有内容都放进去。以下是一个适合多实例场景的结构体示例struct foo_dev { void __iomem *base; /* 寄存器映射基地址 */ int irq; /* 中断号 */ int reset_gpio; /* 复位GPIO */ int instance_id; /* 实例编号 */ int speed; /* 运行参数示例 */ struct device *dev; /* 指向struct device方便日志与sysfs */ struct mutex lock; /* 实例内的互斥锁 */ };这个结构体是每个设备实例一份probe执行多少次就分配多少个。每个字段都服务于这个独立的设备实例。你可能会问instance_id这样的字段有什么用多设备环境下日志打印、状态上报、用户态交互都需要区分设备的身份一个实例编号能省很多事。2.3 probe里分配release里释放分配动作放在probe开头是最干净的。推荐使用devm_kzalloc这是内核的资源管理managed device resource机制它会在设备注销时自动释放内存不需要你在remove里手动kfree。代码骨架如下static int foo_probe(struct platform_device *pdev) { struct foo_dev *foo; foo devm_kzalloc(pdev-dev, sizeof(*foo), GFP_KERNEL); if (!foo) return -ENOMEM; foo-dev pdev-dev; platform_set_drvdata(pdev, foo); ... }platform_set_drvdata把私有数据挂到设备结构体上后续无论在哪只要拿到struct device或struct platform_device就可以用dev_get_drvdata取回自己的私有数据。这是整个多实例驱动的交通枢纽。在remove函数里需要手动做的通常是顺序控制类的清理比如取消工作队列、释放互斥锁、关闭定时器有些资源用devm机制不代表就能彻底撒手。以中断为例虽然devm_request_irq会在设备注销时自动释放中断但你得保证在释放前不再有新的中断进来最稳妥的做法是在remove里先free_irq再释放其他资源。简单说devm帮你兜底但你得安排好清理顺序。3. 技巧二用设备树和匹配表分离“共性与差异”第二个技巧聚焦在“型号差异”和“实例身份”的区分上。实际驱动开发中多设备不只是“多个相同设备”更多时候是“多个相近但不同的设备”。瑞芯微平台最常见的就是一颗PMIC兼容多个型号硬件上引脚兼容但寄存器地址和位定义有差异。这时你要写的是一个驱动控制多个型号而不是为每个型号各写一个驱动。3.1 用of_device_id的data字段做型号分发of_device_id结构体里有个data字段匹配到对应compatible的设备时内核会把命中的of_device_id传给probe而你就可以从这个结构体的data字段里取出自定义的型号编号。看代码static const struct of_device_id foo_of_match[] { { .compatible rockchip,foo-v1, .data (void *)FOO_V1 }, { .compatible rockchip,foo-v2, .data (void *)FOO_V2 }, { } }; MODULE_DEVICE_TABLE(of, foo_of_match); static int foo_probe(struct platform_device *pdev) { const struct of_device_id *match; int version; match of_match_device(foo_of_match, pdev-dev); if (match) version (int)match-data; ... }这个技巧的意义在于compatible匹配交给内核搞定型号差异放到初始化分支里处理。同一套probe框架根据version决定是初始化V1寄存器还是V2寄存器。这样即使板子上同时挂了V1和V2两颗芯片驱动代码也不需要两个probe流程。数据表就是设备差异的“配置文件”代码则保持单一。3.2 多节点设备的身份解析如果多个设备节点共用同一个compatible那么仅靠of_match_table就区分不了实例。这时候需要从设备树节点本身解析属性。瑞芯微的设备树里每个节点天然有一个reg属性这是硬件地址或ID通常全局唯一。把它读出来作为实例标识。struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; foo-base devm_ioremap_resource(pdev-dev, res); foo-instance_id res-start;还可以用of_property_read_u32读取自定义属性比如一个rockchip,speed 200代表该实例的运行速度参数。设备树里的属性是设备之间的差异来源驱动里用统一代码按属性值初始化即可。这是“数据驱动”的思路避免在C代码里写死每一个实例的配置。3.3 日志、中断、sysfs的多实例区分设备身份分离之后剩下的就是如何在运行期区分它们。日志是所有驱动开发者的第一排查手段多实例环境下必须用dev_dbg、dev_info、dev_err这类接口而不是裸的printk。dev_err(pdev-dev, reset failed\n)在内核日志里会自动打印设备名比如rk_foo_multi foo.0: reset failed和rk_foo_multi foo.1: reset failed一眼就能看出是哪个实例出了问题。中断处理函数里同样需要区分实例。注册中断时最后一个参数传入私有数据指针devm_request_irq(pdev-dev, irq, foo_irq_handler, IRQF_TRIGGER_RISING, DRIVER_NAME, foo);这样foo_irq_handler第一个参数是irq号第二个参数就是foo拿到它就可以操作对应实例的寄存器。不要试图在中断里通过全局变量找设备那是灾难。sysfs属性也是区分实例的好办法。为每个实例创建只读属性文件用户态通过cat /sys/devices/platform/foo.0/speed就能看到第一个实例的运行参数foo.1对应第二个实例。属性回调里通过dev_get_drvdata(dev)获取私有数据天然按设备分开了。4. 完整示例基于RK3568的platform多实例驱动只讲原理不给代码等于没说。这一节我给出一个可在RK3568上落地的最小示例。设备树里放两个同compatible的节点驱动注册后分别probe把两张实例各自的参数通过sysfs暴露出来。场景不一定完全贴合你的项目但骨架可以直接抄。4.1 设备树节点写法在RK3568的设备树文件里添加以下两个节点挂根节点下即可我这里保留reg地址空间和两个属性作为示例。foo0: foo10000 { compatible rockchip,foo-multi; reg 0x0 0x10000 0x0 0x1000; rockchip,instance-id 0; rockchip,speed 100; }; foo1: foo20000 { compatible rockchip,foo-multi; reg 0x0 0x20000 0x0 0x1000; rockchip,instance-id 1; rockchip,speed 200; };这里两个节点共用compatible驱动只需匹配一次。reg声明了两段不同的地址空间platform_get_resource能正确区分。实际项目中这两个节点可以是I2C控制器下的两个client也可以是两个独立的平台设备节点原理相同。注意reg的写法用了两个32位值这是RK3568这类64位SoC的标准做法。0x0 0x10000 0x0 0x1000表示高32位地址为0低32位地址为0x10000长度为0x1000。读取时resource_size(res)返回0x1000res-start是0x10000。4.2 驱动源码示例下面是一个完整的platform驱动骨架去掉了实际业务逻辑保留了多设备支持的关键环节。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/sysfs.h #include linux/io.h #include linux/mutex.h #define DRIVER_NAME rk_foo_multi struct foo_dev { void __iomem *base; int instance_id; int speed; struct device *dev; struct mutex lock; }; static ssize_t speed_show(struct device *dev, struct device_attribute *attr, char *buf) { struct foo_dev *foo dev_get_drvdata(dev); return sprintf(buf, %d\n, foo-speed); } static DEVICE_ATTR_RO(speed); static ssize_t instance_id_show(struct device *dev, struct device_attribute *attr, char *buf) { struct foo_dev *foo dev_get_drvdata(dev); return sprintf(buf, %d\n, foo-instance_id); } static DEVICE_ATTR_RO(instance_id); static struct attribute *foo_attrs[] { dev_attr_speed.attr, dev_attr_instance_id.attr, NULL, }; ATTRIBUTE_GROUPS(foo); static int foo_probe(struct platform_device *pdev) { struct foo_dev *foo; struct resource *res; foo devm_kzalloc(pdev-dev, sizeof(*foo), GFP_KERNEL); if (!foo) return -ENOMEM; foo-dev pdev-dev; mutex_init(foo-lock); platform_set_drvdata(pdev, foo); res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; foo-base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(foo-base)) return PTR_ERR(foo-base); of_property_read_u32(pdev-dev.of_node, rockchip,instance-id, foo-instance_id); of_property_read_u32(pdev-dev.of_node, rockchip,speed, foo-speed); dev_info(pdev-dev, probed instance %d, speed %d, base %pa\n, foo-instance_id, foo-speed, res-start); return 0; } static int foo_remove(struct platform_device *pdev) { struct foo_dev *foo platform_get_drvdata(pdev); dev_info(pdev-dev, remove instance %d\n, foo-instance_id); return 0; } static const struct of_device_id foo_of_match[] { { .compatible rockchip,foo-multi }, { } }; MODULE_DEVICE_TABLE(of, foo_of_match); static struct platform_driver foo_driver { .probe foo_probe, .remove foo_remove, .driver { .name DRIVER_NAME, .of_match_table foo_of_match, .dev_groups foo_groups, }, }; module_platform_driver(foo_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(RK3568 multi-instance foo driver);这段代码的逻辑不复杂。probe里分配私有结构、获取寄存器资源、读取设备树属性、注册sysfs属性组。dev_groups是驱动层面统一创建的内核为每个设备实例分别生成对应的sysfs目录。speed_show里的dev参数是每个实例自己的设备dev_get_drvdata自然取到各自的私有数据。4.3 上板验证结果把设备树编译进RK3568内核加载驱动模块后dmesg里应该能看到两次probe的打印类似rk_foo_multi foo.0: probed instance 0, speed 100, base 0x10000 rk_foo_multi foo.1: probed instance 1, speed 200, base 0x20000然后在板子上执行cat /sys/devices/platform/foo.0/speed cat /sys/devices/platform/foo.1/speed cat /sys/devices/platform/foo.0/instance_id cat /sys/devices/platform/foo.1/instance_id第一组分别输出100和200第二组输出0和1说明两个实例的状态完全隔离互不干扰。这一步验证的是整个多设备支持的核心分配了多个私有数据每个设备各回各家。5. 多实例驱动开发的常见坑与排查顺序代码能跑通不代表就完事了。多实例驱动开发里几个坑我都在瑞芯微平台上踩过梳理成一份速查表方便大家按图索骥。现象可能原因排查手段probe只调用一次第二个节点status为disabled、compatible写错、父节点未启用检查设备树节点和源码里的compatible确认of_match_table已导出两个实例寄存器地址冲突设备树reg写错、同一个reg被两个节点引用对比设备树实际编译产物查看生成的dtb片段第二个设备无中断响应中断号解析失败或共享中断未正确注册查看/proc/interrupts确认两个irq都存在两个实例互相干扰驱动代码使用了全局变量保存状态全局变量彻底禁止实例数据一律放私有结构体日志无法区分设备代码里用了pr_info/pr_err全部替换为dev_info/dev_err卸载时崩溃remove里释放顺序不对先cancel work再free_irq最后释放其他资源sysfs只有一份属性挂在了驱动而不是设备使用dev_groups内核会自动按设备实例展开5.1 probe只调用一次这是最常见的现象。代码写好了加载驱动后发现只有第一个节点probe了。排查顺序先看设备树第二个节点有没有status disabled或者父节点被禁用。瑞芯微平台设备树里很多节点默认disabled需要在板级dts里显式打开。其次看compatible设备树里的字符串和of_match_table里的必须完全一致多一个空格都不行。还有一个容易被忽略的点如果两个节点挂在同一个I2C控制器下而I2C控制器本身只使能了一个那第二个client就不会被枚举。可以先在板子上ls /sys/bus/i2c/devices/确认内核有没有为每个地址生成i2c client。5.2 实例互相“打架”两个instance都能probe但运行起来数据错乱多半是全局变量作祟。一个典型的例子是中断处理函数里用了全局保存的寄存器基地址两个设备的中断都往同一个寄存器基地址上操作。解决办法就是全文搜索代码里有没有非static const的全局变量凡是和硬件实例相关的全部移入私有结构体。static const结构如of_match_table是共享只读的可以保留其他的一律禁止。5.3 资源释放顺序devm机制能自动释放内存映射、中断、GPIO等资源但释放顺序不一定是业务逻辑需要的顺序。比如你有一个workqueue在定时轮询设备设备注销时如果workqueue还在跑devm已经帮你释放了寄存器映射那workqueue访问的就是非法地址。正确做法是在remove里先cancel_delayed_work_sync停止任务再free_irq确保中断不会再来之后依赖devm统一释放剩余资源。多实例下每个实例的remove都会被调用清理逻辑都一样顺序一致就不会出问题。5.4 调试多实例驱动的额外建议多实例调试比单设备麻烦但有几个工具能帮大忙。ls /sys/devices/platform/ | grep foo可以快速确认哪几个实例被创建。cat /proc/interrupts | grep foo可以确认每个实例的中断是否已注册。cat /sys/kernel/debug/devices_debug开启内核配置后可以查看设备与驱动的绑定关系。dev_err每个实例自动带设备名这是最快的定位手段。我实际调试时几乎不看pr_info日志全部用dev_级别打印配合grep关键字能从上万行内核日志里快速筛出某个实例的信息。写在最后多设备支持这件事框架已经帮你铺好了路你要做的是别把路堵死。把每个设备的资源放进私有结构体用设备树属性承载差异用dev级别的日志和sysfs区分实例这一套组合拳在瑞芯微平台和别的Linux平台上都通用。我在RK3568上调过多路同型号Sensor最深的体会是多实例驱动的问题从来不在怎么写而在怎么隔离。先确保每份数据各回各家剩下的事情都会顺理成章。如果你在移植驱动时也遇到两个设备互踩的问题先回头检查一下代码里还有没有不该存在的全局变量。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。