Linux驱动---按键

目录

一、Input子系统

1.1、简介

在前面的LED驱动文章中,我们知道Linux为了方便GPIO操作设计了GPIO子系统。那么对于键盘、鼠标、触摸屏、游戏控制器这一类的输入设备呢?为了给这一类的输入设备提供统一的接口和管理,Linux设计了Input子系统,设计该系统的主要目的就是将输入设备驱动中的共性部分提取出来,形成一个通用的框架,开发者只需关注差异化的部分 。这样,不仅降低了驱动开发的难度,也提高了驱动的通用性和兼容性。

Input子系统为各种输入设备提供了统一的接口,将输入事件转化为统一的事件格式,并通过input接口传递给应用空间程序,应用程序可以通过这些统一的接口来访问和操作输入设备,而不需要关心设备的具体实现。

1.2、Input子系统构成

输入子系统主要由三部分构成:

(1)设备驱动层(struct input_dev):通过获取设备树中硬件的信息,对硬件各寄存器的读写访问和将底层硬件的状态变化转换为标准的输入事件,将相应事件上报。

(2)核心层:用于将设备驱动层和事件处理层进行匹配,由内核完成。

(3)事件处理层(struct input_handler):将核心层生成的输入事件传递给系统的高层应用,并确保这些事件被正确处理。

我们本篇文章属于驱动开发,所以主要整理设备驱动层。

1.3、input_dev结构体

input_dev 是 Linux Input 子系统中用于描述输入设备的核心结构体,它的定义如下:
点击查看代码

struct input_dev {
	const char *name;  // 设备名称,例如 "Keyboard" 或 "Mouse"
	const char *phys;  // 设备在系统中的物理路径,例如 "usb-0000:00:14.0-1/input0"
	const char *uniq;  // 设备的唯一标识符,通常用于匹配特定硬件
	struct input_id id; // 包含设备识别信息的结构体(例如供应商ID、产品ID、版本号)

	// 属性位图,用于表示设备支持的属性类型
	unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];

	// 事件位图,用于表示设备支持的事件类型
	unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
	// 键位图,用于表示设备支持的按键类型
	unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];
	// 相对位图,用于表示设备支持的相对轴事件。 例如鼠标
	unsigned long relbit[BITS_TO_LONGS(REL_CNT)];
	// 绝对位图,用于表示设备支持的绝对轴事件,例如触摸屏
	unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];
	// 杂项位图,用于表示设备支持的其他事件类型
	unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
	// 指示灯位图,用于表示设备支持的LED灯类型
	unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
	// 声音位图,用于表示设备支持的声音类型
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
	// 力反馈位图,用于表示设备支持的力反馈事件
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
	// 开关位图,用于表示设备支持的开关类型
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];

	unsigned int hint_events_per_packet; // 每个数据包中的建议事件数量

	unsigned int keycodemax;   // 最大按键码数量
	unsigned int keycodesize;  // 每个按键码的大小
	void *keycode;             // 指向按键码数据的指针

	// 设置按键码的函数指针
	int (*setkeycode)(struct input_dev *dev,
			  const struct input_keymap_entry *ke,
			  unsigned int *old_keycode);
	// 获取按键码的函数指针
	int (*getkeycode)(struct input_dev *dev,
			  struct input_keymap_entry *ke);

	struct ff_device *ff;  // 力反馈设备的指针

	unsigned int repeat_key;  // 重复按键
	struct timer_list timer;  // 用于处理重复按键的定时器
	
	int rep[REP_CNT];  // 用于存储重复延迟和重复率
	struct input_mt *mt;  // 多点触控相关信息的指针
	struct input_absinfo *absinfo;  // 绝对轴相关信息的指针

	// 当前设备状态的位图(按键、指示灯、声音、开关)
	unsigned long key[BITS_TO_LONGS(KEY_CNT)];
	unsigned long led[BITS_TO_LONGS(LED_CNT)];
	unsigned long snd[BITS_TO_LONGS(SND_CNT)];
	unsigned long sw[BITS_TO_LONGS(SW_CNT)];

	// 打开设备的函数指针
	int (*open)(struct input_dev *dev);
	// 关闭设备的函数指针
	void (*close)(struct input_dev *dev);
	// 刷新设备的函数指针
	int (*flush)(struct input_dev *dev, struct file *file);
	// 处理事件的函数指针
	int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);

	struct input_handle __rcu *grab;  // 用于处理独占设备的指针

	spinlock_t event_lock;  // 用于保护事件处理的自旋锁
	struct mutex mutex;     // 设备访问的互斥锁

	unsigned int users;  // 使用该设备的用户数量
	bool going_away;     // 标志设备是否正在关闭

	struct device dev;  // 设备的基础信息

	struct list_head h_list;  // 处理句柄的链表
	struct list_head node;    // 设备的链表节点

	unsigned int num_vals;  // 当前输入值的数量
	unsigned int max_vals;  // 最大输入值的数量
	struct input_value *vals;  // 输入值数组的指针

	bool devres_managed;  // 标志设备资源是否由设备资源管理器管理
};

