瑞芯微Linux驱动支持多设备:compatible匹配与私有数据隔离

📅 发布时间:2026/9/7 10:13:20
瑞芯微Linux驱动支持多设备:compatible匹配与私有数据隔离
接到一个挺典型的题目在瑞芯微平台上写 Linux 驱动希望一个驱动能同时支持多个设备。说实话这个需求在国产平台的项目里出现频率非常高尤其是 RK3568、RK3588 这类芯片经常要挂多路同型号的串口芯片、多路 ADC、多路 GPIO 扩展器或者同一套硬件方案里兼容不同型号的器件。驱动本身倒不难写难的是很多人一开始把“驱动”和“设备”理解成一对一的关系结果代码里全是全局变量第二个设备一 probe 就把第一个设备的状态覆盖了或者设备树里加了两个节点却只有一个能正常工作。这篇文章我就围绕瑞芯微平台把驱动支持多个设备时最核心的两个技巧拆开讲一个是设备树 compatible 匹配怎么做另一个是多实例的私有数据怎么管。顺便把我实际调板过程中踩过的坑和排查路径一起放出来给后面做同类项目的人省点时间。1. 先捋清楚同一个驱动“支持多个设备”到底难在哪在进入技巧之前先把问题的本质说透。Linux 驱动模型里driver 和 device 本来就是多对多的关系一个驱动匹配多个设备是内核早就支持的基本能力。但从“能匹配”到“能正常工作”中间隔着几个很现实的坎。1.1 瑞芯微平台上最常见的多设备场景拿 RK3568 的实际项目举例。最常见的是这种主控通过 I2C 挂了两颗同型号的触摸屏控制芯片或者通过 SPI 挂了两路 CAN 控制器再或者 GPIO 扩展芯片一挂就是三四颗地址不同但驱动完全一样。这时候设备树里会写多个节点内核会为每个节点创建一个 platform_device / i2c_client然后同一个 driver 的 probe 函数会被调用多次。另一个常见场景是兼容多型号。比如一颗电源管理芯片硬件上有两个版本寄存器布局略有差异或者同一个板子有两种物料驱动要同时支持。这时候设备树节点里 compatible 字段会写不同的字符串但都指向同一个驱动。这两种场景一个考验“实例隔离”一个考验“型号识别”。两个技巧正好对应解决这两件事。1.2 全局变量是第一个拦路虎很多刚从单片机转过来写驱动的同事最容易犯的错就是用模块全局变量保存设备状态。比如static struct my_dev *g_dev; static int g_open_count;第一个设备 probe 时g_dev指向设备 A第二个设备 probe 时又被覆盖成设备 B。之后任何操作都只作用于设备 B设备 A 虽然节点还在实际上已经聋了。如果是同型号多实例场景这个 bug 还不是必现的和 probe 顺序有关排查起来特别恶心。正确的思路是一个设备实例一份数据所有状态都放在这份数据里通过platform_set_drvdata或者i2c_set_clientdata挂到设备上需要的时候再取出来。这就是第二个技巧要讲的核心。1.3 设备树节点和驱动实例并不是一对一设备树里一个节点对应一个 device但一个 driver 可以匹配无数个节点。probe 函数每次被调用都带着不同的struct platform_device *pdev这个 pdev 就是当前正在被匹配的这个设备实例。驱动要做的第一件事就是为这个 pdev 分配独立的私有数据然后把 pdev 和这份数据绑定。理解了这一点后面两个技巧就順理成章了。技巧一解决“怎么匹配上”技巧二解决“匹配上之后怎么不串数据”。2. 技巧一用 of_device_id 的 compatible 数组让一个驱动挂多个设备节点先说匹配。在设备树环境下驱动匹配设备靠的是compatible字符串。一个驱动要支持多个设备核心动作就是把所有要支持的 compatible 都写进of_device_id数组里。2.1 设备树侧这样写节点假设我们这颗芯片是 I2C 设备同一块板子上挂了两颗同型号芯片设备树里写两个节点地址不同i2c1 { status okay; sensor_a: sensor1a { compatible vendor,multi-sensor; reg 0x1a; reset-gpios gpio3 RK_PA0 GPIO_ACTIVE_LOW; }; sensor_b: sensor1b { compatible vendor,multi-sensor; reg 0x1b; reset-gpios gpio3 RK_PA1 GPIO_ACTIVE_LOW; }; };两个节点的 compatible 相同内核会创建两个 i2c_client然后在总线上找 driver 来匹配。同型号多实例设备树层面没啥特殊写法就是把节点多写几个。如果是要兼容不同型号比如 V1 和 V2 两个版本sensor1a { compatible vendor,sensor-v2, vendor,sensor-v1; reg 0x1a; };这里有一个容易忽略的细节compatible 属性是一个字符串数组内核匹配的时候按顺序从左到右在驱动注册的 of_device_id 表里查找查到第一个匹配的就停。所以通常把更具体的型号放前面通用兼容型号放后面。2.2 驱动侧用 of_device_id 覆盖多个型号驱动里对应这样写static const struct of_device_id multi_sensor_of_match[] { { .compatible vendor,multi-sensor, .data sensor_v1_data }, { .compatible vendor,sensor-v1, .data sensor_v1_data }, { .compatible vendor,sensor-v2, .data sensor_v2_data }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, multi_sensor_of_match); static struct platform_driver multi_sensor_driver { .probe multi_sensor_probe, .remove multi_sensor_remove, .driver { .name multi-sensor, .of_match_table multi_sensor_of_match, }, };这里.data字段可以放一个结构体指针指向这个型号特有的信息比如寄存器地址偏移、初始化序列、工作模式等。probe 里用of_device_get_match_data(pdev-dev)取出来就能自动区分当前是哪个型号的设备。MODULE_DEVICE_TABLE这行宏很多人不写但它在模块化编译时作用很大。它会把 of_device_id 表导出到模块的别名信息里系统触发模块自动加载时udev / kmod 靠这个别名判断要不要加载驱动。不写这行手动 insmod 也能用但设备树里有了对应节点也不会自动加载模块经常造成“节点明明在驱动却没人匹配”的假象。2.3 多实例 probe 与设备型号识别当设备树里有两个同 compatible 的节点时probe 会被调用两次。每次传入的 pdev 不同但驱动代码是同一份。在 probe 里我们可以先从 pdev 上拿到设备树节点然后从 of_match_table 的 data 里拿到型号信息static int multi_sensor_probe(struct platform_device *pdev) { struct device *dev pdev-dev; const struct sensor_chip_info *info; struct multi_sensor *sensor; int ret; info of_device_get_match_data(dev); if (!info) return -EINVAL; sensor devm_kzalloc(dev, sizeof(*sensor), GFP_KERNEL); if (!sensor) return -ENOMEM; sensor-dev dev; sensor-info info; sensor-id pdev-id; // 或者用 device_property_read_u32 读自定义属性 dev_info(dev, probe sensor, chip %s, id %d\n, info-name, sensor-id); // 初始化硬件、注册中断、创建字符设备等 platform_set_drvdata(pdev, sensor); return 0; }sensor-id可以来自pdev-id也可以从设备树节点里自定义属性读。如果是同型号多实例需要区分“设备 A 是哪一颗”我习惯在设备树节点里加一个reg或者自定义属性probe 时读出来存进私有数据。这样后续 open、ioctl、read、write 的时候每个实例都能分清自己是谁。值得一提的是devm_kzalloc这类 managed 版本分配的内存不需要手动释放设备 remove 时会自动回收。在 probe 里能用的资源尽量都用 devm 系列能少写很多错误处理的代码。3. 技巧二用 per-device 私有数据隔离多实例别再写全局变量匹配只是第一步真正让多设备稳定工作的是数据隔离。所有硬件资源、运行时状态、缓冲区都应该放在每个设备实例自己的结构体里。3.1 核心数据结构一个实例一份数据多实例驱动的核心结构体设计是这样的struct multi_sensor { struct device *dev; const struct sensor_chip_info *info; unsigned int id; struct cdev cdev; struct device *class_dev; void __iomem *base; // 或 struct i2c_client *client int irq; struct mutex lock; struct work_struct work; // 其他运行时状态 };probe 里为每个设备分配一份这样的结构体然后挂到 pdev 上。以后无论是中断处理、文件操作还是 sysfs 回调都通过container_of或者platform_get_drvdata拿到属于自己的那份数据。这里有一个很实用的原则模块作用域只允许放“所有设备共享的只读数据”比如 of_match_table、文件操作结构体、型号信息表。凡是可读写的状态一律放进 per-device 结构体。3.2 主次设备号从 register_chrdev 到 cdev 次设备号驱动支持多个设备时字符设备这块很容易踩坑。最传统的写法是register_chrdev(major, multi_sensor, fops);这个接口一次占用一个主设备号下的 256 个次设备号所有设备共用一套 fops在 open 的时候靠次设备号区分是哪一个设备。对于设备数量不多、结构简单的驱动这么写确实省事但在多实例场景下会导致次设备号不可控你在驱动里根本不知道哪次 probe 分配到哪个次设备号而且同一个设备多次 open 时次设备号是固定的想区分打开的是哪一个实例又得靠全局数组。更推荐的做法是先用alloc_chrdev_region动态分配一段设备号再为每个实例单独cdev_addstatic int multi_sensor_setup_chrdev(struct multi_sensor *sensor) { dev_t devno; int ret; // 主设备号统一分配一次次设备号按实例递增 if (multi_sensor_major 0) { ret alloc_chrdev_region(sensor-devno, 0, MAX_SENSOR_NUM, multi_sensor); if (ret 0) return ret; multi_sensor_major MAJOR(sensor-devno); } sensor-devno MKDEV(multi_sensor_major, sensor-id); cdev_init(sensor-cdev, multi_sensor_fops); sensor-cdev.owner THIS_MODULE; ret cdev_add(sensor-cdev, sensor-devno, 1); if (ret) return ret; sensor-class_dev device_create(multi_sensor_class, sensor-dev, sensor-devno, sensor, multi_sensor%d, sensor-id); return PTR_ERR_OR_ZERO(sensor-class_dev); }这个方案的关键点是每个实例的次设备号从一开始就是确定的等于设备 id。创建出的设备节点是/dev/multi_sensor0、/dev/multi_sensor1用户空间看得清清楚楚。3.3 open 的时候怎么找到“当前这个设备”文件操作结构体是所有实例共享的打开设备节点时内核通过inode-i_rdev里的次设备号告诉我们当前打开的是哪个实例static int multi_sensor_open(struct inode *inode, struct file *filp) { unsigned int minor MINOR(inode-i_rdev); struct multi_sensor *sensor; sensor multi_sensor_find_by_minor(minor); if (!sensor) return -ENXIO; filp-private_data sensor; return 0; }multi_sensor_find_by_minor的实现可以有几种用一个ida管理实例 id并维护一个sensor[id]指针数组遍历class下的设备维护一个全局链表每次遍历查找最简单的还是 ida 数组。实例数量不大时数组最直接static struct multi_sensor *multi_sensor_table[MAX_SENSOR_NUM]; static struct multi_sensor *multi_sensor_find_by_minor(unsigned int minor) { if (minor MAX_SENSOR_NUM) return NULL; return multi_sensor_table[minor]; }probe 时把 sensor 放进数组remove 时置空。注意这个数组属于“共享的可写数据”需要加锁保护或者在 open 里用 RCU / mutex 方式访问。设备数量不多、open 频率不高的时候一个 mutex 就够了。3.4 miscdevice 的坑单实例驱动不该无脑用它miscdevice 是很多简单驱动的首选因为它自动分配主设备号自动创建 /dev 节点省心。但它有个硬限制整个 miscdevice 子系统只有一个主设备号每个 miscdevice 占用一个次设备号而且是全局统一分配的。如果驱动只有一个设备用 miscdevice 没问题。但要支持多个设备虽然理论上可以注册多个 miscdevice每个设备一个但次设备号不是你自己控制的后续想通过次设备号判断实例会非常别扭。更关键的是misc 子系统的次设备号资源是全局共享的注册多了还可能出现冲突。所以我个人的建议是多实例设备驱动直接用 cdev device_create 这套组合别图省事用 miscdevice。前期多写几行后面扩展和维护会舒服很多。4. 瑞芯微平台上的完整验证流程从编译到 sysfs 检查技巧讲完了实际操作才是重点。我每次在瑞芯微平台上调试完驱动都会按下面这套流程做验证基本能把“匹配、加载、实例、读写”四个环节都覆盖到。4.1 编译与预加载检查如果驱动编成模块make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- Mdrivers/misc/multi_sensor modules建议编成模块的原因是可以快速 insmod / rmmod 反复测试不用每次改代码都重烧整个内核。瑞芯微 SDK 里模块编译的 Kconfig / Makefile 路径要确认好编出来的 .ko 放到板子的文件系统里。加载之前先确认设备树里节点有没有被内核识别。在板子上执行ls /proc/device-tree/ find /proc/device-tree -name *sensor*如果能找到 sensor1a 这样的目录说明设备树编译和解析没问题。接下来加载驱动insmod multi_sensor.ko立刻看一下 dmesgdmesg | tail -20如果 probe 成功应该看到两次 probe 日志分别打印 id 0 和 id 1。如果只看到一次就要进入下一步排查。4.2 设备树匹配状态检查确认驱动和设备是否匹配上有四种方式我习惯组合着看。检查方式命令/路径说明查看驱动绑定设备ls /sys/bus/i2c/drivers/multi-sensor/目录下会出现 1a-0030、1b-0030 这样的软链接查看设备状态cat /sys/bus/i2c/devices/1a-0030/uevent能看到 OF_NAME 和 OF_COMPATIBLE 是否正确查看自动加载别名modinfo multi_sensor.ko检查 alias 里有没有 vendor,multi-sensor查看设备号分配cat /proc/devicesls /dev/multi_sensor*确认字符设备和设备节点存在sysfs 是最直观的。如果/sys/bus/i2c/drivers/multi-sensor/下面只有一颗设备的链接说明另一颗设备没匹配成功或者设备树里 status 有问题或者驱动 probe 失败被拒了。4.3 多实例读写实测设备节点都创建好之后写一个简单的测试程序分别打开/dev/multi_sensor0和/dev/multi_sensor1各自发指令确认数据是分开的。这一步非常重要因为很多 bug 只有在两个设备同时工作的时候才暴露出来。我遇到过一种典型情况两个实例读到的数据一模一样。排查后发现是驱动里用了全局变量存 i2c_client 指针第二次 probe 覆盖了第一次的值。然后两个节点读的都是设备 B 的数据。这就是全局变量的典型症状。正确的验证方式是在两个设备上分别设置不同的寄存器值然后交叉读取确认 A 的寄存器变化不会影响 B。测试通过后再上真实业务逻辑。5. 调试排障匹配不上、实例错乱、probe 一闪而过最后这部分是实战中最花时间的。我把多设备驱动开发里最容易出问题的几个点列出来每一个都是我真实踩过的坑。5.1 compatible 属性不一致导致的静默失败设备树里写compatible vendor,multi-sensor;驱动里 of_match_table 却写了vendor,multi_sensor下划线和连字符不一样整个匹配直接失败而且 dmesg 里不报任何错误就是安静地不匹配。这种问题排查起来最烦。我的经验是遇到设备没绑上驱动第一件事去/sys/bus/platform/devices/或/sys/bus/i2c/devices/下面找对应节点目录看uevent里的OF_COMPATIBLE到底是什么字符串然后和驱动源码里的字符串逐字节对比包括大小写、连字符、下划线。另外瑞芯微 SDK 的设备树源文件经过编译器处理可以在板子上用dtc -I fs -O dts /proc/device-tree把实际生效的设备树 dump 出来看看节点里的 compatible 是不是你想要的值。5.2 设备树节点里 status disabled 的陷阱多设备场景下经常会为某个设备预留节点但暂时不启用于是写上了status disabled。问题在于如果这个节点被某个 overlays 或其他地方重新引用了或者你改的设备树文件本身没被实际编译进去就会出现“我以为节点启用了实际上内核拿到的是 disabled”的情况。在瑞芯微平台检查设备树是否正确的常用命令是cat /proc/device-tree/sensor1a/status如果输出是空说明节点没有 status 属性默认是启用状态如果输出是 disabled那驱动再怎么写也匹配不上。还有一种隐蔽情况节点名改了但驱动里用of_find_node_by_name或of_find_compatible_node查找时用的还是旧名字。多设备场景尤其要少用“按名字找节点”这种全局查找方式尽量通过传入的 dev 和 of_node 来获取资源。5.3 多实例并发 probe 带来的资源共享问题最后说说并发。多个设备在系统启动时大概率是同一时间被内核扫描匹配的probe 函数会被并发调用。如果 probe 里用了共享资源比如共用的寄存器、共用的电源、共用的 GPIO 控制器一定要加锁。我遇到过 SPI 总线上挂了两颗芯片probe 里都要配置同一个 SPI 片选相关的 pinctrl结果第二颗设备 probe 时把第一颗已经配置好的管脚状态改了导致第一颗芯片初始化不完整。原因就是 probe 逻辑里用了共享的全局标志位但没有同步。处理方式不复杂共享数据加 mutex或者用 devm 申请时保证资源按设备隔离。如果第二颗设备的初始化依赖第一颗完成还需要在驱动内部做好状态检查和等待机制不要想当然认为 probe 顺序就是设备树节点顺序。调试并发问题最常见的现象是 dmesg 里 probe 日志打印顺序错乱或者时好时坏。遇到这种情况优先检查共享资源的使用把 probe 关键路径上的日志加上%px打印 dev 指针快速确认是否是同一实例在跑。说实话在瑞芯微这类 SoC 上开发多设备驱动原理并不复杂内核机制早就把这些能力给你了。真正考验人的是细节compatible 字符串的一致性、私有数据是否彻底隔离、字符设备分配是否合理、并发边界有没有处理。把这两个技巧用扎实多设备支持基本不会再出幺蛾子。后面如果再遇到类似问题按照我第 4 章的验证流程和第 5 章的排查链路走一遍大多数情况都能定位到具体原因。