I.MX6ULL ARM驱动开发---INPUT子系统

引言

  按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux 内核为此专门做了一个叫做input子系统的框架来处理输入事件。输入设备本质上还是字符设备,只是在此基础上套上了 input 框架,用户只需要负责上报输入事件,比如按键值、坐标等信息,input 核心层负责处理这些事件。我们就来学习一下 Linux 内核中的 input 子系统。

一、input子系统

  input 就是输入的意思,因此 input 子系统就是管理输入的子系统,和 pinctrl、gpio 子系统一样,都是 Linux 内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等,这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,我们只需要按照要求上报这些输入事件即可。为此,input子系统分为 input 驱动层、input 核心层、input 事件处理层,最终给用户空间提供可访问的设备节点。

I.MX6ULL ARM驱动开发---INPUT子系统_第1张图片

  左边就是最底层的具体设备,比如按键、USB 键盘/鼠标等,中间部分属于 Linux 内核空间,分为驱动层、核心层和事件层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出 input 子系统用到了我们前面讲解的驱动分层模型,我们编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:

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

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

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

二、input 驱动编写流程

  input 核心层会先向 Linux 内核注册一个字符设备和一个input 类。input 子系统的所有设备主设备号都为 13,我们在使用 input 子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个 input_device 即可。

1、注册 input_dev

  在使用 input 子系统的时候我们只需要注册一个 input 设备即可,input_dev 结构体表示 input 设备,此结构体定义在 include/linux/input.h 文件中,定义如下(有省略):

struct input_dev {
	const char *name;
	const char *phys;
	const char *uniq;
	struct input_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)]; /* 杂项事件的位图 */
	unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /*LED 相关的位图 */
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];/* sound 有关的位图 */
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; /* 压力反馈的位图 */
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; /*开关状态的位图 */
	......
	bool devres_managed;
};

  evbit 表示输入事件类型,可选的事件类型定义在 include/uapi/linux/input.h 文件中,事件类型如下:

#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 就是按键事件使用的位图,Linux 内核定义了很多按键值,这些按键值定义在 include/uapi/linux/input.h 文件中,按键值如下:

#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
......
#define BTN_TRIGGER_HAPPY39 0x2e6
#define BTN_TRIGGER_HAPPY40 0x2e7

  比如我们会将 I.MX6U-ALPHA 开发板上的 KEY 按键值设置为 KEY_0。

(1)申请input_dev

在编写 input 设备驱动的时候我们需要先申请一个 input_dev 结构体变量,使用 input_allocate_device 函数来申请一个 input_dev,此函数原型如下所示:

struct input_dev *input_allocate_device(void)

函数参数和返回值含义如下:
  参数:无。
  返回值:申请到的 input_dev。

(2)释放input_dev

如果要注销的 input 设备的话需要使用 input_free_device 函数来释放掉前面申请到的input_dev,input_free_device 函数原型如下:

void input_free_device(struct input_dev *dev)

函数参数和返回值含义如下:
  dev:需要释放的 input_dev。
  返回值:无。

(3)注册input_dev

  申请好一个 input_dev 以后就需要初始化这个 input_dev,需要初始化的内容主要为事件类型(evbit)和事件值(keybit)这两种。input_dev 初始化完成以后就需要向 Linux 内核注册 input_dev了,需要用到 input_register_device 函数,此函数原型如下:

int input_register_device(struct input_dev *dev)

函数参数和返回值含义如下:
  dev:要注册的 input_dev 。
  返回值:0,input_dev 注册成功;负值,input_dev 注册失败。

(4)注销input_dev

同样的,注销 input 驱动的时候也需要使用 input_unregister_device 函数来注销掉前面注册的 input_dev,函数原型如下:

void input_unregister_device(struct input_dev *dev)

函数参数和返回值含义如下:
  dev:要注销的 input_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。

2、上报输入事件

(1)input_event 函数

此函数用于上报指定的事件以及对应的值,函数原型如下:

void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)

函数参数和返回值含义如下:
  dev:需要上报的 input_dev。
  type: 上报的事件类型,比如 EV_KEY。
  code:事件码,也就是我们注册的按键值,比如 KEY_0、KEY_1 等等。
  value:事件值,比如 1 表示按键按下,0 表示按键松开。
  返回值:无。

(2)input_sync 函数

当我们上报事件以后还需要使用 input_sync 函数来告诉 Linux 内核 input 子系统上报结束,input_sync 函数本质是上报一个同步事件,此函数原型如下所示:

