【Linux驱动】驱动框架的进化 | 总线设备驱动模型

发布时间:2023年12月26日

🐱作者:一只大喵咪1201
🐱专栏:《Linux驱动》
🔥格言:你只管努力,剩下的交给时间!
图

🥩驱动框架的进化

  • Linux驱动程序 = 驱动框架 + 单板操作

重点在于驱动框架,一个好的驱动框架非常容易扩展和修改,面对不同单板,只需要进行简单的修改就可以实现移植。

图
如上图所示,在上篇文章中实现的LED驱动程序中,驱动层的led_openled_write函数中通过映射后的虚拟地址直接操作LED相关的寄存器。

  • 驱动层代码和硬件强相关。

如果此时换了一个开发板,仍然实现上面的功能,就需要修改驱动函数中要操作的寄存器,每一换一次开发板,就需要修改一次,代码的可维护性和扩展性非常低。

🥠分层

有没有办法在更换开发板时,驱动程序不用做太多的修改,而是由开发板提供相应的openwrite函数呢?

  • 将驱动层分为上下两层。

图
如上图所示,当应用层使用openwrite系统调用时:

  • open:调用驱动层上层的led_init函数对LED进行初始化。
  • write:调用驱动层上层的led_ctl函数对LED进行控制。

而驱动成上层中的led_initled_ctl是通用函数,这两个函数可以操作任何类型的开发板,所以势必不会涉及到开发板的任何硬件操作,硬件操作由具体的开发板提供:

  • led_init:调用驱动层下层的board_led_init函数对具体的LED硬件进行初始化。
  • led_ctl:调用驱动层下层的board_led_ctl函数对具体的LED硬件进行操作。

这样一来,更换不同的开发板,只需要提供自己的board_led_initboard_led_ctl函数给驱动层上层使用即可,将驱动层和开发板实现了一定程度上的解耦。

🥠面向对象

在Linux中面向对象使用的非常多,就像file_operations一样,将驱动层的open/write等函数指针放在该结构体中,文件系统通过管理该结构体这一个对象就可以实现对多个驱动函数的管理。

在驱动层分为上下两层以后,为了驱动层的上层可以更方便管理下层中的board_led_init/board_led_ctl等函数,将这些函数指针放入led_operations结构体中,上层只需要管理这一个结构体对象就可以实现对多个下层驱动函数的管理。

图
如上图所示,由具体的开发板提供一个struct led_operations结构体对象,该对象中有初始化函数指针成员init和控制函数指针成员ctl,用操作自己板子上LED的board_led_initboard_led_ctl函数来初始化。

驱动层上层的led_initled_ctl函数直接调用struct led_operations结构体对象中的initctl成员即可,它根本不用关心自己操作的是哪块开发板。

  • 开发板仅提供struct led_operations中的操作函数。
  • 注册设备节点以及入口函数等仍然是由驱动层的上层完成。

🥠编程

驱动层上层:

图

如上图所示,创建file_operations结构体,用led_initled_ctl函数来初始化,通过宏LED_NUM定义LED灯数量,定义一个led_operations结构体指针供这两个函数使用。

  • 使用iminor(node)函数来获得设备节点的次设备号,传入的参数是文件的inode,得到结果的是次设备号。
  • 次设备号用于标识同一设备类中的不同设备实例,相当于使用主设备号创建的不同设备对象。
  • led_init函数中,通过p_led_opr结构体调用驱动层下层中init初始化函数。
  • led_ctl函数中,通过p_led_opr结构体调用驱动层下层中ctl控制函数。

图
如上图所示,在驱动层上层中,正常进行设备节点的注册,销毁等工作。

  • 在创建设备节点时,循环调用LED_NUMdevice_create函数创建多个LED节点。
  • 再调用get_board_led_opr函数获取驱动层下层的led_operation结构体。
  • device_create是一个可变参数的函数。

可以看到,此时整个驱动层的上层中,完成了驱动函数的注册,初始化等步骤,但是看不到一点具体硬件的操作,完成了一定程度上的解耦。

