I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统

这篇具有很好参考价值的文章主要介绍了I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

input 子系统

简介

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

上图中左边就是最底层的具体设备,比如按键、 USB 键盘/鼠标等,中间部分属于Linux 内核空间,分为驱动层、核心层和事件层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出 input 子系统用到了我们前面讲解的驱动分层模型,我们编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:
驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理。
事件层:主要和用户空间进行交互。

input 驱动框架

input 核心层会向 Linux 内核注册一个字符设备,大家找到 drivers/input/input.c 这个文件,input.c 就是 input 输入子系统的核心层,此文件里面有如下所示代码:

1767 struct class input_class = {
1768     .name = "input",
1769     .devnode = input_devnode,
1770 };
......
2414 static int __init input_init(void)
2415 {
2416     int err;
2417
2418     err = class_register(&input_class);
2419     if (err) {
2420         pr_err("unable to register input_dev class\n");
2421         return err;
2422     }
2423
2424     err = input_proc_init();
2425     if (err)
2426         goto fail1;
2427
2428     err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),
2429                 INPUT_MAX_CHAR_DEVICES, "input");
2430     if (err) {
2431         pr_err("unable to register char major %d", INPUT_MAJOR);
2432         goto fail2;
2433     }
2434
2435     return 0;
2436
2437 fail2: input_proc_exit();
2438 fail1: class_unregister(&input_class);
2439 return err;
2440 }

第 2418 行,注册一个 input 类,这样系统启动以后就会在/sys/class 目录下有一个 input 子目录,如图所示:
I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统,Linux_NXP_IMX6ULL,linux,arm

第 2428~2429 行,注册一个字符设备,主设备号为 INPUT_MAJOR, INPUT_MAJOR 定义在 include/uapi/linux/major.h 文件中,定义如下:

#define INPUT_MAJOR 13

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

注册 input_dev

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

121 struct input_dev {
122     const char *name;
123     const char *phys;
124     const char *uniq;
125     struct input_id id;
126
127     unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
128
129     unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /* 事件类型的位图 */
130     unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /* 按键值的位图 */
131     unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /* 相对坐标的位图 */
132     unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /* 绝对坐标的位图 */
133     unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; /* 杂项事件的位图 */
134     unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /*LED 相关的位图 */
135     unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];/* sound 有关的位图 */
136     unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; /* 压力反馈的位图 */
137     unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; /*开关状态的位图 */
......
189     bool devres_managed;
190 };

第 129 行, 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 /* 压力状态事件 */

我们可以将开发板上的按键值设置为上述代码中的任意一个,比如我们本章实验会将 I.MX6U-ALPHA 开发板上的 KEY 按键值设置为 KEY_0。在编写 input 设备驱动的时候我们需要先申请一个 input_dev 结构体变量,使用input_allocate_device 函数来申请一个 input_dev,此函数原型如下所示:

struct input_dev *input_allocate_device(void)

函数参数和返回值含义如下:
参数:无。
返回值: 申请到的 input_dev。
如果要注销的 input 设备的话需要使用 input_free_device 函数来释放掉前面申请到的input_dev, input_free_device 函数原型如下:

void input_free_device(struct input_dev *dev)

函数参数和返回值含义如下:
dev:需要释放的 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 注册失败。
同样的,注销 input 驱动的时候也需要使用 input_unregister_device 函数来注销掉前面注册的 input_dev, input_unregister_device 函数原型如下:

void input_unregister_device(struct input_dev *dev)

函数参数和返回值含义如下:
dev:要注销的 input_dev 。
返回值: 无。
综上所述, input_dev 注册过程如下:
①、使用 input_allocate_device 函数申请一个 input_dev。
②、初始化 input_dev 的事件类型以及事件值。
③、使用 input_register_device 函数向 Linux 系统注册前面初始化好的 input_dev。
④、卸载input驱动的时候需要先使用input_unregister_device函数注销掉注册的input_dev,然后使用 input_free_device 函数释放掉前面申请的 input_dev。 input_dev 注册过程示例代码如下
所示:

1 struct input_dev *inputdev; /* input 结构体变量 */
2 
3 /* 驱动入口函数 */
4 static int __init xxx_init(void)
5 {
6     ......
7     inputdev = input_allocate_device(); /* 申请 input_dev */
8     inputdev->name = "test_inputdev"; /* 设置 input_dev 名字 */
9
10     /*********第一种设置事件和事件值的方法***********/
11     __set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
12     __set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
13     __set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
14     /************************************************/
15
16     /*********第二种设置事件和事件值的方法***********/
17     keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
18     keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
19     /************************************************/
20
21     /*********第三种设置事件和事件值的方法***********/
22     keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
23     input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
24     /************************************************/
25
26     /* 注册 input_dev */
27     input_register_device(inputdev);
28     ......
29     return 0;
30 }
31
32 /* 驱动出口函数 */
33 static void __exit xxx_exit(void)
34 {
35     input_unregister_device(inputdev); /* 注销 input_dev */
36     input_free_device(inputdev); /* 删除 input_dev */
37 }

第 1 行,定义一个 input_dev 结构体指针变量。
第 4~30 行,驱动入口函数,在此函数中完成 input_dev 的申请、设置、注册等工作。
第 7 行调用 input_allocate_device 函数申请一个 input_dev。
第 10~23 行都是设置 input 设备事件和按键值,这里用了三种方法来设置事件和按键值。
第 27 行调用 input_register_device 函数向 Linux内核注册 inputdev。
第 33~37 行,驱动出口函数
第 35 行调用 input_unregister_device 函数注销前面注册的input_dev
第 36 行调用 input_free_device 函数删除前面申请的 input_dev。

上报输入事件

当我们向 Linux 内核注册好 input_dev 以后还不能高枕无忧的使用 input 设备, input 设备都是具有输入功能的,但是具体是什么样的输入值 Linux 内核是不知道的,我们需要获取到具体的输入值,或者说是输入事件,然后将输入事件上报给 Linux 内核。比如按键,我们需要在按键中断处理函数,或者消抖定时器中断函数中将按键值上报给 Linux 内核,这样 Linux 内核才能获取到正确的输入值。不同的事件,其上报事件的 API 函数不同,我们依次来看一下一些常用的事件上报 API 函数。

首先是 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 表示按键松开。
返回值: 无。
input_event 函数可以上报所有的事件类型和事件值, Linux 内核也提供了其他的针对具体事件的上报函数,这些函数其实都用到了 input_event 函数。比如上报按键所使用的
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_event 函数,如果要上报按键事件的话还是建议大家使用 input_report_key 函数。同样的还有一些其他的事件上报函数,这些函数如下所示:

void input_report_rel(struct input_dev *dev, unsigned int code, int value)
void input_report_abs(struct input_dev *dev, unsigned int code, int value)
void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
void input_report_switch(struct input_dev *dev, unsigned int code, int value)
void input_mt_sync(struct input_dev *dev)

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

void input_sync(struct input_dev *dev)

函数参数和返回值含义如下:
dev:需要上报同步事件的 input_dev。
返回值: 无。
综上所述,按键的上报事件的参考代码如下所示:

1 /* 用于按键消抖的定时器服务函数 */
2 void timer_function(unsigned long arg)
3 {
4     unsigned char value;
5 
6     value = gpio_get_value(keydesc->gpio); /* 读取 IO 值 */
7     if(value == 0){ /* 按下按键 */
8         /* 上报按键值 */
9         input_report_key(inputdev, KEY_0, 1); /* 最后一个参数 1, 按下 */
10         input_sync(inputdev); /* 同步事件 */
11     } else { /* 按键松开 */
12         input_report_key(inputdev, KEY_0, 0); /* 最后一个参数 0, 松开 */
13         input_sync(inputdev); /* 同步事件 */
14     }
15 }

第 6 行,获取按键值,判断按键是否按下。
第 9~10 行,如果按键值为 0 那么表示按键被按下了,如果按键按下的话就要使用input_report_key 函数向 Linux 系统上报按键值,比如向 Linux 系统通知 KEY_0 这个按键按下了。
第 12~13 行,如果按键值为 1 的话就表示按键没有按下,是松开的。向 Linux 系统通知KEY_0 这个按键没有按下或松开了。

input_event 结构体

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

24 struct input_event {
25     struct timeval time;
26     __u16 type;
27     __u16 code;
28     __s32 value;
29 };

我们依次来看一下 input_event 结构体中的各个成员变量:
time:时间,也就是此事件发生的时间,为 timeval 结构体类型, timeval 结构体定义如下:

1 typedef long __kernel_long_t;
2 typedef __kernel_long_t __kernel_time_t;
3 typedef __kernel_long_t __kernel_suseconds_t;
4 
5 struct timeval {
6     __kernel_time_t tv_sec; /* 秒 */
7     __kernel_suseconds_t tv_usec; /* 微秒 */
8 };

从上述代码可以看出, tv_sec 和 tv_usec 这两个成员变量都为 long 类型,也就是 32位,这个一定要记住,后面我们分析 event 事件上报数据的时候要用到。
type: 事件类型,比如 EV_KEY,表示此次事件为按键事件,此成员变量为 16 位。
code: 事件码,比如在 EV_KEY 事件中 code 就表示具体的按键码,如: KEY_0、 KEY_1等等这些按键。此成员变量为 16 位。
value: 值,比如 EV_KEY 事件中 value 就是按键值,表示按键有没有被按下,如果为 1 的话说明按键按下,如果为 0 的话说明按键没有被按下或者按键松开了。

input_envent 这个结构体非常重要,因为所有的输入设备最终都是按照 input_event 结构体呈现给用户的,用户应用程序可以通过 input_event 来获取到具体的输入事件或相关的值,比如
按键值等。关于 input 子系统就讲解到这里,接下来我们就以开发板上的 KEY0 按键为例,讲解一下如何编写 input 驱动。

实验

驱动程序

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#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");

1、首先,在设备结构体中定义一个 input_dev 指针变量。
2、在按键消抖定时器处理函数timer_function中上报输入事件,也就是使用 input_report_key函数上报按键事件以及按键值,最后使用 input_sync 函数上报一个同步事件,这一步一定得做!
3、在keyio_init函数中,使用 input_allocate_device 函数申请 input_dev,然后设置相应的事件以及事件码(也就是 KEY 模拟成那个按键,这里我们设置为 KEY_0)。最后使用 input_register_device函数向 Linux 内核注册 input_dev。
4、在keyinput_exit函数,当注销 input 设备驱动的时候使用 input_unregister_device 函数注销掉前面注册的 input_dev,最后使用 input_free_device 函数释放掉前面申请的 input_dev。

应用程序

#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 <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
#include <linux/input.h>

