Linux驱动之INPUT设备驱动

目录

一、开发环境

二、编写按键input设备的注册与事件上报

        2.1 修改设备树文件

                1 添加 pinctrl 节点

                2、添加 KEY 设备节点

                3、检查 PIN 是否被其他外设使用

        2.2 驱动程序编写

        2.3 测试APP编写

        2.4 运行测试

三、Linux内核自带按键input设备驱动

        3.1 自带按键驱动程序源码简析

        3.2 自带按键驱动程序的使用

        3.3 运行测试


        上一章已经了解了input子系统的大体框架和input设备的注册以及对应事件的上报流程,现在就写一个简单的input设备驱动实验来更加深入的理解input子系统。

        本章将分别采用以下两种方法来进行按键input设备驱动的实验:

  • 1、编写按键input设备的注册与事件上报

  • 2、Linux内核自带按键input设备驱动

一、开发环境

  • CPU:IMX6ULL

  • 内核版本:Linux-5.19

二、编写按键input设备的注册与事件上报

2.1 修改设备树文件

1 添加 pinctrl 节点

        I.MX6U-ALPHA开发板上的 KEY 使用了 UART1_CTS_B这个 PIN,打开 imx6ul-14x14-evk.dtsi ,在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_key”的子节点,节点内容如下所示:

pinctrl_key: keygrp {
    fsl,pins = <
        MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 /* key0 */
    >;
};

        第 3 行,将 GPIO_IO18 这个 PIN 复用为 GPIO1_IO18。

2、添加 KEY 设备节点

        在根节点“/”下创建 KEY 节点,节点名为“key”,节点内容如下:

key {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "imx6ull-key";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_key>;
    key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
    status = "okay";
};

        第 6 行, pinctrl-0 属性设置 KEY 所使用的 PIN 对应的 pinctrl 节点。

        第 7 行, key-gpio 属性指定了 KEY 所使用的 GPIO。

3、检查 PIN 是否被其他外设使用

        本次实验中按键使用的 PIN 为 UART1_CTS_B,因此先检查 PIN 为 UART1_CTS_B 这个 PIN 有没有被其他的 pinctrl 节点使用,如果有使用的话就要屏蔽掉,然后再检查 GPIO1_IO18这个 GPIO 有没有被其他外设使用,如果有的话也要屏蔽掉。

        设备树编写完成以后使用“make dtbs”命令重新编译设备树,然后使用新编译出来的imx6ull-toto.dtb 文件启动 Linux 系统。启动成功以后进入“/proc/device-tree”目录中查看“key”节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如下所示:

/ # ls /proc/device-tree/
#address-cells      clock-osc           pmu
#size-cells         compatible          regulator-can-3v3
aliases             cpus                regulator-peri-3v3
backlight-display   dts_led             regulator-sd1-vmmc
beep                key                 soc
chosen              memory@80000000     sound-wm8960
clock-cli           model               spi4
clock-di0           name                timer
clock-di1           panel

2.2 驱动程序编写

        设备树准备好以后就可以编写驱动程序了,在 input_key.c 里面输入如下内容:

/*
 * Copyright © toto Co., Ltd. 1998-2029. All rights reserved.
 * @Description: 
 * @Version: 1.0
 * @Autor: Seven
 * @Date: 2023-09-17 13:19:32
 * @LastEditors: Seven
 * @LastEditTime: 2023-09-17 16:57:23
 */
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/timer.h>

#define KEYINPUT_CNT    1           /* 设备数量 */
#define KEYINPUT_NAME   "inputkey"  /* 设备名字 */
#define KEY_VALUE       0x1         /* 按键值 */
#define KEY_INVALID     0xFF        /* 无效的按键值 */
#define KEY_NUM         1           /* 按键数量 */

/* 定义按键中断操作结构体 */
struct irq_keydesc {
    int gpio;           /* 中断使用的gpio */
    int irqnum;         /* 中断号 */
    unsigned char value;/* 按键对应的键值 */
    char name[10];      /* 中断名 */
    irqreturn_t (*handler)(int, void *); /* 中断服务函数 */
};

/* inputkey设备信息 */
struct inputkey_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *dev_nd;
    struct timer_list timer;
    struct irq_keydesc irq_desc[KEY_NUM];
    unsigned char cur_keynum;
    struct input_dev *inputdev;
};