其中,evbit为事件类型,常见的有以下几种:

EV_KEY    键盘按键事件
EV_REL    鼠标事件
EV_ABS    触摸屏事件

二、输入设备驱动开发流程

2.1、分配和初始化输入设备

输入设备驱动首先需要分配一个input_de结构体,并设置它的基本属性,如设备名称、事件类型、支持的按键等。

struct input_dev *input_device;

input_device = input_allocate_device();
if (!input_device) {
    pr_err("Failed to allocate input device\n");
    return -ENOMEM;
}

input_device->name = "my_key_device";
input_device->evbit[0] = BIT_MASK(EV_KEY);  // 设置支持按键事件

EV_KEY 是一个枚举值,定义在 <linux/input.h> 中,通常值为 1。BIT_MASK(EV_KEY) 展开后相当于 1 << EV_KEY,即 1 << 1,结果是 0x02。因此,最后这行代码相当于:

input_device->evbit[0] = 0x02;

也可以使用 __set_bit 宏来设置位图,它的用法如下:

__set_bit(EV_KEY, input_device->evbit);

2.2、注册设备

设置好设备的属性后,调用input_register_device()函数来注册输入设备,使其可以开始接收并处理事件。

ret = input_register_device(input_device);
if (ret) {
    pr_err("Failed to register input device\n");
    return ret;
}

2.3、事件上报

输入设备需要在状态发生变化时,通过input_report_key()向Input子系统报告事件。

input_report_key(input_device, KEY_ENTER, 1);  // 报告按下事件
input_sync(input_device);  // 同步事件

该函数将按键按下的事件报告给系统,用户空间应用程序可以通过evdev等接口读取到这些事件。在驱动中,我们往往需要监听GPIO引脚上按键的状态变化,这通常需要通过硬件中断(IRQ)来触发。按键状态的改变将会触发相应的中断处理函数,在中断处理函数中再通过input_report_key()来报告事件。

2.4、释放和注销设备

在驱动退出时,需释放资源,并通过input_unregister_device()注销输入设备。

input_unregister_device(input_device);
input_free_device(input_device);

三、事件同步与事件队列

在 2.3事件上报 时,我们调用了input_sync函数,为什么要进行该操作呢?这就要聊一下事件同步与事件队列了。

  • 事件同步:在报告完事件后,驱动需要调用input_sync来同步事件,确保事件被正确地传递到Input子系统中。
  • 事件队列:输入子系统通过事件队列的方式管理输入事件,驱动程序负责将事件传递到队列中,用户空间程序通过evdev等接口从队列中读取事件。

四、按键消抖

4.1、按键抖动

按键通常是由两个金属点组成,当按键按下或释放时,这些触点会发生接触或断开。由于物理原因,触点在短时间内可能会发生多次闭合和断开,而不是单次稳定地触发,这种现象称为"抖动"。

如果没有进行去抖动处理,一个按键的按下或释放被多次记录,硬件中断可能会频繁触发,增加系统的处理负担。