驱动层下层:

tu
如上图,在led_opr.h中定义struct led_operations结构体,该结构体中包含initctl两个函数指针成员。

在表示开发板A的board_A.c源文件中,创建led_operations结构体全局变量,使用board_led_int函数和board_led_ctl进行初始化。

再提供一个get_board_led_opr函数,供驱动层上层获取led_operations结构体指针。

图
如上图board_led_init函数所示,初始化开发板A的时候,根据上层传入的次设备号which去初始化某个LED。

  • 调用ioremap函数将开发板上LED寄存器的物理地址映射为虚拟地址。
  • 给相应寄存器赋值,进行初始化。

图

如上图board_led_ctl函数所示,根据上层传下来的次设备号和状态status来控制LED灯状态,向GPIO5_DR寄存器中的bit3写0或者1。

可以看到,开发板的硬件操作全部都在驱动层下层中实现。此时整个驱动程序就完成了。

上板子运行:

图
如上图Makefile文件所示,对led_drv.cboard_A.c进行编译,生成BigMiaomi_LED.ko设备模块文件。

  • 当更换开发板时,只需要在Makefile中稍作修改,让其编译另一个开发板提供的源文件,如board_B.c

图
如上图所示,在开发板的控制函数board_led_ctl中增加一个打印信息,打印驱动层下层接收到的次设备号which和控制状态status

图
如上图,将程序编译好以后,执行insmod BigMiaomi_LED.ko指令安装LED设备后,此时存在/dev/BigMiaomi_LED0/dev/BigMiaomi_LED1两个设备节点。

图

如上图应用层测试函数,仍然使用之前的led_drv_test,只是在命令行中输入的时候,有/dev/BigMiaomi_LED0/dev/BigMiaomi_LED1两个设备节点之分。
图
如上图所示,在命令行输入./led_drv_test /dev/BigMiao_LED0 on指令后,可以看到调试信息中打印的次设备号iminor = 0,状态status = 1

还有控制LED1,以及其他状态,都有相应的调试信息输出。而且板子的LED灯状态也是正确的,本喵就不贴图了。

函数调用关系:

图
如上图所示是该模型的函数调用关系:

  • led_drv_test.c:调用openwrite系统调用。
  • led_drv.c:通过文件系统中file_operations中的openwrite函数指针调用驱动层上层中的led_initled_ctl函数。
  • board_A.c:通过led_operations中的initctl函数指针调用驱动成下层的board_led_initboard_led_ctl函数。

🥠分离

在上面的驱动程序结构中,可以很好的应对不同开发板,只需要提供相应的board_X.c源文件即可,但是就拿开发板A来说,此时控制LED灯使用的GPIO5_3引脚,如果此时我要更换成GPIO3_1引脚呢?

图
如上图,此时GPIO5_3硬件是绑定在了开发板A的board_led_init函数中,board_led_ctl函数也是一样。

  • 要想实现更换引脚,只能重新定义board_led_intboard_led_ctl函数,重新绑定为GPIO3_1引脚。

如果要控制的引脚同时有100个呢?定义100个board_led_intx函数吗?这显然是不现实的,但是用分层可以解决这个问题。


图
如上图所示,驱动层仍然分为上下两层,上层保持不变,将下层分离为左右两部分:

  • 左半部分的led_resources结构体提供引脚信息,包括使用哪组GPIO中的哪个引脚。
  • 右半部分的led_operations结构体仍然管理intctl函数。

此时led_operations中的initctl函数,从led_resources中获取引脚资源后进行初始化和相应的控制,所以此时这两个函数就不再操作具体的寄存器,实现再一次解耦。

  • 只要是同一款芯片,对所有GPIO引脚的实现LED功能的操作步骤都是相同的。

提供资源的头文件:

在代码上,led_drv.c源文件不需要作任何改变,因为这是驱动层上层的代码,同时也体现出来解耦的作用。