void input_sync(struct input_dev *dev)

函数参数和返回值含义如下:
  dev:需要上报同步事件的 input_dev。
  返回值:无。

3、input_event 结构体

Linux 内核使用 input_event 这个结构体来表示所有的输入事件,input_envent 结构体定义在 include/uapi/linux/input.h 文件中,结构体内容如下:

struct input_event {
	struct timeval time;
	__u16 type;
	__u16 code;
	__s32 value;
};

time:时间,也就是此事件发生的时间,为 timeval 结构体类型,timeval 结构体定义如下:

typedef long __kernel_long_t;
typedef __kernel_long_t __kernel_time_t;
typedef __kernel_long_t __kernel_suseconds_t;
struct timeval { 
	__kernel_time_t tv_sec; /* 秒 */
	__kernel_suseconds_t tv_usec; /* 微秒 */
};

tv_sec 和 tv_usec 这两个成员变量都为 long 类型,也就是32位。

type:事件类型,比如 EV_KEY,表示此次事件为按键事件,此成员变量为 16 位。

code:事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,如:KEY_0、KEY_1等等这些按键。此成员变量为 16 位。

value:值,比如 EV_KEY 事件中 value 就是按键值,表示按键有没有被按下,如果为 1 的话说明按键按下,如果为 0 的话说明按键没有被按下或者按键松开了。

三、程序编写

1、驱动程序编写

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define KEYINPUT_CNT		1			/* 设备号个数 	*/
#define KEYINPUT_NAME		"keyinput"	/* 名字 		*/
#define KEY0VALUE			0X01		/* KEY0按键值 	*/
#define INVAKEY				0XFF		/* 无效的按键值 */
#define KEY_NUM				1			/* 按键数量 	*/

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

/* keyinput设备结构体 */
struct keyinput_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	struct device_node	*nd; /* 设备节点 */
	struct timer_list timer;/* 定义一个定时器*/
	struct irq_keydesc irqkeydesc[KEY_NUM];	/* 按键描述数组 */
	unsigned char curkeynum;				/* 当前的按键号 */
	struct input_dev *inputdev;		/* input结构体 */
};

struct keyinput_dev keyinputdev;	/* key input设备 */

/* @description		: 中断服务函数,开启定时器,延时10ms,
 *				  	  定时器用于按键消抖。
 * @param - irq 	: 中断号 
 * @param - dev_id	: 设备结构。
 * @return 			: 中断执行结果
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct keyinput_dev *dev = (struct keyinput_dev *)dev_id;

	dev->curkeynum = 0;
	dev->timer.data = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
	return IRQ_RETVAL(IRQ_HANDLED);
}

/* @description	: 定时器服务函数,用于按键消抖,定时器到了以后
 *				  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
 * @param - arg	: 设备结构变量
 * @return 		: 无
 */
void timer_function(unsigned long arg)
{
	unsigned char value;
	unsigned char num;
	struct irq_keydesc *keydesc;
	struct keyinput_dev *dev = (struct keyinput_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];
	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
	if(value == 0){ 						/* 按下按键 */
		/* 上报按键值 */
		//input_event(dev->inputdev, EV_KEY, keydesc->value, 1);
		input_report_key(dev->inputdev, keydesc->value, 1);/* 最后一个参数表示按下还是松开,1为按下,0为松开 */
		input_sync(dev->inputdev);
	} else { 									/* 按键松开 */
		//input_event(dev->inputdev, EV_KEY, keydesc->value, 0);
		input_report_key(dev->inputdev, keydesc->value, 0);
		input_sync(dev->inputdev);
	}	
}

