Linux驱动开发八.按键操作
今天我们来试一下在GPIO子系统下实现按键的使用。这篇总结主要目的是让我们直到GPIO在作为输入的时候是怎么使用的,真正的使用环境中是不可能将按键输入按照这个模式使用的。同时,我们还可以回顾一下上一章的原子操作,一同来完成按键驱动操作
设备树修改
回顾一下我们写裸机的时候,KEY是复用在UART1_CTS引脚上,所以要对该引脚进行设置
pinctrl设置
按键的pinctrl设置节点在iomuxc节点下
&iomuxc { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hog_1>; imx6ul-evk { pinctrl_enet1_reset: enet1resetgrp { fsl,pins = < MX6ULL_PAD_SNVS_TAMPER7__GPIO5_IO07 0x10B0 >; }; //按键pinctrl设置 pinctrl_key: keygrp { fsl,pins = < MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 >; }; . . . .
复用UART1_CTS_B为GPIO1_IO18,电气属性值为0xF080(电气属性不再解释)。节点名为pinctrl_key,在设备节点中会被调用
gpio设置
按键gpio节点可以直接放在根节点下
key{ compatible = "alientek,key"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_key>; key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>; status = "okay"; };
没什么可解释的,主要就是关联了pinctrl节点(pinctrl_key),还有gpio节点
驱动编写
驱动框架跟前面已经写过的所有的驱动都差不多,还是跟前面的beep驱动一样,先修改下设备结构体
struct key_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int key_gpio; atomic_t keyvalue; //按键值 };
结构体最后添加了个原子整形变量,保存的是按键值,防止我们在操作按键时有其他的线程同时对其进行操作。
然后是把gpio初始化单独提出来构建个函数
1 static int key_gpio_init(struct key_dev *dev){ 2 int ret = 0; 3 //搜索设备树节点 4 dev->dev_nd = of_find_node_by_path("/key"); 5 if(dev->dev_nd == NULL){ 6 printk("can't find device key\r\n"); 7 ret = -EINVAL; 8 goto fail_nd; 9 } 10 printk("find device key\r\n"); 11 12 //获取设备gpio属性 13 dev->key_gpio = of_get_named_gpio(dev->dev_nd,"key-gpios",0); 14 if(dev->key_gpio < 0){ 15 ret = -EINVAL; 16 goto fail_gpio; 17 } 18 printk("key gpio = %d",dev->key_gpio); 19 20 //请求gpio,检查io是否被使用 21 ret = gpio_request(dev->key_gpio,"key0"); 22 printk("gpio=%d\r\n",dev->key_gpio); 23 if(ret){ 24 ret = -EBUSY; 25 printk("IO %d can't request\r\n",dev->key_gpio); 26 goto fail_request; 27 } 28 29 //gpio设置为输入 30 ret = gpio_direction_input(dev->key_gpio); 31 if(ret<0){ 32 ret = -EINVAL; 33 goto fail_input; 34 } 35 36 return 0; 37 fail_input: 38 //释放gpio资源 39 gpio_free(dev->key_gpio); 40 fail_request: 41 //不需要释放资源 42 fail_gpio: 43 fail_nd: 44 return ret; 45 }
跟前面的蜂鸣器驱动GPIO使用流程基本一样,无非就是先获取设备树设备节点,在获取GPIO属性,检查GPIO资是否被使用,最后设置GPIO为输入就可以了。区别就是修改了下异常处理的思路,将GPIO异常处理放在该函数中,并且只有最后一步设置方向时候如果有异常需要释放GPIO资源,其余操作不需要任何处理。
最主要看的是文件操作的部分
1 static int key_open(struct inode *inode, struct file *filp) 2 { 3 printk("file open!\r\n"); 4 filp->private_data = &key_dev; /* 设置私有数据 */ 5 return 0; 6 } 7 8 static ssize_t key_read(struct file *filp,__user char *buf,size_t count,loff_t *ppos) 9 { 10 11 struct key_dev *dev = filp->private_data; 12 int ret=0; 13 int value; 14 15 if(gpio_get_value(dev->key_gpio)==0){ //读取值0为低电平,按钮按下 16 while(!gpio_get_value(dev->key_gpio)); //等待key弹起来,值变为1,非运算跳出循环 17 atomic_set(&dev->keyvalue,KEY0VALUE); //原子操作,将结构体里的value设置为指定的值(宏定义) 18 } 19 else { 20 atomic_set(&dev->keyvalue,INVAKEY); //读取值为1,按钮未按下,原子操作 21 } 22 23 value =atomic_read(&dev->keyvalue); //保存按键值 24 25 ret = copy_to_user(buf,&value,sizeof(value)); 26 return ret; 27 } 28 /** 29 * @brief 文件操作集合 30 * 31 */ 32 static const struct file_operations key_fops = { 33 .owner = THIS_MODULE, 34 .open = key_open, 35 .read = key_read, 36 };
我们前面写的驱动中,好像还没有用到文件read操作,这里主要就是添加的read过程。
在open函数中,我们指定了成员的私有数据,供我们在其他文件操作中使用
当我们运行APP函数通过read操作调用read函数以后,会进行if判断,如果按钮未按下,值为1,执行else结构里的内容(第20行)将设备结构dev里的keyvalue通过原子整形变量操作写入指定的值(宏INVAKEY对应的值),如果按下按钮,会执行if结构体内(16~17行)的while循环,while循环体内没有语句,是个空循环,一直执行到那件弹起值回复为1后跳出循环,将预先设置的KEY0VALUE这个宏对应的值写入原子变量。完成上面的操作后通过第23行语句读取原子变量的值将该值作为read函数返回的内容(注意不是函数返回值,APP里要传个指针变量进来指向这个值,也就是第25行的内容)。
最后把整个驱动的代码放出来
/** * @file key.c * @author your name (you@domain.com) * @brief 按键驱动 * @version 0.1 * @date 2022-07-10 * * @copyright Copyright (c) 2022 * */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include <linux/types.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_irq.h> #include <linux/gpio.h> #include <linux/of_gpio.h> #define DEVICE_CNT 1 #define DEVICE_NAME "key" #define KEY0VALUE 0xF0 #define INVAKEY 0x00 struct key_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int key_gpio; atomic_t keyvalue; //按键值 }; struct key_dev key_dev; static int key_open(struct inode *inode, struct file *filp) { printk("file open!\r\n"); filp->private_data = &key_dev; /* 设置私有数据 */ return 0; } static ssize_t key_read(struct file *filp,__user char *buf,size_t count,loff_t *ppos) { struct key_dev *dev = filp->private_data; int ret=0; int value; if(gpio_get_value(dev->key_gpio)==0){ //读取值0为低电平,按钮按下 while(!gpio_get_value(dev->key_gpio)); //等待key弹起来,值变为1,非运算跳出循环 atomic_set(&dev->keyvalue,KEY0VALUE); //原子操作,将结构体里的value设置为指定的值(宏定义) } else { atomic_set(&dev->keyvalue,INVAKEY); //读取值为1,按钮未按下,原子操作 } value =atomic_read(&dev->keyvalue); //保存按键值 ret = copy_to_user(buf,&value,sizeof(value)); return ret; } /** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, .open = key_open, .read = key_read, }; static int key_gpio_init(struct key_dev *dev){ int ret = 0; //搜索设备树节点 dev->dev_nd = of_find_node_by_path("/key"); if(dev->dev_nd == NULL){ printk("can't find device key\r\n"); ret = -EINVAL; goto fail_nd; } printk("find device key\r\n"); //获取设备gpio属性 dev->key_gpio = of_get_named_gpio(dev->dev_nd,"key-gpios",0); if(dev->key_gpio < 0){ ret = -EINVAL; goto fail_gpio; } printk("key gpio = %d",dev->key_gpio); //请求gpio,检查io是否被使用 ret = gpio_request(dev->key_gpio,"key0"); printk("gpio=%d\r\n",dev->key_gpio); if(ret){ ret = -EBUSY; printk("IO %d can't request\r\n",dev->key_gpio); goto fail_request; } //gpio方向设置 ret = gpio_direction_input(dev->key_gpio); if(ret<0){ ret = -EINVAL; goto fail_input; } return 0; fail_input: //释放gpio资源 gpio_free(dev->key_gpio); fail_request: //不需要释放资源 fail_gpio: fail_nd: return ret; } static int __init key_init(void){ int ret = 0; atomic_set(&key_dev.keyvalue,INVAKEY); //申请设备号 key_dev.major = 0; if(key_dev.major){ //手动指定设备号,使用指定的设备号 key_dev.dev_id = MKDEV(key_dev.major,0); ret = register_chrdev_region(key_dev.dev_id,DEVICE_CNT,DEVICE_NAME); } else{ //设备号未指定,申请设备号 ret = alloc_chrdev_region(&key_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME); key_dev.major = MAJOR(key_dev.dev_id); key_dev.minor = MINOR(key_dev.dev_id); } printk("dev id geted!\r\n"); if(ret<0){ //设备号申请异常,跳转至异常处理 goto faile_devid; } //字符设备cdev初始化 key_dev.cdev.owner = THIS_MODULE; cdev_init(&key_dev.cdev,&key_fops); //文件操作集合映射 ret = cdev_add(&key_dev.cdev,key_dev.dev_id,DEVICE_CNT); if(ret<0){ printk("cdev_add err\r\n"); //cdev初始化异常,跳转至异常处理 goto fail_cdev; } printk("chr dev inited!\r\n"); //自动创建设备节点 key_dev.class = class_create(THIS_MODULE,DEVICE_NAME); if(IS_ERR(key_dev.class)){ //class创建异常处理 printk("class err!\r\n"); ret = PTR_ERR(key_dev.class); goto fail_class; } printk("dev class created\r\n"); key_dev.device = device_create(key_dev.class,NULL,key_dev.dev_id,NULL,DEVICE_NAME); if(IS_ERR(key_dev.device)){ //设备创建异常处理 printk("device err!\r\n"); ret = PTR_ERR(key_dev.device); goto fail_device; } printk("device created!\r\n"); ret = key_gpio_init(&key_dev); if(ret<0){ //在gpio_init函数中已经做了后面需要对异常处理,这里只要跳转到最上面的异常就可以 goto fail_device; } return ret; fail_device: //device创建失败,意味着class创建成功,应该将class销毁 printk("device create err,class destroyed\r\n"); class_destroy(key_dev.class); fail_class: //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉 printk("class create err,cdev del\r\n"); cdev_del(&key_dev.cdev); fail_cdev: //cdev初始化异常,意味着设备号已经申请完成,应将其释放 printk("cdev init err,chrdev register\r\n"); unregister_chrdev_region(key_dev.dev_id,DEVICE_CNT); faile_devid: //设备号申请异常,由于是第一步操作,不需要进行其他处理 printk("dev id err\r\n"); return ret; } static void __exit key_exit(void){ cdev_del(&key_dev.cdev); unregister_chrdev_region(key_dev.dev_id,DEVICE_CNT); device_destroy(key_dev.class,key_dev.dev_id); class_destroy(key_dev.class); gpio_free(key_dev.key_gpio); } module_init(key_init); module_exit(key_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");
编译的时候记得修改makefile文件。
APP修改
这个app程序要做相应修改
1 /** 2 * @file keyAPP.c 3 * @author your name (you@domain.com) 4 * @brief 按键驱动对应应用程序 5 * @version 0.1 6 * @date 2022-07-10 7 * 8 * @copyright Copyright (c) 2022 9 * 10 */ 11 #include <sys/types.h> 12 #include <sys/stat.h> 13 #include <fcntl.h> 14 #include <unistd.h> 15 #include <stdio.h> 16 #include <stdlib.h> 17 18 #define KEY_0 0xF0 19 #define KEY_1 0x00 20 21 /** 22 * @brief 23 * 24 * @param argc //参数个数 25 * @param argv //参数 26 * @return int 27 */ 28 int main(int argc,char *argv[]) 29 { 30 char *filename; //文件名 31 filename = argv[1]; //文件名为命令行后第二个参数(索引值为1) 32 int value = 0; 33 int ret = 0; //初始化操作返回值 34 int f = 0; //初始化文件句柄 35 unsigned char databuf[1]; 36 37 if(argc != 2){ //输入参数个数不为3,提示输入格式错误 38 printf("input format error!\r\n"); 39 } 40 41 f = open(filename, O_RDWR); //打开文件 42 43 if(f < 0){ 44 printf("file open error\r\n"); 45 return -1; 46 } 47 48 while(1){ 49 read(f,&value,sizeof(value)); 50 if(value == KEY_0){ 51 printf("KEY 0 Press value = %d\r\n",value); 52 } 53 } 54 close(f); //关闭文件 55 return 0; 56 }
主要就是加了个for循环,循环体里一直调用驱动里read函数,如果按键有按下过程,value值应该是KEY_0对应的值(KEY_0这个宏在驱动和app里设置的应该一样,否则if语句判定为false就没信息打印出来了),加载完驱动ko模块以后运行一下程序
每当按钮按下时都会有个信息打印出来,因为我们定义的KEY_0宏的值为0xF0,按照十进制打印出来的值就是240。并且有时候我们按下按钮时会打印不止1行信息,是因为我们没有对按键做消抖处理。
存在的问题
在本章节一开始的我说了,一般按键不是按照这种方式使用的,在运行APP的时候后面加上&将运行方式改为后台运行,然后运行一下top命令看一下资源使用情况
可以看出来,APP程序占用率99.6%的CPU资源,是因为程序中我们使用了个近似与死循环的for循环。正常情况下我们使用按键输入时应该类似个中断操作而不是这种“死等“的做法。具体实现方法我们后期会讲到,这次只是为了演示下GPIO子系统在作为输入设备时是如何结合设备树使用的。