Linux INPUT 子系统实验

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

input 子系统

input 子系统简

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

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

#define INPUT_MAJOR 13

因此,input 子系统的所有设备主设备号都为13,我们在使用input 子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个input_device 即可。
1、注册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 /* 压力状态事件*/

比如本章我们要使用到按键,那么就需要注册EV_KEY 事件,如果要使用连按功能的话还需要注册EV_REP 事件。

继续回到示例代码58.1.2.2 中,第129 行~137 行的evbit、keybit、relbit 等等都是存放不同事件对应的值。比如我们本章要使用按键事件,因此要用到keybit,keybit 就是按键事件使用的位图,Linux 内核定义了很多按键值,这些按键值定义在include/uapi/linux/input.h 文件中,按键值如下:

215 #define KEY_RESERVED 0
216 #define KEY_ESC 1
217 #define KEY_1 2
218 #define KEY_2 3
219 #define KEY_3 4
220 #define KEY_4 5
221 #define KEY_5 6
222 #define KEY_6 7
223 #define KEY_7 8
224 #define KEY_8 9
225 #define KEY_9 10
226 #define KEY_0 11
......
794 #define BTN_TRIGGER_HAPPY39 0x2e6
795 #define BTN_TRIGGER_HAPPY40 0x2e7

我们可以将开发板上的按键值设置为示例代码58.1.2.4 中的任意一个,比如我们本章实验会将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。

2、上报输入事件
当我们向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);
}

从示例代码58.1.2.6 可以看出,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 };

从示例代码58.1.3.2 可以看出,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 驱动。

硬件原理图分析

本章实验硬件原理图参考15.2 小节即可。

实验程序编写

本实验对应的例程路径为:开发板光盘-> 2、Linux 驱动例程-> 20_input。

修改设备树文件

直接使用49.3.1 小节创建的key 节点即可。

按键input 驱动程序编写

新建名为“20_input”的文件夹,然后在20_input 文件夹里面创建vscode 工程,工作区命名为“keyinput”。工程创建好以后新建keyinput.c 文件,在keyinput.c 里面输入如下内容:

#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>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: keyinput.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: Linux按键input子系统实验
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/8/21 左忠凯创建
***************************************************************/
#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");
MODULE_AUTHOR("zuozhongkai");

keyinput.c 文件内容其实就是实验“13_irq”中的imx6uirq.c 文件中修改而来的,只是将其中与字符设备有关的内容进行了删除,加入了input_dev 相关的内容,我们简单来分析一下示例代码58.3.2.1 中的程序。

第57 行,在设备结构体中定义一个input_dev 指针变量。

第93~102 行,在按键消抖定时器处理函数中上报输入事件,也就是使用input_report_key函数上报按键事件以及按键值,最后使用input_sync 函数上报一个同步事件,这一步一定得做!

第156~180 行,使用input_allocate_device 函数申请input_dev,然后设置相应的事件以及事件码(也就是KEY 模拟成那个按键,这里我们设置为KEY_0)。最后使用input_register_device函数向Linux 内核注册input_dev。

第211~212 行,当注销input 设备驱动的时候使用input_unregister_device 函数注销掉前面注册的input_dev,最后使用input_free_device 函数释放掉前面申请的input_dev。

编写测试APP

新建keyinputApp.c 文件,然后在里面输入如下所示内容:

#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>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: keyinputApp.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: input子系统测试APP。
其他	   	: 无
使用方法	 :./keyinputApp /dev/input/event1
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/8/26 左忠凯创建
***************************************************************/

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

第58.1.3 小节已经说过了,Linux 内核会使用input_event 结构体来表示输入事件,所以我们要获取按键输入信息,那么必须借助于input_event 结构体。第28 行定义了一个inputevent 变量,此变量为input_event 结构体类型。

第56 行,当我们向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)、获取按键状态,按下还是松开的?

运行测试

编译驱动程序和测试APP

1、编译驱动程序
编写Makefile 文件,本章实验的Makefile 文件和第四十章实验基本一样,只是将obj-m 变量的值改为“keyinput.o”,Makefile 内容如下所示:

KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)

obj-m := keyinput.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第4 行,设置obj-m 变量的值为“keyinput.o”。
输入如下命令编译出驱动模块文件:

make -j32

编译成功以后就会生成一个名为“keyinput.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试keyinputApp.c 这个测试程序:

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

编译成功以后就会生成keyinputApp 这个应用程序。

运行测试

将上一小节编译出来keyinput.ko 和keyinputApp 这两个文件拷贝到rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录lib/modules/4.1.15 中。在加载keyinput.ko 驱动模块之前,先
看一下/dev/input 目录下都有哪些文件,结果如图58.4.2.1 所示:
在这里插入图片描述

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

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

当驱动模块加载成功以后再来看一下/dev/input 目录下有哪些文件,结果如图58.4.2.2 所示:
在这里插入图片描述

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

./keyinputApp /dev/input/event1

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

hexdump /dev/input/event1

然后按下按键,终端输出如图58.4.2.4 所示信息:
在这里插入图片描述
图58.4.2.4 就是input_event 类型的原始事件数据值,采用十六进制表示,这些原始数据的含义如下:

/*****************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 的话表示松开。

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

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

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

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 内核中,如图58.5.1.1 所示:

在这里插入图片描述
选中以后就会在.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 }

从示例代码58.5.1.1 可以看出,这就是一个标准的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";

第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 要模拟的按键,也就是示例代码58.1.2.4 中的这些按键。
④、如果按键要支持连按的话要加入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 目录,看看都有哪些文件,结果如图58.5.2.1 所示:
在这里插入图片描述
从图58.5.2.1 可以看出存在event1 这个文件,这个文件就是KEY 对应的设备文件,使用hexdump 命令来查看/dev/input/event1 文件,输入如下命令:

hexdump /dev/input/event1

然后按下ALPHA 开发板上的按键,终端输出图58.5.2.2 所示内容:
在这里插入图片描述

如果按下KEY 按键以后会在终端上输出图58.5.2.2 所示的信息那么就表示Linux 内核的按键驱动工作正常。至于图58.5.2.2 中内容的含义大家就自行分析,这个已经在58.4.2 小节详细的分析过了,这里就不再讲解了。
大家如果发现按下KEY 按键以后没有反应,那么请检查一下三方面:
①、是否使能Linux 内核KEY 驱动。
②、设备树中gpio-keys 节点是否创建成功。
③、在设备树中是否有其他外设也使用了KEY 按键对应的GPIO,但是我们并没有删除掉这些外设信息。检查Linux 启动log 信息,看看是否有类似下面这条信息:

gpio-keys gpio_keys:Failed to request GPIO 18, error -16

上述信息表示GPIO 18 申请失败,失败的原因就是有其他的外设正在使用此GPIO。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值