Linux驱动支持多个设备的两个小技巧 | 基于瑞芯微平台 一、前言对于一款嵌入式产品来说往往包含很多同种类型设备比如多个**串口、网口等这些驱动比较类似仅仅是一些寄存器基地址不一样还有就是厂家出厂的同类型的SOC很多控制器驱动功能类似【比如gpio、I2S、I2C】但是他们又有一些差异。针对这些情况我们完全可以把硬件信息解耦出来将公用部分的软件逻辑功能分离出来没有必须要为每一个设备单独写一个驱动。本届结合瑞芯微平台给大家介绍2种实现方法通过次设备号区分相同功能的外设通过设备树节点的compatible匹配方式得知当前设备版本定义struct of_device_id结构体数组并SOC名称以及差异内容封装并将地址填充到私有成员中。二、通过次设备号区分外设此种方式我们只需要在驱动中区分出设备的次设备号然后根据次设备号的访问不同的内存地址空间即可。1、原理假定我们创建两个串口com0、com1他们共用同一个主设备号250次设备号分别为0、1如果要让他们共用同一个字符设备驱动那么我们的驱动要能够知道应用进程打开的是设备com0还是com1并操作不同的串口。首先创建两个设备节点mknod/dev/com0 c2500mknod/dev/com1 c2501执行结果如下内核为了维护这两个文件节点内核需要创建结构体维护这两个文件具体如下图所示当我们通过命令mknod创建一个字符设备文件那么内核就会创建好一个inode会存在存储器中创建和该文件实体一一对应的inode。这个inode和其他的inode一样通常用来存储关于这个文件的静态信息(不变的信息)包括这个设备文件对应的设备号文件的路径以及对应的驱动对象等。inode作为VFS四大对象之一在驱动开发中很少需要自己进行填充更多的是在open()方法中进行查看并根据需要填充我们的file结构。创建字符设备/dev/com0、 /dev/com1,只是增加了对应的inode节点此时VFS层并没有并没有创建file结构体而且inode和驱动也并没有产生联系。当进程试图打开设备文件的时候Linux系统做了什么事如果应用程序执行以下代码fd0open(/dev/com0,O_RDWR);fd1open(/dev/com1,O_RDWR);各个结构体之间关系入下图所示当应用程序执行open函数该函数会调用到内核的sys_open该函数会根据该设备节点inode保存的信息i_flags:文件类型, i_rdev:设备号初始化结构体inode其他信息比如inode-i_cdev此时已经指向我们注册的cdev结构体。通过设备号可以很容易找到该设备在设备号全局管理数组**chedevs[]**的下标进而找到我们注册的驱动cdev以及file_operations。同时内核会在VFS层为创建结构体file该函数调用成功之后应用层会返回整型值用来和该file对应就是上图的文件描述符fd0、fd1。其中file-f_dentry-d_inode-i_rdev 保存对应的设备节点的设备号 file-f_op保存我们注册的file_operations 字符设备接口函数集合。由此可得在read和write等其他接口函数中我们可以通过file来得到次设备号。【注意】同一个文件如果打开了两次那么第二次linux内核仍然会重新分配1个新的file结构体和文件描述符。驱动的read、write可以通过以下方式获得设备号:file-f_dentry-d_inode-i_rdev这样我们就可以通过宏MINOR来提取此设备号。2、核心代码如下ssize_tdev_fifo_read(structfile*file,char__user*buf,size_tsize,loff_t*pos){intminorMINOR(file-f_dentry-d_inode-i_rdev);structmydev*cd;printk(read() MINOR(file-f_dentry-d_inode-i_rdev)%d\n,minor);cd(structmydev*)file-private_data;printk(read() file-private_data cd-test%d\n,cd-test);if(copy_to_user(buf,minor,size)){return-EFAULT;}returnsize;}当驱动可以提取次设备号之后便可以访问不同的地址信息或者中断从而实现一份驱动支持多个同种类型的设备。3、如何获得注册的设备结构体私有地址?在大多情况下我们会创建一个自定义的设备信息维护结构体同时创建一个指针数组用来管理不同的设备。#defineMAX_COM_NUM2structmydev{structcdevcdev;char*reg;inttest;};structmydev*pmydev[MAX_COM_NUM];然后通过成员cdev注册字符设备for(i0;iMAX_COM_NUM;i){pmydev[i]-testi;cdev_init(pmydev[i]-cdev,dev_fifo_ops);devnoMKDEV(major,i);errorcdev_add(pmydev[i]-cdev,devno,1);if(error0){printk(cdev_add fail \n);gotoERR2;}}想一个问题:如果我们为每一个同类型设备分配独立的设备结构体分别注册对应的cdev假如我打开/dev/com0 进行操作的时候我怎么知道com0对应我们自己定义的设备管理结构体变量的地址呢?有问题是好的我们带着问题出发看看大牛们是怎么做的。//打开设备staticintdev_fifo_open(structinode*inode,structfile*file){structmydev*cd;cdcontainer_of(inode-i_cdev,structmydev,cdev);file-private_datacd;return0;}该函数功能字符设备架构调用我们注册的接口函数open会传递参数inode和fileinode-i_cdev指向了我们注册的pmydev[i]-cdev在open中通过inode-cdev来识别具体的设备通过container_of来找到对应的pmycdev结构体变量并将其私有数据隐藏到file结构的private_data中进而识别同一个驱动操作一类设备。而readwrite接口函数可以直接通过file的 private_data获取对应的pmycdev结构体变量。cd(structmydev*)file-private_data;执行结果如下:由结果可知,应用程序正确读取了minor的值。从内核log来看MINOR(file-f_dentry-d_inode-i_rdev)可以成功读取此设备号。而read接口函数也成功通过file-private_data得到了设备结构体变量初始化的时候为不同设备的test成员附了不同的值。4、驱动程序#includelinux/init.h#includelinux/module.h#includelinux/kdev_t.h#includelinux/fs.h#includelinux/cdev.h#includelinux/slab.h#includelinux/uaccess.hstaticintmajor250;staticintminor0;staticdev_tdevno;#defineMAX_COM_NUM2structmydev{structcdevcdev;char*reg;inttest;};structmydev*pmydev[MAX_COM_NUM];ssize_tdev_fifo_read(structfile*file,char__user*buf,size_tsize,loff_t*pos){intminorMINOR(file-f_dentry-d_inode-i_rdev);structmydev*cd;printk(read() MINOR(file-f_dentry-d_inode-i_rdev)%d\n,minor);cd(structmydev*)file-private_data;printk(read() file-private_data cd-test%d\n,cd-test);if(copy_to_user(buf,minor,size)){return-EFAULT;}returnsize;}intdev_fifo_close(structinode*inode,structfile*file){printk(dev_fifo_close()\n);return0;}//打开设备staticintdev_fifo_open(structinode*inode,structfile*file){structmydev*cd;cdcontainer_of(inode-i_cdev,structmydev,cdev);file-private_datacd;return0;}staticstructfile_operationsdev_fifo_ops{.opendev_fifo_open,.readdev_fifo_read,.releasedev_fifo_close,};staticintdev_fifo_init(void){intresult;interror;inti0;printk(dev_fifo_init \n);devnoMKDEV(major,minor);resultregister_chrdev_region(devno,MAX_COM_NUM,test);if(result0){printk(register_chrdev_region fail \n);gotoERR1;}for(i0;iMAX_COM_NUM;i){pmydev[i]kmalloc(sizeof(structmydev),GFP_KERNEL);}for(i0;iMAX_COM_NUM;i){pmydev[i]-testi;cdev_init(pmydev[i]-cdev,dev_fifo_ops);devnoMKDEV(major,i);errorcdev_add(pmydev[i]-cdev,devno,1);if(error0){printk(cdev_add fail \n);gotoERR2;}}return0;ERR2:devnoMKDEV(major,0);unregister_chrdev_region(devno,MAX_COM_NUM);for(i0;iMAX_COM_NUM;i){kfree(pmydev[i]);}returnerror;ERR1:returnresult;}staticvoiddev_fifo_exit(void){inti;printk(dev_fifo_exit \n);for(i0;iMAX_COM_NUM;i){cdev_del(pmydev[i]-cdev);}for(i0;iMAX_COM_NUM;i){kfree(pmydev[i]);}devnoMKDEV(major,0);unregister_chrdev_region(devno,MAX_COM_NUM);return;}MODULE_LICENSE(GPL);MODULE_AUTHOR(daniel.peng);module_init(dev_fifo_init);module_exit(dev_fifo_exit);测试程序#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.hmain(){intfd0,fd1;intminor;fd0open(/dev/com0,O_RDWR);if(fd00){perror(open fail \n);return;}printf(open /dev/com0 OK\n);read(fd0,minor,sizeof(minor));printf(minor of /dev/com0 %d\n,minor);close(fd0);fd1open(/dev/com1,O_RDWR);if(fd10){perror(open fail \n);return;}printf(open /dev/com1 OK\n);read(fd1,minor,sizeof(minor));printf(minor of /dev/com1 %d\n,minor);close(fd1);}【补充】我们也可以在回调cdev.fops-open()阶段重新填充file结构的fop进而实现同一个驱动操作不同的设备这种思想就是内核驱动中常用的分层三、通过compatible匹配方式比如瑞芯微平台由很多种SOCRV1126、rk1888、rk3188、rk3399、rk3568、rk 3588等很多控制器的IP核架构类似但是会有部分差异。厂家在定制sdk的时候往往多种SOC只会公用一套驱动而不是专门针对不同SOC出不同版本的驱动这无疑会大大增加维护难度。下面我们就以rk3568平台的pinctrl控制器初始化来讲解是如何区分不同SOC的gpio资源的。1、rk3568 GPIO资源rk3568一共有5组GPIO(GPIO0~4)每组GPIO为一个Bank共32个引脚每个Bank分为4个 GROUP(GPIOA(07)、GPIOB(07) 、GPIOC(0~7) 、GPIOD( 0~7)) 2、pinctrl设备树瑞芯微pinctrl服务端设备树节点定义kernel\arch\arm64\boot\dts\rockchip\rk3568.dtsi3532pinctrl:pinctrl{3533compatiblerockchip,rk3568-pinctrl;3534rockchip,grfgrf;3535rockchip,pmupmugrf;3536#address-cells 2;3537#size-cells 2;3538ranges;35393540gpio0: gpiofdd60000{3541compatiblerockchip,gpio-bank;3542reg0x0 0xfdd60000 0x0 0x100;3543interruptsGIC_SPI33IRQ_TYPE_LEVEL_HIGH;3544clockspmucru PCLK_GPIO0,pmucru DBCLK_GPIO0;35453546gpio-controller;3547#gpio-cells 2;3548gpio-rangespinctrl0032;3549interrupt-controller;3550#interrupt-cells 2;3551};......3604};3607#include rk3568-pinctrl.dtsi与gpio相关属性。3536~3537 行#address-cells和#size-cells属性值为均为2也就是说pinctrl下的所有子节点的reg地址和长度均为2个元素所以3542行reg属性有4个元素3540~3604 行 rk3568有五组GPIOGPIO0~GPIO4每组GPIO对应的寄存器地址不同3541 行 compatible 属性值为“rockchip,gpio-bank”所以在 linux 内核中搜索这个字符串就可以找到对应的gpio驱动源文件为drivers/pinctrl/pinctrl-rockchip.c。3542 行 reg 属性设置了 GPIO0 控制器的寄存器基地址为 0XFDD60000内存的长度为0x100驱动会得到GPIO0的基地址0XFDD60000然后加上偏移得到GPIO0的其他寄存器地址3543 行 interrupts 属性描述 GPIO0 控制器对应的中断信息3544 行 clocks 属性指定这个 GPIO0 控制器的时钟3546 行“gpio-controller”表示 gpio0 节点是个gpio控制器每个gpio控制器节点必须包含“gpio-controller”属性3547 行“#gpio-cells”属性和“#address-cells”类似 #gpio-cells 应该为 2表示一共有两个 cell。3548 行gpio-ranges可选pinctrl 关联3、原理将所有瑞芯微SOC的pinctrl信息填充到下面数组static const struct of_device_id rockchip_pinctrl_dt_match[] { ...... #ifdef CONFIG_CPU_RK3568 { .compatible rockchip,rk3568-pinctrl, .data rk3568_pin_ctrl }, #endif {}, };其中.compatible存放与设备相同名称的字符串.data存放针对不同SOC的私有信息地址rk3568平台私有信息采用结构体struct rockchip_pin_ctrlstatic struct rockchip_pin_ctrl rk3568_pin_ctrl根据pinctrl控制器设备树节点信息compatible “rockchip,rk3568-pinctrl”;利用函数of_match_node()进行匹配然后得到私有信息的地址.data rk3568_pin_ctrl后续就可以根据该信息进行初始化。函数of_match_node函数of_match_node()原型conststructof_device_id*of_match_node(conststructof_device_id*matches,conststructdevice_node*node)作用遍历of_device_id匹配表用设备树节点device_node去逐一比对返回第一个匹配成功的of_device_id指针全部不匹配返回NULL。参数matches驱动定义的设备树兼容列表数组必须以.compatible NULL结尾做哨兵内核靠 NULL 判断遍历终止。node内核中代表一个设备树节点的结构体指针包含name、compatible属性字符串、reg、中断等信息一般从platform_get_device()-of_node获取。返回值:非 NULL匹配到的of_device_id结构体指针可读取.data私有数据NULL该设备树节点不在驱动支持列表内不绑定此驱动。rk3568私有信息1rk3568_pin_ctrlrk3568_pin_ctrl是SOC rk358的rockchip_pin_ctrl大表该表包含多少个bank、每个bank多少引脚、默认iomux/drv基础偏移、特殊引脚规则等。staticstructrockchip_pin_ctrlrk3568_pin_ctrl __maybe_unused{/*指向 rk3568_pin_banks[] 数组数组内每一项是 struct rockchip_pin_bank 对应GPIO0~GPIO4五个GPIO域*/.pin_banksrk3568_pin_banks,.nr_banksARRAY_SIZE(rk3568_pin_banks),.labelRK3568-GPIO,.typeRK3568,.grf_mux_offset0x0,.pmu_mux_offset0x0,.grf_drv_offset0x0200,.pmu_drv_offset0x0070,.iomux_routesrk3568_mux_route_data,.niomux_routesARRAY_SIZE(rk3568_mux_route_data),c.pull_calc_regrk3568_calc_pull_reg_and_bit,.drv_calc_regrk3568_calc_drv_reg_and_bit,.slew_rate_calc_regrk3568_calc_slew_rate_reg_and_bit,.schmitt_calc_regrk3568_calc_schmitt_reg_and_bit,};rk3568_pin_banks数组rk3568_pin_banks[]描述了GPIO0~GPIO4五个GPIO控制器信息staticstructrockchip_pin_bankrk3568_pin_banks[]{PIN_BANK_IOMUX_FLAGS(0,32,gpio0,IOMUX_SOURCE_PMU|IOMUX_WIDTH_4BIT,IOMUX_SOURCE_PMU|IOMUX_WIDTH_4BIT,IOMUX_SOURCE_PMU|IOMUX_WIDTH_4BIT,IOMUX_SOURCE_PMU|IOMUX_WIDTH_4BIT),PIN_BANK_IOMUX_FLAGS(1,32,gpio1,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT),PIN_BANK_IOMUX_FLAGS(2,32,gpio2,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT),PIN_BANK_IOMUX_FLAGS(3,32,gpio3,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT),PIN_BANK_IOMUX_FLAGS(4,32,gpio4,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT,IOMUX_WIDTH_4BIT),};其中宏PIN_BANK_IOMUX_FLAGS定义如下#definePIN_BANK_IOMUX_FLAGS(id,pins,label,iom0,iom1,iom2,iom3)\{\.bank_numid,\.nr_pinspins,\.namelabel,\//数组用于描述一个Bank内4组IOMUX复用源.iomux{\/*type标志位合集寄存器归属 GRF/PMU、MUX 位宽 2/3/4bit、是否需要特殊修正等 offset该组复用功能寄存器在 GRF/PMU 内字节偏移*/{.typeiom0,.offset-1},\{.typeiom1,.offset-1},\{.typeiom2,.offset-1},\{.typeiom3,.offset-1},\},\}比如gpio4定义如下.bank_num4.nr_pins32.namegpio4.iomux{{.typeIOMUX_WIDTH_4BIT.offset-1}{.typeIOMUX_WIDTH_4BIT.offset-1}{.typeIOMUX_WIDTH_4BIT.offset-1}{.typeIOMUX_WIDTH_4BIT.offset-1}}rk3568_mux_route_datark3568_mux_route_data是引脚功能强制路由锁定表属于 RK3568 硬件特殊引脚补丁。部分外设CAN、SPI、SDIO、ETH、PWM 等存在多路引脚选择寄存器不在常规PIN MUX寄存器里而是在GRF额外路由控制寄存器用来二选一或多选一硬件通路。内核驱动通过这张表在 pinctrl 配置对应功能时自动顺带改写路由寄存器完成硬件通路切换。staticstructrockchip_mux_route_datark3568_mux_route_data[]{......RK_MUXROUTE_GRF(4,RK_PC3,3,0x0300,WRITE_MASK_VAL(2,2,1)),/* CAN1 IO mux M1 */......};2四大寄存器位计算回调函数四大寄存器位计算回调函数功能如下回调函数作用硬件配置项pull_calc_reg计算上下拉寄存器地址与bit位pull-up /pull-down/高阻drv_calc_reg驱动强度寄存器地址与bit位2mA/4mA/8mA/12mA驱动能力slew_rate_calc_reg压摆率寄存器地址与bit位上升/下降沿快慢EMC优化schmitt_calc_reg施密特触发器寄存器地址与bit位输入施密特开启 / 关闭抗干扰3其他成员此外瑞芯微其他SOC可能还会包括以下成员ctrl_data_re_init芯片级数据二次修正回调RK3568 硬件规整无需运行时修正soc_data_initprobe 后期 SOC 私有初始化回调RK3568 无特殊 errata 修复逻辑。4pinctrl初始化rk平台pinctrl控制器驱动初始化入口函数 rockchip_pinctrl_probe()其调用流程如下其中用于初始化私有信息函数rockchip_pinctrl_get_soc_data()该函数主要功能通过设备树compatible匹配取出当前芯片静态硬件引脚描述表rockchip_pin_ctrl对该表做运行时动态二次修正PX30S 特殊兼容、芯片私有重初始化回调遍历每一个 GPIO Bank自动递推计算每一组 IOMUX 复用寄存器、驱动强度 DRV 寄存器在 GRF/PMU 中的偏移地址为每个 Bank 填充自旋锁、父驱动私有指针、引脚基地址、特殊引脚掩码recalced_mask/route_mask返回修正完成的rockchip_pin_ctrl *ctrl给上层 probe 使用。

