资讯详情

资讯详情

瑞芯微平台一驱多设备:of_device_id配置表与miscdevice动态节点实践

在瑞芯微平台上调驱动我碰到最多的需求之一就是一个驱动要同时照顾多个设备。注意这里说的是“同一个驱动文件”不是给每个设备各写一份驱动。比如RK3568板子上I2C2总线挂了两片同型号传感器或者硬件改版后同一个外设换了寄存器地址和初始化流程再或者双屏异显时两个触摸屏共用一套驱动逻辑。这些需求藏在项目里并不起眼但真做起来坑很多probe只执行一次、/dev节点只出现一个、两个设备的数据互相串扰。这篇文章就分享我在瑞芯微平台上总结出的两个小技巧一是用of_device_id.data把设备差异做成一张配置表二是用MISC_DYNAMIC_MINOR加per-device私有数据让多个实例各自独立、互不干扰。代码基于RK3568、内核4.19/5.10其它瑞芯微芯片同样适用。1. 先搞清楚内核怎么让“一个驱动”驱动“多个设备”1.1 驱动的匹配机制probe为什么会被调用多次很多刚接触Linux驱动开发的同学容易搞混一个概念驱动和设备的对应关系到底是一对一还是一对多。在Linux的设备模型里驱动是一个“方法集合”它本身不带状态设备是内核里一个个独立的struct device对象每个对象对应设备树里的一个节点或者对应总线上扫描到的一个真实硬件。当驱动注册到内核后总线会拿着驱动里的匹配表去扫描所有挂在这条总线上的设备每匹配上一个设备就会为这个设备调用一次probe函数。换句话说一个I2C驱动注册一次同一条I2C总线上挂两片地址不同的设备probe就会被调用两次。如果这两个设备还分别注册成两个平台设备或者两个I2C client那更是互不干涉。这个机制是Linux内核天生支持多设备的根基理解它的关键点在于probe里的参数比如struct i2c_client *client每次都是不同的你必须把每次调用看成一次“新开张”所有跟具体设备有关的数据都要在这一轮probe里分配并保存好。1.2 我在瑞芯微平台上遇到的实际场景我最近在RK3568上做双传感器数据采集I2C2上挂了两片不同的传感器型号A是老款初始化需要往寄存器0x10写值型号B是新款寄存器布局变了初始化流程也不同。同时硬件上还留了一个扩展位允许客户在另一个I2C地址上再挂一片同型号设备。这就意味着一个驱动文件要同时兼容两种型号且每个型号可能有多片实例。如果按最简单的思路给A和B各写一个驱动代码大量重复不说后续型号C出来又要复制一遍。更麻烦的是当两片设备同时工作时如果驱动里用了全局变量保存状态两个设备的数据必然串扰。所以我把设计目标定为驱动只有一份probe和一份remove逻辑型号差异通过配置数据区分每个设备实例有自己独立的私有数据结构并且能在/dev下生成不重名的节点。2. 小技巧一把设备差异放进匹配表的.data里2.1 传统麻烦做法在probe里堆if-else在没有技巧的情况下很多人会写这样的代码static int sensor_probe(struct i2c_client *client) { if (of_device_is_compatible(client-dev.of_node, rockchip,sensor-a)) { /* 型号A初始化 */ i2c_smbus_write_byte_data(client, 0x10, 0x01); } else if (of_device_is_compatible(client-dev.of_node, rockchip,sensor-b)) { /* 型号B初始化 */ i2c_smbus_write_byte_data(client, 0x20, 0x02); } ... }这种写法在只有两个型号时还能忍一旦型号变多probe会膨胀得非常难看。更致命的是如果初始化函数里要用到不同寄存器地址、不同延时、不同频率参数你得在probe里维护一大堆分支新增一个型号就要改probe代码还要担心改坏已有型号。而设备树的compatible字符串本质上是驱动与设备“握手”的凭证它已经帮你做了第一层区分你只需要在这个基础上把差异数据化。2.2 技巧核心of_device_id.data指向一个配置结构体Linux提供的of_device_id结构体里有两个关键字段compatible和data。compatible用于匹配设备树节点的compatible字符串而data是一个void *指针可以在匹配表里预先挂上任何你想要的数据。驱动probe时用device_get_match_data()配套的是of_device_get_match_data()就能把匹配表里对应的data指针取出来。这意味着什么意味着你可以把每个型号差的东西——名字、默认寄存器值、初始化函数指针、工作频率、校准参数——全部打包进一个结构体然后把结构体地址塞进匹配表的.data字段。probe里的if-else分支被替换成“取出配置表查表执行”。整个过程没有字符串比较没有散落的magic number所有型号差异收拢成一个只读配置数组。struct sensor_model_info { const char *name; u8 id_reg; u8 id_value; int (*init)(struct i2c_client *client); }; static int sensor_a_init(struct i2c_client *client) { return i2c_smbus_write_byte_data(client, 0x10, 0x01); } static int sensor_b_init(struct i2c_client *client) { return i2c_smbus_write_byte_data(client, 0x20, 0x02); } static const struct sensor_model_info sensor_a_info { .name sensor-a, .id_reg 0x00, .id_value 0xA0, .init sensor_a_init, }; static const struct sensor_model_info sensor_b_info { .name sensor-b, .id_reg 0x00, .id_value 0xB0, .init sensor_b_init, };匹配表写成这样static const struct of_device_id sensor_dt_match[] { { .compatible rockchip,sensor-a, .data sensor_a_info }, { .compatible rockchip,sensor-b, .data sensor_b_info }, { } }; MODULE_DEVICE_TABLE(of, sensor_dt_match); static const struct i2c_device_id sensor_i2c_id[] { { sensor-a, (kernel_ulong_t)sensor_a_info }, { sensor-b, (kernel_ulong_t)sensor_b_info }, { } }; MODULE_DEVICE_TABLE(i2c, sensor_i2c_id);注意这里同时维护了of_match_table和i2c_device_id表。为什么两处都要因为设备树匹配走of_match_table而传统的板级描述非设备树模式走i2c_device_id。瑞芯微平台基本都是设备树但有些老SDK或者ACPI环境会走到非设备树路径。两个表都维护兼容性最好。probe函数里取出配置表static int sensor_probe(struct i2c_client *client) { const struct sensor_model_info *info; struct device *dev client-dev; if (dev-of_node) info device_get_match_data(dev); else info (const struct sensor_model_info *)i2c_match_id(sensor_i2c_id, client)-driver_data; if (!info) return -ENODEV; dev_info(dev, probe sensor %s\n, info-name); if (info-init) { int ret info-init(client); if (ret) return ret; } ... }2.3 这个技巧为什么适合瑞芯微平台瑞芯微的SDK里芯片级dtsi和板级dts分层明确板级dts里经常通过改compatible来切换同一外设的不同驱动物理实现。比如有些开发板有多个硬件版本音频Codec、触摸屏、传感器都可能换了厂商。如果驱动里全是if-else客户换硬件就要改驱动用了.data配置表客户只需要改设备树compatible然后我这边加一行匹配表和一个配置结构体就能兼容新硬件。这个技巧还有一个隐藏好处内核模块加载时of_match_table里的compatible会被编译进模块的modinfo你可以用modinfo直接查看这个驱动支持哪些设备。配上了.config数据和MODULE_DEVICE_TABLE模块的自动加载、热插拔识别也会更可靠。对于产线要烧录多个硬件版本的场景这个信息非常有用。3. 小技巧二每个设备一份私有数据/dev节点不重名3.1 全局变量是万恶之源数据隔离是“一驱多设备”的核心难点之一。很多驱动为了省事把设备状态、缓冲区、计数变量直接声明成全局变量。在只有一个设备的时候这个问题不明显但挂两片同型号设备后第二次probe会把全局变量覆盖掉第一个设备再读数据时拿到的可能是第二个设备的寄存器值。我见过最典型的故障是两个触摸屏共用一个驱动全局变量里保存的event缓冲被后一个设备覆盖导致第一个屏幕触控失灵。正确的做法是在每次probe里通过devm_kzalloc为当前设备分配一份独立的私有数据结构把所有该设备相关的状态都装进去然后用i2c_set_clientdata()平台设备用platform_set_drvdata()把这份数据和当前client绑定。需要取数据的地方用i2c_get_clientdata()或container_of把它取出来。这样每片设备都有一本自己的“账本”谁都不会动谁。3.2 动态设备号miscdevice的妙用驱动要给应用层暴露接口常见三种方式字符设备cdev alloc_chrdev_region、miscdevice、input子系统等专有框架。对于多设备场景我推荐miscdevice因为它天生支持每个设备一个独立的/dev节点而且次设备号可以动态分配。miscdevice结构体里的minor字段填MISC_DYNAMIC_MINOR内核会自动分配一个空闲的次设备号不会和系统里已有的设备冲突。name字段就是/dev节点名只要保证每个设备实例的name不重复就行。miscdevice还有一个fops字段应用层open、read、write、ioctl都走这个文件操作集合。光有miscdevice还不够还得让应用层能区分“我打开的到底是哪个设备”。misc_open会把struct miscdevice指针放到file-private_data里你只需要在open函数里用container_of把这个指针反解回你自己的私有数据结构再存回file-private_data即可。后续read/write/ioctl直接从file-private_data里取值就永远不会串设备。3.3 完整驱动代码串联下面是我在RK3568上调通的精简版驱动去掉了业务逻辑只保留多设备支持和数据隔离框架。#include linux/module.h #include linux/i2c.h #include linux/of_device.h #include linux/device.h #include linux/miscdevice.h #include linux/fs.h #include linux/uaccess.h #include linux/slab.h #define DRIVER_NAME rk_multi_sensor struct sensor_model_info { const char *name; u8 id_reg; u8 id_value; int (*init)(struct i2c_client *client); }; struct sensor_dev_data { struct i2c_client *client; const struct sensor_model_info *info; struct miscdevice mdev; int counter; }; /* 型号A和B的model info见上一节这里省略重复定义 */ static int sensor_a_init(struct i2c_client *client) { return i2c_smbus_write_byte_data(client, 0x10, 0x01); } static int sensor_b_init(struct i2c_client *client) { return i2c_smbus_write_byte_data(client, 0x20, 0x02); } static const struct sensor_model_info sensor_a_info { .name sensor-a, .init sensor_a_init, }; static const struct sensor_model_info sensor_b_info { .name sensor-b, .init sensor_b_init, }; static const struct of_device_id sensor_dt_match[] { { .compatible rockchip,sensor-a, .data sensor_a_info }, { .compatible rockchip,sensor-b, .data sensor_b_info }, { } }; MODULE_DEVICE_TABLE(of, sensor_dt_match); static const struct i2c_device_id sensor_i2c_id[] { { sensor-a, (kernel_ulong_t)sensor_a_info }, { sensor-b, (kernel_ulong_t)sensor_b_info }, { } }; MODULE_DEVICE_TABLE(i2c, sensor_i2c_id); static int sensor_open(struct inode *inode, struct file *file) { struct sensor_dev_data *data container_of(file-private_data, struct sensor_dev_data, mdev); file-private_data data; return 0; } static long sensor_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct sensor_dev_data *data file-private_data; int val; switch (cmd) { case 0x01: /* 获取型号 */ val >i2c2 { status okay; clock-frequency 400000; sensor_a: sensor-a1a { compatible rockchip,sensor-a; reg 0x1a; status okay; }; sensor_b: sensor-b1b { compatible rockchip,sensor-b; reg 0x1b; status okay; }; };在瑞芯微SDK里dts文件路径一般是arch/arm64/boot/dts/rockchip/不同内核版本可能略有差异。修改后用SDK提供的编译命令重新生成boot.img或者单独编译dtb。如果用的是模块方式只需要把dtb替换进boot分区驱动模块单独insmod即可如果把驱动编进内核那就得连uImage/Image一起更新。4.2 编译驱动为模块在驱动源码目录下用内核的Kbuild机制编译obj-m rk_multi_sensor.o然后执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -C /path/to/kernel M$(pwd) modules具体交叉编译工具链路径取决于SDK瑞芯微v2/v3 SDK一般用buildroot目录下的工具链。编出来的rk_multi_sensor.ko拷到板子上先确认I2C2总线上的设备地址能被扫到i2cdetect -y 2如果看到0x1a和0x1b都有编号出现说明硬件和设备树没问题可以加载模块insmod rk_multi_sensor.ko dmesg | tail -20正常会看到两条probe日志rk_multi_sensor: registered sensor-a-1a at addr 0x1a rk_multi_sensor: registered sensor-b-1b at addr 0x1b同时在/dev下看到两个节点ls -l /dev/sensor-*输出类似crw------- 1 root root 10, 55 Jan 1 00:00 /dev/sensor-a-1a crw------- 1 root root 10, 65 Jan 1 00:00 /dev/sensor-b-1b主设备号都是10miscdevice共用主设备号10次设备号不同这正是MISC_DYNAMIC_MINOR动态分配的效果。4.3 应用层验证数据隔离光看/dev节点还不够还要验证两个设备确实各管各的。写一个简单测试程序#include stdio.h #include fcntl.h #include sys/ioctl.h #include unistd.h int main(int argc, char **argv) { int fd, val; if (argc 2) { fprintf(stderr, usage: %s /dev/sensor-xxx\n, argv[0]); return 1; } fd open(argv[1], O_RDONLY); if (fd 0) { perror(open); return 1; } if (ioctl(fd, 0x02, val) 0) { perror(ioctl); return 1; } printf(%s counter: %d\n, argv[1], val); close(fd); return 0; }分别对两个节点执行三次会看到两个计数器各自增加互不影响。初始开两个终端各跑几遍输出类似sensor-a-1a counter: 1 sensor-a-1a counter: 2 sensor-a-1a counter: 3 sensor-b-1b counter: 1 sensor-b-1b counter: 2 sensor-b-1b counter: 3这基本就能证明per-device私有数据生效了。如果两个节点的计数值互相跳那就是全局变量导致的串扰回到3.1去检查。5. 常见问题与排查技巧实录5.1 问题速查表把我在瑞芯微平台调试多设备驱动时踩过和帮别人排过的坑整理一下现象根因排查命令/手段解决方法probe只执行一次只注册了一个节点设备树第二个节点status不是okay或reg地址和实际不符i2cdetect -y 总线号检查dts修正设备树reg和status/dev一个节点注册失败dmesg报device register失败miscdevice的name重名ls /dev/sensor-*name加地址或序号后缀device_get_match_data返回NULLof_match_table没有填.data或compatible大小写错检查源码匹配表、dts补全.data配置两个同型号设备数据串扰驱动用了全局变量保存状态看dmesg应用层验证counter改用devm_kzallocset_clientdata模块加载后probe日志都没有驱动没有编进内核或模块没加载成功lsmod、modprobe、dmesg确认insmod成功compatible匹配换内核版本后编译报错remove函数类型不匹配4.19内核i2c_driver.remove返回int5.10后返回void查看内核头文件按对应内核版本修改remove返回类型应用层open成功但ioctl报Bad addresscopy_to_user传入了非法用户指针检查应用层传参用copy_to_user/copy_from_user安全拷贝调用端检查arg5.2 详细排查思路第一类问题是“根本不出节点”。先不要看驱动首先确认设备树有没有生效ls /proc/device-tree/i2c2/sensor-a1a/ ls /proc/device-tree/i2c2/sensor-b1b/如果这两个目录不存在说明设备树没编进去或者节点被status disabled挡住了。直接用i2cdetect确认I2C地址排除硬件虚焊。确认设备和总线上都OK再回头查驱动匹配表。第二类问题是“出了节点但数据不对”。重点检查probe里的私有数据分配和保存环节。我见过有同事把per-device的data错误地挂到全局指针上第二次probe就把第一次的覆盖了。正确逻辑是每个设备在probe时都独立分配且通过i2c_set_clientdata绑定到自己的client上。任何时候想拿当前设备的私有数据都从client反查绝不要从全局变量取。第三类是兼容性问题。瑞芯微不同内核版本的API差异很大4.19和5.10之间改了一些函数签名。比如i2c_driver的remove4.19是int类型5.10改成了void。还有miscdevice注册的返回判断老内核用IS_ERR新内核统一用负数错误码。遇到编译错误先别急着改业务代码去内核头文件里确认当前版本的API原型这是经验之谈能省很多时间。我在实际调试中还发现一个特别实用的习惯在probe里多打几条dev_info日志把匹配到的型号名、I2C地址、动态名字都打出来。多设备驱动最容易出现“不知道自己当前在操作哪个设备”的迷失感日志里带上设备地址后配合wireshark抓I2C数据或者加dump_registers调试节点问题定位效率能提升一倍。另外顺便多说一句如果后续要让驱动支持热插拔或者支持运行时动态创建设备可以在probe基础上再补一个sysfs接口。比如在私有数据结构里创建attr_group这样应用层通过echo写/sys/class/xxx/yyy就能触发某个设备的重置或参数配置。多设备场景下sysfs里每个设备一个目录天然隔离比纯ioctl更直观。但这是另一个话题了核心还是先把数据隔离和节点命名做好。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →