【Linux驱动】设备树模型的LED驱动 | 查询方式的按键驱动

发布时间:2024年01月02日

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

🍮设备树模型的LED驱动

目前有三种方式来写LED驱动程序:

  • 最简单的驱动模型——硬件操作绑定在驱动函数中。
  • 总线驱动模型。
  • 设备树驱动模型。

下面设备树驱动模型来实现一下LED驱动程序,该模型主要分为两部分,设备树文件和驱动程序。

🍩设备树文件

图
如上图所示设备树文件,在设备树中增加Big-Miaomi-LED@0Big-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文件。

🍩驱动程序

驱动程序在总线驱动模型的基础上进行修改,驱动层的上层不用动,只需要改变下层中的部分代码:

tu
如上图所示,由于现在支持了设备树,所以需要初始化platform_deiver结构体中driver成员里的of_match_table成员,这是一个struct of_device_id类型的数组。

所以需要定义一个struct of_device_id类型的数组,名为BigMiaomi_LEDs

  • 只用platform_deviceplatform_driver匹配规则中优先级最高的compatible属性来匹配。
  • 只支持LED设备,所以compatible属性只有一个值。

compatible属性的值,必须和设备树中要支持节点的compatible属性值相同,才能匹配成功

然后就是在匹配成功以后,会自动调用paltform_driver中的probe函数,在该函数中,原本是从paltform_deviceresources数组中获取硬件资源,但是此时不能这样干了:

tu
如上图所示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中。

获取到引脚资源后的其他操作和总线模型中相同,也是要使用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.kochip_led_opr.ko驱动文件,还有led_drv_test测试文件拷贝到网络根文件系统中。

在开发板上将dtb设备树文件拷贝到/boot目录下,然后重启开发板.

图

如上图所示,在/sys/firmware/devicetree/base/路径下,存在Big-Miaomi-LED@0Big-Miaomi-LED@1两个设备节点,这是我们在设备树文件中添加的两个节点,此时加载到了内核中。然后使用insmod led_drv.koinsmod chip_led_opr.ko安装驱动程序。

图

如上图所示,此时执行测试程序,在命令行中输入./led_drv_test /dev/BigMiaomi_LED0 on,内核打印信息现实操作了GPIO3_1

🍮应用层读取按键值

应用层读取按键值有4种方式:

  • 查询方式
  • 休眠-唤醒方式
  • poll方式
  • 异步通知方式

无论使用哪个方式都需要有按键驱动程序,通过这四种方式可以掌握一些驱动的基本技能:中断、休眠、唤醒、poll等机制

这些基本技能是驱动开发的基础,其他大型驱动复杂的地方是它的框架及设计思想,但是基本技能就只有这些。

🍩查询方式

图
如上图所示查询方式的驱动模型,这种方式最简单,这里并不考虑驱动层中的架构,只看驱动层所做的工作。

在驱动程序中构造并注册一个file_operations结构体,里面提供对应的drv_opendrv_read函数,当应用层调用open系统调用时,在驱动层的drv_open函数中配置相应的引脚为输入引脚。

当应用层调用read系统调用时,在驱动层的drv_read函数中读取该GPIO引脚的寄存器,把引脚的状态返回给应用层。

  • 读取引脚状态时,直接返回寄存器中的值,没有其他多余的动作。

🍩休眠唤醒方式

图

如上图所示休眠唤醒方式的驱动模型,在驱动层中的drv_open函数中,除了要把GPIO设置为输入引脚,还有注册GPIO的中断处理函数

当应用层调用read系统调用时,在驱动层的drv_read驱动函数中:

  1. 如果有按键数据,则直接返回给应用层。
  2. 如果没有按键数据,则应用层的APP在内核态休眠。

当用户按下按键时,GPIO中断被触发,导致drv_open中注册的中断服务程序被执行,在中断服务程序中:

  1. 记录按键数据。
  2. 唤醒休眠中的应用层APP。

应用层的APP被唤醒以后,继续在内核态运行,即执行驱动层代码,把中断服务程序中记录的按键数据返回给应用层的APP。

  • 没有读取到数据时,就会休眠,直到有按键数据到来才被唤醒。

🍩poll方式

上面的休眠-唤醒方式存在一个缺点:如果用户一直没有按下按键,那么应用层的APP就永远休眠阻塞不再执行了,所以可以给APP定个闹钟,这就是poll方式:

图

如上图所示poll驱动模型,poll是应用层实现多路转接的系统调用接口,在驱动层的file_operations结构体中,同样有一个poll函数指针:

tu
如上图所示file_operations结构体的定义,所以当应用层的APP调用poll系统调用时,会调用到驱动层该结构体中poll函数指针指向的函数。

所以需要我们在驱动层去定义poll函数指针指向的函数,使得整个驱动层符合poll驱动模型。驱动层总体步骤为:

  • 注册file_operations结构体,里面提供openreadpoll等驱动层的函数。
  • 应用层APP调用open时,驱动层的drv_open会将GPIO设置为输入引脚,并且注册中断处理函数。
  • 应用层APP调用poll/select时,意图是查询按键数据是否就绪,并且可以指定一个超时时间:
    • 当按键数据就绪时,驱动层的poll向应用层返回就绪状态,APP继续使用read读取按键数据。
    • 当按键数据没有就绪时,驱动层的poll就会在内核态休眠一段时间。

当APP被唤醒时,有两种情况:

  1. 在休眠期间,硬件按键被按下,按键数据就绪。
  2. 超时时间到了,硬件按键仍然没有按下,按键数据没有就绪。

被唤醒后进行判断,如果是数据就绪被唤醒,则调用read从按键的寄存器中读取按键数据,如果是超时被唤醒,则不调用read去读取了。

  • poll/select起到监视事件就绪的作用,驱动层的drv_poll都会告诉应用层APP所监视事件的状态。
  • APP根据驱动层告知的事件状态进行下一步动作。

🍩异步通知方式

图

如上图所示异步通知方式,在该模型中,应用层在打开要操作的设备时,要调用fcntl设置其fdFASYNC标志,此时会调用驱动层的drv_fasync函数:

tu
如上图所示,在file_operations结构体中也有一个fsync函数指针,在该模型中,该指针指向的函数只需要记录当前进程的PID

除了设置给fd设置FASYNC表示异步通知外,还需要使用signal系统调用注册信号处理函数my_func

此时该模型的处理步骤为:

  • APP调用open配置GPIO引脚为输入方式,并注册中断服务函数。
  • APP调用fcntl设置fd指向的文件为异步通知方式,并且注册信号处理函数.
  • 当硬件按键被按下时,中断服务程序会给记录下来的进程PID表示的进程发送信号,信号递达后执行注册的my_func信号处理函数。
  • 在信号处理函数中,调用read来读取按键数据,此时必然是有按键数据的。
  • 在没有按键按下时,APP正常执行,当按键按下后立刻去读取按键数据,使得应用层实现了中断的处理方式。

我们的驱动程序可以实现上述 4 种提供按键驱动的方法,但是驱动程序不应该限制APP使用哪种方法。

  • 这就是驱动设计的一个原则:只提供能力,不提供策略

就是说,APP想用哪种方法都行,驱动程序都可以提供;但是驱动程序不能限制APP使用哪种方法。

🍮查询方式实现按键驱动

前面介绍了按键的四种驱动模型,但是由于后面三种都涉及到中断方面的知识,而到目前为止本喵还没有介绍驱动程序中的中断,所以这里先仅用查询方式实现一下按键驱动程序:

tu

如上图所示,采用简单的驱动层分层模型来实现查询方式的按键驱动层数:

  • 应用层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根据设备号确定哪个按键,并读取对应寄存器中的值返回引脚电平。

驱动层上层:

tu
如上图所示,在button_operations.h中定义button_operations结构体:

  • count:表示按键设备个数
  • init:驱动层下层提供的初始化按键设备方法。
  • read:驱动层下层提供的读取按键状态方法。

tu
如上图所示,在button_drv.c中,创建file_operations结构体,并且用drv_opendrv_read初始化openread函数指针:

  • 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)。
  1. 使能GPIO

TU
如上图所示使能GPIO的寄存器:

  • CCM_CCGR1:物理地址是0x020C406C,其中的[31,30]控制GPIO5的使能,但是这里保留了,GPIO5默认使能,
  • CCM_CCGR3:物理地址是0x020C4074,其中的[13,12]控制GPIO的使能,当这两个比特位为11时,GPIO4使能。
  1. 选择GPIO模式

tu
如上图所示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。
  1. 设置GPIO方向

TU
如上图所示内存映射表:

  • GPIO5:该组寄存器的基地址是0x020AC000
  • GPIO4:该组寄存器的基地址是0x020A8000

图
如上图所示GPIO所有寄存器的内存映射表,以GPIO4为例:

  • 一共8个寄存器,每组GPIO都是这样。
  • DR寄存器开始,到EDGE_SEL寄存器结束,地址从低到高,每个寄存器所占4个字节。

所以定义一个结构体来描述GPIO组中的所有寄存器:

tu
如上图所示结构体,用该结构体创建gpio5gpio4结构体对象来操作相应的GPIO。

图
如上图所示GDIR寄存器:

  • 对于GPIO5_1:将gpio5->gdirbit1设置为0,表示输入。
  • 对于GPIO4_14:将gpio4->gdirbit14设置为0,表示输入。
  1. 读取按键状态:

tu
如上图所示PSR寄存器:

  • 对于GPIO5_1gpio5->psrbit1为0,表示按键按下,为低电平,为1,表示按键没有按下,为高电平。
  • 对于GPIO4_14gpio4->gdirbit14为0,表示按键按下,为低电平,为1,表示按键没有按下,为高电平。

编程

tu

如上图所示,在驱动层下层board_button.c中,将用到的寄存器全部定义出来,并且创建gpio4gpio5两个结构体变量来表示GPIO。

board_button_init:

图

如上图所示初始化函数中:

  • 将所有涉及到的寄存器都在内存中映射相应的虚拟地址,只映射一次。
    • 其中GPIO组进行整体映射,大小为struct imx6ull_gpio结构体的大小。
  • 根据次设备号对GPIO口进行初始化,控制相关寄存器。
    • 使能GPIO组,设置引脚模式为通用GPIO,设置方向为输入。

board_button_read:

图
如上图所示读取按键数据的函数,根据次设备号确定读取gpio5还是gpio4中的psr寄存器,然后返回该寄存器中的值。

应用层测试函数:

tu
如上图应用层测试函数,在测试的时候,命令行中输入./button_test /dev/BigMiaomi_button0或者./button_test /dev/BigMiaomi_button1,在mian函数中会使用read系统调用去获取按键状态,最终会调用驱动层下层的board_button_read函数。

  • 如果打印1,表示按键没有按下。
  • 如果打印0,表示按键按下。

图
如上图所示Makefile文件中,make以后:

  • 会生成button_test可执行程序,用来测试。
  • 会生成button_drv.koboard_button.ko两个模块文件,用来安装驱动程序。

图
如上图所示,在开发板上安装两个按键的驱动程序,可以看到在./dev目录下有BigMiaomi_button0BigMiaomi_button1两个设备节点。

TU

如上图所示,在开发板上执行测试程序:

  • 未在开发板上按下KEY1KEY2两个按键时,打印出的值是1,表示高电平,和电路逻辑相符。
  • 按下开发板上按下KEY1KEY2两个按键时,打印出的值是0,表示低电平,和电路逻辑相符。

🍮总结

要会使用设备树向内核中注册设备节点,并且会对驱动程序做相应的修改。除此之外,要知道APP读取按键的四种方式,以及实现简单的APP按键驱动程序编程。

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