图
如上图所示,在led_resources.h中定义led_resources结构,用来给驱动层下层右半部分提供引脚资源和寄存器地址。

  • pin:用来表示引脚资源,[31,16]表示所用的GPIO组,[15,0]表示所用的引脚编号。
  • IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3:表示模式选择寄存器的物理地址。
  • GPIO5_GDIR:表示方向选择寄存器的物理地址。
  • GPIO5_DR:表示数据寄存器的物理地址。

除此之外还提供了几个用来设置引脚的宏函数:

  • GROUP_PIN:用来将GPIO组的编号和引脚编号设置成一个32位的整数,方便初始化结构体中的pin成员。
  • GROUP:从结构体pin成员中得到所用GPIO组的编号。
  • PIN:从结构体pin成员中得到所用引脚的编号。

还有一个用来获取led引脚资源结构体led_resources指针的函数get_led_resources

右半部分:
图

如上图所示,在chip_led_opr.c中重新实现一遍原本board_A.c中的代码,并且作一些改动。定义一个资源结构体全局指针变量led_rsc,在初始化函数board_led_init函数中:

  • 使用get_led_resources函数获取资源结构体,只需要获取一次就可以了。
  • 使用资源结构体led_rsc中的寄存器物理地址进行虚拟地址映射。
  • 初始化LED寄存器,根据led_src中的pin成员获得具体引脚,从而设置寄存器中对应比特位。

图

如上图所示board_led_ctl控制函数,在该函数中,通过资源结构体led_src中的pin成员来决定改变改变哪个比特位的状态。

图
如上图所示,和之前的board_A.c一样,也要定义led_operations结构体并使用上面两个函数进行初始化,这一点是一样的。

  • chip_led_opr.c操作的某一款芯片中的GPIO引脚,不涉及任何具体寄存器操作。
  • 全部依赖资源结构体led_resources中的引脚信息才能操作。

左半部分:

tu
如上图所示,此时用来表示开发板A的源文件board_A.c中就不再有LED初始化board_led_init和控制board_led_ctl函数的定义了,只有该开发板所用引脚的资源:

  • pin:使用GROUP(5,3)GPIO5_3引脚资源组合成一个32位的整形,用来初始化该成员。
  • IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3:用模式选择寄存器的物理地址来初始化。
  • GPIO5_GDIR:用方向选择寄存器的物理地址来初始化。
  • GPIO5_DR:用数据寄存器的物理地址来初始化。

这是属于开发板A的LED引脚资源,所有成员的值都围绕GPIO5_3所涉及到寄存器。还需要定义一个给右半部提供资源结构体的get_led_resources函数。


图
如上图所示Makefile文件中,无论是使用哪个引脚,或者是不同的单板,只要提供board_X.c中对应的资源结构体,编译的时候将该文件一起编译即可。

  • 此时修改引脚或者更换同型号芯片单板的代价就更小了,只需要提供相应的资源结构体led_resources

图
如上图所示,完成编译以后,在开发板上输入之前的指令,得到和之前同样的效果,而且板子的LED灯状态也是正确的,本喵就不贴图了。

函数调用关系:

图
如上图所示当前分离后的模型函数调用关系:

  • led_drv_test.c:调用openwrite系统调用。
  • led_drv.c:通过文件系统中file_operations中的openwrite函数指针调用驱动层上层中的led_initled_ctl函数。
  • chip_led_opr.c:通过led_operations中的initctl函数指针调用驱动成下层的board_led_initboard_led_ctl函数。
  • board_A.c:通过led_resources向驱动层下层提供引脚资源。

🥩总线设备驱动模型

在上面的模型中,LED的引脚资源通过led_resources结构体来提供,甚至可以通过一个led_resources数组来提供多个LED引脚的资源,但是这也仅局限于LED。

如果现在又要增加按键、LCD等硬件呢?难道还要在定义key_resources,以及lcd_resources等结构体来提供引脚资源吗?如果硬件的种类多达100种呢?要定义100个结构体吗?

