瑞芯微Linux驱动多设备管理:compatible兼容与多实例隔离实践
在瑞芯微平台上做Linux驱动开发最常遇到的一个需求就是“一个驱动要管多个设备”。比如板子上接了两颗同型号的I2C传感器或者一颗SoC通过不同总线挂了同一款触摸屏芯片再或者一个系列的产品线用了不同型号的codec你想只维护一份驱动代码就把它们全撑起来。这事说大不大说小不小处理不好就是probe冲突、资源覆盖、中断抢断调试的时候能把人折磨到怀疑人生。这篇文章我结合瑞芯微RK3568、RK3588、RV1106这几个平台上实际跑过的项目梳理出两个最实用的技巧一个是怎么用compatible兼容多个不同型号的设备另一个是怎么让同一个compatible下的多个实例各自安好、互不干扰。内容偏实践代码和设备树片段都是可以直接拿去改的适合正在做BSP、驱动移植或者被多设备驱动困扰的朋友参考。1. 多设备场景与驱动设计思路1.1 哪些场景下需要“一个驱动管多个设备”很多刚接触驱动开发的同学会觉得一个驱动对应一个设备一一对应不是挺清晰吗但在实际产品里情况远没有这么理想。我列几个最常见的场景你看看是不是都遇到过第一个场景同型号芯片的多路接入。比如一个RK3588主板上同时接了四路同型号的ADC芯片分别采集不同的模拟信号。这些芯片挂在不同的I2C总线上地址可能相同也可能不同。如果按常规思路每路写一个独立驱动代码重复度极高而且后续如果要统一修改某个初始化流程得改四个文件维护成本直接起飞。第二个场景不同型号芯片的兼容替换。消费类产品经常会遇到“这颗料涨价了换一颗兼容的”。通常替换芯片的功能寄存器基本一致但芯片型号、部分配置参数不同。这时候你希望驱动能自动识别当前板子上到底是哪一颗然后走对应的初始化流程而不是为了换料单独fork一个驱动分支。第三个场景同一设备树节点下有多个子设备。例如一个MIPI-DSI接口上挂了触摸屏触摸芯片和显示驱动虽然是两个不同的驱动但它们共享同一个电源域、同一个复位引脚。这种场景下两个驱动要协调好资源不能各自乱抢。这三个场景的共同点就是硬件上存在多个实例或者多个变种但驱动逻辑高度相似。如果你不做统一管理最终代码会变成一坨到处是ifdef的分支补丁过两个月你自己都看不懂当时是怎么想的。1.2 设备树如何参与驱动与设备的匹配要说清楚多设备支持必须先搞明白Linux里设备和驱动是怎么牵线搭桥的。在设备树Device Tree进入主流视野之前驱动和设备之间通常靠平台设备编号、总线ID这类硬编码方式匹配代码里到处是数组和宏定义扩展一个新设备要改驱动源码再重新编译非常痛苦。现在瑞芯微平台的标准做法是设备树里描述硬件长什么样驱动里声明自己支持哪些compatible内核在启动阶段自动完成匹配。整个过程可以简化成三步第一步设备树节点中有一个compatible属性比如compatible vendor,device-model;这个字符串就是设备的“身份证”。它可以有多个值按优先级从高到低排列。第二步驱动里定义一个of_device_id数组声明自己认哪几张“身份证”static const struct of_device_id mydrv_of_match[] { { .compatible vendor,device-model, }, { }, }; MODULE_DEVICE_TABLE(of, mydrv_of_match);第三步当设备树解析到这个节点时内核会遍历所有已注册驱动的of_match_table找到compatible匹配的那个驱动然后调用驱动的probe函数把struct device指针传进去。这里有一个关键点一次匹配就会调用一次probe。也就是说如果设备树里有两个同compatible的节点驱动只写一份但probe会被调用两次。这个特性正是我们实现多设备支持的基础。理解了这一层后面的两个技巧就顺理成章了。2. 技巧一用compatible兼容多个型号的设备2.1 设备树侧的写法先说第一种情况同一个驱动要兼容多个芯片型号。这种需求在国产替代的浪潮里特别常见。比如原来用的是A公司的传感器后来换成B公司管脚兼容、寄存器兼容的芯片或者同一家厂商的芯片分标准版和高配版初始化流程略有差异。设备树里最简单直接的做法就是在不同板型或者不同外设节点上写不同的compatible。比如标准版用GT911高配版用GT9271/* 标准版设备树 */ i2c2 { status okay; gt911_std: touchscreen5d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio3; interrupts 15 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio3 14 GPIO_ACTIVE_LOW; }; }; /* 高配版设备树同一个驱动支持 */ i2c2 { status okay; gt9271_high: touchscreen5d { compatible goodix,gt9271; reg 0x5d; interrupt-parent gpio3; interrupts 15 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio3 14 GPIO_ACTIVE_LOW; irq-flags 8; }; };两个节点的寄存器地址相同都在0x5d中断脚也是同一个唯一的区别是compatible字符串。这样一来同一块主板只需要更换触摸芯片和对应的设备树驱动不用重新编译。这个“设备树描述硬件差异、驱动只保留通用逻辑”的边界一定要把握好。2.2 驱动侧的of_device_id与私有配置设备树侧已经做了区分驱动侧怎么感知到当前是哪一个型号呢关键就在of_device_id数组的.data字段。这个字段可以挂一个指针指向任意类型的数据结构。我们可以把不同型号的设备差异放到一个结构体里让匹配的过程直接把对应配置“带”出来。看一段实际可用的内核驱动框架#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h /* 每个型号的差异配置 */ struct gt9xx_chip_config { const char *chip_name; u32 irq_flags; // 中断触发方式 int reset_delay_ms; // 复位延时 bool support_gesture; // 是否支持手势唤醒 }; static const struct gt9xx_chip_config gt911_config { .chip_name GT911, .irq_flags IRQ_TYPE_LEVEL_LOW, .reset_delay_ms 20, .support_gesture false, }; static const struct gt9xx_chip_config gt9271_config { .chip_name GT9271, .irq_flags IRQ_TYPE_LEVEL_LOW, .reset_delay_ms 50, .support_gesture true, }; /* 匹配表compatible 字符串 与 配置结构体绑定 */ static const struct of_device_id gt9xx_of_match[] { { .compatible goodix,gt911, .data gt911_config }, { .compatible goodix,gt9271, .data gt9271_config }, { }, }; MODULE_DEVICE_TABLE(of, gt9xx_of_match);然后在probe函数里通过of_match_device拿到当前匹配到的配置static int gt9xx_probe(struct i2c_client *client) { const struct gt9xx_chip_config *cfg; const struct of_device_id *match; match of_match_device(gt9xx_of_match, client-dev); if (match match-data) { cfg match-data; dev_info(client-dev, detected %s\n, cfg-chip_name); /* 按照 cfg 里的参数执行差异化初始化 */ } else { return -EINVAL; } /* 剩余通用初始化代码 */ return 0; }如果用的是platform_driver而不是i2c_driver逻辑完全一样只是of_match_device的第一个参数类型略有区别。核心思路就是把“变”的部分抽出来放到data里“不变”的部分留在probe里通用处理。2.3 一个完整的兼容匹配示例为了让大家看得更清晰我补一个platform_driver的完整骨架。这个例子模拟的是瑞芯微平台上常见的PWM背光驱动分别兼容两种不同厂商的背光控制芯片#include linux/module.h #include linux/platform_device.h #include linux/pwm.h #include linux/of.h #include linux/of_device.h #include linux/delay.h struct backlight_chip_cfg { const char *name; int max_brightness; int min_duty_ns; int max_duty_ns; }; static const struct backlight_chip_cfg chip_a_cfg { .name chip-a, .max_brightness 255, .min_duty_ns 1000, .max_duty_ns 1000000, }; static const struct backlight_chip_cfg chip_b_cfg { .name chip-b, .max_brightness 1023, .min_duty_ns 500, .max_duty_ns 2000000, }; static const struct of_device_id backlight_of_match[] { { .compatible vendor,backlight-a, .data chip_a_cfg }, { .compatible vendor,backlight-b, .data chip_b_cfg }, { }, }; MODULE_DEVICE_TABLE(of, backlight_of_match); static int backlight_probe(struct platform_device *pdev) { struct device *dev pdev-dev; const struct of_device_id *match; const struct backlight_chip_cfg *cfg; struct pwm_device *pwm; match of_match_device(backlight_of_match, dev); if (!match || !match-data) { dev_err(dev, no matching chip config\n); return -EINVAL; } cfg match-data; dev_info(dev, init %s backlight\n, cfg-name); pwm devm_pwm_get(dev, NULL); if (IS_ERR(pwm)) return PTR_ERR(pwm); /* 用 cfg-min_duty_ns 和 cfg-max_duty_ns 做参数配置 */ /* ... */ return 0; } static struct platform_driver backlight_driver { .probe backlight_probe, .driver { .name vendor-backlight, .of_match_table backlight_of_match, }, }; module_platform_driver(backlight_driver); MODULE_LICENSE(GPL);实际编译这个模块加载后如果设备树里的compatible是“vendor,backlight-a”就会打印“init chip-a backlight”如果是“vendor,backlight-b”就打印“init chip-b backlight”。所有不同的参数都从cfg结构体里取probe主体逻辑完全复用。2.4 常见误区与经验这里有几个我踩过坑之后总结出的经验值得多说两句。第一compatible的命名规范。强烈建议按照“厂商名,器件型号”的格式例如“goodix,gt911”厂商名一般和芯片厂商一致。有些同学随意写“mytouch”这种不带厂商名的字符串虽然能跑起来但是一旦后面要引入其他厂家的驱动很容易冲突。内核的of_match_table匹配是严格字符串比较小写、逗号都不能写错大小写也会导致匹配失败而且这类报错在日志里不太显眼排查起来很费劲。第二data字段的生命周期。.data指向的内容通常是static的全局结构体驱动生命周期内一直存在。不要在这里放那些需要在probe期间动态申请的东西。如果配置里有可变内容建议在probe里拷贝一份到动态申请的结构体中。第三不要把芯片型号的区分完全依赖在compatible上。有时候你会遇到同一个芯片、同一个compatible但不同批次或者不同硬件版本需要不同的初始化时序。这种情况建议在设备树里增加自定义属性比如“vendor,init-version”在驱动中用device_property_read_u32读取。compatible负责“选型”自定义属性负责“细节调参”两者配合使用才是完整方案。3. 技巧二同型号多实例的设备拆分管理3.1 多实例场景的典型问题说完了兼容多个型号现在来聊第二个技巧同型号芯片、多个实例。这是很多人写驱动时栽跟头最多的地方。典型场景就是在RK3568上通过两个不同的I2C控制器挂了两个同型号的IO扩展芯片或者通过两个SPI片选控制两颗同型号的ADC。这类场景最容易踩的坑有三个第一个坑是全局变量。很多从单片机和裸机开发转过来的同学习惯用全局变量保存设备状态比如static struct my_chip *g_chip;这个写法在单实例下没有任何问题但两个实例一加载第二个probe就会把g_chip覆盖掉导致第一个实例的所有操作都指向了第二个实例的硬件。轻则数据错乱重则直接操作到不存在的地址系统直接oops。第二个坑是设备号管理。如果你用alloc_chrdev_region手动分配字符设备号并且把设备号存在全局变量里第二个probe会尝试重复注册一个已经存在的设备号返回-EBUSY。你需要为每个实例分配独立的设备号并建立设备号与私有数据的映射关系。第三个坑是资源释放。如果probe过程中某个步骤失败了不能简单return了事。前面申请的中断、GPIO、时钟、内存都要释放干净否则第二次probe或者驱动卸载后再加载系统会报告资源忙好像设备被“卡死”了一样。3.2 私有数据结构与devm_资源管理解决这些问题的方法论其实很简单永远不要使用全局变量保存设备相关状态把每个实例的数据放到独立的私有数据结构中并通过dev_set_drvdata绑定到对应的struct device上。每一路设备都有一份独立的上下文互不影响。所以驱动里最核心的设计就是定义好这个私有数据结构struct my_chip_priv { struct device *dev; void __iomem *base; int irq; struct clk *clk; struct gpio_desc *reset_gpio; int id; /* 当前实例编号 */ /* 其他运行时状态 */ };配合devm系列API使用资源释放的问题也能大幅简化。devm的意思是device-managed内核会在设备driver detach时自动释放这个设备申请过的所有devm资源包括内存、IRQ、GPIO、时钟等。用devm_kzalloc、devm_request_irq、devm_clk_get、devm_gpiod_get这些接口probe失败时可以少写很多清理代码。注意devm资源是和struct device绑定的不是和struct driver绑定的。所以多实例场景下每个设备都可以有一份独立的devm资源天然适合多设备管理。3.3 完整的多实例驱动框架我写一个简化的platform_driver展示多实例场景下probe函数的正确姿势。假设瑞芯微平台的FPGA外挂了两个相同的中断控制器寄存器地址不同但驱动逻辑一样#include linux/module.h #include linux/platform_device.h #include linux/of_address.h #include linux/interrupt.h #include linux/of_irq.h struct intc_priv { struct device *dev; void __iomem *base; int irq; u32 instance_id; }; static irqreturn_t intc_isr(int irq, void *data) { struct intc_priv *priv data; u32 status; status readl(priv-base 0x00); dev_info(priv-dev, intc-%u irq status: 0x%x\n, priv-instance_id, status); writel(status, priv-base 0x04); /* 清中断 */ return IRQ_HANDLED; } static int intc_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct intc_priv *priv; struct resource *res; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-dev dev; priv-instance_id pdev-id; /* 多实例编号 */ res platform_get_resource(pdev, IORESOURCE_MEM, 0); priv-base devm_ioremap_resource(dev, res); if (IS_ERR(priv-base)) return PTR_ERR(priv-base); priv-irq platform_get_irq(pdev, 0); if (priv-irq 0) return priv-irq; ret devm_request_irq(dev, priv-irq, intc_isr, 0, dev_name(dev), priv); if (ret) { dev_err(dev, failed to request irq %d\n, priv-irq); return ret; } /* 设备私有数据与 struct device 绑定 */ dev_set_drvdata(dev, priv); dev_info(dev, intc-%u probed, base%px irq%d\n, priv-instance_id, priv-base, priv-irq); return 0; } static const struct of_device_id intc_of_match[] { { .compatible vendor,multi-intc, }, { }, }; MODULE_DEVICE_TABLE(of, intc_of_match); static struct platform_driver intc_driver { .probe intc_probe, .driver { .name vendor-multi-intc, .of_match_table intc_of_match, }, }; module_platform_driver(intc_driver); MODULE_LICENSE(GPL);设备树侧这么写/ { intc0: intc10001000 { compatible vendor,multi-intc; reg 0x0 0x10001000 0x0 0x1000; interrupts 0 45 IRQ_TYPE_LEVEL_HIGH; }; intc1: intc10002000 { compatible vendor,multi-intc; reg 0x0 0x10002000 0x0 0x1000; interrupts 0 46 IRQ_TYPE_LEVEL_HIGH; }; };内核在启动阶段扫描到两个节点会分别调用两次probe每次都会实例化一个独立的intc_priv结构体各自拥有自己的寄存器地址和中断号。第二次probe不会影响第一次probe的任何状态。3.4 如何监听和管理多个设备实例驱动加载后怎么确认两个实例都正常工作了我常用的命令是dmesg配合sysfs。如果probe成功dmesg里能看到两条“intc-0 probed”和“intc-1 probed”的日志。另外在/sys/bus/platform/devices/目录下你会看到两个以设备树节点名命名的目录比如“10001000.intc”和“10002000.intc”。这表示内核确实为这两个节点创建了独立的platform_device。代码里如果需要从某个具体的struct device回溯到对应的私有数据用dev_get_drvdata(dev)即可。比如你在某个子系统回调里拿到的是struct device *通过dev_get_drvdata就能找回你自己的intc_priv结构体。有一点要注意pdev-id在纯设备树情况下经常是-1并不是我代码里注释里写的“实例编号”。设备树派生的platform_device多数没有设置id如果你确实需要区分实例建议根据寄存器地址或者设备树里的reg属性来赋予一个逻辑编号使用of_get_address或者platform_get_resource拿到物理地址后再做一次数值映射。4. 瑞芯微平台上的实操验证流程4.1 在RK3568/RK3588上添加设备树节点前面几个章节都是通用Linux驱动知识现在结合瑞芯微平台说说实际操作。瑞芯微官方SDK中内核设备树的路径一般是kernel/arch/arm64/boot/dts/rockchip/RK3568和RK3588的主设备树文件分别是rk3568-evb.dts、rk3588-evb.dts这类以板型命名的文件。不同方案公司可能会改名为自己项目的后缀比如rk3588-myproduct.dts这个具体看SDK版本。添加设备树节点之前先确认你要挂的外设走的是什么总线。如果是I2C确认对应I2C控制器在设备树里的status是否为okay。比如RK3588的i2c2节点在SoC dtsi中默认是disabled的你必须在自己板级的dts里打开它i2c2 { status okay; pinctrl-names default; pinctrl-0 i2c2m0_xfer; /* 具体引脚 mux 看 SoC 手册 */ clock-frequency 400000; temp_sensor_0: tempsensor48 { compatible vendor,tmp117; reg 0x48; }; temp_sensor_1: tempsensor49 { compatible vendor,tmp117; reg 0x49; }; };这里我在同一个i2c2上挂了两颗地址不同的温度传感器芯片都配了compatible “vendor,tmp117”。两颗芯片共用一条I2C总线靠从机地址区分这是单总线上多个同型号设备的标准做法。如果两个设备挂不同I2C总线那设备树节点分别写在两个控制器下面就行。4.2 编译与烧录内核dtb设备树修改完需要编译生成新的dtb。瑞芯微SDK一般建议直接用SDK顶层脚本但如果你想单独编译内核和设备树也可以进到kernel目录手动执行cd kernel make ARCHarm64 rockchip_linux_defconfig # 如果还没配置过 make ARCHarm64 dtbs -j$(nproc)编译好的dtb会输出到kernel/arch/arm64/boot/dts/rockchip/目录下比如rk3588-evb.dtb。如果你的SDK支持单独打包boot.img执行./build.sh bootimg即可。烧录的时候只需要替换boot分区或者resource分区不同平台分区名不一样RK3568和RK3588常用的是boot分区。这个没太多可说的用瑞芯微的RKDevTool或者upgrade_tool烧录都行。如果你不想整机烧录也可以用uboot的tftp命令直接把dtb加载到内存里启动验证开发阶段用这种方法迭代会快很多。不过这种操作需要uboot环境变量支持而且每次重启都要重新加载适合调试不适合量产具体看你手头环境。4.3 设备与驱动的匹配验证设备树编译烧录完成后启动系统第一步看内核日志dmesg | grep tmp117如果你看到两条probe日志说明两个设备都绑定成功。如果只看到一条大概率是第二颗芯片的I2C地址写错了或者芯片没有正常上电。这时候可以用i2cdetect工具扫描一下总线设备地址i2cdetect -y -r 2其中2对应i2c-2控制器。在输出的地址矩阵里0x48和0x49位置会显示设备编号比如“48”和“49”。如果扫描不到那就是硬件连接问题先检查I2C上拉电阻、设备供电和地址引脚。确认I2C设备都能探测到之后再看设备树是否被正确解析。可以通过/proc/device-tree来读取当前生效的设备树内容cat /proc/device-tree/i2cfe8a0000/temp_sensor_0/compatible cat /proc/device-tree/i2cfe8a0000/temp_sensor_1/compatible如果都能正常输出“vendor,tmp117”说明设备树解析正常。注意/proc/device-tree里的节点名顺序和dts里的顺序存在差别最好对照/sys/firmware/devicetree/base/路径来看后者更直观。4.4 多设备的运行日志与sysfs检查设备绑定成功后再检查/sys/bus/i2c/devices/目录会看到两个以总线号和地址命名的符号链接ls -l /sys/bus/i2c/devices/ ... 2-0048 - ../../../devices/platform/fe8a0000.i2c/i2c-2/2-0048 2-0049 - ../../../devices/platform/fe8a0000.i2c/i2c-2/2-0049每个目录下都有一个driver符号链接指向你加载的驱动。这表示设备与驱动已经正确绑定。接下来可以实测两个设备是否互相独立。写一个简单的应用层测试程序通过/dev或sysfs分别对两个传感器发起读取操作观察返回值是否符合预期。如果你做的是字符设备驱动确认/dev下的两个设备节点分别对应不同硬件可以用dd或echo写入不同的寄存器地址再读取对应硬件寄存器确认。我在项目里就遇到过一种情况两个设备的I2C寄存器都能读到值但写入某个寄存器时第二个设备的值会覆盖第一个设备。排查半天发现我的驱动里用了静态变量保存I2C client指针两个probe共享了同一个client。改成每个实例独立保存后问题立刻消失。这就是典型的多设备“看起来正常、实际上串线”的问题。5. 常见问题与排查技巧实录5.1 常见问题速查表下面这几个问题是我在瑞芯微平台上调多设备驱动时反复遇到的整理成表方便你对照排查。问题现象可能原因排查方向第一个设备正常第二个probe失败全局变量覆盖导致设备状态串扰检查驱动中是否有全局或static变量保存设备私有数据两个设备只有第一个能打开设备号重复注册检查是否有全局主设备号、是否每个实例都执行了register_chrdev_region中断只触发一次之后失效中断处理函数没有正确清中断标志或两个设备复用了同一个中断号但没加IRQF_SHARED检查中断状态寄存器读写时序确认request_irq的flags是否带IRQF_SHARED设备树有两个节点但只有一个probe另一个设备节点status属性为disabled或compatible拼写错误检查dmesg中是否有“failed to match”的提示用i2cdetect确认硬件地址i2cdetect能扫到设备但驱动不probe设备树reg值与实际I2C地址不一致或驱动模块未加载检查设备树reg地址是否与芯片地址引脚配置一致确认驱动module已insmod或built-inprobe成功后读寄存器全是0xff或0x00I2C上拉异常、芯片复位引脚没有释放、供电异常用i2cdetect检查地址是否存在然后用逻辑分析仪抓I2C波形5.2 排查思路与命令多设备问题有一个通用的排查思路先确认硬件可见再确认设备树匹配最后确认驱动状态。这个顺序不能乱否则很容易被表面的驱动问题带偏。硬件可见性用i2cdetect、spidev_test这类工具验证。如果硬件都探不到驱动再怎么调也没用。设备树匹配用dmesg里的一条标志性日志mac上注册驱动后每次匹配成功内核的driver core会打印类似“tmp117 2-0048: supply vcc not found, using dummy regulator”这类信息不同内核版本略有差异。最后一个阶段看驱动内部状态可以用dev_dbg、tracepoint必要时候在probe里临时加printk打印关键变量的值。排查过程中不要忽略内核日志的优先级。用dmesg -n 8打开所有内核消息有时候resource busy这类关键信息被隐藏了只有设置日志级别才能看到。还有一个小建议开发阶段把内核的initcall_debug开关打开可以在启动日志里看到所有驱动probe的调用情况和返回值对定位“哪个驱动匹配不上”很有帮助。5.3 三个我在项目里踩过的坑说来惭愧这三个坑都是在同一个项目中踩的每次排查都花了大半天。第一个坑是把内存申请写成了普通kmalloc而不是devm_kzalloc。当时两个实例分别申请了内存第一个实例probe完成后手动释放了内存导致第二个实例的私有数据指针变成悬空指针访问时系统直接panic。后来换成devm系列接口由内核统一管理资源生命周期这个坑再也没出现过。第二个坑是中断共享。两路外部中断在SoC内部复用了同一个中断号我在request_irq时没有加IRQF_SHARED标志导致第二个设备注册中断时直接返回-EBUSY。当时查了很久后来看了SoC手册才发现这两个中断源确实共享同一个GIC中断线。解决方法是注册时加IRQF_SHARED并在中断处理函数里判断硬件寄存器状态确认中断事件是否属于自己的设备。第三个坑最有意思我在驱动里用了一个静态变量作为“当前正在处理的实例序号”想在中断下半部里区分是哪个设备触发了中断。理论上没毛病但实际上两个设备的中断频率都不低竞态条件极容易触发。要么A设备的中断下半部里处理了B设备的数据要么两个设备同时触发时只有一个能正确执行。最后改成用struct work_struct嵌入私有数据每个实例独立分配工作队列项彻底解决了并发问题。这三个坑本质上都是同一个教训多设备驱动的核心是多实例隔离。无论是内存、中断还是工作队列凡是有“状态”的东西都必须做到每设备一份。我在实际项目中还总结出了一个更省心的套路先写一个单实例驱动验证所有寄存器读写和中断逻辑没问题再扩展成多实例。单实例阶段只有全局变量排查起来非常简单一旦功能跑通再把所有全局变量“沉入”私有结构体改成devm系列接口。这样拆成两个阶段调试定位问题会快很多。如果你正打算把现有驱动改造成多设备支持不妨先试试这个思路。