/*
 * @description	: 按键IO初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keyio_init(void)
{
	unsigned char i = 0;
	char name[10];
	int ret = 0;
	
	keyinputdev.nd = of_find_node_by_path("/key");
	if (keyinputdev.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;
	} 

	/* 提取GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		keyinputdev.irqkeydesc[i].gpio = of_get_named_gpio(keyinputdev.nd ,"key-gpio", i);
		if (keyinputdev.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);
		}
	}
	
	/* 初始化key所使用的IO,并且设置成中断模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(keyinputdev.irqkeydesc[i].name, 0, sizeof(name));	/* 缓冲区清零 */
		sprintf(keyinputdev.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
		gpio_request(keyinputdev.irqkeydesc[i].gpio, name);
		gpio_direction_input(keyinputdev.irqkeydesc[i].gpio);	
		keyinputdev.irqkeydesc[i].irqnum = irq_of_parse_and_map(keyinputdev.nd, i);
	}
	/* 申请中断 */
	keyinputdev.irqkeydesc[0].handler = key0_handler;
	keyinputdev.irqkeydesc[0].value = KEY_0;
	
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(keyinputdev.irqkeydesc[i].irqnum, keyinputdev.irqkeydesc[i].handler, 
		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, keyinputdev.irqkeydesc[i].name, &keyinputdev);
		if(ret < 0){
			printk("irq %d request failed!\r\n", keyinputdev.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

	/* 创建定时器 */
	init_timer(&keyinputdev.timer);
	keyinputdev.timer.function = timer_function;

	/* 申请input_dev */
	keyinputdev.inputdev = input_allocate_device();
	keyinputdev.inputdev->name = KEYINPUT_NAME;
#if 0
	/* 初始化input_dev,设置产生哪些事件 */
	__set_bit(EV_KEY, keyinputdev.inputdev->evbit);	/* 设置产生按键事件          */
	__set_bit(EV_REP, keyinputdev.inputdev->evbit);	/* 重复事件,比如按下去不放开,就会一直输出信息 		 */

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

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

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

	/* 注册输入设备 */
	ret = input_register_device(keyinputdev.inputdev);
	if (ret) {
		printk("register input device failed!\r\n");
		return ret;
	}
	return 0;
}

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init keyinput_init(void)
{
	keyio_init();
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit keyinput_exit(void)
{
	unsigned int i = 0;
	/* 删除定时器 */
	del_timer_sync(&keyinputdev.timer);	/* 删除定时器 */
		
	/* 释放中断 */
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(keyinputdev.irqkeydesc[i].irqnum, &keyinputdev);
	}
	/* 释放input_dev */
	input_unregister_device(keyinputdev.inputdev);
	input_free_device(keyinputdev.inputdev);
}

module_init(keyinput_init);
module_exit(keyinput_exit);
MODULE_LICENSE("GPL");

2、测试APP编写

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include 
#include 
#include 
#include 
#include 
#include 

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

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

	filename = argv[1];

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

	fd = open(filename, O_RDWR);
	if (fd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;
	}

	while (1) {
		err = read(fd, &inputevent, sizeof(inputevent));
		if (err > 0) { /* 读取数据成功 */
			switch (inputevent.type) {


				case EV_KEY:
					if (inputevent.code < BTN_MISC) { /* 键盘键值 */
						printf("key %d %s\r\n", inputevent.code, inputevent.value ? "press" : "release");
					} else {
						printf("button %d %s\r\n", inputevent.code, inputevent.value ? "press" : "release");
					}
					break;

				/* 其他类型的事件,自行处理 */
				case EV_REL:
					break;
				case EV_ABS:
					break;
				case EV_MSC:
					break;
				case EV_SW:
					break;
			}
		} else {
			printf("读取数据失败\r\n");
		}
	}
	return 0;
}

3、Makefile

KERNELDIR := /home/sh/Desktop/MUL/zimage
CURRENT_PATH := $(shell pwd)

obj-m := keyinput.o

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

build: kernel_modules

kernel_modules:
	$(MAKE) -j32 -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -j32 -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(CURRENT_PATH) clean

四、运行测试

输入如下测试命令:

./keyinputApp /dev/input/event1

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

I.MX6ULL ARM驱动开发---INPUT子系统_第2张图片

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

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

hexdump /dev/input/event1

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

I.MX6ULL ARM驱动开发---INPUT子系统_第3张图片

/*****************input_event 类型********************/
/* 编号 */ /* tv_sec */ /* tv_usec */ /* type */ /* code */ /* value */
0000000 0c41 0000 d7cd 000c 0001 000b 0001 0000
0000010 0c41 0000 d7cd 000c 0000 0000 0000 0000
0000020 0c42 0000 54bb 0000 0001 000b 0000 0000
0000030 0c42 0000 54bb 0000 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 的话表示松开。

综上所述,原始事件值含义如下:
第 1 行,按键(KEY_0)按下事件。
第 2 行,EV_SYN 同步事件,因为每次上报按键事件以后都要同步的上报一个 EV_SYN 事件。
第 3 行,按键(KEY_0)松开事件。
第 4 行,EV_SYN 同步事件,和第 2 行一样。

你可能感兴趣的