struct platform_device:

当然不是这样的,在Linux中提供了一个struct platform_device结构体,用该结构体来描述所有硬件资源。每一个硬件都用该结构体创建一个实例对象,来提供引脚等硬件资源。

图
如上图所示struct platform_device结构体的定义,该结构体用来描述硬件设备的诸多属性:

  • name:硬件设备的名称。
  • num_resourecs:硬件设备的所拥有的同类型资源数量。
  • resource:这是一个数组指针,数组中每个元素的类型都是struct_resource

暂时就介绍这几个属性,其他的等用到再讲解。

struct resource结构体是用来描述具体硬件资源的,如引脚等属性:

  • start:硬件资源的起始地址
  • end:硬件资源的结束地址
  • name:硬件资源的名称
  • flags:硬件资源的类型

拿LED来举例,假设现在有多个LED设备:

TU
如上图所示,一个用来描述LED资源的struct platform_device LED对象,其成员resource指向一个存放多个LED具体资源信息的数组,每个元素描述一个LED的引脚等资源信息。

struct platform_driver:

有硬件资源描述,就得有对应的驱动程序,所以Linux又提供了一个struct platform_driver结构体来提供驱动程序,每一类硬件资源platform_device对象都对应一个驱动程序:

图
如上图所示struct platform_drive结构体的定义,同样包含很多属性:

  • probe:驱动程序的具体逻辑都放在该函数中,当设备安装时会自动调用该函数。
  • remove:设备卸载时会自动调用该函数。
  • driver:该结构体中有一个成员是name,用来记录驱动程序的名称。
  • id_table:是一个数组,用来记录该驱动程序所支持的设备名称。

其他的属性同样暂时不再介绍,用到的时候再详细说明。


使用这两个Linux提供的结构体后,之前的驱动分离模型就要做出相应的变化了:

tu
如上图所示驱动模型,应用层使用openwrite系统调用后,会调用file_operations结构体中的led_initled_ctl函数,这两个函数再调用led_operations结构体中的initctl函数:

  • led_opr->initled_opr->ctl使用platform_driver结构体中的硬件资源来进行初始化和控制。
  • platform_device结构体给platform_driver结构体提供硬件资源信息,如引脚信息。
  • 真正的操作硬件的驱动代码仍然在led_operations结构体中。

platform_driver针对硬件设备做了两件事情:

  • 记录platform_device结构体中的硬件资源信息。
  • 使用create_device创建设备节点。

其他的注册设备节点等工作仍然是在驱动层的上层完成。

🥠BUS模型

不同类型的设备都有一个platform_device结构体对象用来提供硬件资源,和一个与之对应的platform_driver结构体对象用来提供程序,这些结构体对象由谁来管理呢?

图

如上图所示,在Linux中存在一个虚拟总线,该总线维护着两个链表:

  • Dev链表:存放描述硬件资源的platform_device结构体对象。
  • Drv链表:存放含有驱动程序的platform_driver结构体对象。

每一个platform_device对象都对应着一个platform_driver对象,分别位于总线的两侧。

图
如上图所示,这个虚拟总线由一个platform_bus_type结构体对象来管理,其中的match成员函数是用来匹配总线两侧的platform_device对象和platform_driver对象的。

匹配规则:

  • 使用platform_device_register()函数向总线中注册一个platfrom_device对象插入到Dev链表中。
    • 会自动调用platform_match函数从总线的另一侧Drv链表中寻找匹配的platform_driver结构体对象。

或者

  • 使用platform_driver_register()函数向总线中注册一个platform_driver对象插入到Drv链表中。
    • 也会自动调用platform_match函数从总线的另一侧Dev链表中寻找匹配的platform_device结构体对象。

当匹配成功以后,会自动调用platform_driver结构体对象中的probe函数,在该函数中要记录:

  • platform_device结构体对象提供的硬件资源。
  • 使用device_create创建设备节点。