相关新闻

最新新闻

iPCA随流检测:让园区网每一跳的时延丢包无处遁形

iPCA随流检测:让园区网每一跳的时延丢包无处遁形

简介:面向园区网络规划、运维人员及网络技术爱好者,华为敏捷园区解决方案的质量感知iPCA技术主打胶片重点解决传统网络监控在多点丢包、故障定界上的痛点。内容系统讲解iPCA包守恒算法的实现原理,对比Y.1731、IP PM、RFC6374/6375等传统P2P测…

2026/9/7 1:17:46
Java开发中的常见性能陷阱与优化实践

Java开发中的常见性能陷阱与优化实践

古罗马哲人塞涅卡有言:“不在于缺乏力量,而在于缺乏意志。 ”这句话放在Java性能优化上同样振聋发聩。Java平台本身经过数十年迭代,早已具备强大的JIT编译和高效的内存管理能力。可为何同一个应用、同一套测试、同一个JDK,有人能跑…

2026/9/7 1:17:46
中国到英国物流专线怎么选才不踩坑?

中国到英国物流专线怎么选才不踩坑?

盛林英国物流专线怎么样?一文说清时效、费用与适用人群核心结论盛林英国物流专线是盛林国际物流(盛林物流)旗下覆盖中国至英国的海运、空运专线服务,主打门到门、双清包税的一站式运输,适合有FBA送仓或大宗贸易发货需求的跨境电商卖家与外贸企业。 该结论成立的前提是:货物属于…