通常可以通过软件或硬件方法消除按键抖动。硬件去抖动通常是在按键硬件的设计中加以改进,例如使用RC滤波器、专用的去抖动IC或使用晶振稳定信号,这些方法能在硬件层面消除抖动,无需依赖软件。

接下来,将主要为大家介绍两种软件去抖动的方法。

4.2、延时去抖动

此方法的思路是等待按键接触或断开后的一段时间(例如10~50ms),在检测一次按键状态,已确定状态变化是否稳定。此方法实现简单,但是可能导致响应时间较长,不能非常精准的去抖动。

#define DEBOUNCE_DELAY_MS 20  // 延时 20ms

static irqreturn_t gpio_key_irq(int irq, void *arg)
{
    struct keys_desc *key = arg;
    static unsigned long last_irq_time = 0;
    unsigned long now = jiffies;

    // 检查抖动延迟
    if (time_after(now, last_irq_time + msecs_to_jiffies(DEBOUNCE_DELAY_MS))) {
        int value = gpio_get_value(key->gpio);
        if (value == 0) {
            input_report_key(input_device, key->key_code, 1);  // 按下事件
        } else {
            input_report_key(input_device, key->key_code, 0);  // 释放事件
        }
        input_sync(input_device);
        last_irq_time = now;
    }

    return IRQ_HANDLED;
}

4.3、轮询去抖动

这种方法是对按键状态进行多次连续检查,只有在按键状态一致时才认为状态已稳定。通常在硬件中断中进行,读取按键状态并检查是否稳定。这种方法可以更可靠地过滤抖动,适合处理快速的按键状态变化。但是它增加了额外的处理复杂度,需要做更多的状态检测和计数。

#define DEBOUNCE_COUNT 5  // 检查连续的 5 次状态

static irqreturn_t gpio_key_irq(int irq, void *arg)
{
    struct keys_desc *key = arg;
    static int stable_state = -1;
    static int count = 0;
    int value = gpio_get_value(key->gpio);

    if (stable_state == value) {
        count++;
        if (count > DEBOUNCE_COUNT) {
            // 状态稳定,报告事件
            if (value == 0) {
                input_report_key(input_device, key->key_code, 1);  // 按下事件
            } else {
                input_report_key(input_device, key->key_code, 0);  // 释放事件
            }
            input_sync(input_device);
            count = 0;
        }
    } else {
        stable_state = value;
        count = 0;
    }

    return IRQ_HANDLED;
}

五、实现按键驱动

5.1、硬件原理图

下面是按键的原理图,从中我们可以看到该按键连接到了NAND_nCE1这个引脚上,通过设备树的头文件我们可以查到它使用MX6UL_PAD_NAND_CE1_B__GPIO4_IO14这个引脚。如果没有按下按键时,左侧的上拉电阻R25将该GPIO引脚拉成高电平;而一旦按键按下,则该引脚与GND导通变成低电平。由此可见,我们应该将GPIO4_14中断设置成下降沿触发。

5.2、设备树修改

接下来,我们需要修改DTS文件中关于按键的配置,因为BSP默认已经使能了该设备和Linux内核自带的按键驱动,这里只需将compatible修改成我们自己即将编写的按键驱动"my,keys"即可,别的都不需要修改。

    keys {
        compatible = "my,keys";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_gpio_keys>;
        autorepeat;
        status = "okay";

        key_user {
            lable = "key_user";
            gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
            linux,code = <KEY_ENTER>;
        };
    };
... ...
&iomuxc {
    pinctrl-names = "default";
... ...
    pinctrl_gpio_keys: gpio-keys {
        fsl,pins = <
            MX6UL_PAD_NAND_CE1_B__GPIO4_IO14        0x17059 /* gpio key */
        >;
    };
... ...
};

我们的按键使用的是GPIO4_14引脚,并且低电平有效GPIO_ACTIVE_LOW。我们设置该按键的键值linux.code为回车KEY_ENTER,按下该按键即相当于按下了回车。

