基于pinctrl和GPIO子系统的按键驱动程序

news/2024/9/30 21:24:20

嵌入式驱动开发中pinctrl和GPIO子系统使用频率非常高,其中pinctrl子系统主要用于复用和配置引脚,GPIO子系统用于设置GPIO的输入/输出,向引脚写入数据或者从引脚读取数据。一个引脚可以复用为多种不同的功能,因此要使用GPIO子系统首先要先把引脚配置为GPIO功能。下面将分为两部分记录如何基于pinctrl子系统和GPIO子系统编写按键驱动程序。

本实验基于imx6ull开发板

1、向设备树添加内容
(1)使用pinctrl子系统复用引脚为GPIO功能
基于pinctrl和GPIO子系统进行驱动开发本质上还是基于设备树的总线设备驱动模型,但是此时设备树中节点代码的格式要遵守对应芯片厂家提供的pinctrl和GPIO子系统的格式,这样他们的软件才能正确运行。
pinctrl子系统对应的引脚的复用、配置,因此它必然要获取引脚相关的寄存器信息和配置信息,因此要在设备树中pinctrl对应的节点下添加子节点用于描述要用到的引脚。

使用NXP提供的i.MX Pins Tool v6工具,可以方便的得到配置pinctrl子系统设备树节点的代码。
在前面的按键驱动笔记中已经给出了按键的原理图,根据引脚名去芯片参考手册可以查到引脚属于GPIO1_IO18,打开i.MX Pins Tool v6工具后,在GPIO1组中找到IO18,并点击选中(打勾),如下图所示。

在图中的右侧可以看到右侧iomux节点下有一个子节点

点击查看代码
BOARD_InitPins: BOARD_InitPinsGrp {                /*!< Function assigned for the core: Cortex-A7[ca7] */fsl,pins = <MX6UL_PAD_UART1_CTS_B__GPIO1_IO18          0x000010B0>;};
把这个子节点代码复制到我们的设备树下的iomuxc节点下,并改一下名称就可以。iomux节点可以理解为就是pinctrl子系统对应的设备树节点,它会到这里来寻找引脚的复用、配置信息,并且它的格式要和它的代码相适应。 将代码复制到设备树中iomuxc节点下并改名后如下:
点击查看代码
pinctrl_key: keyGrp {                /*!< Function assigned for the core: Cortex-A7[ca7] */fsl,pins = <MX6UL_PAD_UART1_CTS_B__GPIO1_IO18          0x000010B0>;};

在上述代码中,fsl,pins是芯片提供的pinctrl子系统规定的属性名MX6UL_PAD_UART1_CTS_B__GPIO1_IO18表示了UART1_CTS_B引脚对应的一组寄存器,它是一个宏定义,猜测它应该包含了引脚对应的复用寄存器、配置寄存器地址。具体的分析过程参考正点原子驱动开发文档中的pinctrl和gpio子系统章节。

(2)使用GPIO子系统
向iomux添加子节点是为了使用引脚的相关寄存器信息,接下来还要在设备树的根节点下添加设备对应的子节点(因为我们刚才在iomux节点下添加的子节点并不会被转换为platform_device),并引用iomux节点下的内容。这个过程与基于设备树的总线设备模型是类似,只不过我们没有直接在设备节点下写上寄存器信息,而是引用了定义好的节点的信息。
下面是在根节点下创建设备节点,并引用前面在iomux下添加的子节点

点击查看代码
pinctrl_gpio_mykey {#address-cells = <1>;#size-cells = <1>;compatible = "mykey_driver";	/*用于驱动匹配*/pinctrl-names = "default";	/*pinctrl-前缀的属性是给pinctrl子系统使用的,-gpio后缀的属性是给GPIO子系统使用的*/pinctrl-0 = <&pinctrl_key>;key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;	/*这里设置了低电平有效,因此引脚物理状态和逻辑值相反*/status = "okay";};

在上面代码中,pinctrl-names = "default";表示它只有一个默认状态default,对应这个状态pinctrl子系统所使用的节点信息由pinctrl-0指定,而pinctrl-0又引用了pinctrl_key节点,因此最终pinctrl子系统使用pinctrl_key节点中定义的属性来进行寄存器的设置从而实现引脚的复用、配置。
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>表示使用gpio1组的IO18,并且低电平有效,其中-gpio后缀是必须的,因此GPIO子系统会根据这个后缀找到对应的属性,&gpio1表示使用GPIO1组,它是一个GPIO控制器,里面包含了这一组GPIO的寄存器地址信息,根据18就可以找到GPIO1_IO18的寄存器地址信息。
status = "okay";就是使能这个节点,让它能够被内核转换为platform_device,如果设置为disable,就是屏蔽这个节点,相当于注释掉了

添加好设备树节点信息后,在设备树文件中全局搜索GPIO1_IO18和gpio1 18,查看是否有其它节点也使用了这个引脚,如果有就注释掉,否则会导致引脚冲突。

2、基于pinctrl和GPIO子系统的驱动程序代码编写
按照规定的格式写好设备树文件后,在驱动代码中并不需要调用pinctrl子系统,因为在内核加载设备树文件时,就已经根据设备树中节点的信息转换为platform_device并初始化好引脚的复用、配置了。我们要做的其实就是调用GPIO子系统提供的函数对GPIO进行方向设置、读写GPIO即可,下面是代码

点击查看代码
/*
本次实验是基于pinctrl子系统和gpio子系统进行按键驱动程序编写
platform_device由设备树生成并由pinctrl子系统所使用的引脚进行复用为gpio
在该文件中实现:
(1)定义platform_drvier结构体变量并填充内容(a)实现probe函数:从设备树获取gpio引脚信息、注册驱动、创建设备类class,创建设备节点(b)实现remove函数:释放gpio引脚、删除设备节点、删除设备类class、删除节点
(2)实现file_operations结构体的内容,并注册驱动(a)实现open、read、write、release函数
*/#include "asm/uaccess.h"
#include "linux/export.h"
#include "linux/mod_devicetable.h"
#include <linux/module.h>
#include <linux/platform_device.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>static int major;  //主设备号
static struct class *key_class;
static struct gpio_desc *key_gpio;  //在使用gpdio子系统时,需要通过gpio-desc这个结构体来获取引脚/*实现file_operations结构体
实现open read 和close函数
*/ssize_t key_read(struct file *file, char __user *user, size_t size, loff_t *offset)
{   /*读取按键状态并传递到用户空间*/char state;    int err;// printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__ );/*在原理图中,按键按下时是低电平,但是因为设备树中设置了低电平有效,所以引脚物理状态和gpiod_get_value的返回值相反因此按键按下时返回的是1,不按时返回0*/state = gpiod_get_value(key_gpio);  //读取GPIO状态// state = ~state; //直接取反,并不会从1变成0。通过输出发现,不按下时返回0,取反后输出255,按下时返回1,取反后是254因为1的二进制是0b00000001,取反后是0b11111110,对应10进制就是254state ^= (1);   //使用异或取反printk("state = %d\n", state);err = copy_to_user(user, &state, 1);return 1;   //读取按键的状态,只需要一个字节的char字符表示就行
}int key_open(struct inode *node, struct file *file)
{/*应用程序打开按键对应的设备文件时会调用这个函数,此时相当于要开始使用设备了,那么需要执行一些初始化操作配置所用的引脚为输入*/printk("%s %s %d",__FILE__, __FUNCTION__, __LINE__ );gpiod_direction_input(key_gpio);    //设置GPIO为输入return 0;
}int key_close(struct inode *node, struct file *file)
{/*可以将引脚恢复为高电平输出状态*/printk("%s %s %d",__FILE__, __FUNCTION__, __LINE__ );gpiod_direction_output(key_gpio, 0);    //设置gpio为输出,并默认输出高电平return 0;
}struct file_operations key_oprs = {.owner = THIS_MODULE,.open = key_open,.release = key_close,.read = key_read,};/*实现platform_driver结构体,主要填充两个函数
(1)probe,设备和驱动相互匹配时调用
(2)remove函数 卸载设备时调用,如果直接卸载驱动,那么每个匹配的设备都会执行这个函数一次
*/int key_probe(struct platform_device *pdev)
{/*设备和驱动匹配时执行,因此在这里要注册设备的操作函数,创建类,创建设备,要使用GPIO子系统获取引脚*/printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/*获取按键对应的gpio*/key_gpio = gpiod_get(&pdev->dev, "key", 0);if (IS_ERR(key_gpio)) {dev_err(&pdev->dev, "Failed to get GPIO for key\n");return PTR_ERR(key_gpio);}/*向内核注册file_operations结构体*/major = register_chrdev(0, "key_driver", &key_oprs);//创建classkey_class = class_create(THIS_MODULE, "key_class");if (IS_ERR(key_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "key_driver");gpiod_put(key_gpio);return PTR_ERR(key_class);}/*创建设备节点*/device_create(key_class, NULL, MKDEV(major, 0), NULL, "key_0");return 0;
}
int key_remove(struct platform_device *pdev)
{/*与probe函数的操作相反*/device_destroy(key_class, MKDEV(major, 0));class_destroy(key_class);unregister_chrdev(major, "key_driver");gpiod_put(key_gpio);    //释放引脚return 0;
}/*创建匹配表,驱动能够与设备树中下列节点匹配*/
static const struct of_device_id keys_match_table[] = {{.compatible = "mykey_driver"}
};struct platform_driver key_drv = {.probe = key_probe,.remove = key_remove,.driver = {.name = "mykey",        //driver中的.name字段必须加上,不然运行时报错,暂时不懂得为什么.of_match_table = keys_match_table,}
};static int __init key_drv_init(void)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = platform_driver_register(&key_drv);printk("err = %d", err);return err;
}static void __exit key_drv_exit(void)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);platform_driver_unregister(&key_drv);
}module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");

