在瑞芯微平台做Linux驱动开发,总有那么一个瞬间让你怀疑人生:驱动代码明明写得很顺,一个外设跑得好好的,但只要同类设备接到第二个,系统就开始出幺蛾子。要么第二个设备根本没被probe,要么两个设备的数据互相覆盖,要么中断申请直接失败。这几乎是每个写过平台驱动、I2C驱动或者SPI驱动的人都会撞上的墙。今天我就结合自己在RK3568、RK3588这类板子上的实际调试经历,聊聊Linux驱动支持多个设备的两个小技巧:怎么让同一个驱动干净利落地接管多份硬件资源,以及怎么写才能避免多实例互相踩踏。这两个技巧不一定能让你的代码一步到位,但至少能帮你少走好几条弯路。
很多刚开始接触Linux驱动的朋友会有个误区,觉得一个驱动模块对应一个硬件设备是天经地义的。但在真实产品里,完全不是这么回事。瑞芯微这类SoC平台外设资源非常丰富,同一颗芯片上经常要挂多路同型号器件,驱动侧如果设计不好,后面量产、改版、调试都会很痛苦。下面我从实际场景出发,把这两个技巧掰开揉碎讲清楚。
1. 为什么“一个驱动管多个设备”在瑞芯微方案里是刚需
1.1 BSP日常最多见的几类多实例场景
拿RK3568来说,这颗芯片的I2C、SPI、UART、PWM、GPIO资源都不少,实际产品里最常见的多实例需求有这么几类:
一是同型号Sensor多路接入。比如两片一样的温度传感器挂在两个不同的I2C总线上,或者四路同型号触摸屏控制芯片接在同一个I2C控制器下面。这些外设用的是同一份通讯协议,寄存器定义完全一样,唯一的区别是总线地址或挂在哪个控制器上。这种情况最适合共用一个驱动。
二是多路PWM输出。散热风扇、背光、蜂鸣器、指示灯,一个产品里用三四路PWM太常见了。每路PWM的周期和占空比需求可能不一样,但控制逻辑完全相同。如果每一路都单独写一个驱动,代码里除了寄存器地址和GPIO编号不同,剩下几乎一模一样,纯粹是给自己找维护负担。
三是多路音频Codec或者多路摄像头。这个在RK3588这种偏旗舰的平台更明显,一片Codec管喇叭,另一片Codec管麦克风阵列,或者几路Camera Sensor共用同一个控制器。这些外设往往还要配合不同的电源时序和复位引脚,更需要驱动层面把每路实例的数据隔离开。
1.2 两个技巧分别解决“匹配”和“隔离”两件事
支持多个设备说起来简单,但实际操作里其实是两个完全不同层面的问题。
第一个层面是匹配问题:内核怎么知道系统里有几个这样的设备,并且把同一个驱动“分配”给每一个设备?这个主要靠设备树描述解决。每个外设节点在设备树里写好compatible属性,驱动侧通过of_match_table声明自己能处理哪个compatible,内核在启动阶段就会自动为每个节点创建一个device,然后调用对应驱动的probe。节点有几个,probe就被调用几次。
第二个层面是运行时隔离问题:probe被调用几次之后,驱动手里的全局变量、缓冲区、锁、设备号这些资源如果不分开管理,两个设备的数据就会互相干扰。常见翻车现场就是驱动里写了一个static结构体指针,第二个外设probe时直接把指针覆盖了,结果打开/dev/fan0设置占空比,实际改的是/dev/fan1对应的PWM。
这两个技巧一次讲透,正好对应上面的两个层面。设备树负责把硬件描述清楚,驱动私有数据负责把软件运行状态隔离好。二者配合起来,一个驱动管八个设备也不会乱。
2. 技巧一:设备树多节点 + compatible 匹配,让同一个驱动接管N个外设
2.1 从设备树到probe的内核匹配过程
很多人把设备树和驱动匹配想得很玄乎,其实原理并不复杂。内核在启动阶段会把设备树里带compatible属性的节点解析出来,生成相应的struct device,并挂到对应的总线上。比如根节点下的PWM风扇节点,会被当成platform_device挂到platform总线上;I2C控制器子节点下的外设,会被I2C core实例化成i2c_client。
驱动模块加载时,内核会拿驱动里of_match_table中的compatible列表,去和每个设备的compatible属性比对。只要相等,就会调用这个驱动的probe函数,并把当前这个设备的struct device指针传进去。关键点在于:这个过程是逐节点进行的。设备树里有几个同compatible节点,就会构造几个设备,probe就会被调用几次。
所以同一个驱动支持多个设备的第一个前提非常简单:把多个设备节点写进设备树,并且保持compatible一致。你不需要为第二个设备写第二个驱动,也不需要给compatible加后缀去区分“第一个风扇”“第二个风扇”。
2.2 多节点设备树怎么写
以双路PWM风扇为例,板级dts里可以这样描述:
/ { aliases { fan0 = &fan0; fan1 = &fan1; }; fan0: pwm-fan0 { compatible = "vendor,pwm-fan"; pwms = <&pwm0 0 25000 0>; default-speed = <128>; status = "okay"; }; fan1: pwm-fan1 { compatible = "vendor,pwm-fan"; pwms = <&pwm1 0 25000 0>; default-speed = <64>; status = "okay"; }; }; &pwm0 { status = "okay"; }; &pwm1 { status = "okay"; };这里有几个细节值得注意。第一,两个节点虽然设备名不同,一个是pwm-fan0,一个是pwm-fan1,但compatible都是"vendor,pwm-fan",驱动只需要匹配这一条就行。第二,default-speed是自定义属性,用来描述每路风扇的默认转速,后面的驱动代码会分别读取。第三,aliases里的fan0/fan1不是必需的,但有了它,驱动里可以用of_alias_get_id拿到一个从0开始的实例编号,给设备节点命名、申请资源都会方便很多。
如果不加aliases,也没有关系,那就需要驱动自己维护一份实例数组,或者用自增全局编号。前者多写一点代码,后者容易踩并发与新设备热插拔的坑。所以我建议能加alias就加。
这里要提醒一下,瑞芯微SDK里的dtsi文件通常已经定义好了pwm0、pwm1这些控制器节点,而且很多默认状态是disabled。光在板级dts里写客户设备节点不够,控制器节点本身必须status = "okay",否则PWM时钟和管脚配置不会生效,驱动在pwm_get阶段就会失败。
2.3 驱动侧of_match_table与每个实例独立执行
驱动侧的标准做法是这样的:
static const struct of_device_id pwm_fan_of_match[] = { { .compatible = "vendor,pwm-fan" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, pwm_fan_of_match); static struct platform_driver pwm_fan_driver = { .probe = pwm_fan_probe, .remove = pwm_fan_remove, .driver = { .name = "pwm_fan", .of_match_table = pwm_fan_of_match, }, }; module_platform_driver(pwm_fan_driver);当这两个pwm-fan0和pwm-fan1节点都在,且驱动加载成功后,dmesg里应该看到两行probe日志。每一行probe里拿到的struct platform_device *pdev都是不同的,pdev->dev.of_node分别指向不同节点。所以probe函数里用of_property_read_u32读取default-speed时,天然就只读到当前节点自己的属性,不需要额外处理。
这也是很多人没想明白的一点:同一个驱动模块在系统里只有一个代码副本,但因为probe会被调用多次,每次带着不同的device参数,所以运行起来其实是多份“实例”在各自干活。只要你别把状态数据放到全局变量里,Linux的设备模型已经替你完成了绝大多数区分工作。
2.4 属性读取的常见误区
提到属性读取,很多初学朋友会犯一个很低级的错误:在probe外面用某个全局变量去保存当前节点的device_node。比如写一个static struct device_node *fan_np;,然后probe里执行fan_np = pdev->dev.of_node;。这样的代码在单设备场景下不会出问题,但第二路风扇probe时,这个指针就被改成第二路的节点了。之后第一个风扇想读取自己的default-speed,拿到的全是第二路的配置。
正确的做法是只在probe函数栈上使用of_node,把读出来的属性值存进该实例的私有数据里。比如读个u32转速,就存到一个结构体成员中。千万不要把device_node指针留到其他回调函数里再用,因为你根本不知道它什么时候会被覆盖。
如果确实需要在读写接口里重新查设备树,也应该是基于当前实例自己的struct device *去拿of_node,再配合形参里的属性名字符串去读取,而不是用一个全局np。
3. 技巧二:多实例私有数据管理,绕开全局变量这个巨坑
3.1 翻车典型:一个全局指针导致两个设备互相覆盖
如果说设备树是“能不能匹配上”的问题,那驱动内部的运行状态管理就是“匹配上之后会不会乱”的问题。我见过太多次这种翻车了:
static struct pwm_fan_priv *g_fan; static int pwm_fan_probe(struct platform_device *pdev) { struct pwm_fan_priv *priv; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); g_fan = priv; ... }第一路风扇probe之后,g_fan指向实例A;第二路风扇probe之后,g_fan被改成实例B。所有read、write、ioctl回调里如果都用g_fan,那打开/dev/fan0去设置占空比,最后操作的实际是第二路PWM。这种问题在单设备测试时完全发现不了,只有硬件接上第二路设备、或者做双设备同时读写测试时才会暴露。
比这更隐蔽的是全局锁、全局缓冲区、全局计数器的滥用。比如两个实例都用同一个static struct mutex lock;,理论上不会数据错乱,但两路风扇本来可以独立调节,却被一把锁串行化,稍微高频访问一点,性能损耗就会显现。又比如ioctl里的临时缓冲区用的是static u8 buf[64];,两个实例并发操作时,数据互相踩踏,最后调出来的PWM波形可能完全不对。
3.2 私有数据结构体 + dev_set_drvdata 的标准套路
正确的姿势是把每个实例所有必要的状态变量都塞进一个结构体,在probe里一次性分配,然后用dev_set_drvdata挂到这个设备的struct device上。后面任何回调想拿当前实例数据,都只跟这个device打交道。
还是以PWM风扇为例,私有数据结构体可以这么设计:
struct pwm_fan_priv { struct device *dev; unsigned int index; unsigned int duty; struct pwm_device *pwm; struct mutex lock; struct miscdevice mdev; };probe函数里:
static int pwm_fan_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct pwm_fan_priv *priv; struct device_node *np = dev->of_node; u32 default_speed = 0; int ret; priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->dev = dev; mutex_init(&priv->lock); priv->pwm = devm_of_pwm_get(dev, np, NULL); if (IS_ERR(priv->pwm)) return PTR_ERR(priv->pwm); of_property_read_u32(np, "default-speed", &default_speed); priv->duty = default_speed; priv->index = of_alias_get_id(np, "fan"); if (priv->index < 0) priv->index = 0; priv->mdev.minor = MISC_DYNAMIC_MINOR; priv->mdev.name = devm_kasprintf(dev, GFP_KERNEL, "fan%d", priv->index); priv->mdev.fops = &pwm_fan_fops; priv->mdev.parent = dev; ret = misc_register(&priv->mdev); if (ret) return ret; dev_set_drvdata(dev, priv); dev_info(dev, "pwm fan%d probed, pwm=%s, default duty=%d\n", priv->index, pwm_get_name(priv->pwm), priv->duty); return 0; }注意这里用了devm_kzalloc和devm_of_pwm_get,这种devm系列接口的好处是资源跟着设备生命周期走,probe失败或者设备移除时内核自动帮你释放,不容易内存泄漏。pwm_get回来的struct pwm_device *也是实例独有,不是全局共用。
dev_set_drvdata(dev, priv)这行很关键。它把当前probe创建出来的priv指针,绑定到当前这个platform_device对应的struct device上。等remove被调用的时候,你用dev_get_drvdata(&pdev->dev)就能拿回同一个实例,不会搞混。
3.3 字符设备节点与fops回调里如何找回当前实例
有了私有数据,还需要知道fops回调里怎么找到当前打开的是哪个实例。这里根据用的设备框架不同,写法略有区别。现在做单设备或多个同类设备的字符驱动,我强烈推荐miscdevice框架,省心。
miscdevice本质上是一个字符设备,但次设备号由内核动态分配,设备名直接通过misc_register创建在/dev下,对多实例非常友好。关键点在于,misc_open在调你的open之前,已经帮你在file->private_data里放好了当前打开的设备对应的struct miscdevice *。
所以open回调里可以这么拿实例:
static int pwm_fan_open(struct inode *inode, struct file *filp) { struct pwm_fan_priv *priv = container_of(filp->private_data, struct pwm_fan_priv, mdev); filp->private_data = priv; return 0; }这里filp->private_data指向的是miscdevice成员,也就是priv结构体里的mdev字段,所以用container_of反推出整个priv结构体的首地址。后续的ioctl、read、write、release里直接struct pwm_fan_priv *priv = filp->private_data;,就是当前打开节点的实例数据了。
ioctl示例:
static long pwm_fan_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct pwm_fan_priv *priv = filp->private_data; unsigned int duty; switch (cmd) { case FAN_IOC_SET_DUTY: if (copy_from_user(&duty, (void __user *)arg, sizeof(duty))) return -EFAULT; if (duty > 255) return -EINVAL; mutex_lock(&priv->lock); pwm_config(priv->pwm, pwm_get_period(priv->pwm) * duty / 255, pwm_get_period(priv->pwm)); pwm_enable(priv->pwm); priv->duty = duty; mutex_unlock(&priv->lock); return 0; default: return -ENOTTY; } }整个过程没有使用任何全局变量,每个实例独享自己的priv、锁、PWM句柄和占空比状态。
如果你用的是传统字符设备框架,自己调用register_chrdev_region和cdev_add,那么open里可以通过container_of(inode->i_cdev, struct my_priv, cdev)来拿实例,原理也是类似的。区别只是miscdevice帮你省掉了主设备号分配和cdev_add这些样板代码。
3.4 中断、锁和资源的每实例隔离
多实例驱动里还有一个经常被忽视的深坑:中断处理函数。很多人在单设备驱动里习惯这么写:
static int irq_num; static irqreturn_t my_isr(int irq, void *dev_id) { struct my_priv *priv = dev_id; ... } static int my_probe(struct platform_device *pdev) { irq_num = platform_get_irq(pdev, 0); request_irq(irq_num, my_isr, 0, "my_device", NULL); }这里irq_num是全局变量,request_irq的最后一个参数又传了NULL。单设备时没问题,多设备时就出事了:中断来了以后,内核调用的是同一个irq handler,但handler根本不知道本次中断属于哪个实例。如果两个设备都触发中断,你连是哪个设备产生的中断都分不清。
正确做法是request_irq(irq, my_isr, 0, "my_device", priv);,把当前实例的priv作为dev_id传进去,handler里再把dev_id转回priv。这样即使两个设备共用同一个中断号,也能通过检查每个设备的硬件状态寄存器来确认到底是谁要处理。使用共享中断时,还要在request_irq里加上IRQF_SHARED标志。
锁也是一样。每个实例一把锁,写在priv结构体里就够了。只有需要跨实例保护全局资源(比如同一个外设总线的公共寄存器区域)时,才考虑用全局锁或驱动级锁。
4. 实操:RK3568上双路PWM风扇共用一个驱动
4.1 硬件连接与设备树节点规划
具体跑一遍比干讲理论有用得多。假设场景是RK3568核心板加一块底板,底板上使用PWM0和PWM1这两路控制器各接一个4线PWM风扇,需要提供一个驱动同时输出/dev/fan0和/dev/fan1两个控制节点。
硬件连接可以简单理解成两路PWM输出,两路风扇的转速反馈先不接,只需要驱动输出可调占空比的PWM信号。温度监控和自动调速逻辑可以放在应用层,也可以后续再加thermal策略,驱动层面先把基本控制能力做出来。
设备树按照前面写的方案,在板级dts根节点下增加两个子节点,并在aliases里登记实例编号。PWM控制器节点确保使能:
/ { aliases { fan0 = &fan0; fan1 = &fan1; }; fan0: pwm-fan0 { compatible = "vendor,pwm-fan"; pwms = <&pwm0 0 25000 0>; default-speed = <128>; status = "okay"; }; fan1: pwm-fan1 { compatible = "vendor,pwm-fan"; pwms = <&pwm1 0 25000 0>; default-speed = <64>; status = "okay"; }; }; &pwm0 { status = "okay"; }; &pwm1 { status = "okay"; };pwms = <&pwm0 0 25000 0>这行里,第一个字段是PWM控制器phandle,第二个字段是PWM通道号,第三个字段是周期25000ns,也就是40kHz,第四个字段是PWM默认极性,0表示正常极性。实际频率选择要看风扇规格,有些风扇对PWM频率比较敏感,一般20kHz到40kHz问题不大。
4.2 驱动代码骨架
驱动整体代码骨架如下,为了篇幅我做了精简,但关键点都在:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/pwm.h> #include <linux/of.h> #include <linux/of_device.h> #include <linux/miscdevice.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/mutex.h> #define FAN_IOC_SET_DUTY _IOW('F', 0x01, unsigned int) struct pwm_fan_priv { struct device *dev; unsigned int index; unsigned int duty; struct pwm_device *pwm; struct mutex lock; struct miscdevice mdev; }; static int pwm_fan_open(struct inode *inode, struct file *filp) { struct pwm_fan_priv *priv = container_of(filp->private_data, struct pwm_fan_priv, mdev); filp->private_data = priv; return 0; } static long pwm_fan_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct pwm_fan_priv *priv = filp->private_data; unsigned int duty; switch (cmd) { case FAN_IOC_SET_DUTY: if (copy_from_user(&duty, (void __user *)arg, sizeof(duty))) return -EFAULT; if (duty > 255) return -EINVAL; mutex_lock(&priv->lock); pwm_config(priv->pwm, pwm_get_period(priv->pwm) * duty / 255, pwm_get_period(priv->pwm)); pwm_enable(priv->pwm); priv->duty = duty; mutex_unlock(&priv->lock); return 0; default: return -ENOTTY; } } static const struct file_operations pwm_fan_fops = { .owner = THIS_MODULE, .open = pwm_fan_open, .unlocked_ioctl = pwm_fan_ioctl, }; static int pwm_fan_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct pwm_fan_priv *priv; struct device_node *np = dev->of_node; u32 default_speed = 0; int ret; priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->dev = dev; mutex_init(&priv->lock); priv->pwm = devm_of_pwm_get(dev, np, NULL); if (IS_ERR(priv->pwm)) return PTR_ERR(priv->pwm); of_property_read_u32(np, "default-speed", &default_speed); priv->duty = default_speed; priv->index = of_alias_get_id(np, "fan"); if (priv->index < 0) priv->index = 0; priv->mdev.minor = MISC_DYNAMIC_MINOR; priv->mdev.name = devm_kasprintf(dev, GFP_KERNEL, "fan%d", priv->index); priv->mdev.fops = &pwm_fan_fops; priv->mdev.parent = dev; ret = misc_register(&priv->mdev); if (ret) return ret; dev_set_drvdata(dev, priv); dev_info(dev, "pwm fan%d probed, pwm=%s, default duty=%d\n", priv->index, pwm_get_name(priv->pwm), priv->duty); return 0; } static int pwm_fan_remove(struct platform_device *pdev) { struct pwm_fan_priv *priv = dev_get_drvdata(&pdev->dev); if (priv->pwm) pwm_disable(priv->pwm); misc_deregister(&priv->mdev); return 0; } static const struct of_device_id pwm_fan_of_match[] = { { .compatible = "vendor,pwm-fan" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, pwm_fan_of_match); static struct platform_driver pwm_fan_driver = { .probe = pwm_fan_probe, .remove = pwm_fan_remove, .driver = { .name = "pwm_fan", .of_match_table = pwm_fan_of_match, }, }; module_platform_driver(pwm_fan_driver); MODULE_LICENSE("GPL");代码里有几个容易踩坑的点。of_alias_get_id(np, "fan")依赖aliases里fan0/fan1的定义,如果拿不到alias会返回负数,所以后面兜底设置为0。这样就保证即使只有一个设备节点,也能注册成fan0。devm_kasprintf生成的“fan%d”设备名必须唯一,这就是为什么每个实例要么用alias编号、要么用自增编号,不能让两个实例都叫“fan”。
4.3 编译、加载与验证
在瑞芯微SDK的kernel源码目录下,把驱动文件放到drivers/misc或者其他你喜欢的目录,也可以做成外部模块单独编译。外部模块编译命令大致是:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -C /path/to/kernel M=$(pwd) modules编译通过后,把pwm_fan.ko推到板子上,加载:
insmod pwm_fan.ko dmesg | tail -n 20正常时你应该看到两类信息:一类是PWM控制器自己的probe日志,另一类是我们的pwm_fan驱动打印的:
pwm_fan pwm-fan0: pwm fan0 probed, pwm=..., default duty=128 pwm_fan pwm-fan1: pwm fan1 probed, pwm=..., default duty=64同时检查设备节点:
cat /proc/misc | grep fan ls -l /dev/fan0 /dev/fan1/proc/misc里能看到两个独立的次设备号,/dev下也有两个节点,说明多实例注册成功。
写一个简单的测试程序调用ioctl设置占空比,验证两个节点是否互不影响:
#include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <fcntl.h> #include <unistd.h> #define FAN_IOC_SET_DUTY _IOW('F', 0x01, unsigned int) int main(int argc, char *argv[]) { int fd; unsigned int duty = 128; if (argc > 1) duty = (unsigned int)atoi(argv[1]); fd = open("/dev/fan0", O_RDONLY); if (fd < 0) { perror("open fan0"); return 1; } if (ioctl(fd, FAN_IOC_SET_DUTY, &duty) < 0) perror("ioctl fan0"); close(fd); fd = open("/dev/fan1", O_RDONLY); if (fd < 0) { perror("open fan1"); return 1; } if (ioctl(fd, FAN_IOC_SET_DUTY, &duty) < 0) perror("ioctl fan1"); close(fd); return 0; }先给fan0设128,再给fan1设64,然后分别读回priv->duty或者用示波器看PWM波形,确认两路互相不干扰。如果两个设备的状态全部正常,说明私有数据隔离这一层已经做对了。
5. 多设备驱动常见问题与排查套路
5.1 为什么第二个设备没有被probe
这是多设备驱动最常遇到的问题,现象是dmesg里只有一行probe日志,第二路设备完全没有反应。排查顺序一般是先看设备树最终生成的内容,再看驱动匹配。
瑞芯微平台通常会在编译uboot或kernel时生成dtb,设备树经过编译后可以直接用工具反编译确认。比较快的办法是板子上执行fdtdump或者使用dtc -I dtb -O dts看一下dtb里是否真的包含两个节点。很多时候你以为改了dts,但实际上编译用的dts不是你想的那一份,或者kernel没有继承新的dtb,最终加载的还是老设备树。
确认设备树没问题后,检查两个节点的compatible是否一致、status是否为okay。然后看/sys/bus/platform/devices/下有没有对应的pwm-fan节点出现。如果设备节点都出现了,但驱动只有一次probe,大概率是of_match_table里只有一个compatible条目,又或者两个节点的compatible写得不完全一样,比如多了个空格、大小写不同。
另一种情况是probe返回了-EPROBE_DEFER。PWM控制器或者时钟domain还没准备好时,内核会把设备放回等待队列,等依赖资源available后再重新probe。这时候dmesg里会反复出现“probe deferred”相关日志,一般等一段时间自己会好。如果是长期挂在那里不probe,就要查PWM控制器的status是否okay,pinctrl配置是否冲突。
5.2 设备节点被打开了但数据错乱
设备节点存在,open也成功,但操作一个设备时另一个设备跟着变,或者操作返回的数据是另一路的。这种情况十有八九是私有数据拿错了。
先搜索驱动里有没有全局指针、全局缓冲区、全局数组下标。如果所有状态都放在priv结构体里,再看open里的container_of写得对不对。最常见的错误是container_of用的不是正确的成员名,比如miscdevice在结构体里的字段是mdev,却写成了misc,编译不报错但运行时拿到的priv地址就是错的。
还有一点需要特别注意,如果你从filp->private_data里拿出来的不是miscdevice指针,而是自己在open里重新赋值的指针,那就要确认open调用顺序。misc_open在回调驱动open之前,已经把struct miscdevice放进了file->private_data。你在open里覆盖成priv是合法的,但容器宏必须基于原始的miscdevice指针来做,否则结构体偏移会算错。
5.3 中断、资源申请与并发问题
多个实例probe成功,但一运行就重启、死机或者数据不对,多半和中断、资源共享、并发保护有关。
两个设备如果使用独立中断号,中断handler里的dev_id必须传priv,不能传NULL。两个设备如果共用同一个中断号,request_irq必须加IRQF_SHARED标志,并且handler里要通过读硬件寄存器判断当前中断属于谁,不需要自己处理时要返回IRQ_NONE,避免干扰另一个设备的中断处理。
并发问题也很容易在双风扇操作时暴露。两个风扇节点同时被应用层打开,同时调用ioctl,如果两路实例共用了一个全局缓冲区,或者PWM配置寄存器组被两个实例同时读写,波形就会乱。解决思路就是每个实例的PWM操作都加上自己的锁,不要把锁做在驱动模块级别的全局变量上。
我把几个高频问题整理成了速查表,方便现场排查时对照:
| 现象 | 大概率原因 | 处理方法 |
|---|---|---|
| 只有一路probe | 设备树节点缺失/status disabled/compatible不一致 | dtc反编译dtb确认节点,检查of_match_table |
| probe一直没执行 | 依赖的PWM控制器或时钟资源未ready | 检查PWM节点status、pinctrl配置 |
| 打开设备后操作错乱 | 全局变量或container_of用错 | 改用priv结构体+dev_set_drvdata,检查open容器宏 |
| misc_register报名字重复 | 两个实例mdev.name相同 | 用of_alias_get_id或自增编号生成不同节点名 |
| 中断混乱 | dev_id传NULL或共享中断未加IRQF_SHARED | request_irq里传priv,共享中断查状态寄存器 |
| 并发操作数据异常 | 多个实例共用全局锁/全局缓冲区 | 锁和缓冲区放到每个实例的priv结构体里 |
最后分享一个调试小习惯。遇到多实例驱动问题,我一般先不急着改代码,而是在probe和关键回调里加几行dev_info/dev_err,把实例的prt指向、priv->index、设备节点名都打出来。驱动模型只要能看到probe调用了两次、两个priv地址不同,问题范围就已经缩小了一大半。剩下的无非是设备树描述和回调里取实例数据这两个方向。调试完记得把这些临时日志改成dev_dbg,避免刷屏影响后续稳定性测试。
我在瑞芯微平台上做过不少多实例外设驱动,总的感觉是:写probe之前先把所有实例相关的变量往一个结构体里塞,再想设备树要配几个节点。结构体没设计好,后面调多设备就是无底洞。这两个技巧其实并不神奇,设备树负责把硬件描述清楚,私有数据负责把软件运行状态隔离好,二者配合到位,一个驱动管八个外设也不会乱。希望这篇东西能帮你在做RK系列或者其他Linux平台的多设备驱动时少踩几个坑。