2026/9/7 1:17:46
K8s从入门到精通:核心概念、集群部署与排障实战笔记

K8s从入门到精通:核心概念、集群部署与排障实战笔记

简介:《容器集群k8s从入门到精通》是一份面向Kubernetes初学者与云原生运维人员的PDF电子书,系统梳理了应用部署从传统物理机、虚拟化到容器化的演进脉络,解释了容器编排需求产生的原因,并引出Kubernetes自我修复、弹性伸缩、服务…

2026/9/7 1:17:46
基于MATLAB的SPWM变频调速系统建模与仿真全解析

基于MATLAB的SPWM变频调速系统建模与仿真全解析

简介:这是一份面向电气工程、自动化及电力电子方向研究者的完整技术文档,内容基于MATLAB/Simulink对SPWM变频调速系统进行建模与仿真分析。文档从变频调速技术发展现状出发,系统讲解SPWM基本原理、系统组成及波形生成方法,并逐一拆…

2026/9/7 1:17:46
网神SecGate 3600防火墙配置实战:从安全策略到双机热备

网神SecGate 3600防火墙配置实战:从安全策略到双机热备

简介:本资源是奇安信网神SecGate 3600防火墙的官方配置指南,版本覆盖V3.6.6.0(含6.1.14、6.90.14、6.91.14等多个分支),共1391页,面向负责防火墙部署与运维的安全管理员、网络工程师及企业IT运维人员。文档…

2026/9/7 1:12:46