/* 定义 key 输入设备 */
struct inputkey_dev inputdev;

/*
 * @Brief   中断服务函数,开启定时器,
 *          延时10ms用于按键消抖
 * @Param   irq:中断号
 * @Param   dev_id:设备结构
 * @Note    NOne
 * @RetVal  中断执行结果
 */
static irqreturn_t key_handler(int irq, void *dev_id)
{
    struct inputkey_dev *dev = (struct inputkey_dev *)dev_id;

    dev->cur_keynum = 0;
    // dev->timer.data = (volatile long)dev_id;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));

    return IRQ_RETVAL(IRQ_HANDLED);
}

/*
 * @Brief   定时器服务函数,用于按键消抖,
 *          定时器到再次读取按键值,按键还处于按下则按键有效
 * @Param   t:设备结构体
 * @Note    NOne
 * @RetVal  NOne
 */
void timer_function(struct timer_list *t)
{
    unsigned char value, num;
    struct irq_keydesc *keydesc;
    struct inputkey_dev *dev = from_timer(dev, t, timer);

    num = dev->cur_keynum;
    keydesc = &dev->irq_desc[num];
    
    /* 读取IO值 */
    value = gpio_get_value(keydesc->gpio);
    /* 按键按下 */
    if(value == 0) {
        /* 上报按键值 */
        input_report_key(dev->inputdev, keydesc->value, 1);
        input_sync(dev->inputdev);
    } else { /* 按键松开 */
        input_report_key(dev->inputdev, keydesc->value, 0);
        input_sync(dev->inputdev);
    }
}

/*
 * @Brief   按键IO初始化
 * @Param   None
 * @Note    NOne
 * @RetVal  NOne
 */
static int keyio_init(void)
{
    unsigned char i = 0;
    char name[10];
    int ret = 0;

    inputdev.dev_nd = of_find_node_by_path("/key");
    if (!inputdev.dev_nd) {
        printk(KERN_ERR "key node not found\r");
        return -EINVAL;
    }

    /* 提取GPIO */
    for (i = 0; i < KEY_NUM; i++) {
        inputdev.irq_desc[i].gpio = 
                of_get_named_gpio(inputdev.dev_nd, "key-gpio", i);
        if (inputdev.irq_desc[i].gpio < 0) {
            printk(KERN_ERR "can't get key:%d\n", i);
        }
    }

    /* 初始化key所使用的IO, 并设置中断模式 */
    for (i = 0; i < KEY_NUM; i++) {
        memset(inputdev.irq_desc[i].name, 0, sizeof(name));
        sprintf(inputdev.irq_desc[i].name, "KEY%d", i);
        gpio_request(inputdev.irq_desc[i].gpio, 
                        inputdev.irq_desc[i].name);
        gpio_direction_input(inputdev.irq_desc[i].gpio);
        inputdev.irq_desc[i].irqnum = 
                        irq_of_parse_and_map(inputdev.dev_nd, i);
    }

    /* 申请中断 */
    inputdev.irq_desc[0].handler = key_handler;
    inputdev.irq_desc[0].value = KEY_0;

    for (i = 0; i < KEY_NUM; i++) {
        ret = request_irq(inputdev.irq_desc[i].irqnum,
                        inputdev.irq_desc[i].handler,
                        IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                        inputdev.irq_desc[i].name, &inputdev);
        if (ret < 0) {
            printk(KERN_ERR "irq %d request failed\n", 
                            inputdev.irq_desc[i].irqnum);
            return -EFAULT;
        }
    }

    /* 6.初始化timer */
    inputdev.timer.expires = jiffies + msecs_to_jiffies(10);
    timer_setup(&inputdev.timer, timer_function, 0);

    /* 申请input_dev */
    inputdev.inputdev = input_allocate_device();
    inputdev.inputdev->name = KEYINPUT_NAME;

    inputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) 
                                | BIT_MASK(EV_REP);
    input_set_capability(inputdev.inputdev, EV_KEY, KEY_0);

    /* 注册input设备*/
    ret = input_register_device(inputdev.inputdev);
    if (ret) {
        printk(KERN_ERR "register input device failed\n");
        return ret;
    }

    return 0;
}

/*
 * @Brief   驱动入口函数
 * @Param   None
 * @Note    NOne
 * @RetVal  NOne
 */
