重点在于驱动框架,一个好的驱动框架非常容易扩展和修改,面对不同单板,只需要进行简单的修改就可以实现移植。
如上图所示,在上篇文章中实现的LED驱动程序中,驱动层的led_open
和led_write
函数中通过映射后的虚拟地址直接操作LED相关的寄存器。
如果此时换了一个开发板,仍然实现上面的功能,就需要修改驱动函数中要操作的寄存器,每一换一次开发板,就需要修改一次,代码的可维护性和扩展性非常低。
有没有办法在更换开发板时,驱动程序不用做太多的修改,而是由开发板提供相应的open
和write
函数呢?
open
:调用驱动层上层的led_init
函数对LED进行初始化。write
:调用驱动层上层的led_ctl
函数对LED进行控制。而驱动成上层中的led_init
和led_ctl
是通用函数,这两个函数可以操作任何类型的开发板,所以势必不会涉及到开发板的任何硬件操作,硬件操作由具体的开发板提供:
led_init
:调用驱动层下层的board_led_init
函数对具体的LED硬件进行初始化。led_ctl
:调用驱动层下层的board_led_ctl
函数对具体的LED硬件进行操作。这样一来,更换不同的开发板,只需要提供自己的board_led_init
个board_led_ctl
函数给驱动层上层使用即可,将驱动层和开发板实现了一定程度上的解耦。
在Linux中面向对象使用的非常多,就像file_operations
一样,将驱动层的open/write
等函数指针放在该结构体中,文件系统通过管理该结构体这一个对象就可以实现对多个驱动函数的管理。
在驱动层分为上下两层以后,为了驱动层的上层可以更方便管理下层中的board_led_init/board_led_ctl
等函数,将这些函数指针放入led_operations
结构体中,上层只需要管理这一个结构体对象就可以实现对多个下层驱动函数的管理。
如上图所示,由具体的开发板提供一个struct led_operations
结构体对象,该对象中有初始化函数指针成员init
和控制函数指针成员ctl
,用操作自己板子上LED的board_led_init
和board_led_ctl
函数来初始化。
驱动层上层的led_init
和led_ctl
函数直接调用struct led_operations
结构体对象中的init
和ctl
成员即可,它根本不用关心自己操作的是哪块开发板。
- 开发板仅提供
struct led_operations
中的操作函数。- 注册设备节点以及入口函数等仍然是由驱动层的上层完成。
驱动层上层:
如上图所示,创建file_operations
结构体,用led_init
和led_ctl
函数来初始化,通过宏LED_NUM
定义LED灯数量,定义一个led_operations
结构体指针供这两个函数使用。
iminor(node)
函数来获得设备节点的次设备号,传入的参数是文件的inode
,得到结果的是次设备号。
- 次设备号用于标识同一设备类中的不同设备实例,相当于使用主设备号创建的不同设备对象。
led_init
函数中,通过p_led_opr
结构体调用驱动层下层中init
初始化函数。led_ctl
函数中,通过p_led_opr
结构体调用驱动层下层中ctl
控制函数。
如上图所示,在驱动层上层中,正常进行设备节点的注册,销毁等工作。
LED_NUM
次device_create
函数创建多个LED节点。get_board_led_opr
函数获取驱动层下层的led_operation
结构体。
device_create
是一个可变参数的函数。
可以看到,此时整个驱动层的上层中,完成了驱动函数的注册,初始化等步骤,但是看不到一点具体硬件的操作,完成了一定程度上的解耦。
驱动层下层:
如上图,在led_opr.h
中定义struct led_operations
结构体,该结构体中包含init
和ctl
两个函数指针成员。
在表示开发板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.c
,board_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
:调用open
和write
系统调用。led_drv.c
:通过文件系统中file_operations
中的open
和write
函数指针调用驱动层上层中的led_init
和led_ctl
函数。board_A.c
:通过led_operations
中的init
和ctl
函数指针调用驱动成下层的board_led_init
和board_led_ctl
函数。在上面的驱动程序结构中,可以很好的应对不同开发板,只需要提供相应的board_X.c
源文件即可,但是就拿开发板A来说,此时控制LED灯使用的GPIO5_3
引脚,如果此时我要更换成GPIO3_1
引脚呢?
如上图,此时GPIO5_3
硬件是绑定在了开发板A的board_led_init
函数中,board_led_ctl
函数也是一样。
board_led_int
和board_led_ctl
函数,重新绑定为GPIO3_1
引脚。如果要控制的引脚同时有100个呢?定义100个board_led_intx
函数吗?这显然是不现实的,但是用分层可以解决这个问题。
如上图所示,驱动层仍然分为上下两层,上层保持不变,将下层分离为左右两部分:
led_resources
结构体提供引脚信息,包括使用哪组GPIO中的哪个引脚。led_operations
结构体仍然管理int
和ctl
函数。此时led_operations
中的init
和ctl
函数,从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_src
中的pin
成员获得具体引脚,从而设置寄存器中对应比特位。如上图所示board_led_ctl
控制函数,在该函数中,通过资源结构体led_src
中的pin
成员来决定改变改变哪个比特位的状态。
如上图所示,和之前的board_A.c
一样,也要定义led_operations
结构体并使用上面两个函数进行初始化,这一点是一样的。
chip_led_opr.c
操作的某一款芯片中的GPIO引脚,不涉及任何具体寄存器操作。- 全部依赖资源结构体
led_resources
中的引脚信息才能操作。
左半部分:
如上图所示,此时用来表示开发板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
:调用open
和write
系统调用。led_drv.c
:通过文件系统中file_operations
中的open
和write
函数指针调用驱动层上层中的led_init
和led_ctl
函数。chip_led_opr.c
:通过led_operations
中的init
和ctl
函数指针调用驱动成下层的board_led_init
和board_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设备:
如上图所示,一个用来描述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提供的结构体后,之前的驱动分离模型就要做出相应的变化了:
如上图所示驱动模型,应用层使用open
和write
系统调用后,会调用file_operations
结构体中的led_init
和led_ctl
函数,这两个函数再调用led_operations
结构体中的init
和ctl
函数:
led_opr->init
和led_opr->ctl
使用platform_driver
结构体中的硬件资源来进行初始化和控制。platform_device
结构体给platform_driver
结构体提供硬件资源信息,如引脚信息。
- 真正的操作硬件的驱动代码仍然在
led_operations
结构体中。
platform_driver
针对硬件设备做了两件事情:
platform_device
结构体中的硬件资源信息。create_device
创建设备节点。其他的注册设备节点等工作仍然是在驱动层的上层完成。
不同类型的设备都有一个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
创建设备节点。那么两个结构体对象匹配的规则到底是什么呢?
如上图所示两个结构体,匹配有三次机会,某一次匹配成功就返回:
platform_device
中有一个driver_override
成员,该成员如果不是NULL
,则将该成员所表示的字符串和platform_driver
中driver
成员中的字符串进行比较,如果匹配,则成功返回。
如果第一步没有匹配成功,则用platform_device
中的name
去和platform_driver
中的id_table
中所支持的所有设备名称逐个比较,只要有一个匹配则成功返回。
如果前两步都没有匹配成功,则只能比较platform_device
中的name
和platform_driver
中driver
里的name
了,如果匹配,则成功返回,如果不匹配就说明真的没有,失败返回。
- 只要匹配成功,就会调用
platform_driver
中的probe
函数。
接下来使用总线模型来实现一下LED驱动。
paltform_device:
如上图代码所示,在resources
数组中定义三个LED资源:
start
:表示GPIO组和引脚编号。flags
:表示资源类型。name
:资源名称。其中flags
资源类型有好几种:
如上图所示,有表示内存类型的,寄存器类型的,以及中断类型的等等,本质上就是一个地址。本喵这两使用的是IORESOURCE_IRQ
中断类型。
- 选用什么类型无所谓,只要能和
platform_driver
中创建设备节点时的类型匹配上就行。
在board_A.c
中定义了platform_device
结构体变量,成员只有三个并进行初始化:
name
:表示硬件资源名称。num_resources
:表示resources
资源数组中的资源个数,使用ARRAY_SIZE
宏函数求得,传入资源数组即可。resource
:指向资源数组,将resources
赋值给它即可。
如上图所示,在入口函数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:
如上图所示,在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_resource
从platform_device
结构体对象中遍历获取引脚资源。此时得到的是struct resource
结构体指针,该结构体对象中的start
就是所提供的GPIO组和引脚编号信息。在获取资源信息时,资源类型必须和前面的一致,都是IORESOURCE_IRQ
。
g_ledpins
中。由于设备节点个数是通过platform_device
对象知道的,所以就在匹配成功后调用probe
函数时就能创建相应个数设备节点。
如上图chip_led_drv_remove
函数所示,在移除设备节点时,也要调用platform_get_resource
遍历获取引脚资源,并且销毁设备节点。
如上图所示,具体操作LED寄存器还是通过led_operations
结构体中的init
和ctl
成员函数。
本来应该是在board_led_init
和board_led_ctl
函数中操作LED寄存器的,但是本喵这里仅提供了一些调试信息,来验证是否执行到了这里,具体的操作就不写了。
应用层的open
和write
系统调用会调用这两个函数。
如上图所示,在入口函数chip_led_drv_init
中,调用platform_driver_register
将platform_driver
结构体对象注册到总线的Drv
链表中。
- 并且向驱动层上层注册
board_A_led_opr
这个led_operations
结构体对象。
在出口函数chip_led_drv_exit
中,调用platform_driver_unregister
将platform_driver
结构体对象从总线中移除。
最后再完善一下设备信息。
chip_led_opr.c
在编译后会生成chip_led_opr.ko
,也是一个设备模块。
驱动层上层:
在总线结构中存在一个依赖关系:
probe
函数调用led_class_create_device
来创建设备节点。remove
函数调用led_class_destroy_device
来删除设备节点。
如上图,这是两个由驱动层上层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.ko
,chip_led.ko
,board_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
中提供的引脚一致。
函数调用关系:
如上图所示,在应用层调用open
和write
函数后:
file_operations
结构体中的open
和write
函数指针来调用该层的led_init
和led_ctl
函数。led_operations
结构体中的init
和ctl
函数指针调用该层的board_led_init
,board_led_ctl
来操作硬件。platform_device
提供,并且记录在platform_driver
中的硬件资源,并且在probe
函数中创建了设备节点。本文重在理解Linux的驱动框架,要理解是如何从最简单的驱动程序框架,到含有面向对象和分层,再到含有面向对象,分层和分离的。
又引出了platform_device
和platform_driver
两个结构体类型,以及管理这两类结构体对象的总线模型。