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

资讯详情

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

瑞芯微平台Linux驱动:一个驱动支持多个设备的两大技巧

瑞芯微平台Linux驱动:一个驱动支持多个设备的两大技巧 做Linux驱动开发的朋友尤其是搞瑞芯微这类SoC的多半遇到过这样的需求一块板子上挂两个型号接近的外设或者同一条I2C总线上有两颗完全相同的传感器。第一次写驱动时我们很自然地给每个外设写一个驱动文件反正Copy-Paste也快等到设备树里加新节点、调寄存器、修Bug时才发现维护成本高得离谱改一个逻辑要同步改好几个文件漏一个就出奇奇怪怪的问题。这篇文章就围绕我在瑞芯微平台上的实际经历讲两个让一个驱动支持多个设备的小技巧一个是用of_device_id里的data字段区分同类型但不同型号的设备另一个是用设备树alias加miscdevice动态生成实例节点让驱动能优雅地管理多个同型号设备实例。这两个技巧我会配完整的设备树和驱动代码也会把排查思路写出来适合在RK3568、RV1126、RK3588这类平台上做嵌入式Linux驱动开发的工程师参考。1. 为什么一个驱动支持多个设备会变成难题1.1 瑞芯微平台上的多设备场景瑞芯微平台在工控、AIoT、商显领域用得特别多板子上外设数量往往很夸张。一类典型场景是“多颗同型号传感器”比如双目摄像头方案里经常用到两颗同样的图像传感器或者机器人主板上放两颗IMU用于冗余校验。另一类场景是“同协议不同型号”比如I2C总线上挂了一颗温度传感器和一颗湿度传感器它们的寄存器地址、转换系数完全不同但都是标准的I2C设备。这两类需求在驱动层都会转化为同一个问题怎么用一套驱动代码把多个设备实例管理得井井有条。一开始我图省事把驱动写成一个大杂烩probe里根据pdev-name或者of_node-name判断是哪个设备然后分叉执行不同逻辑。这个方案在只有一个设备时跑得挺欢一旦挂两个以上probe顺序、设备树节点名、平台总线匹配机制这些因素掺和进来立刻就开始失控。后来我拆开看过内核的匹配流程才意识到问题出在“设备是谁匹配给驱动的”和“驱动怎么知道自己服务的是哪个设备”这两件本该分离的事情上被我搅在一起了。1.2 驱动模型与设备树匹配机制的底层逻辑Linux内核的platform驱动和设备树之间有一条清晰的匹配链路。设备树每个节点有compatible属性内核在启动时会把节点注册成platform_device驱动注册时platform_match会拿驱动的of_match_table和节点的compatible去比较。命中以后内核调用驱动的probe函数把对应的platform_device指针传进来。这时候驱动能通过pdev-dev.of_node拿到设备树节点进一步解析寄存器地址、中断号、GPIO等资源。关键点在于of_match_table的每一个条目除了compatible字符串还可以挂一个const void *data指针。这个data不参与匹配只是跟着条目走。也就是说驱动可以同时声明支持多个compatible并且为每个compatible准备好对应的配置结构体。等到probe执行时用of_device_get_match_data就能把当前设备对应的配置取出来。这个机制就是技巧一的基础。以前不知道这个用法时我都是在probe里自己比对字符串代码又长又容易错现在想想纯属绕远路。1.3 不处理好会踩什么坑先盘点一下“一个驱动硬撑多个设备”时最常见的翻车现场设备节点只创建一个第二个设备的/dev节点直接被覆盖或者注册失败probe只为第一个节点调用第二个设备完全无驱动寄存器配置被后加载的设备覆盖用固定次设备号注册字符设备时返回EBUSY。这些坑我基本都踩过一遍尤其是次设备号冲突排查起来特别头疼因为错误信息往往指向另一个设备。后面要讲的两个技巧正好对应这两类问题同型号但不同硬件配置的设备用data字段去区分多个完全相同的实例用动态次设备号加实例编号去管理。2. 技巧一巧用of_match_table的data字段区分设备型号2.1 原始方案为每个型号新建驱动很多新手包括早期的我遇到“驱动要支持A和B两个型号”时第一个反应是写两个驱动文件。比如rk_sensor_a.c和rk_sensor_b.c各自定义一套probe、remove、file_operations。这样做短期内很快但很快就会发现公共代码开始膨胀地址映射、中断申请、字符设备注册这些逻辑几乎一模一样。你复制过来时还不敢随便删怕A改过的Bug在B里没改回去。这个项目的教训让我彻底放弃了多文件复制方案。同样的逻辑也可以写在同一个文件里用probe函数里的字符串判断区分。比如if (of_node_is_compatible(pdev-dev.of_node, vendor,sensor-a)) init_type_a(); else if (of_node_is_compatible(pdev-dev.of_node, vendor,sensor-b)) init_type_b();这个方法比复制文件强一些但同样不够优雅。随着型号增多if-else链越来越长如果某个型号和另一个型号只有寄存器地址差一个偏移量你很快会开始怀疑人生。2.2 核心代码在匹配表里挂上数据正确做法是把“有什么型号”和“每个型号长什么样”声明在of_device_id表里。比如我这个项目里有两颗外设一颗寄存器基址是0x10另一颗是0x20控制的默认开关状态也不一样#include linux/module.h #include linux/platform_device.h #include linux/of_device.h #include linux/of.h #include linux/miscdevice.h #include linux/slab.h enum rk_sensor_type { SENSOR_TYPE_TEMP, SENSOR_TYPE_HUMI, }; struct rk_sensor_cfg { enum rk_sensor_type type; u32 default_reg; const char *name; }; static const struct rk_sensor_cfg rk_sensor_temp_cfg { .type SENSOR_TYPE_TEMP, .default_reg 0x10, .name rk-tsensor, }; static const struct rk_sensor_cfg rk_sensor_humi_cfg { .type SENSOR_TYPE_HUMI, .default_reg 0x20, .name rk-hsensor, }; static const struct of_device_id rk_sensor_of_match[] { { .compatible vendor,temp-sensor, .data rk_sensor_temp_cfg }, { .compatible vendor,humi-sensor, .data rk_sensor_humi_cfg }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, rk_sensor_of_match);注意逗号后面那个rk_sensor_temp_cfg就是我说的高价值data字段。设备树节点里写compatible vendor,temp-sensor时内核匹配到这个条目后会把data指针原样留给驱动。2.3 在probe里读取data并动态配置probe函数里的取法就三行static int rk_sensor_probe(struct platform_device *pdev) { struct device *dev pdev-dev; const struct rk_sensor_cfg *cfg; struct rk_sensor_priv *priv; int ret; cfg of_device_get_match_data(dev); if (!cfg) { dev_err(dev, failed to get match data\n); return -EINVAL; } priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-cfg cfg; priv-reg_base cfg-default_reg; /* 后面初始化寄存器、注册misc设备时都从cfg里取参数 */ dev_info(dev, probe %s, type%d, default_reg0x%02x\n, cfg-name, cfg-type, cfg-default_reg); ret rk_register_misc_device(dev, priv); if (ret) return ret; platform_set_drvdata(pdev, priv); return 0; }对比之前的if-else版本这个写法的优势在新增型号时特别明显。假设明年加了一颗第三方的气体传感器只需要三步在设备树里给节点配一个新的compatible字符串在驱动里加一个rk_sensor_gas_cfg结构体在of_device_id表里加一行条目。probe函数一行都不用改。2.4 注意事项与边界情况这个技巧看着简单但有几个坑得提醒一下。第一of_device_get_match_data返回的是const void *类型不安全建议在取出来后立刻转成实际的配置结构体指针。第二MODULE_DEVICE_TABLE一定不能漏否则设备树匹配信息不会编进模块的alias里可能出现在板子上没问题、编成模块加载时匹配不上的怪现象。第三如果两个compatible对应的配置结构体差别很大建议在结构体里放一个type枚举做辅助判断不要用字符串硬比字符串比较容易因为大小写、命名不统一而出错。我在RK3568上调试时还遇到过一个隐蔽问题同一个设备树节点里如果不小心写了两个compatible比如vendor,temp-sensor, vendor,humi-sensor内核会按顺序找第一个匹配到的of_device_id条目data也会取那个条目的配置。这种写法并不会报错但很容易让你懵半天——明明设备是温度传感器初始化的却是湿度传感器的寄存器。所以设备树里一个节点只保留一个最有代表性的compatible别贪多。3. 技巧二用alias加动态次设备号实现多实例管理3.1 miscdevice的局限与设备号分配问题处理完“不同型号”之后真正让我头疼的是“多个同型号设备”。还是以RK3568为例我在一个项目里要同时挂两片同样的触摸控制器它们走的是同一套I2C控制器各自有自己的中断引脚和复位引脚。设备树里可以创建两个节点probe也会被调用两次但字符设备注册就出问题了。刚开始我用的是register_chrdev它申请主设备号、次设备号固定为0第二次调用时直接失败。后来换成miscdevice情况好一点因为misc子系统里每个设备会分配独立的次设备号。但紧接着又踩了另一个坑如果两个实例的miscdevice.name都是同一个字符串device_create会在/dev下创建两个同名的节点第二次注册会失败甚至错乱。解决思路很朴素给每个实例起一个带编号的名字。3.2 通过设备树alias获取实例编号给设备编号的办法有好几种最直观的是直接在设备树节点里加reg属性比如reg 0和reg 1在probe里解析。但在瑞芯微平台上我更推荐用aliases节点加of_alias_get_id因为它语义清晰而且编号顺序可以由产品定义不受设备树节点排序影响。设备树里这样写/ { compatible rockchip,rk3568; aliases { rk_ts0 ts0; rk_ts1 ts1; }; ts0: touch0 { compatible vendor,multi-touch; reg 0x0; interrupt-parent gpio1; interrupts RK_PB2 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio1 RK_PB1 GPIO_ACTIVE_LOW; status okay; }; ts1: touch1 { compatible vendor,multi-touch; reg 0x1; interrupt-parent gpio1; interrupts RK_PC0 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio1 RK_PC1 GPIO_ACTIVE_LOW; status okay; }; };注意aliases里的rk_ts0和rk_ts1这两个名字前缀rk_ts会被of_alias_get_id用到。驱动的probe里这样拿编号static int rk_multi_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct rk_multi_priv *priv; int instance_id; int ret; instance_id of_alias_get_id(dev-of_node, rk_ts); if (instance_id 0) { dev_err(dev, failed to get alias id: %d\n, instance_id); return instance_id; } priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-instance_id instance_id; ... }这样ts0节点的probe拿到0ts1节点拿到1编号稳定调试时一眼就能看懂。3.3 完整代码为每个实例动态创建设备节点拿到编号之后核心就是动态拼接设备名并注册miscdevice。我把这一段的完整逻辑整理在下面#define DRIVER_NAME rk_multi_touch static const struct file_operations rk_multi_fops { .owner THIS_MODULE, .open rk_multi_open, .read rk_multi_read, .unlocked_ioctl rk_multi_ioctl, .release rk_multi_release, }; struct rk_multi_priv { struct device *dev; struct miscdevice mdev; void __iomem *base; int irq; int instance_id; /* 其他业务字段 */ }; static int rk_multi_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct rk_multi_priv *priv; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv-dev dev; priv-instance_id of_alias_get_id(dev-of_node, rk_ts); if (priv-instance_id 0) { dev_err(dev, failed to get alias id\n); return priv-instance_id; } /* 根据实例编号动态生成设备节点名/dev/rk_ts0、/dev/rk_ts1 */ priv-mdev.name devm_kasprintf(dev, GFP_KERNEL, rk_ts%d, priv-instance_id); if (!priv-mdev.name) return -ENOMEM; priv-mdev.minor MISC_DYNAMIC_MINOR; priv-mdev.fops rk_multi_fops; priv-mdev.parent dev; ret misc_register(priv-mdev); if (ret) { dev_err(dev, failed to register misc device: %d\n, ret); return ret; } /* 这里继续做gpio、irq、寄存器映射等初始化 */ platform_set_drvdata(pdev, priv); dev_info(dev, registered /dev/%s, instance%d\n, priv-mdev.name, priv-instance_id); return 0; } static int rk_multi_remove(struct platform_device *pdev) { struct rk_multi_priv *priv platform_get_drvdata(pdev); misc_deregister(priv-mdev); return 0; } static const struct of_device_id rk_multi_of_match[] { { .compatible vendor,multi-touch }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, rk_multi_of_match); static struct platform_driver rk_multi_driver { .probe rk_multi_probe, .remove rk_multi_remove, .driver { .name DRIVER_NAME, .of_match_table rk_multi_of_match, }, }; module_platform_driver(rk_multi_driver);devm_kasprintf把动态字符串和设备生命周期绑在一起设备释放时会自动回收内存省得在remove里手动kfree。misc_register内部会为每个设备分配独立的次设备号所以两个实例互不干扰。我实测过挂在/sys/class/misc/下也是两个独立条目一个叫rk_ts0一个叫rk_ts1。3.4 用户空间如何区分多个设备节点驱动注册好了用户空间拿到的就是熟悉的/dev/rk_ts0和/dev/rk_ts1。Application端只需要根据业务逻辑选择打开哪个设备。如果产品里两个设备的物理位置有明确区分比如一个在正面一个在背面建议在驱动里把instance_id通过read、ioctl或sysfs暴露给用户态这样App就可以动态识别。如果想走sysfs可以在probe里给设备节点创建属性文件。用DEVICE_ATTR定义一个instance_id属性然后放到平台的sysfs目录比如static ssize_t instance_id_show(struct device *dev, struct device_attribute *attr, char *buf) { struct rk_multi_priv *priv dev_get_drvdata(dev); return sysfs_emit(buf, %d\n, priv-instance_id); } static DEVICE_ATTR_RO(instance_id);然后在probe里device_create_file(dev, dev_attr_instance_id)。这样在板子上执行cat /sys/devices/platform/.../instance_id就能看到编号。实测下来这个属性文件对调试多设备特别有用尤其是设备树里节点顺序和实际硬件位置不一致时能少走很多弯路。4. 瑞芯微平台实操从设备树到驱动的完整移植4.1 设备树编写要点设备树是瑞芯微平台上驱动能不能正常工作的第一步。除了上一节提到的aliases还有几个容易被新手忽略的地方。status属性必须写okay。瑞芯微的SDK里很多节点默认是disabled如果外部设备没有真正启用probe根本不会被调用。我遇到过最坑的一次新加的节点忘了写status调试了一下午最后发现内核日志里压根没有该设备的匹配记录。引脚配置通过pinctrl子节点控制。一般先在pinctrl里定义ts0_irq、ts0_reset这些子节点然后在设备节点里引用比如ts0 { pinctrl-names default; pinctrl-0 ts0_irq ts0_reset; };瑞芯微平台要求中断引脚和GPIO复用配置一致否则gpio_request或者request_irq会报-EBUSY。这个错我印象很深因为报错信息可能出现在另一个设备上不好定位。reg属性在同一个总线上主要用于区分不同设备地址对I2C设备来说就是7位从机地址。多个同型号设备如果挂在同一条I2C总线上它们的I2C地址必须不同否则物理层就冲突了。如果两颗芯片靠电阻配置了不同地址驱动里解析reg时要仔细按十六进制写对。4.2 驱动目录与编译配置在瑞芯微的SDK里驱动代码一般放在kernel/drivers/的对应子目录下。如果是自定义外设驱动我喜欢建一个独立目录比如kernel/drivers/misc/rk_multi_touch。这里选择一个干净的写法。先建Kconfigconfig RK_MULTI_TOUCH tristate Rockchip multi touch controller support depends on OF MISC_DEVICES help Rockchip multi touch controller driver support.再建Makefileobj-$(CONFIG_RK_MULTI_TOUCH) rk_multi_touch.o然后在上级drivers/misc/Kconfig和drivers/misc/Makefile里分别加上对应的source和obj。这样在make menuconfig里就能看到选项编译模块时CONFIG_RK_MULTI_TOUCHm编进内核时CONFIG_RK_MULTI_TOUCHy。瑞芯微的SDK通常会用make rockchip_defconfig生成配置我一般直接改defconfig把选项打开避免每次都要手动选。4.3 内核编译与烧录验证以RK3568为例编译前先确认交叉编译工具链和SDK环境变量已经source过。瑞芯微官方SDK通常提供了编译脚本也可以手动执行make ARCHarm64 rockchip_linux_defconfig make ARCHarm64 menuconfig make ARCHarm64 -j$(nproc) Image dtbs如果驱动编成模块还会生成rk_multi_touch.ko。烧录时我一般把新的boot.img或resource.img单独烧进去不用整包烧写。瑞芯微的烧写工具会在烧录后自动校验如果设备树解析出错板的console一般会打印OF: fdt: ...相关的错误信息这时候优先查设备树语法和节点匹配。设备树编译生成.dtb后可以用fdtdump或者dtc反编译确认节点信息dtc -I dtb -O dts -o extracted.dts boot.dtb grep -A 20 multi-touch extracted.dts我习惯把这一步放到验证环节的第一步先确认硬件描述已经进固件再开始查驱动。4.4 运行验证/dev节点、sysfs与实际读写驱动加载后第一件事是看内核日志dmesg | grep rk_multi正常能看到两条registered /dev/rk_ts0和registered /dev/rk_ts1的日志。然后用ls -l /dev/rk_ts*确认字符设备节点cat /sys/class/misc/rk_ts0/instance_id确认实例编号。如果设备节点没出现排查方向是misc_register是否返回错误、设备名是否重复、驱动的probe是否真的执行了。如果驱动要操作I2C我还会顺手用i2cdetect -y bus号去扫描总线上的设备地址和驱动里解析到的地址核对一遍。瑞芯微平台上有多个I2C控制器设备树里i2cfe5a0000这种节点容易搞混确认半天不如直接在用户态扫一下来得快。5. 常见问题与排查技巧实录5.1 probe进不去或只执行一次这是多设备驱动最常遇到的问题。先说结论probe只执行一次要么是设备树节点没配对要么是of_match_table没声明对要么是节点status不是okay。排查步骤我建议按顺序来先看/sys/bus/platform/devices/下有没有对应的设备节点注册没有就是设备树解析问题再看/sys/bus/platform/drivers/rk_multi_touch/下有没有绑定关系没有就是驱动匹配问题最后确认设备树编译进固件时有没有应用到你正在烧录的dtb。瑞芯微支持多个dtb按board版本加载烧错了dtb是常见人为失误。5.2 次设备号冲突导致注册失败用register_chrdev固定次设备号0时会冲突这个前面已经说过。用了miscdevice也还有可能冲突如果两个驱动模块都指定了同一个固定的minormisc_register会返回-EBUSY。解决方案有两个一是统一改成MISC_DYNAMIC_MINOR让内核自动分配二是通过alloc_chrdev_region加cdev自己管理设备号。我自己在正式项目中更倾向于把这类设备做成标准的cdev驱动因为misc设备共享主设备号10次设备号空间只有0到255如果同一个板子上的misc设备特别多长期维护有隐患。小项目图快用miscdevice完全没问题但心里要清楚这个边界。5.3 中断号或GPIO资源申请失败瑞芯微平台的GPIO和中断经常是同一个引脚申请中断前如果没有正确配置pinctrl很容易出现gpio_request成功但request_irq失败的情况。排查时先看设备树里interrupts属性和pinctrl的引脚是否一致再看dmesg有没有failed to request irq提示。有一回我查了很久最后发现两个设备节点的中断引脚写反了一个设备申请了另一个的设备中断导致真正的request_irq在插入时被占用。5.4 用户空间打开设备后无法读写设备节点存在App也能open成功但read/write返回错误这种情况多在file_operations里没正确获取私有数据。多设备驱动里每个实例有独立的private_dataopen时要从miscdevice反推出驱动的priv结构体。一个稳妥的写法是用container_ofstatic int rk_multi_open(struct inode *inode, struct file *filp) { struct miscdevice *mdev filp-private_data; struct rk_multi_priv *priv container_of(mdev, struct rk_multi_priv, mdev); filp-private_data priv; return 0; }如果直接拿inode-i_cdev去转换有时会因为misc设备的封装拿错指针反推出一个错误地址表现就是读写时Unable to handle kernel NULL pointer dereference。这个细节文档里很少写但多设备场景几乎必踩。5.5 一份问题速查表现象可能原因排查方向probe不执行设备树status不是okay或compatible不匹配dmesg、/sys/bus/platform设备列表设备节点只有一个misc设备名重复或alias编号写死检查name和of_alias_get_idmisc_register返回-16次设备号被占用改MISC_DYNAMIC_MINORrequest_irq失败pinctrl和interrupts不一致对照设备树引脚配置read/write系统态异常file_operations私有数据取错检查container_of对象alias获取失败aliases节点没写前缀或编号越界dtc反编译dtb检查alias模块加载匹配不上MODULE_DEVICE_TABLE缺失检查modinfo的alias字段我的最后一点体会这两个技巧说起来都不复杂但它们的价值不是“能用”而是让驱动代码在设备和需求不断增加时还能保持清晰。我在瑞芯微项目里的习惯是把所有和平台、型号、实例相关的配置尽量收敛到设备树和of_device_id的data字段里probe保持简短可读。新来的同事接到需求后往往只需要改设备树和配置结构体不需要去动那些已经验证过N遍的底层逻辑这对团队协作来说收益非常大。如果你现在也在为“一个驱动扛多个设备”头疼不妨先从这两个技巧改起实际跑一遍就会理解为什么老手都喜欢这么写了。
返回列表