static int __init inputkey_init(void)
{
    keyio_init();

    return 0;
}

/*
 * @Brief   驱动出口函数
 * @Param   None
 * @Note    NOne
 * @RetVal  NOne
 */
static void __exit inputkey_exit(void)
{
    unsigned int i = 0;

    /* 删除定时器 */
    del_timer_sync(&inputdev.timer);

    /* 释放中断 */
    for (i = 0; i < KEY_NUM; i++) {
        free_irq(inputdev.irq_desc[i].irqnum, &inputdev);
    }

    /* 释放IO */
    for (i = 0; i < KEY_NUM; i++) {
        gpio_free(inputdev.irq_desc[i].gpio);
    }

    /* 释放input设备 */
    input_unregister_device(inputdev.inputdev);
    input_free_device(inputdev.inputdev);
}


module_init(inputkey_init);
module_exit(inputkey_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("toto");

2.3 测试APP编写

        input_key_app.c 测试程序具体代码如下:

/*
 * Copyright © toto Co., Ltd. 1998-2029. All rights reserved.
 * @Description: 
 * @Version: 1.0
 * @Autor: Seven
 * @Date: 2023-09-17 16:10:12
 * @LastEditors: Seven
 * @LastEditTime: 2023-09-17 21:12:01
 */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <linux/input.h>

/* 定义一个input_event变量,存放输入事件信息 */
static struct input_event inputevt;

/*
 * @Brief   main 主程序
 * @Param   argc:
 * @Param   argv:
 * @Note    NOne
 * @RetVal  0-成功;其他-失败
 */
int main(int argc, char *argv[])
{
    int fd, err;
    char *filename;

    if (argc != 2) {
        printf("argc != 3\n");
        return -1;
    }

    filename = argv[1];

    /*打开驱动文件*/
    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("open filename:%d failed\n", filename);
        return -1;
    }

    while (1) {
        err = read(fd, &inputevt, sizeof(inputevt));
        if (err <= 0) {
            printf("read inputevt failed\n");
            continue;
        }

        switch (inputevt.type)
        {
        case EV_KEY:
            printf("key %d %s\n", inputevt.code, 
                        inputevt.value ? "down" : "up");
            break;
        case EV_REL:
            break;
        case EV_ABS:
            break;
        case EV_MSC:
            break;
        case EV_SW:
            break;
        default:
            break;
        }
    }

    /*关闭文件*/
    close(fd);

    return 0;
}

2.4 运行测试

        开发板上电,将input_key.ko 和 input_key_app 这两个文件拷贝到 /lib/modules/5.19.0-g794a2f7be62d-dirty/ 目录中,在加载input_key.ko之前,先来看一下/dev/input 目录下都有哪些文件,结果如下所示:

/ # ls -al /dev/input/
total 0
drwxr-xr-x    2 0        0               80 Jan 19 19:30 .
drwxr-xr-x    6 0        0             2880 Jan 19 19:25 ..
crw-rw----    1 0        0          13,  64 Jan  1 00:00 event0

        从上面可以看出,当前/dev/input 目录只有 event0 这一个文件。接下来输入如下命令加载 input_key.ko 这个驱动模块。

/ # insmod /lib/modules/5.19.0-g794a2f7be62d-dirty/input_key.ko 
[  310.665956] input_key: loading out-of-tree module taints kernel.
[  310.678432] input: inputkey as /devices/virtual/input/input2

        当驱动模块加载成功以后再来看一下/dev/input 目录下有哪些文件,结果如下所示:

/ # ls -al /dev/input/
total 0
drwxr-xr-x    2 0        0               80 Jan 19 19:30 .
drwxr-xr-x    6 0        0             2880 Jan 19 19:25 ..
crw-rw----    1 0        0          13,  64 Jan  1 00:00 event0
crw-------    1 0        0          13,  65 Jan 19 19:30 event1

        从上面可以看出,多了一个 event1 文件,因此/dev/input/event1 就是注册的驱动所对应的设备文件。 input_key_app 就是通过读取/dev/input/event1 这个文件来获取输入事件信息的,输入如下测试命令:

./input_key_app /dev/input/event1

        然后按下开发板上的 KEY 按键,结果如下所示:

/home/app # ./input_key_app /dev/input/event1
key 11 down
key 11 up
key 11 down
key 11 up
key 11 down
key 11 up
key 11 down
key 11 up

        从上面可以看出,当我们按下或者释放开发板上的按键以后都会在终端上输出相应的内容,提示我们哪个按键按下或释放了,在 Linux 内核中 KEY_0 为 11。

        另外,我们也可以不用 input_key_app 来测试驱动,可以直接使用 hexdump 命令来查看/dev/input/event1 文件内容,输入如下命令:

hexdump /dev/input/event1

        然后按下按键,终端输出如下所示信息:

/ # hexdump /dev/input/event1
0000000 079e 0019 8806 0004 0001 000b 0001 0000
0000010 079e 0019 8806 0004 0000 0000 0000 0000
0000020 079e 0019 6be6 0007 0001 000b 0000 0000
0000030 079e 0019 6be6 0007 0000 0000 0000 0000
0000040 079f 0019 2861 0003 0001 000b 0001 0000
0000050 079f 0019 2861 0003 0000 0000 0000 0000
0000060 079f 0019 d5a2 0004 0001 000b 0000 0000
0000070 079f 0019 d5a2 0004 0000 0000 0000 0000
0000080 07a0 0019 69a9 0000 0001 000b 0001 0000
0000090 07a0 0019 69a9 0000 0000 0000 0000 0000
00000a0 07a0 0019 14f8 0002 0001 000b 0000 0000
00000b0 07a0 0019 14f8 0002 0000 0000 0000 0000

        上面就是 input_event 类型的原始事件数据值,采用十六进制表示,这些原始数据的含义如下:

/*****************input_event 类型********************/
/* 编号 */    /* tv_sec */    /* tv_usec */    /* type */    /* code */    /* value */

0000000       079e 0019       8806 0004        0001         000b           0001 0000
0000010       079e 0019       8806 0004        0000         0000           0000 0000
0000020       079e 0019       6be6 0007        0001         000b           0000 0000
0000030       079e 0019       6be6 0007        0000         0000           0000 0000

        type 为事件类型,查看示例代码 58.1.2.3 可知, EV_KEY 事件值为 1, EV_SYN 事件值为0。因此第 1 行表示 EV_KEY 事件,第 2 行表示 EV_SYN 事件。

        code 为事件编码,也就是按键号,查看示例代码 58.1.2.4 可以, KEY_0 这个按键编号为 11,对应的十六进制为 0xb,因此第1 行表示 KEY_0 这个按键事件,最后的 value 就是按键值,为 1 表示按下,为 0 的话表示松开。

综上所述,"hexdump /dev/input/event1" 中的原始事件值含义如下:

  • 第 1 行,按键(KEY_0)按下事件。

  • 第 2 行, EV_SYN 同步事件,因为每次上报按键事件以后都要同步的上报一个 EV_SYN 事件。

  • 第 3 行,按键(KEY_0)松开事件。

  • 第 4 行, EV_SYN 同步事件,和第 2 行一样。

三、Linux内核自带按键input设备驱动

3.1 自带按键驱动程序源码简析

        Linux 内核也自带了 KEY 驱动,如果要使用内核自带的 KEY 驱动的话需要配置 Linux 内核,不过 Linux 内核一般默认已经使能了 KEY 驱动,但是我们还是要检查一下。按照如下路径找到相应的配置选项:

-> Device Drivers
    -> Input device support
        -> Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y])
        -> Keyboards (INPUT_KEYBOARD [=y])
                ->GPIO Buttons

选中“GPIO Buttons”选项,将其编译进 Linux 内核中【默认是选中的】,如下图所示:

 

bbab636c0f0b06145393dc015436f532.png

        选中以后就会在.config 文件中出现“CONFIG_KEYBOARD_GPIO=y”这一行, Linux 内核就会根据这一行来将 KEY 驱动文件编译进 Linux 内核。

        Linux 内核自带的 KEY 驱动文件为drivers/input/keyboard/gpio_keys.c, gpio_keys.c 采用了 platform 驱动框架,在 KEY 驱动上使用了 input 子系统实现。在 gpio_keys.c 文件中找到如下所示内容:

static const struct of_device_id gpio_keys_of_match[] = {
    { .compatible = "gpio-keys", },
    { },
};
MODULE_DEVICE_TABLE(of, gpio_keys_of_match);