那么两个结构体对象匹配的规则到底是什么呢?

图
如上图所示两个结构体,匹配有三次机会,某一次匹配成功就返回:

  1. 非他不嫁

platform_device中有一个driver_override成员,该成员如果不是NULL,则将该成员所表示的字符串和platform_driverdriver成员中的字符串进行比较,如果匹配,则成功返回。

  1. 比较设备表

如果第一步没有匹配成功,则用platform_device中的name去和platform_driver中的id_table中所支持的所有设备名称逐个比较,只要有一个匹配则成功返回。

  1. 比较名字

如果前两步都没有匹配成功,则只能比较platform_device中的nameplatform_driverdriver里的name了,如果匹配,则成功返回,如果不匹配就说明真的没有,失败返回。

  • 只要匹配成功,就会调用platform_driver中的probe函数。

🥠编程

接下来使用总线模型来实现一下LED驱动。

paltform_device:

tu
如上图代码所示,在resources数组中定义三个LED资源:

  • start:表示GPIO组和引脚编号。
  • flags:表示资源类型。
  • name:资源名称。

其中flags资源类型有好几种:

tu
如上图所示,有表示内存类型的,寄存器类型的,以及中断类型的等等,本质上就是一个地址。本喵这两使用的是IORESOURCE_IRQ中断类型。

  • 选用什么类型无所谓,只要能和platform_driver中创建设备节点时的类型匹配上就行。

board_A.c中定义了platform_device结构体变量,成员只有三个并进行初始化:

  • name:表示硬件资源名称。
  • num_resources:表示resources资源数组中的资源个数,使用ARRAY_SIZE宏函数求得,传入资源数组即可。
  • resource:指向资源数组,将resources赋值给它即可。

tu
如上图所示,在入口函数led_dev_init中调用platform_device_register函数将前面定义的platform_device结构体对象注册到总线的Dev链表中。

在出口函数led_dev_exit中调用platform_device_unregister函数将platform_device结构体对象从总线中移除。

最后还需要完善一下内核信息。

  • board_A.c在编译后会生成board_A.ko,这也是一个设备模块。

platform_driver:

tu
如上图所示,在chip_led_opr.c中创建platform_driver结构体对象,包含三个成员:

  • probe:用chip_led_drv_probe初始化,匹配后调用该函数。
  • remove:用chip_led_drv_remove初始化,卸载时调用该函数。
  • driver.name:表示驱动名称,一定要和前面platform_device中的名称相同。

图
如上图所示chip_led_drv_probe函数,当匹配后自动调用该函数,在函数中进行以下操作:

  • 使用platform_get_resourceplatform_device结构体对象中遍历获取引脚资源。

此时得到的是struct resource结构体指针,该结构体对象中的start就是所提供的GPIO组和引脚编号信息。在获取资源信息时,资源类型必须和前面的一致,都是IORESOURCE_IRQ

  • 遍历引脚信息并放入到记录引脚资源的全局数组g_ledpins中。
  • 遍历创建设备节点,有多少个引脚资源就创建多少个设备节点。

由于设备节点个数是通过platform_device对象知道的,所以就在匹配成功后调用probe函数时就能创建相应个数设备节点。

体
如上图chip_led_drv_remove函数所示,在移除设备节点时,也要调用platform_get_resource遍历获取引脚资源,并且销毁设备节点。


图
如上图所示,具体操作LED寄存器还是通过led_operations结构体中的initctl成员函数。

本来应该是在board_led_initboard_led_ctl函数中操作LED寄存器的,但是本喵这里仅提供了一些调试信息,来验证是否执行到了这里,具体的操作就不写了。

应用层的openwrite系统调用会调用这两个函数。


图
如上图所示,在入口函数chip_led_drv_init中,调用platform_driver_registerplatform_driver结构体对象注册到总线的Drv链表中。

  • 并且向驱动层上层注册board_A_led_opr这个led_operations结构体对象。

在出口函数chip_led_drv_exit中,调用platform_driver_unregisterplatform_driver结构体对象从总线中移除。