/* 定义一个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;
}

前面已经说过了, Linux 内核会使用 input_event 结构体来表示输入事件,所以我们要获取按键输入信息,那么必须借助于 input_event 结构体。首先定义了一个 inputevent 变量,此变量为 input_event 结构体类型。
当我们向 Linux 内核成功注册 input_dev 设备以后,会在/dev/input 目录下生成一个名为“eventX(X=0….n)”的文件,这个/dev/input/eventX 就是对应的 input 设备文件。我们读取这个文件就可以获取到输入事件信息,比如按键值什么的。使用read函数读取输入设备文件,也就是/dev/input/eventX,读取到的数据按照 input_event 结构体组织起来。获取到输入事件以后
(input_event 结构体类型)使用 switch case 语句来判断事件类型,本章实验我们设置的事件类型为 EV_KEY,因此只需要处理 EV_KEY 事件即可。比如获取按键编号(KEY_0 的编号为 11)、
获取按键状态,按下还是松开的?

测试

将上一小节编译出来 keyinput.ko 和 keyinputApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录 lib/modules/4.1.15 中。在加载 keyinput.ko 驱动模块之前,先
看一下/dev/input 目录下都有哪些文件,结果如图所示:
I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统,Linux_NXP_IMX6ULL,linux,arm

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

depmod //第一次加载驱动的时候需要运行此命令
modprobe keyinput.ko //加载驱动模块

当驱动模块加载成功以后再来看一下/dev/input 目录下有哪些文件,结果如图所示:
I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统,Linux_NXP_IMX6ULL,linux,arm

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

./keyinputApp /dev/input/event1

然后按下开发板上的 KEY 按键,结果如图所示:
I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统,Linux_NXP_IMX6ULL,linux,arm

从图中可以看出,当我们按下或者释放开发板上的按键以后都会在终端上输出相应的内容,提示我们哪个按键按下或释放了,在 Linux 内核中 KEY_0 为 11。另外,我们也可以不用 keyinputApp 来测试驱动,可以直接使用 hexdump 命令来查看/dev/input/event1 文件内容,输入如下命令:

hexdump /dev/input/event1

然后按下按键,终端输出如图所示信息:
I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统,Linux_NXP_IMX6ULL,linux,arm

上图就是 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 为事件类型,查看示例代码可知, EV_KEY 事件值为 1, EV_SYN 事件值为0。因此第 1 行表示 EV_KEY 事件,第 2 行表示 EV_SYN 事件。 code 为事件编码,也就是按键号,查看示例代码, 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 行一样。

Linux 自带KEY驱动程序

简介

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 内核中,如图所示:
I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统,Linux_NXP_IMX6ULL,linux,arm

 选中以后保存就会在.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 文件中找到如下所示内容:

673 static const struct of_device_id gpio_keys_of_match[] = {
674     { .compatible = "gpio-keys", },
675     { },
676 };
......
842 static struct platform_driver gpio_keys_device_driver = {
843     .probe = gpio_keys_probe,
844     .remove = gpio_keys_remove,
845     .driver = {
846         .name = "gpio-keys",
847         .pm = &gpio_keys_pm_ops,
848         .of_match_table = of_match_ptr(gpio_keys_of_match),
849     }
850 };
851
852 static int __init gpio_keys_init(void)
853 {
854     return platform_driver_register(&gpio_keys_device_driver);
855 }
856
857 static void __exit gpio_keys_exit(void)
858 {
859     platform_driver_unregister(&gpio_keys_device_driver);
860 }

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

689 static int gpio_keys_probe(struct platform_device *pdev)
690 {
691     struct device *dev = &pdev->dev;
692     const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);
693     struct gpio_keys_drvdata *ddata;
694     struct input_dev *input;
695     size_t size;
696     int i, error;
697     int wakeup = 0;
698
699     if (!pdata) {
700         pdata = gpio_keys_get_devtree_pdata(dev);
701         if (IS_ERR(pdata))
702             return PTR_ERR(pdata);
703     }
......
713     input = devm_input_allocate_device(dev);
714     if (!input) {
715         dev_err(dev, "failed to allocate input device\n");
716         return -ENOMEM;
717     }
718
719     ddata->pdata = pdata;
720     ddata->input = input;
721     mutex_init(&ddata->disable_lock);
722
723     platform_set_drvdata(pdev, ddata);
724     input_set_drvdata(input, ddata);
725
726     input->name = pdata->name ? : pdev->name;
727     input->phys = "gpio-keys/input0";
728     input->dev.parent = &pdev->dev;
729     input->open = gpio_keys_open;
730     input->close = gpio_keys_close;
731
732     input->id.bustype = BUS_HOST;
733     input->id.vendor = 0x0001;
734     input->id.product = 0x0001;
735     input->id.version = 0x0100;
736
737     /* Enable auto repeat feature of Linux input subsystem */
738     if (pdata->rep)
739         __set_bit(EV_REP, input->evbit);
740
741     for (i = 0; i < pdata->nbuttons; i++) {
742         const struct gpio_keys_button *button = &pdata->buttons[i];
743         struct gpio_button_data *bdata = &ddata->data[i];
744
745         error = gpio_keys_setup_key(pdev, input, bdata, button);
746         if (error)
747             return error;
748
749         if (button->wakeup)
750         wakeup = 1;
751     }
......
760     error = input_register_device(input);
761     if (error) {
762         dev_err(dev, "Unable to register input device, error: %d\n",
763         error);
764 goto err_remove_group;
765 }
......
774 }

