ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux驱动支持多个设备的两个小技巧:基于瑞芯微平台实例解析

2026/9/7 15:43:59 拓冰建站 浏览量
Linux驱动支持多个设备的两个小技巧:基于瑞芯微平台实例解析 Linux驱动支持多个设备的两个小技巧 | 基于瑞芯微平台前阵子在RK3588的板子上调试一个多传感器方案板子上挂了两片同型号的I2C温湿度芯片结果驱动只响应第一片设备第二片的数据怎么读都是同一个值。查到最后问题就出在驱动源码里那个经典全局静态变量上。这个场景在嵌入式Linux里太常见了一颗SoC要同时管理两个同类型外设而网上大量Linux驱动教程只教你写“一个设备”的情况。今天不讲大而全的Linux驱动框架就聊基于瑞芯微平台、在真实项目中能用上的两个小技巧一个是每个设备实例独立管理数据另一个是让一套驱动代码稳定区分并服务多个实例。这两个技巧不是炫技全是踩坑踩出来的经验对刚接触Linux驱动开发和做AIoT方案的工程师都值得花几分钟看完。在瑞芯微平台RK3568、RK3588、RV1106等上做Linux驱动绕不开设备树、platform驱动、I2C/SPI等子系统。很多项目的“多设备需求”其实很简单同一颗芯片同一条总线挂两片同样的传感器或Codec驱动要能分别控制、分别上报。但内核驱动模型和裸机单片机不一样你不能简单定义个数组把设备A的数据放data[0]设备B的放data[1]因为你不知道probe的顺序也不知道设备树哪天会多一个节点。下面把问题拆开再给出两条实测可用的路线。1. 先聊清楚问题什么场景下驱动要同时管多个设备1.1 我遇到的实际案例我在瑞芯微RK3588平台上的一个边缘计算盒子里需要采集两路相同的温湿度数据用于机房不同区域的监控。硬件方案很直接I2C2总线上挂两颗SHT30地址分别是0x44和0x45。驱动是现成的工业级驱动但第一次适配时我把所有状态都存在一个全局结构体里结果第二颗芯片regmap初始化时直接把第一颗覆盖了show结果永远只有一路数据。类似的案例还很多音频产品上I2C总线挂两颗音频Codec做多声道视觉产品里多个相同图像信号处理器要独立配置工业网关里两路RS485都用同一款UART转接芯片。表面上看是“驱动支持多个设备”本质上是个“状态隔离”问题内核把设备抽象成一个个struct device每个设备实例必须有自己独立的运行时状态不能所有实例共用一份容易互相覆盖的全局变量。1.2 多个设备给驱动带来的三个改变第一是存储模型变了。单片机开发里用数组或全局变量管理多路外设是天经地义的但在Linux驱动里设备是动态发现、动态销毁的一个固定数组很容易越界或浪费。第二是设备号分配变了。字符设备需要一个主设备号加多个次设备号来区分同类型设备很多新人在注册cdev时只用固定次设备号第二个设备注册就报“Device or resource busy”。第三是中断和时钟资源变了。多个设备可能共享同一个中断号、同一路时钟驱动不能只考虑“我有中断就响应”还要判断中断到底属于哪个实例。1.3 瑞芯微平台的驱动框架准备在展开技巧之前先明确瑞芯微平台下驱动的基本套路。RK平台和主流ARM SoC一样使用设备树描述硬件资源。比如一个I2C外设节点通过compatible vendor,device和驱动里的of_match_table匹配总线核心在检测到设备时自动调用驱动的probe回调remove回调负责资源回收。整个生命周期是设备树节点 - bus/驱动核心匹配 - probe - 正常工作 - remove。在RK平台上I2C控制器驱动是i2c-rk3x.c它本身支持多controller每个i2c0、i2c1节点就是一个独立的I2C适配器。你驱动要接管的多个设备可能是同一个适配器上的多个客户端也可能是不同适配器上的各自客户端。设备树里每条节点都对应一个struct device所以驱动probe会被调用“设备数量”次。理解这一点后面两个技巧才顺理成章。2. 技巧一每个实例一套私有数据probe里逐一接客2.1 为什么必须放弃全局变量不少Linux驱动教程喜欢写“模块全局变量”比如static struct my_chip_data *g_data; static int major_num;这种写法在只有一个设备时没有问题可一旦板子上出现两个设备probe被调用两次g_data只会指向最后一次分配的内存前一个设备的所有数据就“丢了”。这不是驱动模型不支持而是你用了错误的状态存储方式。正确做法是把每个设备所有状态放进一个私有结构体一次probe分配一份用dev_set_drvdata(dev, data)保存。之后无论读、写、中断回调都可以通过dev_get_drvdata(dev)或container_of找回来。用生活类比就是酒店每个房间有自己的保险柜而不是酒店门口放一个公用储物柜谁住店都往里面塞东西。2.2 设备树里怎么声明多个同型号设备以两个I2C温湿度传感器为例在RK3588设备树里这样描述i2c2 { status okay; temp_sensor0: temp44 { compatible vendor,th-sensor; reg 0x44; label ch0; interrupt-parent gpio3; interrupts RK_PA1 IRQ_TYPE_EDGE_RISING; }; temp_sensor1: temp45 { compatible vendor,th-sensor; reg 0x45; label ch1; interrupt-parent gpio3; interrupts RK_PA2 IRQ_TYPE_EDGE_RISING; }; };内核I2C子系统会为0x44和0x45分别创建i2c_client设备各自触发一次probe。这里的label可以自定义方便驱动里区分通道reg决定设备地址同一总线上不能相同中断可以各接一路也可以共享一路后面技巧二会讲。设备树编写要注意缩进、段落和status字段。status okay表示启用如果哪一路硬件没贴建议改成status disabled或者干脆不写否则probe失败会在内核日志里留下大量错误。RK平台还有pinctrl-0 i2c2_xfer这类引脚配置一般放在控制器节点里不用重复写到每个子节点。2.3 probe里如何接住每个设备看一下标准的probe代码结构这是“多个设备”支持的地基static int th_sensor_probe(struct i2c_client *client) { struct th_sensor_data *data; struct device *dev client-dev; int ret; data devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >struct th_sensor_data { struct i2c_client *client; struct cdev cdev; struct device *dev; struct class *class; int minor; char label[16]; struct mutex lock; u8 last_humidity; s16 last_temperature; };每个实例都带有自己的cdev、自己的次设备号和自己的互斥锁。这里的互斥锁很重要多线程访问多个设备时锁也必须按实例隔离。接着再看次设备号的分配。不要手动硬编码0、1、2而是用内核提供的ida分配器static DEFINE_IDA(th_sensor_ida); static int th_sensor_alloc_minor(void) { return ida_alloc(th_sensor_ida, GFP_KERNEL); } static void th_sensor_free_minor(int minor) { ida_free(th_sensor_ida, minor); }ida_alloc会返回一个空闲的最小次设备号不会冲突。配合cdev_add和device_create每个实例就能在/dev下拥有独立的节点例如/dev/th_sensor0、/dev/th_sensor1。设备的dev_t可以从MKDEV(major, minor)生成其中major可以是动态申请也可以是固定的主设备号。3. 技巧二一套代码慢慢调靠私有数据契约区分不同实例3.1 先理解“一份驱动多个实例”的本质内核驱动模型本身就是为多实例设计的。同样是th_sensor_probeI2C子系统的i2c_add_driver一旦注册成功总线上每出现一个匹配的设备节点就会调用一次probe。所以你可以把驱动想象成一条生产线设备树是订单probe是造一台机器remove是回收机器。问题是用户空间打开/dev/th_sensor0时内核怎么知道该操作哪个实例的数据答案是通过file-private_data。每次open时在struct file里保存对应的struct th_sensor_data *后续read、ioctl、mmap就能从private_data里拿回正确实例。static int th_sensor_open(struct inode *inode, struct file *file) { struct th_sensor_data *data container_of(inode-i_cdev, struct th_sensor_data, cdev); file-private_data data; return 0; } static long th_sensor_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct th_sensor_data *data file-private_data; /* 用>static LIST_HEAD(th_sensor_list); static DEFINE_MUTEX(th_sensor_list_lock); static int th_sensor_probe(...) { ... mutex_lock(th_sensor_list_lock); list_add_tail(data-list, th_sensor_list); mutex_unlock(th_sensor_list_lock); }然后在ioctl里通过th_sensor_get_by_index(idx)遍历链表找到第idx个设备。这种方式灵活但要注意并发问题设备可能在运行时被移除如果查询到一半设备消失链表节点会被释放再访问就是悬空指针。所以更推荐用内核的idr机制给每个设备分配整数ID查询时按ID查找并且持有引用计数避免use-after-free。3.3 idr与container_of两种高效查找实例的方式先看IDR的用法。IDR是内核提供的一种整数ID到指针映射表相当于一个“按ID查找对象的哈希表”适合动态编号、热插拔场景static DEFINE_IDR(th_sensor_idr); static int th_sensor_probe(...) { int id; data devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); ... id idr_alloc(th_sensor_idr, data, 0, 0, GFP_KERNEL); if (id 0) return id; >static irqreturn_t th_sensor_irq_handler(int irq, void *dev_id) { struct th_sensor_data *data dev_id; u8 status; /* 先读状态寄存器确认本次中断是不是属于这个设备 */ status i2c_smbus_read_byte_data(data-client, REG_STATUS); if (!(status DATA_READY)) return IRQ_NONE; >dmesg | grep th_sensor如果出现failed to get ...多半是设备树属性名写错比如label写成了lable。如果出现no such device可能是compatible字符串与驱动里的of_match_table不一致瑞芯微推荐命名规范是“厂家名,型号”例如rockchip,rk3588-saradc、vendor,th-sensor。probe返回负错误码时驱动核心会认为设备匹配失败所以probe函数中每个可能失败的步骤都要带上准确的错误码。比如-ENODEV表示硬件不存在-EINVAL表示参数错误别一律返回-1否则板级调试时你根本看不出问题在哪。4.2 同I2C地址冲突怎么处理同一个I2C适配器上不能有两个reg相同的设备。如果硬件设计失误导致两个芯片地址一样最稳妥的办法是硬件上把其中一片的地址引脚改掉比如SHT30的ADDR引脚接高电平或低电平就能切换地址。软件上也可以用i2c_mux或者pinctrl切换总线但会增加复杂度和不稳定风险我不建议在量产方案里这样干。如果两个设备不在同一总线上而是分别挂在i2c2和i2c3那就不存在地址冲突但驱动probe会各自运行私有数据隔离依然要做。很多工程师以为“不在同一总线就不需要处理多设备”其实只要驱动匹配多个节点存储隔离这件事就逃不掉。4.3 中断只有一路响应另一路没反应这是共享中断最容易踩的坑。可能原因有三个第一设备树里两个节点的interrupts属性没有配成同一GPIO实际硬件却接在了一起第二中断处理函数把两个设备的中断都吞了返回了IRQ_HANDLED但只处理了data指向的那一路第三中断触发类型不一致一个设置了IRQ_TYPE_EDGE_RISING另一个设置了IRQ_TYPE_LEVEL_LOW共享时会出问题。我建议多设备共享中断时统一使用IRQ_TYPE_LEVEL_LOW并且处理函数里主动读设备状态寄存器判断到底有没有数据。千万别依赖中断号的唯一性中断号在这个场景下并不能分辨具体设备。4.4 用sysfs和debugfs快速验证多实例瑞芯微平台的内核默认开启CONFIG_DEBUG_FS用debugfs可以非常方便地验证多个设备实例是否都活着。驱动里加一个简单的debugfs入口static int th_sensor_show(struct seq_file *m, void *v) { struct th_sensor_data *data m-private; seq_printf(m, %s: temp%d, hum%d\n, >debugfs_create_file(data-label, 0444, th_sensor_debugfs_dir, data, th_sensor_debugfs_fops);这样你就能在开发板上的/sys/kernel/debug/th_sensor/下看到ch0、ch1两个文件分别读它们的内容确认两个设备都在独立工作。实测下来这个调试手段比反复改应用层代码高效得多。另外你也可以在sysfs里暴露label和温度等属性方便应用层枚举设备。多设备驱动的sysfs属性目录建议按label或索引命名不要全叫device0这种含糊名字。4.5 常见问题速查表现象可能原因排查方法解决办法只有第一个设备工作第二个设备读取失败全局变量被覆盖、设备树节点未生成dmesg查看probe日志检查/sys/bus/i2c/devices下节点数量使用devm_kzalloc/私有结构体确保每个实例独立数据第二个设备probe失败报Device or resource busy次设备号冲突class/device_create冲突dmesg使用ida_alloc/ idr动态分配编号共享中断只有一路有效IRQF_SHARED未设置、dev_id未传实例查看cat /proc/interrupts对应中断状态添加IRQF_SHARED处理函数返回IRQ_NONE/IRQ_HANDLED准确判断设备节点存在但open失败cdev_add与device_create顺序错误查看/proc/devices和/sys/class先cdev_add再device_create卸载驱动时崩溃remove释放顺序错误打开内核panic日志先device_destroy再cdev_del最后释放私有数据设备树节点改了没生效设备树编译缓存重新编译dtb确认u-boot加载分区使用fdtdump检查实际加载的设备树内容5. 最后再分享一点我的体会这类多设备支持的问题说穿了就是“怎么把一份驱动逻辑跑在多份硬件实例上”。瑞芯微平台和主流内核框架已经把设备模型、总线和设备树这些基础设施都铺好了我们要做的不是去绕开它而是顺着probe、remove、私有数据这条路线走。我在实际项目里的体会是多设备支持不是把代码写复杂而是把状态划分清楚把每个实例的边界收住。优先用devm_*系列函数分配资源优先用device属性暴露差异优先用动态设备号避免人为编号。遇到问题时先查设备树匹配、再查私有数据是否独立、最后查资源冲突基本能覆盖九成以上的坑。如果后面你把这套驱动继续扩展可以考虑给每个实例增加独立电源控制、独立时钟使能、独立sysfs读写接口甚至支持热插拔的USB转串口芯片。万变不离其宗只要每个实例都有自己的私有数据和清晰的查找路径多设备就是水到渠成的事情。这套经验我在RK3568、RK3588和RV1126上都试过希望你也能少走弯路一次点亮所有设备。