修改完设备树之后,我们重新编译成.dtb文件,Makefile文件如下:
点击查看代码

ARCH ?= arm
KERNAL_DIR ?= ${HOME}/igkboard-imx6ull/bsp/kernel/linux-imx

CPP_CFLAGS=-Wp,-MD,.x.pre.tmp -nostdinc -undef -D__DTS__ -x assembler-with-cpp
CPP_CFLAGS+= -I ${KERNAL_DIR}/arch/${ARCH}/boot/dts -I ${KERNAL_DIR}/include/

DTC=${KERNAL_DIR}/scripts/dtc/dtc
DTC_FLAGS=-q -@ -I dts -O dtb

DTS_NAME=igkboard-imx6ull

all:
        @cpp ${CPP_CFLAGS} ${DTS_NAME}.dts -o .${DTS_NAME}.dts.tmp
        ${DTC} ${DTC_FLAGS} .${DTS_NAME}.dts.tmp -o ${DTS_NAME}.dtb
        @rm -f .*.tmp

decompile:
        ${DTC} -q -I dtb -O dts ${DTS_NAME}.dtb -o decompile.dts

clean:
        rm -f *.dtb decompile.dts

5.3、编写按键驱动代码

接下来我们通过Linux内核定时器实现按键消抖,编写代码如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/of_device.h>
#include <linux/jiffies.h>
#include <linux/delay.h>

struct keys_desc {
    const char         *lable;     /* Key name */
    unsigned int        key_code;  /* Key code */
    int                 gpio;      /* GPIO number */
    unsigned int        irq;       /* IRQ number */
    struct timer_list   timer;     /* Timer for debounce */
    int                 last_value;/* Last key value */
};

struct key_priv {
    int                 nkeys; /* number of keys */
    struct keys_desc   *keys;  /* keys array */
};

struct input_dev *input_device;
struct key_priv *priv;

/* Timer callback function for debounce */
static void debounce_timer_func(struct timer_list *t)
{
    struct keys_desc *key = from_timer(key, t, timer);
    int value = gpio_get_value(key->gpio);

    if (value != key->last_value) {
        key->last_value = value;

        if (value == 0) {
            input_report_key(input_device, key->key_code, 1);  /* Key press event */
        } else {
            input_report_key(input_device, key->key_code, 0);  /* Key release event */
        }

        input_sync(input_device);
    }
}

/* GPIO IRQ handler */
static irqreturn_t gpio_key_irq(int irq, void *arg)
{
    struct keys_desc *key = arg;

    /* start debounce timer(20ms) to delay event processing */
    mod_timer(&key->timer, jiffies + msecs_to_jiffies(20));

    return IRQ_HANDLED;
}