最后再完善一下设备信息。

  • chip_led_opr.c在编译后会生成chip_led_opr.ko,也是一个设备模块。

驱动层上层:

在总线结构中存在一个依赖关系:

  • 驱动层下层的probe函数调用led_class_create_device来创建设备节点。
  • 驱动层下层的remove函数调用led_class_destroy_device来删除设备节点。

tu
如上图,这是两个由驱动层上层led_drv.c提供的函数,是对device_create/device_destroy函数的封装。因为在调用这两个函数时,传入的参数有:

  • led_class:提供设备节点的信息类。
  • MKDEV(major, minor):主次设备号。

图

如上图,但是这两个参数是在驱动层上层的入口函数中调用register_chrdev注册设备节点以及调用class_create后才得到的。

  • 原本在上层创建设备节点的工作放在了下层的prboe函数中实现。

所以在下层的probe函数中是无法直接使用device_create/device_destroy这两个函数的,因为此时还没有需要传入的两个参数。

  • 此时,下层依赖上层,有上层的这两个参数下层才能正常工作。

但是,驱动层上层使用到的led_operations结构体对象又来自驱动层下层,此时上层又依赖下层,只有下层创建了led_operations结构体对象后,上层才有的用。

这样就有矛盾了,上层依赖下层,下层依赖上层,交叉依赖,为了解决这个问题,由上层给下层提供几个空头支票:

  • 使用EXPORT_SYMBOL将上层封装的创建设备节点,销毁设备节点这两个函数导出。
  • 使用EXPORT_SYMBOL给下层导出一个注册led_operations结构体对象的函数。

导出以后,下层就认为上层已经实现了创建和销毁设备节点这两个函数,可以直接用,下层在执行的过程中:

  • 调用了注册结构体对象的函数resister_led_operations,将上层提供的这张空头支票填充好了,填入了下层实现的led_operations结构体对象指针。

此时上层不再依赖下层,上层可以顺利执行,在执行的过程中又将给下层的两张空头支票填充好了,如此一来,交叉依赖的问题就解决了。


驱动层上层的其他函数不用改,只是在入口函数和出口函数中不再进行设备节点的注册和销毁,这部分工作由下层的probe函数完成。

测试:

图
如上图Makefile文件,此时编译完以后就会生成led_drv.kochip_led.koboard_A.ko三个设备模块文件,还有一个led_drv_test应用层测试文件,该文件中的代码还是用以前的。

图
如上图所示,在安装设备节点时,board_A.ko什么时候安装都无所谓,因为它没有依赖关系。

但是不能先安装chip_led_opr.ko,否则会报错,因为它依赖驱动层的上层,所以必须先安装led_drv.ko,再安装chip_led_opr,ko

图
如上图,此时就安装驱动程序成功了,可以看到在/dev目录下有三个设备节点,因为我们在platform_device中提供了三个引脚资源。

图
如上图,在命令行中执行测试程序,打开不同LED设备时,从调试信息中可以看到,成功操作了不同组GPIO引脚,这些引脚资源和我们在platform_device中提供的引脚一致。

函数调用关系:

图

如上图所示,在应用层调用openwrite函数后:

  • 驱动层上层:通过file_operations结构体中的openwrite函数指针来调用该层的led_initled_ctl函数。
  • 驱动层下层:通过led_operations结构体中的initctl函数指针调用该层的board_led_initboard_led_ctl来操作硬件。
  • 总线:操作的是由platform_device提供,并且记录在platform_driver中的硬件资源,并且在probe函数中创建了设备节点。

🥩总结

本文重在理解Linux的驱动框架,要理解是如何从最简单的驱动程序框架,到含有面向对象和分层,再到含有面向对象,分层和分离的。

又引出了platform_deviceplatform_driver两个结构体类型,以及管理这两类结构体对象的总线模型。

文章来源:https://blog.csdn.net/weixin_63726869/article/details/135193203
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。