在代码编写过程遇到的一些细节问题都已经写在注释里了,这里再总结一下:
(1)~是按位取反,如果只是把某一位或某几位取反,应该用异或^;
(2)在设备树中定义gpio1 18 是低电平有效,因此gpiod_get_value函数读取到的GPIO逻辑值和真实的引脚物理值相反;
(3)copy_to_user(user, &state, 1)中的state在定义时不需要声明为volatile,否则会产生警告,而且传入的是地址,因此每次都是从state的内存处取值,不用担心被编译器优化导致数据与预期不符;
(4)在platform_driver的driver字段中的name字段必须赋值,否则在编译时不报错,但是驱动运行时会产生空指针错误,因为驱动和设备匹配时首先比较platform_driver中的driver中的name和platform_device中的override;
(5)在驱动中读取按键状态时,不用防抖,驱动应该只提供读取功能,防抖应该是在应用程序中实现。

应用程序代码如下,在里面调用了按键驱动和led驱动,使用按键实现灯的亮灭操作

点击查看代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main(int argc, char** argv)
{int fd_button, fd_led;char val;char state;/*判断参数*/if(argc != 3){printf("Usage: %s <buton_dev> <led_dev>\n", argv[0]);}fd_button = open(argv[1], O_RDWR);fd_led = open(argv[2], O_RDWR);if(fd_button == -1 || fd_led == -1){printf("can not open file %s\n", argv[1]);return -1;}/*读文件, 控制led灯*/state = 0;write(fd_led, &state, 1);while(1){read(fd_button, &val, 1);if(val == 0){usleep(10000);read(fd_button, &val, 1);if(val == 0){state ^= (1);}write(fd_led, &state, 1);// printf("led state: %d", state);sleep(1);}}close(fd_button);close(fd_led);return 0;
}

至此,基于pinctrl子系统和GPIO子系统的按键驱动程序已经完成,可以看到,使用pinctrl和GPIO子系统可以简化驱动开发工作,而不用直接操作寄存器。但是不管如何,直接操作寄存器是驱动开发本质,只有掌握了本质才能用好别人的工具,要知其然,更要知其所以然。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/29757.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

从零开始写 Docker(十四)---重构:实现容器间 rootfs 隔离

本文为从零开始写 Docker 系列第十四篇,实现容器间的 rootfs 隔离,使得多个容器间互不影响。完整代码见:https://github.com/lixd/mydocker 欢迎 Star推荐阅读以下文章对 docker 基本实现有一个大致认识:核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs…

PCI-Express-Technology(三)

3.1 总线/设备/功能/的定义(Definition of Bus,Device and Function)正如PCI一样,每个PCIe功能(Function)的标识在其所在的设备内,以及这个设备所连接的总线内,都是唯一的。其标识符一般被称为“BDF”。对于任意一个 PCIe 拓扑结构,配置软件负责检测出拓扑中的每个Bus、…

一键自动化博客发布工具,用过的人都说好(infoq篇)

使用一键自动化博客发布工具blog-auto-publishing-tools把博客发布到infoq上。infoq的博客发布界面也是非常简洁的。首页就只有基本的标题,内容和封面图片,所以infoq的实现也相对比较简单。 一起来看看吧。 前提条件 前提条件当然是先下载 blog-auto-publishing-tools这个博客…

莫队(板子)

莫队 参考博客 玄学暴力区间操作算法PPT解释的很清楚啦~, 导致我没什么可写的 \(qwq\) 把所有询问离线下来后排序(左端点按块,右端点升序),然后从一个小区间通过左右端点的移动扩大区间,更新答案。 复杂度主要在区间扩展,也就是左右指针的移动,对于莫队所有的优化几乎都是…

更优雅的使用Gson解析Json

Gson背靠Google这棵大树,拥有广泛的社区支持和相对丰富的文档资源,同时因其简单直观的API,一直以来基本稳坐Android开发序列化的头把交椅(直到Google宣布kotlin成为Android开发的首选语言)。本文对Gson的使用及主要流程做下分析。 Gson的基本使用 Gson依赖 kotlin 复制代…

麒麟系统

问题描述 Nginx最新版 Nginx 1.25.0解决方案 开放防火墙端口 开启端口:sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent 关闭端口:sudo firewall-cmd --zone=public --remove-port=8080/tcp --permanent 端口生效:firewall-cmd --reload

C#中Linq的去重方式Distinct详解

一、首先创建一个控制台应用程序,添加一个Person对象 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace Compare {public class Person{public string Name { get; set; }public int Age { ge…