第 700 行,调用 gpio_keys_get_devtree_pdata 函数从设备树中获取到 KEY 相关的设备节点信息。
第 713 行,使用 devm_input_allocate_device 函数申请 input_dev。
第 726~735,初始化 input_dev。
第 739 行,设置 input_dev 事件,这里设置了 EV_REP 事件。
第 745 行,调用 gpio_keys_setup_key 函数继续设置 KEY,此函数会设置 input_dev 的EV_KEY 事件以及事件码(也就是 KEY 模拟为哪个按键)。
第 760 行,调用 input_register_device 函数向 Linux 系统注册 input_dev。
我们接下来再来看一下 gpio_keys_setup_key 函数,此函数内容如下:

437 static int gpio_keys_setup_key(struct platform_device *pdev,
438     struct input_dev *input,
439     struct gpio_button_data *bdata,
440     const struct gpio_keys_button *button)
441 {
442     const char *desc = button->desc ? button->desc : "gpio_keys";
443     struct device *dev = &pdev->dev;
444     irq_handler_t isr;
445     unsigned long irqflags;
446     int irq;
447     int error;
448
449     bdata->input = input;
450     bdata->button = button;
451     spin_lock_init(&bdata->lock);
452
453     if (gpio_is_valid(button->gpio)) {
454
455         error = devm_gpio_request_one(&pdev->dev, button->gpio,
456                 GPIOF_IN, desc);
457         if (error < 0) {
458             dev_err(dev, "Failed to request GPIO %d, error %d\n",
459                 button->gpio, error);
460             return error;
......
488             isr = gpio_keys_gpio_isr;
489             irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;
490
491         } else {
492             if (!button->irq) {
493                 dev_err(dev, "No IRQ specified\n");
494                 return -EINVAL;
495         }
496         bdata->irq = button->irq;
......
506
507         isr = gpio_keys_irq_isr;
508         irqflags = 0;
509     }
510
511 input_set_capability(input, button->type ?: EV_KEY, button->code);
......
540 return 0;
541 }

第 511 行,调用 input_set_capability 函数设置 EV_KEY 事件以及 KEY 的按键类型,也就是 KEY 作为哪个按键?我们会在设备树里面设置指定的 KEY 作为哪个按键。一切都准备就绪以后剩下的就是等待按键按下,然后向 Linux 内核上报事件,事件上报是在 gpio_keys_irq_isr 函数中完成的,此函数内容如下:

392 static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)
393 {
394     struct gpio_button_data *bdata = dev_id;
395     const struct gpio_keys_button *button = bdata->button;
396     struct input_dev *input = bdata->input;
397     unsigned long flags;
398
399     BUG_ON(irq != bdata->irq);
400
401     spin_lock_irqsave(&bdata->lock, flags);
402
403     if (!bdata->key_pressed) {
404         if (bdata->button->wakeup)
405         pm_wakeup_event(bdata->input->dev.parent, 0);
406
407         input_event(input, EV_KEY, button->code, 1);
408         input_sync(input);
409
410         if (!bdata->release_delay) {
411             input_event(input, EV_KEY, button->code, 0);
412             input_sync(input);
413             goto out;
414         }
415
416         bdata->key_pressed = true;
417     }
418
419     if (bdata->release_delay)
420         mod_timer(&bdata->release_timer,
421             jiffies + msecs_to_jiffies(bdata->release_delay));
422 out:
423 spin_unlock_irqrestore(&bdata->lock, flags);
424 return IRQ_HANDLED;
425 }

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

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

应用

