
在瑞芯微平台做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 ARCHarm64 CROSS_COMPILEaarch64-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 duty128 pwm_fan pwm-fan1: pwm fan1 probed, pwm..., default duty64同时检查设备节点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是否okaypinctrl配置是否冲突。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_tableprobe一直没执行依赖的PWM控制器或时钟资源未ready检查PWM节点status、pinctrl配置打开设备后操作错乱全局变量或container_of用错改用priv结构体dev_set_drvdata检查open容器宏misc_register报名字重复两个实例mdev.name相同用of_alias_get_id或自增编号生成不同节点名中断混乱dev_id传NULL或共享中断未加IRQF_SHAREDrequest_irq里传priv共享中断查状态寄存器并发操作数据异常多个实例共用全局锁/全局缓冲区锁和缓冲区放到每个实例的priv结构体里最后分享一个调试小习惯。遇到多实例驱动问题我一般先不急着改代码而是在probe和关键回调里加几行dev_info/dev_err把实例的prt指向、priv-index、设备节点名都打出来。驱动模型只要能看到probe调用了两次、两个priv地址不同问题范围就已经缩小了一大半。剩下的无非是设备树描述和回调里取实例数据这两个方向。调试完记得把这些临时日志改成dev_dbg避免刷屏影响后续稳定性测试。我在瑞芯微平台上做过不少多实例外设驱动总的感觉是写probe之前先把所有实例相关的变量往一个结构体里塞再想设备树要配几个节点。结构体没设计好后面调多设备就是无底洞。这两个技巧其实并不神奇设备树负责把硬件描述清楚私有数据负责把软件运行状态隔离好二者配合到位一个驱动管八个外设也不会乱。希望这篇东西能帮你在做RK系列或者其他Linux平台的多设备驱动时少踩几个坑。