static int gpio_keys_probe(struct platform_device *pdev)
......
static struct platform_driver gpio_keys_device_driver = {
    .probe      = gpio_keys_probe,
    .shutdown   = gpio_keys_shutdown,
    .driver     = {
        .name   = "gpio-keys",
        .pm = &gpio_keys_pm_ops,
        .of_match_table = gpio_keys_of_match,
        .dev_groups = gpio_keys_groups,
    }
};

static int __init gpio_keys_init(void)
{
    return platform_driver_register(&gpio_keys_device_driver);
}

static void __exit gpio_keys_exit(void)
{
    platform_driver_unregister(&gpio_keys_device_driver);
}

late_initcall(gpio_keys_init);
module_exit(gpio_keys_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Phil Blundell <pb@handhelds.org>");
MODULE_DESCRIPTION("Keyboard driver for GPIOs");
MODULE_ALIAS("platform:gpio-keys");

        从上面的代码可以看出,这就是一个标准的 platform 驱动框架,如果要使用设备树来描述 KEY 设备信息的话,设备节点的 compatible 属性值要设置为“gpio-keys”。当设备和驱动匹配以后 gpio_keys_probe 函数就会执行, gpio_keys_probe 函数内容如下:

static int gpio_keys_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);
    struct fwnode_handle *child = NULL;
    struct gpio_keys_drvdata *ddata;
    struct input_dev *input;
    int i, error;
    int wakeup = 0;

    if (!pdata) {
        pdata = gpio_keys_get_devtree_pdata(dev);
        if (IS_ERR(pdata))
            return PTR_ERR(pdata);
    }
    ......
    input = devm_input_allocate_device(dev);
    if (!input) {
        dev_err(dev, "failed to allocate input device\n");
        return -ENOMEM;
    }

    ddata->pdata = pdata;
    ddata->input = input;
    mutex_init(&ddata->disable_lock);

    platform_set_drvdata(pdev, ddata);
    input_set_drvdata(input, ddata);

    input->name = pdata->name ? : pdev->name;
    input->phys = "gpio-keys/input0";
    input->dev.parent = dev;
    input->open = gpio_keys_open;
    input->close = gpio_keys_close;

    input->id.bustype = BUS_HOST;
    input->id.vendor = 0x0001;
    input->id.product = 0x0001;
    input->id.version = 0x0100;

    input->keycode = ddata->keymap;
    input->keycodesize = sizeof(ddata->keymap[0]);
    input->keycodemax = pdata->nbuttons;

    /* Enable auto repeat feature of Linux input subsystem */
    if (pdata->rep)
        __set_bit(EV_REP, input->evbit);

    for (i = 0; i < pdata->nbuttons; i++) {
        const struct gpio_keys_button *button = &pdata->buttons[i];

        if (!dev_get_platdata(dev)) {
            child = device_get_next_child_node(dev, child);
            if (!child) {
                dev_err(dev,
                    "missing child device node for entry %d\n",
                    i);
                return -EINVAL;
            }
        }

        error = gpio_keys_setup_key(pdev, input, ddata,
                        button, i, child);
        if (error) {
            fwnode_handle_put(child);
            return error;
        }

        if (button->wakeup)
            wakeup = 1;
    }

    fwnode_handle_put(child);

    error = input_register_device(input);
    if (error) {
        dev_err(dev, "Unable to register input device, error: %d\n",
            error);
        return error;
    }

    device_init_wakeup(dev, wakeup);

    return 0;
}

大致可以总结如下:

  • 调用 gpio_keys_get_devtree_pdata 函数从设备树中获取到 KEY 相关的设备节点信息。

  • 调用 devm_input_allocate_device 函数申请 input_dev。

  • 初始化 input_dev。

  • 设置 input_dev 事件,这里设置了 EV_REP 事件。

  • 调用 gpio_keys_setup_key 函数继续设置 KEY,此函数会设置 input_dev 的EV_KEY 事件已经事件码(也就是 KEY 模拟为哪个按键)。

  • 调用 input_register_device 函数向 Linux 系统注册 input_dev。

接下来再来看一下 gpio_keys_setup_key 函数,此函数内容如下:

static int gpio_keys_setup_key(struct platform_device *pdev,
                struct input_dev *input,
                struct gpio_keys_drvdata *ddata,
                const struct gpio_keys_button *button,
                int idx,
                struct fwnode_handle *child)
{
    const char *desc = button->desc ? button->desc : "gpio_keys";
    struct device *dev = &pdev->dev;
    struct gpio_button_data *bdata = &ddata->data[idx];
    irq_handler_t isr;
    unsigned long irqflags;
    int irq;
    int error;

    bdata->input = input;
    bdata->button = button;
    spin_lock_init(&bdata->lock);

    if (child) {
        bdata->gpiod = devm_fwnode_gpiod_get(dev, child,
                             NULL, GPIOD_IN, desc);
        if (IS_ERR(bdata->gpiod)) {
            error = PTR_ERR(bdata->gpiod);
            if (error == -ENOENT) {
                /*
                 * GPIO is optional, we may be dealing with
                 * purely interrupt-driven setup.
                 */
                bdata->gpiod = NULL;
            } else {
                if (error != -EPROBE_DEFER)
                    dev_err(dev, "failed to get gpio: %d\n",
                        error);
                return error;
            }
        }
    } else if (gpio_is_valid(button->gpio)) {
        /*
         * Legacy GPIO number, so request the GPIO here and
         * convert it to descriptor.
         */
        unsigned flags = GPIOF_IN;

        if (button->active_low)
            flags |= GPIOF_ACTIVE_LOW;

        error = devm_gpio_request_one(dev, button->gpio, flags, desc);
        if (error < 0) {
            dev_err(dev, "Failed to request GPIO %d, error %d\n",
                button->gpio, error);
            return error;
        }

        bdata->gpiod = gpio_to_desc(button->gpio);
        if (!bdata->gpiod)
            return -EINVAL;
        .......
        INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);

        hrtimer_init(&bdata->debounce_timer,
                 CLOCK_REALTIME, HRTIMER_MODE_REL);
        bdata->debounce_timer.function = gpio_keys_debounce_timer;

        isr = gpio_keys_gpio_isr;
        irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;
        ......
        bdata->irq = button->irq;

        if (button->type && button->type != EV_KEY) {
            dev_err(dev, "Only EV_KEY allowed for IRQ buttons.\n");
            return -EINVAL;
        }

        bdata->release_delay = button->debounce_interval;
        hrtimer_init(&bdata->release_timer,
                 CLOCK_REALTIME, HRTIMER_MODE_REL_HARD);
        bdata->release_timer.function = gpio_keys_irq_timer;

        isr = gpio_keys_irq_isr;
        irqflags = 0;

        /*
         * For IRQ buttons, there is no interrupt for release.
         * So we don't need to reconfigure the trigger type for wakeup.
         */
    }

    bdata->code = &ddata->keymap[idx];
    *bdata->code = button->code;
    input_set_capability(input, button->type ?: EV_KEY, *bdata->code);
    ......
    return 0;
}

        调用 input_set_capability 函数设置 EV_KEY 事件以及 KEY 的按键类型,也就是 KEY 作为哪个按键?我们会在设备树里面设置指定的 KEY 作为哪个按键。

        一切都准备就绪以后剩下的就是等待按键按下,然后向 Linux 内核上报事件,事件上报是在 gpio_keys_irq_isr 函数中完成的,此函数内容如下:

static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)
{
    struct gpio_button_data *bdata = dev_id;
    struct input_dev *input = bdata->input;
    unsigned long flags;

    BUG_ON(irq != bdata->irq);

    spin_lock_irqsave(&bdata->lock, flags);

    if (!bdata->key_pressed) {
        if (bdata->button->wakeup)
            pm_wakeup_event(bdata->input->dev.parent, 0);

        input_event(input, EV_KEY, *bdata->code, 1);
        input_sync(input);

        if (!bdata->release_delay) {
            input_event(input, EV_KEY, *bdata->code, 0);
            input_sync(input);
            goto out;
        }

        bdata->key_pressed = true;
    }

    if (bdata->release_delay)
        hrtimer_start(&bdata->release_timer,
                  ms_to_ktime(bdata->release_delay),
                  HRTIMER_MODE_REL_HARD);
out:
    spin_unlock_irqrestore(&bdata->lock, flags);
    return IRQ_HANDLED;
}

        gpio_keys_irq_isr 是按键中断处理函数,调用 "input_event" 向 Linux 系统上报 EV_KEY 事件,表示按键按下。调用 "input_sync" 使用 input_sync 函数向系统上报 EV_REP 同步事件。

        综上所述, Linux 内核自带的 gpio_keys.c 驱动文件思路和前面编写的 input_key.c 驱动文件基本一致。都是申请和初始化 input_dev,设置事件,向 Linux 内核注册 input_dev。最终在按键中断服务函数或者消抖定时器中断服务函数中上报事件和按键值。