要 使 用 Linux 内 核 自 带 的 按 键 驱 动 程 序 很 简 单 , 只 需 要 根 据Documentation/devicetree/bindings/input/gpio-keys.txt 这个文件在设备树中添加指定的设备节点即可,节点要求如下:
①、节点名字为“gpio-keys”。
②、 gpio-keys 节点的 compatible 属性值一定要设置为“gpio-keys”。
③、所有的 KEY 都是 gpio-keys 的子节点,每个子节点可以用如下属性描述自己:
        gpios: KEY 所连接的 GPIO 信息。
        interrupts: KEY 所使用 GPIO 中断信息,不是必须的,可以不写。
        label: KEY 名字
        linux,code: KEY 要模拟的按键。
④、如果按键要支持连按的话要加入 autorepeat。打开 imx6ull-alientek-emmc.dts,根据上面的要求创建对应的设备节点,设备节点内容如下所示:

1 gpio-keys {
2     compatible = "gpio-keys";
3     #address-cells = <1>;
4     #size-cells = <0>;
5     autorepeat;
6     key0 {
7         label = "GPIO Key Enter";
8         linux,code = <KEY_ENTER>;
9         gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
10     };
11 };

第 5 行, autorepeat 表示按键支持连按。
第 6~10 行, ALPHA 开发板 KEY 按键信息,名字设置为“GPIO Key Enter”,这里我们将开发板上的 KEY 按键设置为“EKY_ENTER”这个按键,也就是回车键,效果和键盘上的回车键一样。后面学习 LCD 驱动的时候需要用到此按键,因为 Linux 内核设计的 10 分钟以后 LCD关闭,也就是黑屏,就跟我们用电脑或者手机一样,一定时间以后关闭屏幕。这里将开发板上的 KEY 按键注册为回车键,当 LCD 黑屏以后直接按一下 KEY 按键即可唤醒屏幕,就跟当电脑熄屏以后按下回车键即可重新打开屏幕一样。
最后设置 KEY 所使用的 IO 为 GPIO1_IO18,一定要检查一下设备树看看此 GPIO 有没有被用到其他外设上,如果有的话要删除掉相关代码!
重新编译设备树,然后用新编译出来的 imx6ull-alientek-emmc.dtb 启动 Linux 系统,系统启动以后查看/dev/input 目录,看看都有哪些文件,结果如图所示:
I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统,Linux_NXP_IMX6ULL,linux,arm

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

hexdump /dev/input/event1

然后按下 ALPHA 开发板上的按键,终端输出图所示内容:
I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统,Linux_NXP_IMX6ULL,linux,arm

如果按下 KEY 按键以后会在终端上输出上图所示的信息那么就表示 Linux 内核的按键驱动工作正常。至于图中内容的含义大家就自行分析
大家如果发现按下 KEY 按键以后没有反应,那么请检查一下三方面:
①、是否使能 Linux 内核 KEY 驱动。
②、设备树中 gpio-keys 节点是否创建成功。
③、在设备树中是否有其他外设也使用了 KEY 按键对应的 GPIO文章来源地址https://www.toymoban.com/news/detail-631572.html

到了这里,关于I.MX6ULL_Linux_驱动篇(45)linux INPUT子系统的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • I.MX6ULL ARM驱动开发---网络设备驱动框架

      网络驱动是 linux 里面驱动三巨头之一,linux 下的网络功能非常强大,嵌入式 linux 中也常常用到网络功能。前面我们已经讲过了字符设备驱动和块设备驱动,本章我们就来学习一下 linux 里面的网络设备驱动。   网络设备驱动程序的体系结构分为4层,依次为网络协议驱

    2023年04月17日
    浏览(27)
  • 012——LED模块驱动开发(基于I.MX6uLL)

    目录 一、 硬件原理图 二、 驱动程序 三、 应用程序 四、 Makefile 五、操作         又是非常经典的点灯环节 ,每次学新语言第一步都是hello world,拿到新板子或者学习新的操作系统,第一步就是点灯。         LED 的驱动方式,常见的有四种。 ① 使用引脚输出 3.3V 点

    2024年04月13日
    浏览(31)
  • 016——DHT11驱动开发(基于I.MX6uLL)

    目录 一、 模块介绍 1.1 简介 1.2 电路描述 1.3 通信协议 二、 驱动程序 三、 应用程序 四、 上机实验         DHT11 是一款可测量温度和湿度的传感器。比如市面上一些空气加湿器,会测量空气中湿度,再根据测量结果决定是否继续加湿。DHT11 数字温湿度传感器是一款含有已

    2024年04月16日
    浏览(25)
  • i.MX6ULL移植NXP官方Linux内核imx_5.4.47_2.2.0

    系统:Ubuntu18.04 参考资料:百问网 IMX6ULL开发板(从零移植篇-预览版)-V0.1,正点原子驱动开发指南 开发板:100ask i.MX6ULL PRO 交叉编译工具链的获取就不写了 打开 .bashrc 文件。 vi ~/.bashrc 。在该文件最后面添加如下(根据自己的交叉编译工具链) (1)直接从官网下载,非常慢而

    2024年02月12日
    浏览(38)
  • 【Linux 裸机篇(五)】I.MX6ULL BSP工程管理下的 Makefile编写、链接脚本

    文件夹 描述 bsp 存放驱动文件 imx6ul 存放跟芯片有关的文件,比如 NXP 官方的 SDK库文件 obj 存放编译生成的.o 文件 project 存放 start.S 和 main.c 文件,也就是应用文件 行 描述 1~7 定义了一些变量,除了第 2 行以外其它的都是跟编译器有关的,如果使用其它编译器的话只需要修改第

    2023年04月20日
    浏览(33)
  • 017——DS18B20驱动开发(基于I.MX6uLL)

    目录 一、 模块介绍 1.1 简介 1.2 主要特点 1.3 存储器介绍 1.4 时序 1.5 命令 1.5.1 命令大全    1.5.2 命令使用 1.5.3 使用示例 1.6 原理图 二、 驱动程序 三、 应用程序 四、 测试         DS18B20 温度传感器具有线路简单、体积小的特点,用来测量温度非常简单,在一根通信线上

    2024年04月12日
    浏览(33)
  • i.MX6ULL驱动开发 | 27 - 使用WM8960 CODEC播放音频

    WM8960是欧胜公司(wolfson)的一款低功耗、高质量的立体声音频编解码芯片。 其内部集成D类喇叭功放,每个通道可以驱动一个1W喇叭(8Ω),内部集成3个立体声输入源,可以灵活配置,拥有一路完整的麦克风接口。 WM8960内部ADC和DAC都为24位,主要特性如下: DAC的SNR(信噪比)

    2024年02月02日
    浏览(32)
  • 018——红外遥控模块驱动开发(基于HS0038和I.MX6uLL)

    目录 一、 模块介绍 1.1 简介 1.2 协议 二、 驱动代码 三、 应用代码 四、 实验 五、 程序优化         红外遥控被广泛应用于家用电器、工业控制和智能仪器系统中,像我们熟知的有电视机盒子遥控器、空调遥控器。红外遥控器系统分为发送端和接收端,如图下图所示。

    2024年04月16日
    浏览(32)
  • i.MX 6ULL 驱动开发 二十九:向 Linux 内核中添加自己编写驱动

    Linux 内核编译流程如下: 1、配置 Linux 内核。 2、编译 Linux 内核。 说明:进入 Linux 内核源码,使用 make help 参看相关配置。 1、 menuconfig 它本身是一个软件,只提供图形界面配置的一些逻辑,并不负责提供内容。 2、 menuconfig 是内核源码树的各目录下的 kconfig 提供的。 3、 m

    2023年04月10日
    浏览(60)
  • I.MX6ull UART

     一 简介 UART 全称叫做串行接口,通常也叫做 COM 接口,串行接口指的是数据一个一个的顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通 信距离远 ,但是速 度相对会低 ,串口是一种很常用的工业接口。I.MX6U 自带的 UART 外设

    2024年02月09日
    浏览(25)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包