十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Linux驱动同时支持多个设备:瑞芯微平台匹配表与实例数据隔离实战

Linux驱动同时支持多个设备:瑞芯微平台匹配表与实例数据隔离实战 做瑞芯微平台驱动开发尤其是从裸机或者单片机转过来的朋友上手 Linux 驱动后遇到的第一个坎往往不是读写寄存器而是搞不明白“一个驱动到底怎么同时管多个设备”。芯片手册上写的是某个 IP 模块支持几路 UART、几个 I2C 控制器可真到设备树里挂了两路同型号传感器或者 USB 口上同时插了 CH340、CP2102、FT232 好几个转串口工具驱动要么只认第一个要么两个一起卡死日志里翻来覆去就是那几句看不出毛病的信息。这篇文章我就把自己在 RK 平台RK3568/RK3399 这类 Cortex-A 系列上处理“一个驱动支持多个设备”的实战经验拆成两个技巧一个是让驱动的匹配表去“认”多个设备另一个是多实例共存时怎么把每个设备的数据隔离开。这两个问题搞定了字符设备、I2C 设备、platform 设备、USB 串口设备基本都能顺手处理。下面内容适合正在做嵌入式 Linux 驱动开发、或者刚把 SDK kernel 目录翻了个底朝天还没找到入口的朋友。1. 先把“多个设备”这事儿掰开揉碎很多新手拿到一个例程就开始改改完发现怎么都不对往往是因为没搞明白内核里“设备”“驱动”“实例”这三个词的关系。在 Linux 的世界里一个驱动文件可以对应很多个实际硬件设备内核每次和其中一个设备匹配成功就会调用一次 probe每次调用 probe 时传进来的参数都是一个独立的设备实例。也就是说驱动代码是模板设备数据是实例模板可以被套用无数次。1.1 驱动开发里“多个设备”到底指哪几种情况实际项目里遇到的“一个驱动支持多个设备”通常可以分成三类处理思路差别很大。第一类是同型号设备挂多个比如板子上接了两个 MPU6050 传感器一个在 I2C1 总线上一个在 I2C2 总线上或者说同一个 I2C 总线上挂了两个地址不同的同型号传感器。这种情况最典型驱动只需要写一份probe 会被调用两次难点在于两次 probe 的数据不能互相覆盖。瑞芯微平台客制化项目里多发串口、多路 sensor 基本都是这个套路。第二类是不同型号但同类的设备由同一个驱动统一管理比如 USB 转串口驱动要同时支持 CH340、CP2102、FT232 这些不同芯片或者一个触摸驱动要兼容两三颗触控 IC。这类问题的核心是“匹配表”你要让内核知道这个驱动能管哪些 ID 的设备然后在 probe 里通过匹配到的信息去区分型号走不同的初始化流程。第三类是一个物理设备在系统里创建多个逻辑节点比如触摸屏驱动既要在 input 子系统注册设备又要在 sysfs 里暴露固件升级接口还要在设备模型下注册一个设备节点。严格说这不属于“驱动支持多个设备”只是驱动内部的多接口设计篇幅有限这次不展开重点讲前两类。1.2 为什么这事在瑞芯微平台上尤其常见瑞芯微的 SoC 有个特点外设接口给得特别足中高端芯片上 I2C 控制器动辄七八个UART 五六路起步USB Host 也经常是三四个起。接口多了客户的玩法就多了最常见的就是把每一路接口都接上实际设备。我自己经手的 RK3568 板卡里有一块板子接了四路 USB 转串口一路给业务调试一路接扫码枪一路接打印机一路接工控屏还有一块 RK3399 的板子I2C0 上挂了 PMIC 和触摸屏I2C2 上挂了三个同型号的温湿度传感器I2C3 上挂了 IMU。这种多设备场景在 RK SDK 里很多驱动只验证过单实例真要同时跑起来问题就全冒出来了。另外 RK 平台的 SDK 更新频率高内核版本跨度大设备树 merge 的时候经常出现 compatible 写错、节点地址冲突的情况。所以做这个平台的驱动必须把“多设备”这个基本功打扎实否则后面全是坑。1.3 内核驱动模型是怎么看待“多个设备”的Linux 驱动模型的核心思想是注册 匹配。设备侧有 device驱动侧有 driver总线负责把两者撮合在一起。匹配成功的标志就是驱动模型把 device 和 driver 关联起来然后调用 driver 的 probe 函数传入一个代表该 device 的结构体指针。所以“一个驱动支持多个设备”这件事在模型层面本来就是你注册一个 driver它天然就可以和多个 device 匹配。你不需要为每个设备写一套驱动你只需要保证匹配表能识别这些设备probe 里能处理不同设备每个设备实例的数据互相隔离。打开系统里 /sys/bus/platform/drivers/ 目录看一眼你会发现每个驱动目录下都链着一堆设备名这就是内核驱动模型的真实面貌。明白这一点后面两个技巧就都好理解了。2. 技巧一让一个驱动的 ID 表“认”多种设备先讲匹配表因为这是“驱动支持多个设备”的门槛。不管什么总线内核都有对应的 ID 表机制你只需要把要支持的设备信息写进去这个驱动就具备了支持这些设备的资格。这里以 I2C 设备为例展开讲platform 和 USB 的原理完全一样。2.1 匹配的本质驱动声明自己“能管谁”设备树里每个节点都有一行 compatible 属性比如mpu605068 { compatible invensense,mpu6050; reg 0x68; };这一行是设备侧在喊话我是 invensense 公司的 mpu6050 传感器。驱动这边则用数组来应答告诉内核我都能管谁static const struct of_device_id imu_of_match[] { { .compatible invensense,mpu6050, .data (void *)SENSOR_MPU6050 }, { .compatible invensense,icm20608, .data (void *)SENSOR_ICM20608 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imu_of_match);总线在匹配时把设备树里的 compatible 字符串和 of_device_id 表里的字符串逐个比较只要有匹配项就认为这个驱动有能力管理这个设备。这个数组里写多少行驱动就能“认”多少种设备。很多新手问这里能不能不写表直接在 probe 里靠字符串判断可以但那样驱动根本没机会被匹配到probe 压根不会执行。内核的驱动模型要求你先声明再匹配然后才 probe这是一个声明式的设计。匹不匹配是总线的事probe 只是匹配成功之后的结果回调。2.2 实操一个 I2C 驱动框架同时支持多型号传感器我写一个支持 MPU6050 和 ICM20608 两种 IMU 传感器的驱动框架这是 RK 平台 I2C 传感器适配最常见的场景。核心就是在 of_device_id 和 i2c_device_id 里各加一行然后用 data 字段携带型号信息probe 里根据型号走不同初始化。#include linux/module.h #include linux/i2c.h #include linux/of_device.h enum sensor_type { SENSOR_MPU6050, SENSOR_ICM20608, }; static const struct of_device_id imu_of_match[] { { .compatible invensense,mpu6050, .data (void *)SENSOR_MPU6050 }, { .compatible invensense,icm20608, .data (void *)SENSOR_ICM20608 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, imu_of_match); static const struct i2c_device_id imu_i2c_id[] { { mpu6050, SENSOR_MPU6050 }, { icm20608, SENSOR_ICM20608 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(i2c, imu_i2c_id); static int imu_probe(struct i2c_client *client) { const struct of_device_id *match; struct imu_dev *imu; enum sensor_type type SENSOR_MPU6050; match of_match_device(imu_of_match, client-dev); if (match match-data) type (enum sensor_type)match-data; imu devm_kzalloc(client-dev, sizeof(*imu), GFP_KERNEL); if (!imu) return -ENOMEM; if (type SENSOR_ICM20608) imu-reg_ctl ICM20608_CTL; else imu-reg_ctl MPU6050_CTL; imu-client client; i2c_set_clientdata(client, imu); return 0; } static struct i2c_driver imu_driver { .probe imu_probe, .id_table imu_i2c_id, .driver { .name rk_imu, .of_match_table imu_of_match, }, }; module_i2c_driver(imu_driver);为什么要两个表都写设备树场景下总线优先用 of_match_table 匹配这个表同时决定了设备节点能不能和驱动对应上。而 i2c_device_id 是给非设备树场景准备的比如板级文件、ACPI 或者手动创建 i2c_client 的早期写法。在 RK SDK 里基本都是设备树但保留 id_table 可以增加驱动的兼容性模块加载时 modprobe 也需要这个表生成 modaliases。.data 字段是这里的关键它让你在 probe 里不用做字符串比较就能知道是哪种传感器。内核匹配的时候会把命中的那一项直接返回给你你用 of_match_device 拿回来取出里面的 data就拿到了型号枚举值。这是多型号支持里最优雅的写法比在 probe 里 strcmp 一百遍干净得多。2.3 设备树里的 compatible 和驱动里的 of_device_id 怎么对设备树这边如果板子上同时挂了 MPU6050 和 ICM20608节点就长这样i2c1 { status okay; mpu605068 { compatible invensense,mpu6050; reg 0x68; }; icm2060869 { compatible invensense,icm20608; reg 0x69; }; };两个节点两个不同的 reg 地址compatible 对应驱动表里的两行。匹配成功之后probe 会被调用两次每次传来的 i2c_client 指针不同分别指向两个不同的设备。这里有个容易踩的坑就是 I2C 地址的进制问题。设备树里 reg 要填 7 位 I2C 地址MPU6050 数据手册上写的是 0xD0 这种 8 位地址那是带读写位的写法设备树里必须填 0x68也就是去掉最低位后的 7 位地址。填错的话驱动也可以 probe 成功但 i2c_transfer 时地址对不上读 WHO_AM_I 永远是错误值。另一个容易忽略的是 status 属性。有些 SDK 默认把一些 I2C 节点 status 设为 disabled如果你忘了改成 okay总线根本不会扫描这个节点驱动表写得再对也没用。遇到“设备树明明配了就是不 probe”的情况先查 status再查 compatible 拼写这是排错顺序问题。2.4 MODULE_DEVICE_TABLE 与自动加载很多人在 RK 平台上把驱动编成 .ko 模块放到 rootfs 里插上设备却发现内核不自动加载模块手动 insmod 又能正常工作。这个问题十有八九就是 MODULE_DEVICE_TABLE 没写。MODULE_DEVICE_TABLE 的作用是把驱动支持的 ID 信息导出到模块文件的 modinfo 字段里这样 udev/mdev 在发现新设备时会去扫描所有模块的 modaliases看哪个模块声明自己能管这个设备找到了就自动 modprobe。没有这个宏设备热插拔后内核根本不知道有模块能处理它。编译完模块之后可以用 modinfo 检查导出结果modinfo imu_driver.ko | grep alias能看到类似 alias of:NTCinvensense,mpu6050 这样的输出就说明自动加载信息已经生成了。注意修改设备树后必须重新编译并打包 boot 分区模块更新后要跑 depmod 重新生成依赖否则新的 modaliases 不会被系统识别。顺带提一句驱动如果直接编译进内核MODULE_DEVICE_TABLE 不是强制的因为不需要自动加载。但从代码规范和可维护性角度我还是建议保留万一哪天要改成模块就不用回头补了。3. 技巧二多个实例共用一套代码数据别串台匹配表解决的是“驱动能不能认出多个设备”接下来要解决“认出之后能不能各管各的”。这才是“支持多个设备”真正容易翻车的地方。我见过太多项目设备树配得漂漂亮亮两个节点都 probe 成功了结果数据全乱套问题全出在数据共用。3.1 最常见的事故全局变量被最后一个实例覆盖新手写驱动最常见的就是在文件顶部定义一个全局变量或者一组 static 变量用来保存寄存器地址、缓冲区、中断号这些信息。单设备场景下没问题两个设备一上电就露馅。举个例子probe 里这么写static void __iomem *global_base; static int my_probe(struct platform_device *pdev) { struct resource *res; res platform_get_resource(pdev, IORESOURCE_MEM, 0); global_base devm_ioremap_resource(pdev-dev, res); return 0; }第一个设备 probe 时global_base 存的是设备 0 的寄存器基地址第二个设备 probe 时global_base 被覆盖成设备 1 的地址。之后两个设备的中断处理、sysfs 读写全都在访问设备 1 的寄存器设备 0 看着是“活着”实际上已经失控了。这种事故在串口驱动上表现尤其明显。两路串口共用一套全局 bufferA 口收到的数据可能被 B 口的读操作拿走应用层收到的数据就串了。传感器驱动上则表现为两个传感器读出来的数据一模一样因为读的都是后 probe 那个设备的寄存器。3.2 正解每实例一份数据 container_of正确做法是为每个设备实例分配独立的数据结构用 devm_kzalloc 在 probe 里分配然后把实例数据挂在设备上需要的时候通过 API 拿回来。这个结构体把设备所有状态都装进去包括寄存器地址、时钟、锁、缓冲区、中断相关字段比如struct my_dev { void __iomem *base; struct clk *clk; struct mutex lock; struct cdev cdev; atomic_t users; u8 rx_buf[256]; };probe 里这样用static int my_probe(struct platform_device *pdev) { struct my_dev *d; d devm_kzalloc(pdev-dev, sizeof(*d), GFP_KERNEL); if (!d) return -ENOMEM; d-base devm_platform_ioremap_resource(pdev, 0); mutex_init(d-lock); platform_set_drvdata(pdev, d); return 0; }这样每个设备都有自己独立的 d寄存器地址、锁、缓冲区全在各自的实例里谁也不干扰谁。devm_kzalloc 的好处是资源跟随设备生命周期设备解绑或者 remove 时自动释放不会泄漏。但这里还有个核心问题内核很多回调函数传进来的参数并不直接是你的 my_dev 指针。比如字符设备的 open 只给你 inode 和 file中断处理函数只给你一个 void *你怎么拿回自己的实例答案就是 container_of这是 Linux 内核里最常用的数据结构技巧。比如你在 my_dev 里嵌了一个 struct cdevstatic int my_open(struct inode *inode, struct file *filp) { struct my_dev *d container_of(inode-i_cdev, struct my_dev, cdev); filp-private_data d; return 0; }container_of 的原理是已知结构体某个成员的地址算出它所属结构体的首地址。内核里这种写法到处都是因为它能保持“结构体嵌套”设计的一致性让一个数据结构同时服务于设备和驱动框架。简单类比一下就像你拿到了一个对象的子对象指针还能安全地找回父对象指针避免了到处用全局数组做下标映射。3.3 多设备节点主次设备号的分配与管理驱动支持多个设备用户态通常期望看到多个可操作的节点比如 /dev/mydev0、/dev/mydev1。这就涉及到字符设备驱动框架中设备号的分配策略。最常用的做法是一个主设备号配合多个次设备号。主设备号标识驱动次设备号标识实例。在模块 init 里用 alloc_chrdev_region 一次性申请多个设备号#define MAX_INSTANCES 4 static dev_t my_devno; static struct class *my_class; static int __init my_init(void) { int ret; ret alloc_chrdev_region(my_devno, 0, MAX_INSTANCES, mydev); if (ret 0) return ret; my_class class_create(THIS_MODULE, mydev); if (IS_ERR(my_class)) { unregister_chrdev_region(my_devno, MAX_INSTANCES); return PTR_ERR(my_class); } return platform_driver_register(my_platform_driver); }每个实例 probe 时从预留的次设备号池里取一个编号初始化 cdev 并创建节点static int my_dev_create(struct my_dev *d, int minor) { d-devno MKDEV(MAJOR(my_devno), minor); cdev_init(d-cdev, my_fops); d-cdev.owner THIS_MODULE; if (cdev_add(d-cdev, d-devno, 1)) { dev_err(d-dev, cdev_add failed\n); return -EIO; } d-dev device_create(my_class, d-dev, d-devno, d, mydev%d, minor); return 0; }这样每个设备实例都有自己的 cdev 和设备节点。用户态打开 /dev/mydev0 时open 拿到的 inode 的次设备号就是 0再用我上面说的 container_of 或者直接按 minor 索引就能定位到对应的实例。我建议用 container_of 而不是直接用次要编号做数组下标因为数组下标意味着你必须预知设备数量上限而且一旦某个实例的 probe 失败数组中间就会留下空洞管理和调试都很别扭。container_of 直接从 inode 的 cdev 成员找回结构体天然映射不需要额外维护状态数组。设备节点创建成功后只要内核开了 devtmpfs/dev 下的节点会自动出现不需要手动 mknod。很多 RK SDK 默认就开着你在 device_create 之后去 /dev 看一眼节点已经在了。3.4 platform 驱动多实例的 drvdata 用法如果是单纯的 platform 设备多实例不涉及字符设备节点那数据管理就更简单了用好 platform_set_drvdata 和 platform_get_drvdata 这一对 API 就行。设备树里两个节点用同一个 compatiblemydevice0: mydevice1000 { compatible vendor,mydevice; reg 0x1000 0x100; }; mydevice1: mydevice2000 { compatible vendor,mydevice; reg 0x2000 0x100; };驱动里 probe 和 remove 成对使用 drvdatastatic int my_probe(struct platform_device *pdev) { struct my_dev *d; d devm_kzalloc(pdev-dev, sizeof(*d), GFP_KERNEL); if (!d) return -ENOMEM; d-base devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(d-base)) return PTR_ERR(d-base); platform_set_drvdata(pdev, d); /* 注册中断、sysfs 等 */ return 0; } static int my_remove(struct platform_device *pdev) { struct my_dev *d platform_get_drvdata(pdev); /* 清理中断、移除 sysfs */ return 0; }remove 里拿回实例数据后释放工作全部交给 devm 机制不要手动 kfree。devm 系列 API 的核心价值就是让资源的释放顺序由内核保证remove 被调用时它会按注册顺序的逆序自动释放你只需要把“非 devm 管理”的资源比如你手动 request_irq 的中断、手动创建的 sysfs 属性清理掉就行。这套 API 在 I2C 驱动里对应的就是 i2c_set_clientdata / i2c_get_clientdata在 USB 驱动里是 usb_set_intfdata / usb_get_intfdata。名字不同思想一样把实例数据挂到设备上随用随取。4. 瑞芯微平台实测串口设备与传感器适配的实战记录讲了这么多理论下面给两段实际调试记录都是我在 RK 平台上真实处理过的情况你可以照着这个流程去排查自己的板子。4.1 实测一让 USB 转串口驱动识别多种芯片RK3568 的主板USB Host 口上同时接了 CH340、CP2102、FT232 三路 USB 转串口客户要求三路都能在 /dev/ttyUSB* 下稳定出现。上电后 dmesg 一看只识别出两路CH340 没认出来。先用 lsusb 看设备 IDlsusb Bus 002 Device 003: ID 1a86:7523 QinHeng Electronics CH340 serial converter Bus 002 Device 004: ID 10c4:ea60 Silicon Labs CP210x UART Bridge Bus 002 Device 005: ID 0403:6001 Future Technology Devices FT232内核里 ch341 驱动确实存在但对应的是 1a86:5523CH340 的新版本用的是 7523老驱动没收录这个 PID。这种问题有两种解决办法一种是在源码 drivers/usb/serial/ch341.c 的 id 表里加一行{ USB_DEVICE(0x1a86, 0x7523) },然后重新编译内核或模块。另一种是不改内核用 sysfs 动态注入 IDecho 1a86 7523 /sys/bus/usb-serial/drivers/ch341/new_id用 echo 加 ID 的办法适合临时验证重启就没了要固化最终还是要改源码。改完重新插拔设备dmesg 里就能看到 ch341-uart 被识别/dev/ttyUSB2 顺利出现。这其实就是“ID 表驱动多个设备”在 USB 总线上的直接应用与 I2C 的 of_device_id 思路完全一致。如果想让一个自己写的 USB 驱动支持多种芯片就用 usb_device_id 数组static const struct usb_device_id my_usb_ids[] { { USB_DEVICE(0x1a86, 0x7523) }, /* CH340 */ { USB_DEVICE(0x10c4, 0xea60) }, /* CP210x */ { USB_DEVICE(0x0403, 0x6001) }, /* FT232 */ { /* sentinel */ } }; MODULE_DEVICE_TABLE(usb, my_usb_ids);别忘了 USB 设备比 I2C 设备多了一个热插拔属性所以 MODULE_DEVICE_TABLE 必须写否则设备插进去系统不会自动加载你的模块。另外多个 USB 转串口同时插拔ttyUSB 节点号和实际设备可能对不上这属于 udev 规则层面的事驱动里保证每个设备都能正确绑定即可。4.2 实测二I2C 总线上两个同型号传感器的适配一块 RK3399 的板子I2C1 和 I2C2 上各挂了一个同型号的温湿度传感器型号是 SHT30。设备树里两个节点 compatible 相同reg 相同只是挂在不同的控制器下i2c1 { sht3044 { compatible sensirion,sht30; reg 0x44; }; }; i2c2 { sht3044 { compatible sensirion,sht30; reg 0x44; }; };驱动是标准写法probe 里 devm_kzalloc 分配实例数据i2c_set_clientdata 保存。上电之后 dmesg 里两条 probe 日志都打了/sys/bus/i2c/devices/ 下也出现了两个设备节点。但应用程序读取数据时发现两个传感器的温度一模一样怀疑是数据串了。检查驱动代码发现中断处理函数里定义的静态缓冲区被两个实例共用读操作把数据写进同一个 buffer后一次读覆盖前一次。改成 per-instance 的 buffer 之后问题消失两个传感器的温度和湿度开始各自正常跳动。这个案例典型到值得多说一句多实例设备驱动排查数据串台问题第一件事就是把全文件搜一遍 static 变量和全局变量看它们是不是被多个实例共用。凡是只属于单个设备的状态都应该放进实例结构体里由 devm_kzalloc 分配。调试 I2C 多设备时i2cdetect 是快速确认地址是否正确的工具i2cdetect -y 1 i2cdetect -y 2能扫到 0x44 说明器件和总线上电都没问题如果扫不到先查硬件上拉和供电再查设备树地址。RK 平台 I2C 控制器的编号从 0 开始/dev/i2c-1 对应设备树里的 i2c1这个对应关系各个 SDK 版本可能不一致用 i2cdetect -l 先看一下。4.3 调试驱动绑定关系的几个高效命令驱动写完之后确认“有没有匹配上”是最关键的调试步骤。下面这几个命令是我在 RK 平台上用得最多的建议收藏。# 查看某个驱动当前绑定了哪些设备 ls -l /sys/bus/platform/drivers/my_driver/ # 查看设备当前被哪个驱动绑定 readlink /sys/bus/platform/devices/mydevice0/driver # 手动解绑再重新绑定测试 remove/probe 流程 echo mydevice0 /sys/bus/platform/drivers/my_driver/unbind echo mydevice0 /sys/bus/platform/drivers/my_driver/bind # 查看设备树里节点匹配到的 compatible cat /sys/firmware/devicetree/base/mydevice0/compatible # 查看已注册的设备号 cat /proc/devices手动 bind/unbind 是调试驱动生命周期最直接的手段不需要重启板子就能反复验证 probe 和 remove 是否存在资源泄漏或者野指针问题。配合 devmem 读寄存器基本能定位 90% 的匹配和数据问题。dmesg 里如果出现 other driver requests probe deferral 这种提示说明设备资源暂时不可用驱动被挂起等待重试常见原因是依赖的时钟或者电源域还没有准备好。RK 平台上这种日志大多是 I2C 或 SPI 控制器依赖的 pinctrl 没配好优先查设备树的 pinctrl 设置。5. 常见问题排查与避坑清单最后把我在实际项目里碰到过的高频问题整理成清单你可以把这一章当成速查手册遇到症状直接对号入座。5.1 设备节点不出现、probe 不执行这三个原因占了八成设备树没生效、驱动没进内核、ID 表没覆盖。设备树层面先确认节点 status 是 okaycompatible 字符串和驱动 of_match_table 完全一致一个字符都不能差。很多 SDK 的设备树模板默认把大量节点 status 设为 disabled你新建的节点也容易漏改。驱动层面确认 .ko 文件放进了文件系统depmod 跑过modprobe 能正常加载。如果设备树匹配成功驱动加载后 dmesg 里必然有 probe 调用日志什么都没有就去查驱动注册本身是否成功。另外别忽略总线匹配规则。platform 设备还有一层 name 匹配如果设备树 compatible 没匹配上有些总线会退回去匹配 platform_driver 的 id_table 或者 driver.name这个兼容逻辑有时会掩盖真正的匹配问题。建议先在 sysfs 里看一眼设备到底有没有 driver 指针没有就从设备树名称开始查。5.2 两个设备只有一个工作一定是数据串了多设备场景最常见的就是这个现象。一个设备正常另一个设备也 probe 成功但操作起来要么没反应要么数据错乱。第一步全文件搜索 static 变量和全局变量凡是驱动逻辑中会被修改的全部挪进实例结构体。第二步查中断如果两个设备共用同一个中断号必须申请成共享中断 IRQF_SHARED并且中断处理函数里通过传入的 dev_id 区分是哪个设备触发的。第三步查 sysfs 或 proc 里导出的接口确认属性文件里拿到的实例指针是通过 drvdata 之类正确取回的不是从某个全局数组里猜的。一个容易忽略的细节是有些驱动里用了全局延时或者全局计数器来调试这种代码在多实例下也会产生诡异行为。调试用的临时变量用完就删别让它混进正式逻辑。5.3 模块卸载崩溃、remove 里野指针卸载时崩溃基本可以断定是 remove 函数取实例数据的方式不对。remove 里必须用 platform_get_drvdata 拿回 probe 时 set 的那个指针不能重新分配也不能从全局数组取。另外一个常见坑是 devm 资源手动释放导致的双重 free。有人习惯了 malloc/free 思维在 remove 里对 devm_kzalloc 出来的指针又调了一次 kfree这不崩溃才怪。devm 资源由内核统一管理remove 返回后自动释放你只要清理非 devm 的资源就行。如果驱动注册了字符设备卸载时还要注意顺序先 device_destroy 删除设备节点再 cdev_del 删除字符设备最后 class_destroy 和 unregister_chrdev_region 收尾。用户态如果还开着设备文件就去删除 cdev内核可能崩溃这种场景很少但一旦发生很难查建议在 remove 里先检查实例的打开计数。5.4 多设备调试速查对照表现象可能原因排查方式解决办法设备节点完全不出现设备树 status disabled查看 sys/firmware/devicetree/base 下节点是否存在改设备树 status 为 okayprobe 不执行compatible 拼写不一致对比设备树和 of_device_id 字符串统一 compatible 命名probe 返回 -ENODEV资源已被占用或共享冲突dmesg 查看具体错误码检查中断号、GPIO、时钟是否冲突两个设备数据一样全局 buffer 被覆盖全文件搜索 static 变量把数据放入实例结构体模块不自动加载MODULE_DEVICE_TABLE 缺失modinfo 查看 modaliases补全 ID 表宏定义I2C 设备读不到寄存器reg 地址写成 8 位格式i2cdetect 扫描改成 7 位地址卸载时内核崩溃devm 资源被手动释放检查 remove 里是否 kfree去掉手动释放USB 设备识别不出内核驱动 ID 表缺 PIDlsusb 获取 VID/PID动态 new_id 或改源码两个设备中断互相干扰中断处理未区分实例检查 dev_id 参数使用 IRQF_SHARED设备树改了没生效没重新编译 boot 分区查看 /proc/device-tree 内容重新编译并烧写 boot这套表不仅是排查工具也是我在代码 review 时重点检查的清单。多设备支持这个需求写起来不难难的是把所有边界情况都想清楚。ID 表多写两行很简单真正体现功力的是数据隔离和资源管理这部分。根据我个人经验Linux 驱动支持多个设备这件事本质就两句话让驱动认得出让数据分得开。认得出靠 ID 表分得开靠实例数据。这两个技巧在瑞芯微平台上验证过了换到全志、MTK、NXP 的平台上思路也完全一样只是总线和设备树的具体写法略有差异。做嵌入式 Linux 驱动开发把这套方法吃透后面遇到再复杂的外设都能快速切入。
返回列表