ARTICLE DETAIL

资讯详情

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

RK3568多设备驱动开发:compatible匹配与实例数据隔离实战

RK3568多设备驱动开发:compatible匹配与实例数据隔离实战 我做了件很朴素的事把RK3568上两颗不同型号的触摸IC塞进同一个驱动里跑通。前后改了两版硬件一颗是GT911一颗是FT5x06引脚几乎兼容但芯片配置、复位时序完全不同。最初同事的方案是每个芯片复制一份platform_driverprobe逻辑90%重复后面排查问题要改两处。重构成一套驱动后代码量反而少了四成。这次改造里真正起作用的就两个小技巧一个解决“驱动怎么同时匹配多款设备”一个解决“同款设备挂着多个实例时怎么把数据隔离开”。这篇文章就讲这两个技巧全部基于瑞芯微平台代码和设备树片段可以直接拿到板子上试。1. 先想清楚一驱多设备到底在解决什么问题1.1 瑞芯微平台上最常见的“一驱多”场景瑞芯微平台一个很典型的特征是一颗SoC外设资源极其丰富同一类控制器动不动就有三四个。I2C控制器有5个SPI控制器不止2个UART更是随便数。所以“同型号外设挂多个”就是常态两块触摸屏分别挂在i2c0和i2c3下、双摄像头各占一条MIPI CSI、四路UART扩展接四个串口屏。这些外设底层寄存器、通信协议几乎一样驱动代码应该只有一份却要为每一路实例都保留独立的运行状态。另一个更麻烦的场景是“同一颗芯片、不同板卡”。同一个RK3568核心板客制化底板可能把触摸屏IC换掉也可能把音频Codec从A厂商换成B厂商。SDK不可能每个客户都维护一份kernel所以设备树里改一两个节点驱动里靠compatible自动区分这才是符合瑞芯微开发习惯的做法。1.2 为什么说设备树才是“一张万能适配表”很多从单片机转过来的人写驱动习惯在驱动源码里用宏、用配置头文件区分硬件版本#define BOARD_VERSION_2这套思路放到Linux驱动里是行不通的。原因很简单内核是面向通用二进制的你不可能为了某个客户的板子单独编一版内核。设备树的职责就是“描述硬件长什么样”驱动只负责“读取描述并按描述工作”。所以在瑞芯微平台上区分多设备的第一个天然入口就是设备树节点里的compatible字符串。内核在注册platform_driver时会扫描设备树里所有compatible和驱动维护的of_match_table一旦匹配上就调用probe。这等于内核帮你做了“这张表指向哪台设备”的决策。1.3 两个技巧的分工匹配差异与数据隔离我在项目里把问题拆成两半处理技巧一管“认亲”。所有需要驱动的设备不管硬件差异多大只要在of_device_id表里登记好compatible和对应的配置数据驱动就能准确认出“我是谁、我该用哪套参数”。技巧二管“分家”。同一个驱动被加载两次、三台设备被probe三次时每台设备都要有自己的运行时数据结构。查状态、读写寄存器、打开中断都必须落在当前这个实例上而不是操作了一个全局变量。两个技巧配合好才能实现“一套驱动支撑多设备”。下面我拆开讲。2. 技巧一用of_device_id匹配表区分不同设备批量适配多个compatible2.1 核心机制compatible是怎么被匹配上的驱动侧维护一个struct of_device_id数组比如static const struct of_device_id rk_demo_of_match[] { { .compatible rk,demo-a, .data rk_demo_a_cfg }, { .compatible rk,demo-b, .data rk_demo_b_cfg }, { /* sentinel */ } };设备树节点写下demo_a: demo-a0 { compatible rk,demo-a; };内核的platform_match会把设备节点的compatible和数组逐个比较。找到相同的compatible就返回匹配成功并且把这个of_device_id的指针保存在设备上。后面驱动在probe里通过device_get_match_data()就能拿到当初挂在.data上的配置数据。这个机制相当于你给驱动装了一个“指纹库”。每个设备节点提供自己的compatible指纹驱动比对后直接取回这台设备专属的配置结构体省掉一串strcmp判断代码干净很多。注意compatible的命名规范是厂商名,器件型号全部小写用逗号分隔。写成rk,demo-a这种不要用下划线也不要用大写。设备树和驱动里必须硬一致差一个字符都匹配不上。2.2 每台设备一份“出厂档案”cfg结构体怎么设计挂到.data里的配置结构体我习惯叫它“出厂档案”。它描述这设备所有差异点struct rk_demo_cfg { const char *model; int max_speed; int gpio_rst; bool use_irq; }; static struct rk_demo_cfg rk_demo_a_cfg { .model demo-a, .max_speed 100, .gpio_rst 0, .use_irq true, }; static struct rk_demo_cfg rk_demo_b_cfg { .model demo-b, .max_speed 200, .gpio_rst 1, .use_irq false, };probe里取出来用static int rk_demo_probe(struct platform_device *pdev) { struct device *dev pdev-dev; const struct rk_demo_cfg *cfg; cfg device_get_match_data(dev); if (!cfg) { dev_err(dev, no matching config\n); return -EINVAL; } dev_info(dev, probe %s, max_speed%d, gpio_rst%d\n, cfg-model, cfg-max_speed, cfg-gpio_rst); ... }device_get_match_data是内核封装好的接口本质是of_match_device(...)-data。推荐优先使用它因为它在ACPI和设备树两种场景下都能工作。数据放进结构体而不是散落在if分支里的好处很明显新增第三款芯片时只要加一个struct rk_demo_cfg实例、一行of_device_idprobe和业务函数一行不用改。产品适配直接变成“填表”。2.3 为什么把配置放match.data比在probe里用strcmp更靠谱早期驱动喜欢这么写if (of_device_is_compatible(dev-of_node, rk,demo-a)) { /* 处理demo-a */ } else if (of_device_is_compatible(dev-of_node, rk,demo-b)) { /* 处理demo-b */ }功能上没错但有一堆隐患每次新增型号都要往probe里塞一个分支代码越堆越长字符串比对分散在代码各个角落后期排查很容易漏改一处而且of_device_is_compatible只能告诉你“是不是这个设备”不能顺便把配置带出来。相比之下匹配表.data这种方式把“识别”和“参数”绑定到一起新硬件适配从“改代码写if”变成“改表格”维护成本明显下降。我在瑞芯微SDK上重构这类驱动时凡是需要区分多型号的设备原则都是差异进结构体逻辑走通用分支。寄存器地址不同、中断号不同、速率不同全部收敛到cfg字段里业务代码抽成公共函数。2.4 瑞芯微平台上的几个实操细节第一MODULE_DEVICE_TABLE这一行不能省。很多人在SDK里把驱动编成模块后modprobe时发现加载不了就是漏了这行。它会在模块编译时生成alias信息modprobe靠它做设备到模块的自动关联。built-in方式编译则影响不大但建议统一加上MODULE_DEVICE_TABLE(of, rk_demo_of_match);第二检查内核配置。CONFIG_OF必须打开瑞芯微默认是y基本不用管。但如果你在一个裁剪很狠的内核上做实验可以先确认一下zcat /proc/config.gz | grep CONFIG_OF第三设备树里status属性没设okay节点不会变成platform_device驱动自然不probe。RK3568的设备树里很多外设节点默认disabled需要底板上显式打开。3. 技巧二多个设备实例的数据隔离别让全局变量毁掉你的驱动3.1 多实例场景同一个节点被probe多次数据怎么办技巧一解决的是“不同型号”技巧二解决的是“同型号多个实例”。比如两块屏都挂rk,demo-a设备树这样写i2c0 { demo0: demo10 { compatible rk,demo-a; reg 0x10; }; }; i2c1 { demo1: demo10 { compatible rk,demo-a; reg 0x10; }; };两个节点compatible相同驱动会被probe两次。probe入口一样传进来的struct platform_device *pdev不同。你代码里所有设备状态都必须挂在pdev对应的私有数据上。3.2 全局变量翻车现场双摄像头花屏问题我之前在RK3568上调过双摄像头驱动。初次接手时驱动里为了省事定义了一个全局指针static struct cam_dev *g_cam;probe里简单g_cam cam_dev_alloc(pdev);。结果就是后probe的摄像头把g_cam覆盖掉了。第一个摄像头的中断处理函数跑起来后通过g_cam访问到的是第二路的寄存器画面直接花掉日志里还经常把A设备的中断挂在B设备上。排查这种问题非常痛苦因为现象飘忽不定重启顺序不同结果都可能不一样。结论很明确多实例驱动里全局变量只允许放“平台级共享资源”比如唯一的class指针、唯一的设备号分配基准绝不允许放“跟设备实例绑定的状态”。3.3 把私有数据绑定到deviceplatform_set_drvdata与devm_kzalloc正确的做法是定义一个运行时数据结构在probe里分配并绑定struct rk_demo_dev { struct device *dev; const struct rk_demo_cfg *cfg; void __iomem *base; int irq; struct cdev cdev; dev_t devno; struct device *clsdev; spinlock_t lock; };probe里这样分配struct rk_demo_dev *rd; rd devm_kzalloc(pdev-dev, sizeof(*rd), GFP_KERNEL); if (!rd) return -ENOMEM; rd-dev pdev-dev; rd-cfg device_get_match_data(pdev-dev); platform_set_drvdata(pdev, rd);devm_kzalloc里的devm是设备资源管理。这块内存的生命周期跟设备绑定设备移除、驱动卸载、probe失败需要回滚时内核自动释放不需要手工kfree。这是内核提供的偷懒技能也是多实例驱动最稳妥的内存管理方式。之后在任何需要拿当前实例的地方static int rk_demo_remove(struct platform_device *pdev) { struct rk_demo_dev *rd platform_get_drvdata(pdev); ... }即使驱动被probe一百次每次拿到的都是自己的那份数据。3.4 中断、字符设备、次设备号的多实例处理多实例最容易踩的第二个坑是字符设备和中断号。字符设备这块每个实例必须注册独立的cdev和独立的次设备号。我推荐用alloc_chrdev_region动态分配不要自己静态指定ret alloc_chrdev_region(rd-devno, 0, 1, rk_demo); if (ret 0) return ret;这样第一路设备自动拿到minor 0第二路自动拿到minor 1第三路以此类推/dev/rk_demo0、/dev/rk_demo1互不干扰。中断这块共享中断尤其要注意。如果两路设备共用一个GPIO中断request_irq最后一个参数dev_id必须传当前实例私有数据ret request_irq(irq, rk_demo_irq_handler, IRQF_SHARED, rk_demo, rd);释放时也一样free_irq(irq, rd);dev_id是内核在中断处理函数里区分实例的唯一凭据。如果传NULL共享中断注册就失败或者中断释放时提示“Trying to free already-free IRQ”。中断处理函数开头用dev_id拿到自己的实例数据static irqreturn_t rk_demo_irq_handler(int irq, void *dev_id) { struct rk_demo_dev *rd dev_id; ... }3.5 资源释放与of_node引用计数remove里应该按逆序释放申请的资源。我通常这样写static int rk_demo_remove(struct platform_device *pdev) { struct rk_demo_dev *rd platform_get_drvdata(pdev); device_destroy(rk_demo_class, rd-devno); cdev_del(rd-cdev); unregister_chrdev_region(rd-devno, 1); if (rd-irq) free_irq(rd-irq, rd); dev_info(pdev-dev, rk_demo removed\n); return 0; }因为内存是用devm_kzalloc分配的不用手动释放。但free_irq、device_destroy、cdev_del这些显式申请的内核资源必须自己释放。顺序上先销毁用户可见的设备节点再卸载中断、删除cdev避免正在打开设备的用户态进程访问到已经释放的资源。4. 实战演示在RK3568上把两个技巧合起来写一套完整驱动4.1 实验环境准备我用的环境是RK3568 EVB开发板内核5.10Buildroot 2023.02交叉编译工具链aarch64-linux-gnu-。如果你是其他版本内核接口基本兼容只有极个别差异我会在文章里标注。驱动编译采用内核外部模块方式源码目录结构rk_demo/ ├── Makefile └── rk_demo.cMakefile内容obj-m rk_demo.o KDIR : /path/to/your/kernel ARCH : arm64 CROSS_COMPILE : aarch64-linux-gnu- all: $(MAKE) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) -C $(KDIR) M$(PWD) clean编译make生成rk_demo.ko后拷到板子上。4.2 完整驱动代码匹配表 实例私有数据下面是一套可以直接跑的完整驱动把技巧一和技巧二都放进去。代码只保留最核心的逻辑便于看结构。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/of_device.h #include linux/cdev.h #include linux/fs.h #include linux/device.h #include linux/slab.h #include linux/uaccess.h #define RK_DEMO_CLASS_NAME rk_demo /* 设备差异档案由of_device_id的data字段携带 */ struct rk_demo_cfg { const char *model; int max_speed; }; static struct rk_demo_cfg rk_demo_a_cfg { .model demo-a, .max_speed 100, }; static struct rk_demo_cfg rk_demo_b_cfg { .model demo-b, .max_speed 200, }; /* 每个设备实例的运行时数据 */ struct rk_demo_dev { struct platform_device *pdev; const struct rk_demo_cfg *cfg; struct cdev cdev; dev_t devno; struct device *clsdev; int open_count; }; static int rk_demo_open(struct inode *inode, struct file *file) { struct rk_demo_dev *rd container_of(inode-i_cdev, struct rk_demo_dev, cdev); file-private_data rd; rd-open_count; dev_info(rd-pdev-dev, open count%d model%s\n, rd-open_count, rd-cfg-model); return 0; } static long rk_demo_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct rk_demo_dev *rd file-private_data; dev_info(rd-pdev-dev, ioctl cmd0x%x model%s\n, cmd, rd-cfg-model); return 0; } static const struct file_operations rk_demo_fops { .owner THIS_MODULE, .open rk_demo_open, .unlocked_ioctl rk_demo_ioctl, }; static int rk_demo_probe(struct platform_device *pdev) { struct device *dev pdev-dev; const struct rk_demo_cfg *cfg; struct rk_demo_dev *rd; int ret; int minor; cfg device_get_match_data(dev); if (!cfg) { dev_err(dev, no matched config\n); return -EINVAL; } rd devm_kzalloc(dev, sizeof(*rd), GFP_KERNEL); if (!rd) return -ENOMEM; rd-pdev pdev; rd-cfg cfg; platform_set_drvdata(pdev, rd); /* 每个实例独立分配次设备号 */ ret alloc_chrdev_region(rd-devno, 0, 1, rk_demo); if (ret 0) return ret; minor MINOR(rd-devno); cdev_init(rd-cdev, rk_demo_fops); rd-cdev.owner THIS_MODULE; ret cdev_add(rd-cdev, rd-devno, 1); if (ret) { unregister_chrdev_region(rd-devno, 1); return ret; } rd-clsdev device_create(rk_demo_class, dev, rd-devno, NULL, rk_demo%d, minor); if (IS_ERR(rd-clsdev)) { ret PTR_ERR(rd-clsdev); cdev_del(rd-cdev); unregister_chrdev_region(rd-devno, 1); return ret; } dev_info(dev, probe ok minor%d model%s max_speed%d\n, minor, cfg-model, cfg-max_speed); return 0; } static int rk_demo_remove(struct platform_device *pdev) { struct rk_demo_dev *rd platform_get_drvdata(pdev); device_destroy(rk_demo_class, rd-devno); cdev_del(rd-cdev); unregister_chrdev_region(rd-devno, 1); dev_info(pdev-dev, remove ok\n); return 0; } /* 技巧一核心一张匹配表登记多个设备 */ static const struct of_device_id rk_demo_of_match[] { { .compatible rk,demo-a, .data rk_demo_a_cfg }, { .compatible rk,demo-b, .data rk_demo_b_cfg }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, rk_demo_of_match); static struct platform_driver rk_demo_driver { .probe rk_demo_probe, .remove rk_demo_remove, .driver { .name rk_demo, .of_match_table rk_demo_of_match, }, }; static struct class *rk_demo_class; static int __init rk_demo_init(void) { int ret; /* 内核6.4之后class_create只需要一个参数5.10需要两个 */ rk_demo_class class_create(THIS_MODULE, RK_DEMO_CLASS_NAME); if (IS_ERR(rk_demo_class)) return PTR_ERR(rk_demo_class); ret platform_driver_register(rk_demo_driver); if (ret) class_destroy(rk_demo_class); return ret; } static void __exit rk_demo_exit(void) { platform_driver_unregister(rk_demo_driver); class_destroy(rk_demo_class); } module_init(rk_demo_init); module_exit(rk_demo_exit); MODULE_LICENSE(GPL);代码里有两点需要根据你的内核版本适配class_create在6.4之前需要传THIS_MODULE和名字两个参数我在示例里按5.10写法如果你的内核是6.1或更新自己对照调整即可。4.3 设备树两个节点、两类compatible怎么写在RK3568的设备树里加一个测试用的simple-bus节点挂在根节点下面/ { demo_bus: demo-bus { compatible simple-bus; #address-cells 1; #size-cells 1; ranges; demo_a: demo0 { compatible rk,demo-a; reg 0x0 0x0; status okay; }; demo_b: demo1 { compatible rk,demo-b; reg 0x1 0x0; status okay; }; }; };如果你是在真实外设上验证只要把这两个节点换到对应I2C总线、SPI总线下面compatible、reg改成实际值即可。匹配机制完全一样。编译设备树、烧录后启动dmesg应该能看到类似输出rk_demo demo0: probe ok minor0 modeldemo-a max_speed100 rk_demo demo1: probe ok minor1 modeldemo-b max_speed200同时查看设备节点ls -l /dev/rk_demo*会看到/dev/rk_demo0和/dev/rk_demo1两个设备文件。4.4 用open测试两个实例的独立性写个简单测试程序打开两个节点分别做一次ioctl#include stdio.h #include fcntl.h #include sys/ioctl.h int main(void) { int fd0, fd1; fd0 open(/dev/rk_demo0, O_RDONLY); fd1 open(/dev/rk_demo1, O_RDONLY); if (fd0 0 || fd1 0) { perror(open); return -1; } ioctl(fd0, 0x100, 0); ioctl(fd1, 0x100, 0); close(fd0); close(fd1); return 0; }板载日志里会看到两条带不同model的打印说明两个设备实例的数据确实隔离了rk_demo demo0: ioctl cmd0x100 modeldemo-a rk_demo demo1: ioctl cmd0x100 modeldemo-b如果这里打印出来的model都是同一个那说明你把platform_get_drvdata或file-private_data取错了。4.5 这套结构怎么扩展到真实项目真实项目里struct rk_demo_cfg里还会放寄存器基地址偏移、中断GPIO编号、时钟频率、Pinctrl状态名等。struct rk_demo_dev里还会放void __iomem *base、struct clk *clk、struct regulator *reg、struct gpio_desc *reset_gpio等运行时资源。probe里逐个申请全部丢到rd里。业务函数通过private_data或platform_get_drvdata拿回rd再用rd-cfg拿差异配置。整个驱动无论支持多少个设备逻辑都是一条线识别型号、拿配置、建实例、注册节点。5. 常见问题与排查技巧实录5.1 明明设备树写对了就是probe不到这是多设备驱动里出现频率最高的问题。我排查顺序一般是先看设备树节点有没有生效。启动后进板子系统执行ls /proc/device-tree/demo-bus/如果没有demo0、demo1目录说明设备树没编进去或者节点被disabled。status没写okay是最常见原因。再看驱动有没有被加载。如果编成模块lsmod | grep rk_demo没加载就手动insmod再查dmesg。如果insmod后依然没probe多半是compatible不一致。把设备树节点里的compatible打出来对比cat /proc/device-tree/demo-bus/demo0/compatible和of_device_id里的字符串一个字符一个字符地比对。rk,demo-a写成rk,demoA或者rk-demo-a都是匹配不上的。还要确认驱动有没有注册成功。platform_driver_register返回0不代表设备匹配成功它只是把驱动挂到了总线上。查看ls /sys/bus/platform/drivers/rk_demo/如果目录下没有demo0、demo1说明匹配失败往compatible方向查。5.2 第二个设备一open就崩溃或者数据错乱这种问题多半是全局变量导致的数据覆盖或者file-private_data和cdev的对应关系没建立好。检查点第一个设备节点的inode里i_cdev在open时通过container_of找struct rk_demo_dev这一步要求cdev必须是嵌入在rk_demo_dev里的成员而不是独立分配的。另一个常见错误是设备号分配方式。两次probe如果用了同一个静态dev_t第二次cdev_add再次注册同一个设备号open时内核找到的cdev可能就是同一个实例自然串了。alloc_chrdev_region动态分配就是为了避免这个问题。5.3 中断注册失败、free_irq报错共享中断在request_irq时没传IRQF_SHARED标志会注册失败。不同实例的中断如果共用一个物理GPIO必须加上这个标志。free_irq报“Trying to free already-free IRQ”时说明传入的dev_id不是注册时那个或者中断已经被其他路径释放。多实例驱动尤其要检查remove里free_irq用的dev_id必须和request_irq用的完全一致。5.4 调试技巧让日志告诉你“这是哪个实例”多实例调试最怕看到日志里一堆打印却分不清是哪路设备。我的经验是每个关键路径都带dev_info(rd-pdev-dev, ...)而不是printk。用dev_xxx打印会自动带上设备名比如rk_demo demo0或rk_demo demo1一眼就能看出是哪个实例。另外ioctl里通过file-private_data拿实例时如果怀疑拿错了直接打印dev_info(rd-pdev-dev, minor%d\n, MINOR(rd-devno));内核日志里会显示当前操作的是minor 0还是minor 1极快定位问题。5.5 问题速查表现象可能原因排查方向probe没有被调用compatible不匹配、status不是okay、驱动未注册对比/proc/device-tree节点和of_match_tablemodprobe加载不了模块缺少MODULE_DEVICE_TABLE确认of_match_table末尾有sentinel且调用宏多实例数据串扰全局变量保存了实例状态数据改用platform_set_drvdata绑定第二个/loop设备节点创建失败设备号冲突、class_device重复用alloc_chrdev_region动态分配中断释放报错dev_id参数不一致request_irq和free_irq都传相同实例指针open里cdev实例不对i_cdev不是rk_demo_dev的成员把cdev嵌入结构体用container_of获取remove时崩溃用户态还持有打开文件remove前半段先销毁设备节点业务函数处理竞态写在后面瑞芯微平台的驱动开发设备树是绕不开的骨架compatible匹配则是驱动和设备树之间的枢纽。把“匹配多设备”交给of_device_id表格把“实例隔离”交给platform_set_drvdata两个技巧组合起来无论板子后面加多少路外设、硬件改多少版驱动代码的主体部分都能稳定不动。我个人习惯是一开始写驱动就先把cfg结构体和of_match_table定义好再把私有数据结构设计出来最后才写业务函数。顺序反过来也可以但总免不了后面返工。如果你也在RK3568或其他瑞芯微芯片上调类似需求不妨按这套结构把你的第一个多设备驱动跑起来跑通之后再往里面填业务会顺手很多。
返回列表