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");
key.c

编译的时候记得修改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子系统在作为输入设备时是如何结合设备树使用的。

 

posted @ 2022-07-10 21:05  银色的音色  阅读(404)  评论(0编辑  收藏  举报