3.2 自带按键驱动程序的使用

        要使用 Linux 内核自带的按键驱动程序很简单,只需要根据 Documentation/devicetree/bindings/input/gpio-keys.yaml 这个文件在设备树中添加指定的设备节点即可,节点要求如下:

  • ①、节点名字为“gpio-keys”。

  • ②、 gpio-keys 节点的 compatible 属性值一定要设置为“gpio-keys”。

  • ③、所有的 KEY 都是 gpio-keys 的子节点,每个子节点可以用如下属性描述:
    • gpios: KEY 所连接的 GPIO 信息

    • interrupts: KEY 所使用 GPIO 中断信息,不是必须的,可以不写

    • label: KEY 名字

    • linux,code: KEY 要模拟的按键。

  • ④、如果按键要支持连按的话要加入 autorepeat。

        打开 imx6ul-14x14-evk.dtsi ,根据上面的要求创建对应的设备节点,设备节点内容如下所示:

gpio-keys {
    compatible = "gpio-keys";
    #address-cells = <1>;
    #size-cells = <0>;
    autorepeat;
    
    key0 {
        labal = "GPIO Key Enter";
        linux,code = <KEY_ENTER>;
        gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
    };
};
  • autorepeat 表示按键支持连按。

  • ALPHA 开发板 KEY 按键信息,名字设置为“GPIO Key Enter”,这里我们将开发板上的 KEY 按键设置为“EKY_ENTER”这个按键键值为28,也就是回车键,效果和键盘上的回车键一样。

  • 设置 KEY 所使用的 IO 为 GPIO1_IO18,一定要检查一下设备树看看此 GPIO 有没有被用到其他外设上,如果有的话要删除掉相关代码!

3.3 运行测试

        重新编译设备树,然后用新编译出来的 设备树dtb 启动 Linux 系统,系统启动以后查看/dev/input 目录,看看都有哪些文件,结果如下所示:

/ # ls -al /dev/input/
total 0
drwxr-xr-x    2 0        0               80 Jan 20 00:34 .
drwxr-xr-x    6 0        0             2880 Jan 20 00:34 ..
crw-rw----    1 0        0          13,  64 Jan  1 00:00 event0
crw-rw----    1 0        0          13,  65 Jan 20 00:34 event1

从上面可以看出存在 event1 这个文件,这个文件就是 KEY 对应的设备文件,使用hexdump 命令来查看/dev/input/event1 文件,输入如下命令:

hexdump /dev/input/event1

然后按下 ALPHA 开发板上的按键,终端输出如下所示内容:

/ # hexdump /dev/input/event1
0000000 157f 0019 80d4 000d 0001 001c 0001 0000
0000010 157f 0019 80d4 000d 0000 0000 0000 0000
0000020 1580 0019 001c 0001 0001 001c 0000 0000
0000030 1580 0019 001c 0001 0000 0000 0000 0000
0000040 1580 0019 f8a0 000b 0001 001c 0001 0000
0000050 1580 0019 f8a0 000b 0000 0000 0000 0000
0000060 1580 0019 0aef 000f 0001 001c 0000 0000
0000070 1580 0019 0aef 000f 0000 0000 0000 0000
0000080 1581 0019 7e19 0009 0001 001c 0001 0000
0000090 1581 0019 7e19 0009 0000 0000 0000 0000
00000a0 1581 0019 30e4 000c 0001 001c 0000 0000
00000b0 1581 0019 30e4 000c 0000 0000 0000 0000

        按下 KEY 按键以后会在终端上输出如上所示的信息,表示 Linux 内核的按键驱动工作正常。至于上述中内容的含义就参照前面2.4节中的介绍,进行分析。


         关于更多嵌入式C语言、FreeRTOS、RT-Thread、Linux应用编程、linux驱动等相关知识,关注公众号【嵌入式Linux知识共享】,后续精彩内容及时收看了解。