ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

瑞芯微Linux驱动实战:多设备支持与实例数据管理技巧

瑞芯微Linux驱动实战:多设备支持与实例数据管理技巧 在瑞芯微平台上做嵌入式 Linux 驱动开发我遇到得最多的一个需求就是“同一个驱动要同时管多个设备”。多路串口、多路 I2C 触摸屏、多路 PWM、多路同型号 ADC基本都是同一颗芯片内部集成好几个相同控制器或者板子上同一个接口挂了多个同型号芯片。很多刚从裸机转过来的朋友第一版驱动往往只写单个设备一接两个设备就各种串数据、找错寄存器、probe 只跑一次。这篇文章我把自己在 RK 平台上调试多设备驱动时最常用的两个小技巧拆开讲一遍一个是设备树侧的匹配方法另一个是驱动内部实例数据的管理方法都是实际项目里能直接抄的方案。1. 先从“一个驱动挂多个设备”这个事说起1.1 为什么驱动必须支持多个设备很多人觉得“一个驱动对应一个设备”是天经地义的但在瑞芯微这类 SoC 平台上情况完全不是这样。一颗 RK3568 内部可能有五路 UART、四路 SPI、多路 I2C、多路 PWM它们虽然基地址不同、中断号不同但 IP 核设计是同源的Linux 内核绝不可能为每一路单独写一个驱动文件那样代码冗余到没法维护。正确的做法是写一个 driver让它能匹配多个设备节点。设备树里每路外设各占一个节点driver 的 probe 函数会被内核多次调用每匹配上一个节点就跑一次 probe相当于一份代码服务多个硬件实例。我见过很多新手写驱动时probe 里不保存任何实例信息全部用全局变量结果第二个设备 probe 时把第一个设备的状态覆盖掉了所有功能乱套。所以“支持多个设备”不是一个锦上添花的功能而是瑞芯微平台驱动的默认要求。你写的每一行驱动代码都要默认“我可能被调用很多次每次都要独立工作”。1.2 瑞芯微平台默认的匹配方式瑞芯微平台的驱动大多是基于设备树Device Tree驱动的。内核对设备与驱动的匹配有一套完整的机制其中最常见的匹配方式就是通过 compatible 字符串。设备树节点里写compatible rockchip,rk3568-uart驱动里定义of_device_id数组并声明compatible rockchip,rk3568-uart内核就会把它们配对。这个机制本身就支持一对多设备树里可以有 10 个节点写同一个 compatible驱动只要一个 of_device_id 条目就能全部匹配。内核会对每一个设备节点调用一次 probe并把当前节点的struct platform_device *pdev传进来。你的驱动能不能正确区分“我这次 probe 的是哪个节点”就成了多设备支持的关键。1.3 两个小技巧分别解决什么问题基于上面的背景我把项目里最实用的两个技巧拆出来第一个技巧解决“驱动怎么匹配多个设备节点”的问题核心是设备树节点设计和of_match_table的写法第二个技巧解决“匹配之后多个实例的数据怎么隔离、怎么管理”的问题核心是实例私有数据和字符设备多设备节点的管理方式。这两个技巧是递进关系。设备树匹配做好了probe 才能被正确调用多次私有数据管理做好了多次 probe 之后驱动才能真正稳定工作。下面分别展开讲。2. 小技巧一设备树多节点 of_match_table让同一个驱动认识多个设备2.1 设备树节点怎么定义在瑞芯微平台设备树一般写在arch/arm64/boot/dts/rockchip/目录下。假设你的板子上有两个同型号的测试外设一个挂在 0x10000 地址另一个挂在 0x20000 地址分别有独立中断那么设备树可以这样描述test_dev0: test-multi10000 { compatible rockchip,test-multi; reg 0x0 0x10000 0x0 0x1000; interrupts GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH; status okay; }; test_dev1: test-multi20000 { compatible rockchip,test-multi; reg 0x0 0x20000 0x0 0x1000; interrupts GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH; status okay; };这里两个节点用了完全相同的 compatible这是允许的。节点的名字和 label 可以不同方便区分reg描述了各自的物理地址和长度interrupts描述各自的中断号。驱动侧看到两个 compatible 相同的节点会各自生成一个platform_device。需要特别注意的是reg中地址和长度的写法。瑞芯微的 ARM64 平台默认使用#address-cells 2和#size-cells 2所以 reg 的一个条目由 4 个数组成高 32 位地址、低 32 位地址、高 32 位长度、低 32 位长度。如果只写两个数设备树解析时会出错驱动里platform_get_resource拿到的资源就可能是错的。2.2 驱动里的 of_match_table 这样写驱动侧的匹配表一般这样定义static const struct of_device_id test_multi_of_match[] { { .compatible rockchip,test-multi, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, test_multi_of_match); static struct platform_driver test_multi_driver { .probe test_multi_probe, .remove test_multi_remove, .driver { .name test_multi, .of_match_table test_multi_of_match, }, }; module_platform_driver(test_multi_driver);这段代码最关键的是MODULE_DEVICE_TABLE(of, test_multi_of_match)。很多人写驱动时容易漏掉这一行如果驱动是编译成模块.ko加载的少了这行内核的模块工具就无法从模块里提取 compatible 信息即使设备树节点写得完全正确modprobe 也可能加载了驱动却匹配不上设备。当两个节点都匹配上之后内核会分别调用test_multi_probe。你可以把这一条理解为“同一个函数分别以 dev0 和 dev1 为参数执行两次”。probe 函数里不能写死任何地址和中断号要从pdev里动态获取。2.3 用 .data 字段区分不同子版本有时候两个节点硬件型号相近但不完全相同比如寄存器配置有差别。我一般会在of_match_table里用.data字段把不同硬件版本的信息带进去static const struct test_multi_config test_v1 { .fifo_depth 64, .has_dma false, }; static const struct test_multi_config test_v2 { .fifo_depth 128, .has_dma true, }; static const struct of_device_id test_multi_of_match[] { { .compatible rockchip,test-multi-v1, .data test_v1 }, { .compatible rockchip,test-multi-v2, .data test_v2 }, { /* sentinel */ } };设备树里对应节点分别写rockchip,test-multi-v1或rockchip,test-multi-v2probe 里通过of_device_get_match_data(pdev-dev)就可以取到对应的配置结构体。这个做法的好处是驱动代码不用写一堆if (of_machine_is_compatible(...))之类的判断硬件差异被数据化加新版本时只需要加一张表和一组节点。我在实际项目里用它区分过 RK3568 和 RK3588 两个平台上的同一个外设 IPprobe 里一行判断都没加全走配置表。2.4 资源要一套一套拿多实例驱动的 probe 里拿资源时最忌讳写死地址。正确做法是按照索引从pdev里取static int test_multi_probe(struct platform_device *pdev) { struct resource *res; struct test_multi_priv *priv; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; irq platform_get_irq(pdev, 0); if (irq 0) return irq; priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(priv-base)) return PTR_ERR(priv-base); priv-irq irq; platform_set_drvdata(pdev, priv); return 0; }这套写法是标准的 platform 驱动资源获取模板。platform_get_resource(..., 0)取第一个reg条目对应的物理地址platform_get_irq(..., 0)取第一个中断。每个设备节点各自维护自己的pdev所以这里的priv对应各自的寄存器基地址和中断号互不干扰。我补充一个细节拿到struct resource之后可以用resource_size(res)校验地址区间大小避免 ioremap 时越界。这看起来是小事情但在多节点设备中一个节点的 reg 写错了长度往往会把相邻控制器的寄存器映射到同一段虚拟地址排查起来极其痛苦。3. 小技巧二实例私有数据与多实例字符设备管理3.1 全局变量是万恶之源第一次写多设备驱动的人最容易犯的错误就是用一堆全局变量保存寄存器地址、缓冲区、设备号。单个设备时一切正常第二个节点 probe 时所有全局变量被覆盖两个设备瞬间乱成一锅粥。我举一个直观的例子如果你在 probe 里写g_base devm_ioremap(...)第二个节点 probe 时这个变量被覆盖成第二个设备的地址。应用层打开第一个设备节点去读寄存器读到的却是第二个设备的寄存器。更恶心的是这种错误不会导致崩溃只是数据完全错误特别难定位。正确的思路是为每个设备实例创建一份独立的私有数据所有状态都放在私有结构体里任何对硬件操作的函数都要先拿到“当前是哪个实例”这个指针。3.2 platform_set_drvdata / get_drvdata 的妙用内核为 platform 驱动提供了现成的实例数据绑定接口platform_set_drvdata(pdev, priv);以及从struct device中反查struct test_multi_priv *priv platform_get_drvdata(pdev);如果拿到的只有struct device *dev也可以通过platform_get_drvdata(to_platform_device(dev))转换。这套接口本质上就是把这个指针挂在struct device的driver_data字段上随设备生命周期管理。因为每个pdev对应一个私有数据指针多个实例之间天然隔离不需要自己维护什么 map。我在项目里习惯在 probe 的最后统一调用platform_set_drvdata(pdev, priv)然后在 remove、suspend/resume、以及 sysfs 回调里都通过platform_get_drvdata(pdev)拿数据。这样整个驱动所有回调函数都不需要碰全局变量。3.3 字符设备怎么做多个实例如果你的驱动要暴露/dev/test_multi0、/dev/test_multi1这样的节点给应用层那就涉及到多实例字符设备管理。有几个细节需要处理。首先是设备号。我推荐用动态分配的方式static int test_multi_cdev_init(void) { int ret; dev_t devno; ret alloc_chrdev_region(devno, 0, CONFIG_TEST_MULTI_MAX_DEVICE, test_multi); if (ret 0) return ret; test_multi_major MAJOR(devno); return 0; }alloc_chrdev_region的第二个参数是起始次设备号第三个参数是设备个数。这里直接传入最大支持多少个实例就能一次性预留一段次设备号空间。然后是每个 probe 实例的 cdev 注册。每个实例都有一份独立的struct cdev放在各自的私有数据里struct test_multi_priv { void __iomem *base; int irq; int id; struct cdev cdev; struct device *dev; dev_t devno; struct mutex lock; // ... 其他状态 };probe 里做 cdev 初始化时要注意次设备号怎么分配。我一般用idr或者简单的atomic_inc_return来分配实例编号保证每个节点的编号唯一。这里不展开 idr 的完整代码只展示一个简单可用的方式在 probe 里给priv-id赋一个自增编号然后priv-devno MKDEV(test_multi_major, priv-id); cdev_init(priv-cdev, test_multi_fops); priv-cdev.owner THIS_MODULE; ret cdev_add(priv-cdev, priv-devno, 1); if (ret 0) goto err_cdev; priv-dev device_create(test_multi_class, pdev-dev, priv-devno, priv, test_multi%d, priv-id);这里关键的一步是device_create的最后一个参数priv它会把私有数据指针绑定到设备上。应用层 open 这个设备节点时内核会调用test_multi_open(struct inode *inode, struct file *filp)你在 open 里通过container_of(inode-i_cdev, struct test_multi_priv, cdev)就能拿到当前打开的是哪个实例。static int test_multi_open(struct inode *inode, struct file *filp) { struct test_multi_priv *priv container_of(inode-i_cdev, struct test_multi_priv, cdev); filp-private_data priv; return 0; }这样 read、write、ioctl 里统一从filp-private_data拿实例指针static ssize_t test_multi_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct test_multi_priv *priv filp-private_data; // 使用 priv-base 操作当前实体的寄存器 }这套流程走下来无论系统里注册了几个设备每个文件描述符都能精确找到自己对应的那路硬件。3.4 中断处理函数里怎么拿当前实例中断处理函数没有filp也没有pdev它只有一个void *dev_id参数。这个参数从哪来从request_irq的第五个参数来。我在 probe 里注册中断时会把私有数据指针传进去ret devm_request_irq(pdev-dev, irq, test_multi_isr, IRQF_TRIGGER_RISING, test_multi, priv); if (ret 0) return ret;中断来临后static irqreturn_t test_multi_isr(int irq, void *dev_id) { struct test_multi_priv *priv dev_id; // 读 priv-base 的寄存器清中断等 return IRQ_HANDLED; }这里有个多设备场景的常见坑如果两个设备的中断号相同比如板子设计把两个外设的 IRQ 都接到了同一个 GPIO 中断线上你注册时就得加IRQF_SHARED共享标志。使用共享中断时内核会把这个 irq 上注册的所有 handler 都执行一遍你的 ISR 里必须判断硬件状态不是本设备的中断要立刻返回IRQ_NONE。把priv作为 dev_id 传入后每个 handler 拿到的都是自己的私有数据判断起来非常干净。我在瑞芯微平台上遇到过类似情况两个外设通过板级逻辑共用了一个 GPIO 中断注册时没加IRQF_SHARED结果第二个设备注册中断直接失败。加了共享标志并把 dev_id 替换为各自的私有数据后问题迎刃而解。4. 实操RK3568 上写一个双实例字符设备驱动4.1 完整设备树片段下面这个例子基于 RK3568设备树里定义了两路 test_multi 设备并加了简单的自定义属性方便演示属性读取/ { test_multi0: test-multi10000 { compatible rockchip,test-multi; reg 0x0 0x10000 0x0 0x1000; interrupts GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH; rockchip,bus-id 0; status okay; }; test_multi1: test-multi20000 { compatible rockchip,test-multi; reg 0x0 0x20000 0x0 0x1000; interrupts GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH; rockchip,bus-id 1; status okay; }; };注意这里我用了两个不同的寄存器地址它们不一定真实存在于 RK3565 内存映射中实际项目里换成你自己的 IP 基地址即可。GIC_SPI 宏在设备树头文件里默认可用。4.2 驱动核心代码这里给出一个最小可工作框架代码省略部分硬件操作重点展示多实例结构。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/interrupt.h #include linux/cdev.h #include linux/device.h #include linux/fs.h #include linux/io.h #include linux/slab.h #include linux/mutex.h #define TEST_MULTI_MAX_DEVICE 8 struct test_multi_priv { void __iomem *base; resource_size_t size; int irq; int id; struct cdev cdev; struct device *dev; dev_t devno; struct mutex lock; u32 bus_id; }; static int test_multi_major; static struct class *test_multi_class; static int test_multi_open(struct inode *inode, struct file *filp) { struct test_multi_priv *priv container_of(inode-i_cdev, struct test_multi_priv, cdev); filp-private_data priv; return 0; } static ssize_t test_multi_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct test_multi_priv *priv filp-private_data; if (!priv || !priv-base) return -EINVAL; // 从 priv-base 读取硬件寄存器再拷贝到用户空间 // 这里只做占位实际项目按硬件协议填充 return count; } static long test_multi_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct test_multi_priv *priv filp-private_data; mutex_lock(priv-lock); // 根据 cmd 执行硬件操作 mutex_unlock(priv-lock); return 0; } static const struct file_operations test_multi_fops { .owner THIS_MODULE, .open test_multi_open, .read test_multi_read, .unlocked_ioctl test_multi_ioctl, }; static irqreturn_t test_multi_isr(int irq, void *dev_id) { struct test_multi_priv *priv dev_id; u32 status; status readl(priv-base); if (!(status 0x1)) return IRQ_NONE; // 清中断、处理数据 writel(status, priv-base); return IRQ_HANDLED; } static int test_multi_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct test_multi_priv *priv; struct resource *res; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; mutex_init(priv-lock); res platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENXIO; priv-size resource_size(res); 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 of_property_read_u32(dev-of_node, rockchip,bus-id, priv-bus_id); if (ret 0) dev_warn(dev, missing rockchip,bus-id property, use default\n); priv-id 0; // 实例编号分配简单方式自增长 /* 实际项目建议用 ida/idr */ priv-id atomic_inc_return(test_multi_inst_cnt) - 1; priv-devno MKDEV(test_multi_major, priv-id); cdev_init(priv-cdev, test_multi_fops); priv-cdev.owner THIS_MODULE; ret cdev_add(priv-cdev, priv-devno, 1); if (ret 0) return ret; priv-dev device_create(test_multi_class, dev, priv-devno, priv, test_multi%d, priv-id); if (IS_ERR(priv-dev)) { cdev_del(priv-cdev); return PTR_ERR(priv-dev); } ret devm_request_irq(dev, priv-irq, test_multi_isr, 0, test_multi, priv); if (ret 0) { device_destroy(test_multi_class, priv-devno); cdev_del(priv-cdev); return ret; } platform_set_drvdata(pdev, priv); dev_info(dev, test_multi%d probed, irq%d, base%pa\n, priv-id, priv-irq, res-start); return 0; } static int test_multi_remove(struct platform_device *pdev) { struct test_multi_priv *priv platform_get_drvdata(pdev); device_destroy(test_multi_class, priv-devno); cdev_del(priv-cdev); // devm_* 资源会自动释放这里只需处理非 devm 的资源 return 0; } static const struct of_device_id test_multi_of_match[] { { .compatible rockchip,test-multi, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, test_multi_of_match); static struct platform_driver test_multi_driver { .probe test_multi_probe, .remove test_multi_remove, .driver { .name test_multi, .of_match_table test_multi_of_match, }, }; static atomic_t test_multi_inst_cnt ATOMIC_INIT(0); static int __init test_multi_init(void) { dev_t devno; int ret; ret alloc_chrdev_region(devno, 0, TEST_MULTI_MAX_DEVICE, test_multi); if (ret 0) return ret; test_multi_major MAJOR(devno); test_multi_class class_create(test_multi); if (IS_ERR(test_multi_class)) { unregister_chrdev_region(devno, TEST_MULTI_MAX_DEVICE); return PTR_ERR(test_multi_class); } ret platform_driver_register(test_multi_driver); if (ret 0) { class_destroy(test_multi_class); unregister_chrdev_region(devno, TEST_MULTI_MAX_DEVICE); return ret; } return 0; } static void __exit test_multi_exit(void) { platform_driver_unregister(test_multi_driver); class_destroy(test_multi_class); unregister_chrdev_region(MKDEV(test_multi_major, 0), TEST_MULTI_MAX_DEVICE); } module_init(test_multi_init); module_exit(test_multi_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Rockchip multi-instance test driver);这段代码是从实际项目中精简出来的有几个点需要特别说明。atomic_t自增分配实例编号的方式在高并发注册时可能会乱序但一般设备节点是固定的、probe 顺序也相对固定够用。不过如果你对编号顺序有严格依赖建议换ida接口它能保证释放和重用的顺序。devm_request_irq用起来非常方便设备 remove 时会自动释放中断不需要在 remove 里手动free_irq。但要注意devm_系列资源释放顺序是后注册的先释放如果你在中断处理函数里引用了其他devm_管理的资源要确认释放顺序是否安全。我在实际代码里更喜欢显示调用free_irq图个心安。4.3 注册与注销顺序注意点模块 init 函数里我先把字符设备区域分配好再创建设备类最后注册 platform 驱动。顺序不能乱先alloc_chrdev_region保证后续 cdev_add 时设备号有效再class_create这样 device_create 才能创建设备节点最后platform_driver_register因为这一步会立刻触发匹配probe 里马上要用到前面两步的成果。一般建议编译成模块用insmod加载时在串口日志里能看到类似test_multi1 probed, irq44, base0x0000000000010000 test_multi0 probed, irq43, base0x0000000000020000probe 顺序不一定要和设备树节点顺序一致由内核遍历顺序决定所以千万别依赖 probe 顺序来判断设备编号。这也是我建议用device_create里的名字或priv-id显式指定设备节点的原因。4.4 应用层怎么验证加载驱动后先检查设备节点ls -l /dev/test_multi*正常会看到两个节点crw------- 1 root root 239, 0 Jan 1 00:00 /dev/test_multi0 crw------- 1 root root 239, 1 Jan 1 00:00 /dev/test_multi1再确认 sysfs 里的匹配关系ls /sys/bus/platform/devices/ | grep test-multi能看到形如test-multi10000和test-multi20000的目录。进去读 of_node 下的 compatible可以确认设备树匹配是否正常。应用层测试时分别打开/dev/test_multi0和/dev/test_multi1做同样的读写操作如果两路数据相互独立说明私有数据隔离生效。如果数据交叉错乱回到驱动里检查是不是还有全局变量被两个实例共享。5. 常见问题与排查技巧实录5.1 常见问题速查表我把在瑞芯微平台上调试多设备驱动时遇到的典型问题整理成一张表按出现频率排序。问题现象可能原因排查方法probe 只调用一次只有第一个节点有设备第二个节点 status 为 disabledcompatible 字符串不匹配检查设备树 status用compatible在 sysfs 中反查两个设备的功能互相串扰私有数据没用全局变量保存了状态检查代码中是否有非 const 全局变量全部改为 priv 成员设备节点只出现一个alloc_chrdev_region第三参数过小cdev_add 使用相同次设备号确认 max 设备数足够确认编号分配唯一中断申请失败第二个设备中断号冲突或已占用检查 dmesg按需加 IRQF_SHARED读取寄存器地址错误设备树 reg 地址或长度写错ioremap 失败用 devm_ioremap_resource 代替手动 ioremap能自动校验卸载驱动时死机remove 里释放顺序错误中断还在跑数据被释放remove 先 free_irq再 del cdev再 device_destroymodprobe 加载 .ko 后驱动不匹配缺少 MODULE_DEVICE_TABLE检查驱动源码补上模块设备表两个实例的 bus-id 属性读出来一样of_property_read_u32 写在公共函数里用同一个 out 变量确认读出来的值存入 priv 成员别存全局5.2 调试现场记录这里分享一个我调试 RK3568 多路 PWM 时的现场。现象是两路 PWM 都能注册成功但明明配置的是第一路输出波形却从第二路引脚上出来。打开 sysfs 里两个节点的of_node对比发现两路 PWM 的reg都指向了同一个物理地址。后来检查设备树发现我写节点时漏加了第二路的reg属性DTC 编译时没报错内核解析后两个节点共用了父节点的默认地址。正确写法是每个节点显式添加reg 0x0 0x20000 0x0 0x1000。这类问题靠代码审查很难看出来但用cat /proc/iomem | grep pwm会立刻暴露。还有一次更隐蔽两个设备用同一个中断号都注册成功但一个设备工作正常另一个设备的中断偶尔丢。加IRQF_SHARED后正常了。原因在于板级设计里两个外设确实复用了一根中断线内核要求共享中断必须显式声明不声明的话第二个请求中断时直接返回 -EBUSY 还好但如果你没检查返回值驱动照样跑中断却永远不会触发。所以我建议platform_get_irq之后devm_request_irq的返回值一定要检查平台上中断资源有限失败是很常见的。5.3 避坑心得多设备驱动调试我的体会是先把日志打好再谈功能。probe 里用dev_info(pdev-dev, ...)把实例编号、物理地址、中断号全部打印出来而不是用printk(probe ok\n)这种没有上下文的日志。dev_dbg系列会自动打印设备名配合dynamic_debug可以在调试时动态开关不用重新编译内核。另外我强烈建议 read/write/ioctl 里也加一层带实例编号的调试日志哪怕只是打印priv-id。多设备问题有一个特点单设备时完全正常加第二个设备才暴露。如果每处操作都能看到实例编号就能快速锁定是哪一路出了问题。关于锁的问题多说一句。多实例驱动中每个实例要有自己的mutex或spinlock放在私有结构体里。千万不要共用一个全局锁否则两个设备本来互不相关却会因为一把锁互相阻塞性能出现问题还特别难排查。如果你在 ioctl 里对两个实例的寄存器都要操作那就分别对两把锁上锁锁顺序要固定避免死锁。最后再分享一个实用小技巧如果你手头没有真实的第二路硬件又想验证多实例逻辑是否正确可以在设备树里临时添加一个测试节点reg复用某个空闲地址compatible用同一个字符串先让驱动跑两个 probe 再说。这样不等硬件回来就能把实例隔离、字符设备节点、中断分配这些逻辑全部测通。我在瑞芯微平台上的经验是多设备支持的相关问题大多在 probe 阶段就会被devm_资源管理接口拦住只要每个实例的私有数据拿对了后面应用层的操作一般不会跑偏。这些方法是这几年做 RK 平台驱动下来最实在的积累照着这套思路写你的驱动从第一版开始就不会留下“只支持单设备”的坑。
返回列表