static int key_probe(struct platform_device *pdev) {
    struct device *dev = &pdev->dev;
    struct device_node *np = pdev->dev.of_node;
    struct device_node *key_node;
    int ret, i=0;

    /* allocate memory for private data structure */
    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* parser the number of keys from the device tree */
    priv->nkeys = device_get_child_node_count(dev);
    if ( priv->nkeys < 1) {
        dev_err(dev, "Failed to read keys gpio from device tree\n");
        return -EINVAL;
    }
    dev_info(dev, "gpio keys driver probe for %d keys from device tree\n", priv->nkeys);

    /* allocate memory for all the keys */
    priv->keys = devm_kzalloc(dev, priv->nkeys*sizeof(*priv->keys), GFP_KERNEL);
    if (!priv->keys )
        return -ENOMEM;

    /* traval all the keys child node */
    for_each_child_of_node(np, key_node) {
        /* read lable information */
        if (of_property_read_string(key_node, "lable", &priv->keys[i].lable)) {
            dev_err(dev, "Failed to read lable from key node\n");
            continue;
        };

        /* read gpio information */
        priv->keys[i].gpio = of_get_named_gpio(key_node, "gpios", 0);
        if( priv->keys[i].gpio < 0 ) {
            dev_err(dev, "Failed to read lable from key node\n");
            continue;
        }

        /* read key code value */
        if (of_property_read_u32(key_node, "linux,code", &priv->keys[i].key_code)) {
            dev_err(dev, "Failed to read linux,code for key %s\n", priv->keys[i].lable);
            continue;
        }

        /* request gpio for this key */
        ret = devm_gpio_request(dev, priv->keys[i].gpio, priv->keys[i].lable);
        if (ret) {
            dev_err(dev, "Failed to request GPIO for key %s\n", priv->keys[i].lable);
            continue;
        }

        /* request interrupt for this key */
        priv->keys[i].irq = gpio_to_irq(priv->keys[i].gpio);
        ret = devm_request_irq(dev, priv->keys[i].irq, gpio_key_irq, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, priv->keys[i].lable, &priv->keys[i]);
        if (ret) {
            dev_err(dev, "Failed to request IRQ for key %s\n", priv->keys[i].lable);
            continue;
        }

        /* initialize debounce timer */
        timer_setup(&priv->keys[i].timer, debounce_timer_func, 0);
        priv->keys[i].last_value = gpio_get_value(priv->keys[i].gpio);

        /* increase to next key */
        i++;
    }
    priv->nkeys = i; /* update valid keys number */

    /* alloc input device */
    input_device = devm_input_allocate_device(dev);
    if (!input_device) {
        dev_err(dev, "failed to allocate input device\n");
        return -ENOMEM;
    }

    /* set input deivce information */
    input_device->name = "mykeys";
    input_device->evbit[0] = BIT_MASK(EV_KEY); /* key event */
    for ( i=0; i<priv->nkeys; i++) {
        set_bit(priv->keys[i].key_code, input_device->keybit);
    }

    /* register input device */
    ret = input_register_device(input_device);
    if (ret) {
        pr_err("Failed to register input device\n");
        return ret;
    }

    return 0;
}

static int key_remove(struct platform_device *pdev)
{
    input_unregister_device(input_device);
    dev_info(&pdev->dev, "gpio keys driver removed.\n");
    return 0;
}

static const struct of_device_id key_of_match[] = {
    { .compatible = "my,keys", },
    { /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, key_of_match);

static struct platform_driver key_driver = {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "keys",
        .of_match_table = key_of_match,
    },
};

module_platform_driver(key_driver);

MODULE_LICENSE("GPL");

key_probe()中,我们为每个按键初始化了一个定时器。当GPIO引脚发生变化时,gpio_key_irq函数会被调用,触发定时器的启动。定时器的延时设为50毫秒,到达时间后,定时器回调函数debounce_timer_func()处理按键消抖并报告按键事件。在key_remove中,我们删除了所有按键的定时器,以确保在驱动移除时不会发生定时器回调。

接下来,进行编译,Makefile文件如下:
点击查看代码

ARCH ?= arm
CROSS_COMPILE ?= /opt/gcc-aarch32-10.3-2021.07/bin/arm-none-linux-gnueabihf-
KERNAL_DIR ?= ~/igkboard-imx6ull/bsp/kernel/linux-imx/

PWD :=$(shell pwd)

obj-m += keys.o

modules:
    $(MAKE) ARCH=${ARCH} CROSS_COMPILE=${CROSS_COMPILE} -C $(KERNAL_DIR) M=$(PWD) modules
    @make clear

clear:
    @rm -f *.o *.cmd *.mod *.mod.c
    @rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
    @rm -f .*ko.cmd .*.o.cmd .*.o.d
    @rm -f *.unsigned

clean:
    @rm -f *.ko

make

5.4、按键驱动测试

首先,在开发板上更新我们的设备树文件。

mount /dev/mmcblk1p1 /media/
通过rz、sz或scp将设备树文件下载至/media目录下
sync && reboot

再将按键驱动文件拷贝到开发板上。并通过insmod安装按键驱动,输入设备的设备文件都在/dev/input路径下。

insmod keys.ko
ls /dev/input/
  by-path  event0  event1

接下来使用 evtest 命令测试我们编写的驱动如下: