23 Linux INPUT 子系统

发布时间 2023-09-12 14:40:22作者: 烟儿公主

一、INPUT 子系统

1. INPUT 子系统简介

  其实 input 子系统就是用来管理输入设备的子系统,它类似于 pinctrl 和 gpio 子系统等,都是 Linux 内核针对某一类设备而创建的框架。input 子系统分为 input 驱动、input 核心层、input 事件处理层,最终给用户空间提供可访问的设备节点:

  在这张图中只需要关注驱动层、核心层和事件层。

  驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。

  核心层:承上启下,为驱动层提供输入设备注册和操作接口,通知事件层对输入事件进行处理。

  事件层:主要和用户空间进行交互。

 

2. INPUT 驱动编写流程

  input 子系统和 misc 驱动类似,都定有一个主设备号,input 子系统的主设备号为 13,并且在用 input 子系统输入设备的时候就不需要去注册字符设备,只需要向系统注册一个 input_device 即可。

 

① 注册 input_device

  这里先了解 input_dev 结构体,此结构体在 include/linux/input.h 文件中:

131 struct input_dev {
    132 const char *name;
    133 const char *phys;
    134 const char *uniq;
    135 struct input_id id;

    137 unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];

    139 unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /* 事件类型的位图 */
    140 unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /* 按键值的位图 */
    141 unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /* 相对坐标的位图*/
    142 unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /* 绝对坐标的位图 */
    143 unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; /* 杂项事件的位图 */
    144 unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /*LED 相关的位图 */
    145 unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; /* sound 有关的位*/
    146 unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; /* 压力反馈的位图 */
    147 unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; /*开关状态的位图 */

......

  evbit、keybit、relbit 等等都是存放不同事件对用的值。

  这里面 evbit 表示输入事件的类型,当使用 input 子系统的时候就要注册一项:

#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /* 按键事件 */
#define EV_REL 0x02 /* 相对坐标事件 */
#define EV_ABS 0x03 /* 绝对坐标事件 */
#define EV_MSC 0x04 /* 杂项(其他)事件 */
#define EV_SW 0x05 /* 开关事件 */
#define EV_LED 0x11 /* LED */
#define EV_SND 0x12 /* sound(声音) */
#define EV_REP 0x14 /* 重复事件 */
#define EV_FF 0x15 /* 压力事件 */
#define EV_PWR 0x16 /* 电源事件 */
#define EV_FF_STATUS 0x17 /* 压力状态事件 */

   比如这次使用按键,那么就要注册 EV_KEY,如果要使用连续按键功能选择 EV_REP

   因为要使用按键,所以需要用到 keybit,keybit 就是按键事件使用的位图。在 Linux 内核中定义了许多按键值,按键值如下:

#define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
#define KEY_3 4
#define KEY_4 5
#define KEY_5 6
#define KEY_6 7
#define KEY_7 8
#define KEY_8 9
#define KEY_9 10
#define KEY_0 11

  我们可以将开发板上的按键值设备为上列按键值的任意一个。

 

  编写 input 设备驱动的时候需要先申请一个 input_dev 结构体变量,使用 input_allocate_device 函数来申请一个 input_dev,需要注销则用 input_free_device

/*
 * @description : 申请 input_dev 结构体变量
 * @param: 无
 * @return : 申请到的 input_dev
 */
struct input_dev *input_allocate_device(void);

/*************** 分割线 ***************/

/*
 * @description : 释放 input_dev 结构体变量
 * @param - dev : 需要释放的 input_dev
 * @return : 无
 */
void input_free_device(struct input_dev *dev);

   申请完成后需要初始化 input_dev,这里需要初始化的内容为事件类型(evbit)和事件值(keybit)两种,如果其他的事件值,keybit 需要更改。使用 input_register_device 来向 Linux 内核注册 input_dev,用 input_unregister_device 来注销注册的 input_dev

/*
 * @description : 注册 input_dev
 * @param - dev : 要注册的 input_dev
 * @return : 0, input_dev 注册成功;负值, input_dev 注册失败
 */
int input_register_device(struct input_dev *dev);

/*************** 分割线 ***************/

/*
 * @description : 注销 input_dev
 * @param - dev : 要注销的 input_dev
 * @return : 无
 */
void input_unregister_device(struct input_dev *dev);

   input_dev 注册过程如下:

  1. 使用 input_allocate_device 函数来申请一个 input_dev;

  2. 初始化 input_dev 事件类型以及事件值;

  3. 使用 input_register_device 函数向 Linux 系统注册前面初始化好的 input_dev;

  4. 最后卸载 input 驱动的时候,先 input_unregister_device 注销 input_dev,然后再用 input_free_device 释放 input_dev。

  下面是 input_dev 注册代码流程:

struct input_dev *inputdev; /* input 结构体变量 */

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    ......
    inputdev = input_allocate_device(); /* 申请 input_dev */
    inputdev->name = "test_inputdev"; /* 设置 input_dev 名字 */

    /*********第一种设置事件和事件值的方法***********/
    __set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
    __set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
    __set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
    /************************************************/

    /*********第二种设置事件和事件值的方法***********/
    keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
    keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
    /************************************************/

    /*********第三种设置事件和事件值的方法***********/
    keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
    input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
    /************************************************/

    /* 注册 input_dev */
    input_register_device(inputdev);
    ......
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    input_unregister_device(inputdev); /* 注销 input_dev */
    input_free_device(inputdev); /* 删除 input_dev */
}

 

② 上报输入事件

  我们需要获取具体的输入值或者输入事件来上报给 Linux 内核。比如按键,我们需要在按键中断处理函数中,或者消抖定时器中断函数中将按键值上报给 Linux 内核。

  input_envent 结构体十分重要,因为所有输入设备都是按照 input_envent 结构体来呈现给用户,用户可以通过 input_event 来获取具体的输入事件或相关的值。上报函数 input_event

/*
 * @description : 上报指定的事件以及对应的值
 * @param - dev : 需要上报的input_dev
 * @param - code : 事件码,就是我们需要注册的按键值,比如KEY_0、KEY_1
 * @param - value : 事件值,比如1按键按下,0按键松开
 * @return : 设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR
 */
void input_event(struct input_dev *dev,
                 unsigned int type,
                 unsigned int code,
                 int value);

  input_event 是可以上报所有的事件类型和事件值,Linux 内核提供了其他的针对具体事件的上报函数,例如上报按键使用的 input_report_key 函数:

static inline void input_report_key(struct input_dev *dev,unsigned int code, int value)
{
    input_event(dev, EV_KEY, code, !!value);
}

   从上面代码可以看到,input_report_key 本质还是 input_evnet

   当上报事件完成后需要使用 input_sync 函数来告诉 Linux 内核 input 子系统上报结束,本质上 input_sync 也是一种上报同步事件:

/*
 * @description : 通知Linux内核input上报结束
 * @param - dev : 需要上报同步事件的 input_dev
 * @return : 无
 */
void input_sync(struct input_dev *dev);

  按键上报事件参考代码如下:

/* 用于按键消抖的定时器服务函数 */
void timer_function(unsigned long arg)
{
    unsigned char value;

    value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */

    if(value == 0){ /* 按下按键 */
        /* 上报按键值 */
        input_report_key(inputdev, KEY_0, 1); /* 最后一个参数 1, 按下 */
        input_sync(inputdev); /* 同步事件 */
    } else { /* 按键松开 */
        input_report_key(inputdev, KEY_0, 0); /* 最后一个参数 0, 松开 */
        input_sync(inputdev); /* 同步事件 */
    }
}

 

二、程序编写

1. 修改设备树

  首先先添加 pinctrl 节点,打开 /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts/stm32mp15-pinctrl.dtsi,输入图中代码:

  这里分开初始化 WK_UP 是因为 KEY0 和 KEY1 都是低电平有效,WK_UP 是高电平有效。

  之后创建按键设备节点,打开 /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts/stm32mp157d-atk.dts,输入图中代码:

  编译设备树,将编译出来的设备树文件复制到 tftpboot 文件夹下。

 

2. 驱动编写

  在 linux/atk-mpl/Drivers 文件夹下创建 20_input,并在里面创建 keyinput.c 文件并输入一下代码:

#include <linux/module.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>

#define KEYINPUT_NAME		"keyinput"	/* 名字 */

/* key设备结构体 */
struct key_dev{
	struct input_dev *idev;  /* 按键对应的input_dev指针 */
	struct timer_list timer; /* 消抖定时器 */
	int gpio_key;			 /* 按键对应的GPIO编号 */
	int irq_key;			 /* 按键对应的中断号 */
};

static struct key_dev key;          /* 按键设备 */

/*
 * @description		: 按键中断服务函数
 * @param – irq		: 触发该中断事件对应的中断号
 * @param – arg		: arg参数可以在申请中断的时候进行配置
 * @return			: 中断执行结果
 */
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
	if(key.irq_key != irq)
		return IRQ_NONE;
	
	/* 按键防抖处理,开启定时器延时15ms */
	disable_irq_nosync(irq); /* 禁止按键中断 */     // 这里失能按键是因为避免按键的抖动导致多次触发中断,我们把使能中断放在了定时器里
	mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));
	
    return IRQ_HANDLED;
}

/*
 * @description			: 按键初始化函数
 * @param – nd			: device_node设备指针
 * @return				: 成功返回0,失败返回负数
 */
static int key_gpio_init(struct device_node *nd)
{
	int ret;
    unsigned long irq_flags;
	
    /* GPIO子系统设置 */
	/* 从设备树中获取GPIO */
	key.gpio_key = of_get_named_gpio(nd, "key-gpio", 0);
	if(!gpio_is_valid(key.gpio_key)) {
		printk("key:Failed to get key-gpio\n");
		return -EINVAL;
	}
	
	/* 申请使用GPIO */
	ret = gpio_request(key.gpio_key, "KEY0");
    if (ret) {
        printk(KERN_ERR "key: Failed to request key-gpio\n");
        return ret;
	}	
	
	/* 将GPIO设置为输入模式 */
    gpio_direction_input(key.gpio_key);
	
    /* 中断设置 */
	/* 获取GPIO对应的中断号 */
	key.irq_key = irq_of_parse_and_map(nd, 0);
	if(!key.irq_key){
        return -EINVAL;
    }

    /* 获取设备树中指定的中断触发类型 */
	irq_flags = irq_get_trigger_type(key.irq_key);
	if (IRQF_TRIGGER_NONE == irq_flags)
		irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
		
	/* 申请中断 */
	ret = request_irq(key.irq_key, key_interrupt, irq_flags, "Key0_IRQ", NULL);
	if (ret) {
        gpio_free(key.gpio_key);
        return ret;
    }

	return 0;
}

/*
 * @description		: 定时器服务函数,用于按键消抖,定时时间到了以后
 * 					  再读取按键值,根据按键的状态上报相应的事件
 * @param – arg		: arg参数就是定时器的结构体
 * @return			: 无
 */
static void key_timer_function(struct timer_list *arg)
{
	int val;
	
	/* 读取按键值并上报按键事件 */
	val = gpio_get_value(key.gpio_key);
	input_report_key(key.idev, KEY_0, !val);    // 按键上报函数
    /* 因为按键按下的情况下,获取到的 val 为 0,按键松开的情况下,获取到的 val 为 1。但是 input
       子系统框架规定按键按下上报 1,按键松开上报 0,所以这里需要进行取反操作。 */
	input_sync(key.idev);       // 同步事件
	
	enable_irq(key.irq_key);    // 启动中断
}

/*
 * @description			: platform驱动的probe函数,当驱动与设备
 * 						 匹配成功以后此函数会被执行
 * @param – pdev			: platform设备指针
 * @return				: 0,成功;其他负值,失败
 */
static int atk_key_probe(struct platform_device *pdev)
{
	int ret;
	
	/* 初始化GPIO */
	ret = key_gpio_init(pdev->dev.of_node);
	if(ret < 0)
		return ret;
		
	/* 初始化定时器 */
	timer_setup(&key.timer, key_timer_function, 0);
	
	/* 申请input_dev */
	key.idev = input_allocate_device();
	key.idev->name = KEYINPUT_NAME;
	
#if 0
	/* 初始化input_dev,设置产生哪些事件 */
	__set_bit(EV_KEY, key.idev->evbit);	/* 设置产生按键事件 */
	__set_bit(EV_REP, key.idev->evbit);	/* 重复事件,比如按下去不放开,就会一直输出信息 */

	/* 初始化input_dev,设置产生哪些按键 */
	__set_bit(KEY_0, key.idev->keybit);	
#endif

#if 0
	key.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
	key.idev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
#endif

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

	/* 注册输入设备 */
	ret = input_register_device(key.idev);
	if (ret) {
		printk("register input device failed!\r\n");
		goto free_gpio;
	}
	
	return 0;
free_gpio:
	free_irq(key.irq_key,NULL);
	gpio_free(key.gpio_key);
	del_timer_sync(&key.timer);
	return -EIO;
	
}

/*
 * @description			: platform驱动的remove函数,当platform驱动模块
 * 						 卸载时此函数会被执行
 * @param – dev			: platform设备指针
 * @return				: 0,成功;其他负值,失败
 */
static int atk_key_remove(struct platform_device *pdev)
{
	free_irq(key.irq_key,NULL);			/* 释放中断号 */
	gpio_free(key.gpio_key);			/* 释放GPIO */
	del_timer_sync(&key.timer);			/* 删除timer */
	input_unregister_device(key.idev);	/* 释放input_dev */
	
	return 0;
}

// 匹配列表
static const struct of_device_id key_of_match[] = {
	{.compatible = "alientek,key"},
	{/* Sentinel */}
};

// platform驱动结构体
static struct platform_driver atk_key_driver = {
	.driver = {
		.name = "stm32mp1-key",
		.of_match_table = key_of_match,
	},
	.probe	= atk_key_probe,
	.remove = atk_key_remove,
};

module_platform_driver(atk_key_driver);     // 向Linux内核注册platform,这里面包含init和exit,这里有点忘了,在Linux自带的LED出现

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

 

3. 测试 APP 编写

  新建 keyinputApp.c,并输入以下代码:

#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>

/*
 * @description			: main主程序
 * @param – argc			: argv数组元素个数
 * @param – argv			: 具体参数
 * @return				: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
    int fd, ret;
    struct input_event ev;

    if(2 != argc) {
        printf("Usage:\n"
             "\t./keyinputApp /dev/input/eventX    @ Open Key\n"
        );
        return -1;
    }

    /* 打开设备 */
    fd = open(argv[1], O_RDWR);
    if(0 > fd) {
        printf("Error: file %s open failed!\r\n", argv[1]);
        return -1;
    }

    /* 读取按键数据 */
    while(1) {

        ret = read(fd, &ev, sizeof(struct input_event));
        if (ret) {
            switch (ev.type) {
            case EV_KEY:				// 按键事件
                if (KEY_0 == ev.code) {		// 判断是不是KEY_0按键
                    if (ev.value)			// 按键按下
                        printf("Key0 Press\n");
                    else					// 按键松开
                        printf("Key0 Release\n");
                }
                break;

            /* 其他类型的事件,自行处理 */
            case EV_REL:
                break;
            case EV_ABS:
                break;
            case EV_MSC:
                break;
            case EV_SW:
                break;
            };
        }
        else {
            printf("Error: file %s read failed!\r\n", argv[1]);
            goto out;
        }
    }

out:
    /* 关闭设备 */
    close(fd);
    return 0;
}

 

三、运行测试

  首先先编译 keyinput.c 和 keyinputApp.c:

make
arm-none-linux-gnueabihf-gcc keyinputApp.c -o keyinputApp

  将编译好的 keyinputApp 和 keyinput.ko 复制:

sudo cp keyinputApp keyinput.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f

  开启开发板,输入以下命令:

cd lib/modules/5.4.31/
depmod
modprobe keyinput.ko

  在 /dev/input 子目录下可以看到:

  这个就是我们注册的驱动所对应的设备文件。keyinputApp 通过读取 /dev/input/event0 文件来获取事件信息的。测试命令如下:

./keyinputApp /dev/input/event0

  卸载驱动:

rmmod keyinput.ko

 

四、Linux 自带按键驱动程序使用

  进入 linux/atk-mpl/linux/my_linux/linux-5.4.31 下,输入命令:make menuconfig。找到相应的配置选项:

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

  Linux 自带的 gpio_keys.c 驱动文件与我们编的基本一致,都是申请和初始化 input_dev、设置时间、向 Linux 内核注册 input_dev,最后在按键中断服务函数或消抖定时器中断服务函数中上报事件和按键值。

  在  Documentation/devicetree/bindings/input/gpio-keys.txt 中可以看到注意事项。

  如果要使用自带的按键驱动程序,需要修改设备树,并且注意以下几点:

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

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

  ③ 所有的 KEY 都是 gpio-keys 的子节点,每个子节点可以用如下属性描述自己: 

  gpiosKEY 所连接的 GPIO 信息。

  interruptsKEY 所使用 GPIO 中断信息,不是必须的,可以不写。

  labelKEY 名字。

  linux,codeKEY 要模拟的按键 。

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

 

  首先打开 stm32mp157d-atk.dts,添加头文件,此文件按是linux,code”属性的按键宏定义:

  头文件添加完成后创建设备节点:

gpio-keys {
		compatible = "gpio-keys";
		pinctrl-names = "default";
		pinctrl-0 = <&key_pins_a>;
		autorepeat;		//按键支持连按

		key0 {
			label = "GPIO KEY L";
			linux,code = <KEY_L>;
			gpios = <&gpiog 3 GPIO_ACTIVE_LOW>;
		};

		key1 {
			labe = "GPIO KEY S";
			linux,code = <KEY_S>;
			gpios = <&gpioh 7 GPIO_ACTIVE_LOW>;
		};

		wkup{
			label = "GPIO KEY Enter";
			linux,code = <KEY_ENTER>;
			gpios = <&gpioa 0 GPIO_ACTIVE_HIGH>;
			gpio-key,wakeup;
		};
	};

  重新编译设备树,并复制到 tftproot。

  开启开发板,输入以下命令:

hexdump /dev/input/event0        # hexdump用于以十六进制格式显示文件内容的命令

  按下 KEY0 或 KEY1 都会显示下面的情况:

/**************************input_event 类型*****************************/
/* 编号 */ /* tv_sec */ /* tv_usec */ /* type */ /* code */ /* value */
 0000000   46ec 386d      b69f 0008      0001       0026     0001 0000
 0000010   46ec 386d      b69f 0008      0000       0000     0000 0000
 0000020   46ec 386d      7fc6 000a      0001       0026     0000 0000
 0000030   46ec 386d      7fc6 000a      0000       0000     0000 0000

/* 
 第一行:按键(KEY_L)按下事件
 第二行:EV_SYN同步事件,每次上报事件都要上报一个EV_SYN事件
 第三行:按键(KEY_L)松开事件
 第四行:EV_SYN同步事件
 */

/* 
 type:事件类型,EV_KEY按键事件值为1,EV_SYN同步事件值为0,所以这里是按下按键,同步,松开按键,同步。
 code:事件编码,就是按键号,也就是KEY_0、KEY_L等等,Linux自带的KEY_L按键编号为38,所以这里是26,这里是16进制
 value:按键值,1表示按下,0代表松开
 */
#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /* 按键事件 */
#define EV_REL 0x02 /* 相对坐标事件 */
#define EV_ABS 0x03 /* 绝对坐标事件 */
#define EV_MSC 0x04 /* 杂项(其他)事件 */
#define EV_SW 0x05 /* 开关事件 */
#define EV_LED 0x11 /* LED */
#define EV_SND 0x12 /* sound(声音) */
#define EV_REP 0x14 /* 重复事件 */
#define EV_FF 0x15 /* 压力事件 */
#define EV_PWR 0x16 /* 电源事件 */
#define EV_FF_STATUS 0x17 /* 压力状态事件 */

 

总结

  INPUT 子系统总体分为三个部分,驱动层、核心层和事件层。简化驱动开发,让代码更加具有可读性。

  对 input 子系统如何编写代码需要掌握:

  在使用platform的前提下:

  ① 首先使用 input_allocate_device 来申请一个 input_dev;(一般在 xxx_probe 中)

  ② 初始化 input_dev 事件类型及事件值(三种方式初始化);(一般在 xxx_probe 中)

  ③ 使用 input_register_device 来注册 input_dev;(一般在 xxx_probe 中)

  ④ 上报事件,每当按键按下或松开都有一个事件去上报,总上报函数为 input_event,之后再上报同步事件 input_sync;(一般在中断处理函数或定时器处理函数中)

  ⑤ 最后卸载 input,使用函数 input_unregister_device,再用 input_free_device 释放 input_dev。(一般在 xxx_remove 中)