🐱作者:一只大喵咪1201
🐱专栏:《Linux驱动》
🔥格言:你只管努力,剩下的交给时间!
目录
- 🍮设备树模型的LED驱动
- 🍩设备树文件
- 🍩驱动程序
- 🍮应用层读取按键值
- 🍩查询方式
- 🍩休眠唤醒方式
- 🍩poll方式
- 🍩异步通知方式
- 🍮查询方式实现按键驱动
- 编程
- 🍮总结
🍮设备树模型的LED驱动
目前有三种方式来写LED驱动程序:
- 最简单的驱动模型——硬件操作绑定在驱动函数中。
- 总线驱动模型。
- 设备树驱动模型。
下面设备树驱动模型来实现一下LED驱动程序,该模型主要分为两部分,设备树文件和驱动程序。
🍩设备树文件
如上图所示设备树文件,在设备树中增加Big-Miaomi-LED@0
和Big-Miaomi-LED@1
两个设备节点:
-
compatible
属性:属性值都是BigMiaomi,LED_Driver
。 -
pin
属性:属性值是各自节点所用GPIO组和引脚编号组成的32位整数。 -
如果在设备树节点里使用
reg
属性,内核在生成对应的platform_device
时,reg
属性会被转换成IORESOURCE_MEM
类型的资源。 -
如果在设备树节点里使用
interrputs
属性,内核在生成对应的platform_device
时,interrupts
属性会被转换成IORESOURCE_IRQ
类型的资源。
但是本喵写的Big-Miaomi-LED
节点中,属性名是pin
,该属性名是本喵自己定义的,不在内核自动转换资源类型的的命名范围内。
所以就不能从转换后的platform_device
结构体中的resources
数组中获得引脚资源了,具体获取方式编程时候再说。
如上图,然后在内核目录中使用make dtbs
指令编译设备树文件,转换为内核认识的dtb
文件。
🍩驱动程序
驱动程序在总线驱动模型的基础上进行修改,驱动层的上层不用动,只需要改变下层中的部分代码:
如上图所示,由于现在支持了设备树,所以需要初始化platform_deiver
结构体中driver
成员里的of_match_table
成员,这是一个struct of_device_id
类型的数组。
所以需要定义一个struct of_device_id
类型的数组,名为BigMiaomi_LEDs
:
- 只用
platform_device
和platform_driver
匹配规则中优先级最高的compatible
属性来匹配。 - 只支持LED设备,所以
compatible
属性只有一个值。
compatible属性的值,必须和设备树中要支持节点的compatible属性值相同,才能匹配成功。
然后就是在匹配成功以后,会自动调用paltform_driver
中的probe
函数,在该函数中,原本是从paltform_device
的resources
数组中获取硬件资源,但是此时不能这样干了:
如上图所示probe
函数,在该函数中首先要获取pin
资源:
- 设备树中的
pin
属性没有被转换到resources
数组中,但是在第一次转换为device_node
里的properties
中是有该属性的。- 从匹配成功的
platform_device
中得到当前节点的device_node
结构体指针of_node
。
- 从匹配成功的
- 使用
of_property_read_32
函数,从np
指向的当前节点deivce_node
中的properties
里找到pin
属性,并且以32位整数的方式读取该属性的value
值。- 将表示引脚资源的32位属性值放入到记录引脚资源的全局数组
g_ledpins
中。
- 将表示引脚资源的32位属性值放入到记录引脚资源的全局数组
获取到引脚资源后的其他操作和总线模型中相同,也是要使用led_class_create_device
在/dev
目录下创建设备节点。
- 设备树文件中的设备节点,内核加载后并不会在
/dev
目录下创建相应的文件,它不属于文件字符设备文件系统。
如上图代码所示,既然probe
中的获取引脚资源的方式变了,那么在remove
中获取引脚资源的方式和其他处理也要做出相应变化:
- 从要移除设备节点的
device_node
中获取引脚资源led_pin
。 - 遍历存放引脚资源的全局数组找到要移除的节点,移除后将对应的值修改为-1。
- 最后判断一下是否该类型的设备节点全部移除了,如果存放引脚资源的全局数组中,所有值都成了-1,则说明全部移除了。
此时整个驱动程序就修改完毕了,相比于总线驱动模型,只是在获取引脚资源的方式上做了改变。
如上图所示Makfile
文件,只需要编译驱动层上层led_drv.c
和下层chip_led_opr.c
即可,board_A.c
不用再参与编译了。
- 因为引脚资源不再由
board_A.c
中的platform_device
结构体提供了。 - 引脚资源由设备树文件提供,由内核将设备节点转换为
platform_device
结构体。
如上图所示,将在Linux服务器中编译好的dtb
设备树文件和led_drv.ko
及chip_led_opr.ko
驱动文件,还有led_drv_test
测试文件拷贝到网络根文件系统中。
在开发板上将dtb
设备树文件拷贝到/boot
目录下,然后重启开发板.
如上图所示,在/sys/firmware/devicetree/base/
路径下,存在Big-Miaomi-LED@0
和Big-Miaomi-LED@1
两个设备节点,这是我们在设备树文件中添加的两个节点,此时加载到了内核中。然后使用insmod led_drv.ko
和insmod chip_led_opr.ko
安装驱动程序。
如上图所示,此时执行测试程序,在命令行中输入./led_drv_test /dev/BigMiaomi_LED0 on
,内核打印信息现实操作了GPIO3_1
。
🍮应用层读取按键值
应用层读取按键值有4种方式:
- 查询方式
- 休眠-唤醒方式
- poll方式
- 异步通知方式
无论使用哪个方式都需要有按键驱动程序,通过这四种方式可以掌握一些驱动的基本技能:中断、休眠、唤醒、poll等机制。
这些基本技能是驱动开发的基础,其他大型驱动复杂的地方是它的框架及设计思想,但是基本技能就只有这些。
🍩查询方式
如上图所示查询方式的驱动模型,这种方式最简单,这里并不考虑驱动层中的架构,只看驱动层所做的工作。
在驱动程序中构造并注册一个file_operations
结构体,里面提供对应的drv_open
和drv_read
函数,当应用层调用open
系统调用时,在驱动层的drv_open
函数中配置相应的引脚为输入引脚。
当应用层调用read
系统调用时,在驱动层的drv_read
函数中读取该GPIO引脚的寄存器,把引脚的状态返回给应用层。
- 读取引脚状态时,直接返回寄存器中的值,没有其他多余的动作。
🍩休眠唤醒方式
如上图所示休眠唤醒方式的驱动模型,在驱动层中的drv_open
函数中,除了要把GPIO设置为输入引脚,还有注册GPIO的中断处理函数。
当应用层调用read
系统调用时,在驱动层的drv_read
驱动函数中:
- 如果有按键数据,则直接返回给应用层。
- 如果没有按键数据,则应用层的APP在内核态休眠。
当用户按下按键时,GPIO
中断被触发,导致drv_open
中注册的中断服务程序被执行,在中断服务程序中:
- 记录按键数据。
- 唤醒休眠中的应用层APP。
应用层的APP被唤醒以后,继续在内核态运行,即执行驱动层代码,把中断服务程序中记录的按键数据返回给应用层的APP。
- 没有读取到数据时,就会休眠,直到有按键数据到来才被唤醒。
🍩poll方式
上面的休眠-唤醒方式存在一个缺点:如果用户一直没有按下按键,那么应用层的APP就永远休眠阻塞不再执行了,所以可以给APP定个闹钟,这就是poll方式:
如上图所示poll
驱动模型,poll
是应用层实现多路转接的系统调用接口,在驱动层的file_operations
结构体中,同样有一个poll
函数指针:
如上图所示file_operations
结构体的定义,所以当应用层的APP调用poll
系统调用时,会调用到驱动层该结构体中poll
函数指针指向的函数。
所以需要我们在驱动层去定义poll
函数指针指向的函数,使得整个驱动层符合poll
驱动模型。驱动层总体步骤为:
- 注册
file_operations
结构体,里面提供open
,read
,poll
等驱动层的函数。 - 应用层APP调用
open
时,驱动层的drv_open
会将GPIO设置为输入引脚,并且注册中断处理函数。 - 应用层APP调用
poll/select
时,意图是查询按键数据是否就绪,并且可以指定一个超时时间:- 当按键数据就绪时,驱动层的
poll
向应用层返回就绪状态,APP继续使用read
读取按键数据。 - 当按键数据没有就绪时,驱动层的
poll
就会在内核态休眠一段时间。
- 当按键数据就绪时,驱动层的
当APP被唤醒时,有两种情况:
- 在休眠期间,硬件按键被按下,按键数据就绪。
- 超时时间到了,硬件按键仍然没有按下,按键数据没有就绪。
被唤醒后进行判断,如果是数据就绪被唤醒,则调用read
从按键的寄存器中读取按键数据,如果是超时被唤醒,则不调用read
去读取了。
poll/select
起到监视事件就绪的作用,驱动层的drv_poll
都会告诉应用层APP所监视事件的状态。- APP根据驱动层告知的事件状态进行下一步动作。
🍩异步通知方式
如上图所示异步通知方式,在该模型中,应用层在打开要操作的设备时,要调用fcntl
设置其fd
的FASYNC
标志,此时会调用驱动层的drv_fasync
函数:
如上图所示,在file_operations
结构体中也有一个fsync
函数指针,在该模型中,该指针指向的函数只需要记录当前进程的PID。
除了设置给fd
设置FASYNC
表示异步通知外,还需要使用signal
系统调用注册信号处理函数my_func
。
此时该模型的处理步骤为:
- APP调用
open
配置GPIO引脚为输入方式,并注册中断服务函数。 - APP调用
fcntl
设置fd
指向的文件为异步通知方式,并且注册信号处理函数. - 当硬件按键被按下时,中断服务程序会给记录下来的进程PID表示的进程发送信号,信号递达后执行注册的
my_func
信号处理函数。 - 在信号处理函数中,调用
read
来读取按键数据,此时必然是有按键数据的。
- 在没有按键按下时,APP正常执行,当按键按下后立刻去读取按键数据,使得应用层实现了中断的处理方式。
我们的驱动程序可以实现上述 4 种提供按键驱动的方法,但是驱动程序不应该限制APP使用哪种方法。
- 这就是驱动设计的一个原则:只提供能力,不提供策略。
就是说,APP想用哪种方法都行,驱动程序都可以提供;但是驱动程序不能限制APP使用哪种方法。
🍮查询方式实现按键驱动
前面介绍了按键的四种驱动模型,但是由于后面三种都涉及到中断方面的知识,而到目前为止本喵还没有介绍驱动程序中的中断,所以这里先仅用查询方式实现一下按键驱动程序:
如上图所示,采用简单的驱动层分层模型来实现查询方式的按键驱动层数:
- 应用层
open/read
系统调用和驱动层的drv_open/drv_read
通过file_operations
结构体来建立联系。 - 驱动层上层的
drv_open/drv_read
和驱动层下层的board_button_init/board_button_read
通过button_opr
结构体连建立联系。
驱动层下层的board_button_init/board_button_read
由具体的单板提供:
- 驱动层下层的
board_button_init
根据设备号确定哪个按键,并将GPIO配置为输入引脚。 - 驱动层下层的
board_button_read
根据设备号确定哪个按键,并读取对应寄存器中的值返回引脚电平。
驱动层上层:
如上图所示,在button_operations.h
中定义button_operations
结构体:
count
:表示按键设备个数init
:驱动层下层提供的初始化按键设备方法。read
:驱动层下层提供的读取按键状态方法。
如上图所示,在button_drv.c
中,创建file_operations
结构体,并且用drv_open
和drv_read
初始化open
和read
函数指针:
- 在
drv_open
函数中,使用p_button_opr
结构体中的init
,根据次设备号进行初始化。 - 在
drv_read
函数中,使用p_button_opr
结构体中的read
,根据次设备号读取按键状态。- 将读取到的按键状态
level
拷贝到用户层缓冲区。
- 将读取到的按键状态
如上图所示,在入口函数button_init
中使用register_chrdev
向内核中注册file_operations
结构体,并且获得主设备号。还要创建button_class
设备类来提供设备信息。
在出口函数button_exit
中,销毁设备类button_class
,并且使用unregister_chrdev
函数从内核中将前面注册的file_operations
移除。
如上图,由于p_button_opr
结构体指针是由驱动层下层提供的,所以驱动层上层要提供一个register_button_operations
函数给下层,让下层向上层注册p_button_opr
结构体。
- 在注册时,还要将所有按键设备使用
device_create
在文件系统中创建设备节点文件。 - 在卸载时,使用
device_destroy
将文件系统中的所有按键设备文件移除掉。
由于下层在使用这两个函数时会用到上层的button_class
类,所以这两个函数需要使用EXPORT_SYMBOL
导出给下层,供下层先使用。
如上图,最后完善一下设备信息,告诉内核哪个是入口函数,哪个是出口函数,并且声明该驱动程序使用GPL
协议。
驱动层下层:
如上图所示,在驱动层下层的board_button.c
文件中,创建button_operations
结构体,并进行初始化:
count
:按键设备有两个。init
:初始化按键设备的函数board_button_init
。read
:读取按键设备状态的函数board_button_read
。
在入口函数中,使用驱动层上层提供的register_button_operations
函数将下层创建的my_button_oprs
结构体对象注册到上层,供上层使用下层提供的初始化和读取数据的方法。
在出口函数汇中,使用上层提供的unregister_button_operations
函数移除在文件系统中创建的设备节点文件。
最后完善一下设备信息。
对于驱动层下层,重点在于初始化和读取数据函数的实现:
如上图所示IMX6ULL
按键的电路原理图:
KEY1
:与GPIO5_1
相连,按键按下时是低电平(0),未按下时是高电平(1)。KEY2
:与GPIO4_IO14
相连,按键按下时是低电平(0),未按下时是高电平(1)。
- 使能GPIO
如上图所示使能GPIO的寄存器:
CCM_CCGR1
:物理地址是0x020C406C
,其中的[31,30]
控制GPIO5
的使能,但是这里保留了,GPIO5
默认使能,CCM_CCGR3
:物理地址是0x020C4074
,其中的[13,12]
控制GPIO
的使能,当这两个比特位为11
时,GPIO4
使能。
- 选择GPIO模式
如上图所示IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1
寄存器:
- 物理地址是
0x0229000C
。 MUX_MODE
:这四个bit为101
时,表示GPIO5_IO01
引脚用作通用GPIO。
如上图所示IOMUXC_SW_MUX_CTL_PAD_NAND_CE1_B
寄存器:
- 物理地址是
0x020E01B0
。 MUX_MODE
:这四个bit为0101
时,表示GPIO4_IO14
引脚用作通用GPIO。
- 设置GPIO方向
如上图所示内存映射表:
GPIO5
:该组寄存器的基地址是0x020AC000
。GPIO4
:该组寄存器的基地址是0x020A8000
。
如上图所示GPIO所有寄存器的内存映射表,以GPIO4
为例:
- 一共8个寄存器,每组GPIO都是这样。
- 从
DR
寄存器开始,到EDGE_SEL
寄存器结束,地址从低到高,每个寄存器所占4个字节。
所以定义一个结构体来描述GPIO组中的所有寄存器:
如上图所示结构体,用该结构体创建gpio5
和gpio4
结构体对象来操作相应的GPIO。
如上图所示GDIR
寄存器:
- 对于
GPIO5_1
:将gpio5->gdir
的bit1
设置为0,表示输入。 - 对于
GPIO4_14
:将gpio4->gdir
的bit14
设置为0,表示输入。
- 读取按键状态:
如上图所示PSR
寄存器:
- 对于
GPIO5_1
:gpio5->psr
的bit1
为0,表示按键按下,为低电平,为1,表示按键没有按下,为高电平。 - 对于
GPIO4_14
:gpio4->gdir
的bit14
为0,表示按键按下,为低电平,为1,表示按键没有按下,为高电平。
编程
如上图所示,在驱动层下层board_button.c
中,将用到的寄存器全部定义出来,并且创建gpio4
和gpio5
两个结构体变量来表示GPIO。
board_button_init:
如上图所示初始化函数中:
- 将所有涉及到的寄存器都在内存中映射相应的虚拟地址,只映射一次。
- 其中GPIO组进行整体映射,大小为
struct imx6ull_gpio
结构体的大小。
- 其中GPIO组进行整体映射,大小为
- 根据次设备号对GPIO口进行初始化,控制相关寄存器。
- 使能GPIO组,设置引脚模式为通用GPIO,设置方向为输入。
board_button_read:
如上图所示读取按键数据的函数,根据次设备号确定读取gpio5
还是gpio4
中的psr
寄存器,然后返回该寄存器中的值。
应用层测试函数:
如上图应用层测试函数,在测试的时候,命令行中输入./button_test /dev/BigMiaomi_button0
或者./button_test /dev/BigMiaomi_button1
,在mian
函数中会使用read
系统调用去获取按键状态,最终会调用驱动层下层的board_button_read
函数。
- 如果打印1,表示按键没有按下。
- 如果打印0,表示按键按下。
如上图所示Makefile
文件中,make
以后:
- 会生成
button_test
可执行程序,用来测试。 - 会生成
button_drv.ko
和board_button.ko
两个模块文件,用来安装驱动程序。
如上图所示,在开发板上安装两个按键的驱动程序,可以看到在./dev
目录下有BigMiaomi_button0
和BigMiaomi_button1
两个设备节点。
如上图所示,在开发板上执行测试程序:
- 未在开发板上按下
KEY1
和KEY2
两个按键时,打印出的值是1,表示高电平,和电路逻辑相符。 - 按下开发板上按下
KEY1
和KEY2
两个按键时,打印出的值是0,表示低电平,和电路逻辑相符。
🍮总结
要会使用设备树向内核中注册设备节点,并且会对驱动程序做相应的修改。除此之外,要知道APP读取按键的四种方式,以及实现简单的APP按键驱动程序编程。