<Linux开发>驱动开发 -之-Linux INPUT 子系统

news2024/11/29 7:42:51

<Linux开发>驱动开发 -之-Linux INPUT 子系统

交叉编译环境搭建:
<Linux开发> linux开发工具-之-交叉编译环境搭建

uboot移植可参考以下:
<Linux开发> -之-系统移植 uboot移植过程详细记录(第一部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第三部分)(uboot移植完结)

Linux内核及设备树移植可参考以下:
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第一部分)
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第二部分完结)

Linux文件系统构建移植参考以下:
<Linux开发>系统移植 -之- linux构建BusyBox根文件系统及移植过程详细记录
<Linux开发>系统移植 -之-使用buildroot构建BusyBox根文件系统

Linux驱动开发参考以下:
<Linux开发>驱动开发 -之-pinctrl子系统
<Linux开发>驱动开发 -之-gpio子系统
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的LED驱动
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的beep驱动
<Linux开发>驱动开发 -之-资源的并发与竞争处理
<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知
<Linux开发>驱动开发 -Linux MISC 驱动

一 前言

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

网上也有很多资料介绍 input子系统相关的,有兴趣也可执行查阅。

二 input 子系统

2.1 input子系统简介

input 就是输入的意思,因此 input 子系统就是管理输入的子系统,和 pinctrl、gpio 子系统一样,都是 Linux 内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,我们只需要按照要求上报这些输入事件即可。为此 input 子系统分为 input 驱动层、input 核心层、input 事件处理层,最终给用户空间提供可访问的设备节点,input 子系统框架如下图所示:
在这里插入图片描述
上图中左边就是最底层的具体设备,比如按键、USB 键盘/鼠标等,中间部分属于Linux 内核空间,分为驱动层、核心层和事件层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出 input 子系统用到了我们前面讲解的驱动分层模型,我们编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:

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

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

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

2.2 input子系统核心层

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

路径:drivers\input\input.c

...........
struct class input_class = {
	.name		= "input",
	.devnode	= input_devnode,
};
EXPORT_SYMBOL_GPL(input_class);
............

static int __init input_init(void)
{
	int err;

	err = class_register(&input_class);
	if (err) {
		pr_err("unable to register input_dev class\n");
		return err;
	}

	err = input_proc_init();
	if (err)
		goto fail1;

	err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),
				     INPUT_MAX_CHAR_DEVICES, "input");
	if (err) {
		pr_err("unable to register char major %d", INPUT_MAJOR);
		goto fail2;
	}

	return 0;

 fail2:	input_proc_exit();
 fail1:	class_unregister(&input_class);
	return err;
}

static void __exit input_exit(void)
{
	input_proc_exit();
	unregister_chrdev_region(MKDEV(INPUT_MAJOR, 0),
				 INPUT_MAX_CHAR_DEVICES);
	class_unregister(&input_class);
}
........

上述代码中:
第 15 行,注册一个 input 类,这样系统启动以后就会在/sys/class 目录下有一个 input 子目录,如下图所示:

第 25~26 行,注册一个字符设备,主设备号为 INPUT_MAJOR,INPUT_MAJOR 定义在 include/uapi/linux/major.h 文件中(在<Linux开发>驱动开发 -Linux MISC 驱动 中2.2小节也介绍过,由很多固定的主设备号在此定义),定义如下:

#define XT_DISK_MAJOR		13
#define INPUT_MAJOR		13
#define SOUND_MAJOR		14

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

2.3 input子系统API

注册 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;		/* id设备(struct input_id) */

	unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];		/* 设备属性和怪癖的位图 */

	unsigned long evbit[BITS_TO_LONGS(EV_CNT)];					/* 设备支持的事件类型位图(EV_KEY,*EV_REL等) */
	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)];				/* 设备支持的音效位图 */
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];					/* 设备支持的力反馈效果位图 */
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];					/* 设备上存在的开关位图 */

	unsigned int hint_events_per_packet;						/* 设备在一个数据包中生成的平均事件数(EV_SYN/SYN_REPORT事件之间)。由事件处理程序用于估计容纳事件所需的缓冲区大小。*/

	unsigned int keycodemax;									/* keycode表的大小 */
	unsigned int keycodesize;									/* keycode表中元素的大小 */
	void *keycode;												/* 该设备的扫描码到密钥码的映射*/

	int (*setkeycode)(struct input_dev *dev,					/* 用于检索当前密钥映射的可选遗留方法 */
			  const struct input_keymap_entry *ke,
			  unsigned int *old_keycode);
	int (*getkeycode)(struct input_dev *dev,					/* 更改当前keymap的可选方法,用于实现稀疏keymap。如果未提供,将使用默认机制。在保持event_lock时调用该方法,因此不能休眠 */
			  struct input_keymap_entry *ke);

	struct ff_device *ff;										/* 如果设备支持力反馈效果,则与设备关联的力反馈结构 */

	unsigned int repeat_key;									/* 存储上一次按键的按键代码;用于实现软件自动监管 */
	struct timer_list timer;									/* 软件自动恢复的计时器 */

	int rep[REP_CNT];											/* autorepeat参数的当前值(延迟、速率) */

	struct input_mt *mt;										/* 指向多点触摸状态的指针 */

	struct input_absinfo *absinfo;								/* &struct input_absinfo元素的数组,包含关于绝对轴的信息(当前值、最小值、最大值、平面、模糊、分辨率) */

	unsigned long key[BITS_TO_LONGS(KEY_CNT)];					/* 反映设备按键的当前状态 */
	unsigned long led[BITS_TO_LONGS(LED_CNT)];					/* 反映设备led的当前状态 */
	unsigned long snd[BITS_TO_LONGS(SND_CNT)];					/* 反映音效的当前状态 */
	unsigned long sw[BITS_TO_LONGS(SW_CNT)];					/* 反映设备开关的当前状态 */

	int (*open)(struct input_dev *dev);							/* 这个方法是在第一个用户调用input_open_device()时调用的。驱动程序必须准备设备开始生成事件(启动轮询线程、请求IRQ、提交URB等) */
	void (*close)(struct input_dev *dev);						/* 这个方法是在最后一个用户调用input_close_device()时调用的。 */
	int (*flush)(struct input_dev *dev, struct file *file);		/* 清除设备。最常用于消除与设备断开连接时加载到设备中的力反馈效应 */
	int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);		/* 发送到设备的事件的事件处理程序,如EV_LED或EV_SND。设备应执行请求的操作(打开LED、播放声音等)该呼叫受@event_lock保护,不得休眠 */

	struct input_handle __rcu *grab;							/* 当前已抓取设备的输入句柄(通过EVIOCGRAB ioctl)。当句柄抓取设备时,它将成为来自该设备的所有输入事件的唯一接收者 */

	spinlock_t event_lock;										/* 当input core接收并处理设备的新事件时(在input_event()中),将获取此spinlock。在设备向输入核心注册后,访问和/或修改设备参数(如keymap或absmin、absmax、absfuzz等)的代码必须使用此锁。 */
	struct mutex mutex;											/* 序列化对open()、close()和flush()方法的调用 */

	unsigned int users;											/* 存储打开该设备的用户数(输入处理程序)。 */
	bool going_away;											/* 标记正在注销的设备,并使用-ENODEV导致input_open_device*()失败。 */

	struct device dev;											/* 驱动程序模型对此设备的视图 */

	struct list_head	h_list;									/* 与设备关联的输入句柄列表。访问列表时,必须持有dev->mutex */
	struct list_head	node;									/* 用于将设备放置在input_dev_list上 */

	unsigned int num_vals;										/* 当前帧中排队的值数 */
	unsigned int max_vals;										/* 一帧中排队的最大值数 */
	struct input_value *vals;									/* 当前帧中排队的值数组 */

	bool devres_managed;										/* 表示设备是用devres框架管理的,不需要显式注销或释放。 */
};

第 10 行,evbit 表示输入事件类型,可选的事件类型定义如下:

路径:include\uapi\linux\input.h
/*
 * Event types
 */

#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	/* 压力状态事件 */
#define EV_MAX			0x1f
#define EV_CNT			(EV_MAX+1)

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

继续回到input_dev结构体代码中,第 10 行~18 行的 evbit、keybit、relbit 等等都是存放不同事件对应的值。比如我们本文要使用按键事件,因此要用到 keybit,keybit 就是按键事件使用的位图,Linux 内核定义了很多按键值,这些按键值定义如下:

路径:include\uapi\linux\input.h
/*
 * Keys and buttons
 *
 * Most of the keys/buttons are modeled after USB HUT 1.12
 * (see http://www.usb.org/developers/hidpage).
 * Abbreviations in the comments:
 * AC - Application Control
 * AL - Application Launch Button
 * SC - System Control
 */

#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 KEY_MINUS		12
#define KEY_EQUAL		13
#define KEY_BACKSPACE		14
#define KEY_TAB			15
#define KEY_Q			16
#define KEY_W			17
#define KEY_E			18
#define KEY_R			19
#define KEY_T			20
#define KEY_Y			21
..........
#define BTN_TRIGGER_HAPPY28		0x2db
#define BTN_TRIGGER_HAPPY29		0x2dc
#define BTN_TRIGGER_HAPPY30		0x2dd
#define BTN_TRIGGER_HAPPY31		0x2de
#define BTN_TRIGGER_HAPPY32		0x2df
#define BTN_TRIGGER_HAPPY33		0x2e0
#define BTN_TRIGGER_HAPPY34		0x2e1
#define BTN_TRIGGER_HAPPY35		0x2e2
#define BTN_TRIGGER_HAPPY36		0x2e3
#define BTN_TRIGGER_HAPPY37		0x2e4
#define BTN_TRIGGER_HAPPY38		0x2e5
#define BTN_TRIGGER_HAPPY39		0x2e6
#define BTN_TRIGGER_HAPPY40		0x2e7

我们可以将开发板上的按键值设置为上述代码中的任意一个,比如我们本文例程会将开发板上的 KEY 按键值设置为 KEY_0。

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

路径:drivers\input\input.c
struct input_dev *input_allocate_device(void)

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

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

路径:drivers\input\input.c
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 函数,此函数原型如下:

路径:drivers\input\input.c
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 函数原型如下:

路径:drivers\input\input.c
void input_unregister_device(struct input_dev *dev)

函数参数和返回值含义如下:
dev:要注销的 input_dev 。
返回值:无。

2.4 input_dev注册流程

根据2.3小节的分析,归纳一下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 注册过程示例代码如下所示:

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 */
	.................
}


2.5 input_dev输入事件上报

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

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

路径:drivers\input\input.c
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 函数,此函数内容如下:

路径:include\linux\input.h
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 函数。

同样的还有一些其他的事件上报函数,这些函数如下所示:

路径:include\linux\input.h
static inline void input_report_rel(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_REL, code, value);
}

static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_ABS, code, value);
}

static inline void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_FF_STATUS, code, value);
}

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

static inline void input_sync(struct input_dev *dev)
{
	input_event(dev, EV_SYN, SYN_REPORT, 0);
}

static inline void input_mt_sync(struct input_dev *dev)
{
	input_event(dev, EV_SYN, SYN_MT_REPORT, 0);
}

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

路径:include\linux\input.h
static inline void input_sync(struct input_dev *dev)
{
	input_event(dev, EV_SYN, SYN_REPORT, 0);
}

static inline void input_mt_sync(struct input_dev *dev)
{
	input_event(dev, EV_SYN, SYN_MT_REPORT, 0);
}

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

按键的上报事件的参考代码如下所示:
以<Linux开发>驱动开发 -之-内核定时器与中断中的按键驱动为例。

void timer_function(unsigned long arg)
{
	unsigned char value;
	unsigned char num;
	struct irq_keydesc *keydesc;
	struct keyirq_dev *dev = (struct keyirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];

	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); /* 同步事件 */
	}	
}

2.6 input_event 结构体

在Linux 内核中,使用 input_event 这个结构体来表示所有的输入事件,input_envent 结构体定义内容如下:

路径:include\uapi\linux\input.h
/*
 * The event structure itself
 */
struct input_event {
	struct timeval time;
	__u16 type;
	__u16 code;
	__s32 value;
};

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

路径:include\uapi\asm-generic\posix_types.h
#ifndef __kernel_long_t
typedef long		__kernel_long_t;
typedef unsigned long	__kernel_ulong_t;
#endif
....
typedef __kernel_long_t	__kernel_time_t;
....
#ifndef __kernel_suseconds_t
typedef __kernel_long_t		__kernel_suseconds_t;
#endif

路径:include\uapi\linux\time.h
struct timeval {
	__kernel_time_t		tv_sec;		/* seconds */
	__kernel_suseconds_t	tv_usec;	/* microseconds */
};

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 驱动。

三 编写驱动

3.1 硬件原理图

在这里插入图片描述
在这里插入图片描述

3.2 寄存器地址信息

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.3 设备树

添加pinctrl节点:

路径:arch\arm\boot\dts\imx6ull-water-emmc.dts
pinctrl_key: keygrp {
			fsl,pins = <
				MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 	/* KEY0 */
			>;
		};

添加设备节点:

路径:arch\arm\boot\dts\imx6ull-water-emmc.dts
key {	
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "water-key";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_key>;
		key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
		interrupt-parent = <&gpio1>;
		interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
		status = "okay";
	};

3.4 编写驱动代码

新建文件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 © 2018-2023  XINWEN Electronic Technology Co., Ltd.
/// All rights reserved.                                       
/// @file(文件)            keyinput.c                                   
/// @brief(简述)           Linux按键input子系统实验              
/// @details(详情)         Linux按键input子系统实验                
/// @version(版本)         1.0                                       
/// @author(作者)          water                                    
/// @date(日期)            2023-06-19                                
/// Edit History                                               
/// -----------------------------------------------------------
/// DATE        NAME          DESCRIPTION                      
/// 2023-06-19  water     Create.                             
///
***************************************************************/
#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("water");

3.5 编写测试app代码

新建文件keyinput_app.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 © 2018-2023  XINWEN Electronic Technology Co., Ltd.
/// All rights reserved.                                       
/// @file(文件)            keyinput_app.c                                   
/// @brief(简述)           input子系统测试APP              
/// @details(详情)         input子系统测试APP               
/// @version(版本)         1.0                                       
/// @author(作者)          water                                    
/// @date(日期)            2023-06-19                                
/// Edit History                                               
/// -----------------------------------------------------------
/// DATE        NAME          DESCRIPTION                      
/// 2023-06-19  water     Create.                             
///
***************************************************************/

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

//编译命令: arm-linux-gnueabihf-gcc keyinput_app.c -o keyinput_app

四 编译

4.1 编写Makefile

编写Makefile,内容如下:

RCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

KERNELDIR := /home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/
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.2 编译驱动

使用make命令编译驱动,如下:
在这里插入图片描述

4.3 编译测试app

使用以下命令编译测试app:

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

在这里插入图片描述

五 运行验证

5.1 资源文件放入设备

cp keyinput.ko  ../../nfs/buildrootfs/lib/modules/4.1.15+/
cp app/keyinput_app ../../nfs/buildrootfs/root/water_soft/input/

在这里插入图片描述

5.2 挂载驱动

depmod
modprobe keyinput.ko 

在这里插入图片描述
由挂载的输出信息可看出,对应的设备文件是 /devices/virtual/input/input1 。

5.3 运行测试app

# cd /root/water_soft/input/
# ./keyinput_app  /dev/input/event1

在这里插入图片描述
当我们按下或者释放开发板上的按键以后都会在终端上输出相应的内容,提示我们哪个按键按下或释放了,在 Linux 内核中 KEY_0 为 11(2.3小节)。

5.4 其它验证方法

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

hexdump   /dev/input/event1

在这里插入图片描述

 /*****************input_event 类型********************/
/* 编号 */ 	/* tv_sec */ 	/* tv_usec */ 	/* type */	/* code */ 		/* value */
0000000 	4488 0005 		2d64 0009 		0001 		000b 			0001 0000
0000010 	4488 0005 		2d64 0009 		0000 		0000 			0000 0000
0000020 	4488 0005		c576 000b 		0001 		000b 			0000 0000
0000030 	4488 0005 		c576 000b 		0000 		0000 			0000 0000
0000040		448a 0005 		2456 0004 		0001 		000b 			0001 0000
0000050 	448a 0005 		2456 0004 		0000 		0000 			0000 0000
0000060 	448a 0005 		2021 0006 		0001 		000b 			0000 0000
0000070 	448a 0005 		2021 0006 		0000 		0000 			0000 0000

编号:表示第几条信息;
tv_sec :表示input_event 结构体中记录的按键发送时间秒;
tv_usec :表示input_event 结构体中记录的按键发送时间微秒;
type :事件类型,input_dev 结构体中的evbit成员变量,此次使用的是EV_KEY事件,值为1(2.3小节); 由于我们在驱动中也上报了同步事件,所以还有EV_SYN时间,值为0;

code :为事件编码,本次设置的是KEY_0事件编码(2.3小节),值为11,即0x0b;

value :按键值,为 1 表示按下,为 0 的话表示松开。

5.5 卸载驱动

# cd /lib/modules/4.1.15\+/
# rmmod keyinput.ko 

在这里插入图片描述

六 Linux原生按键驱动

6.1 分析Linux原生按键驱动

我们知道Linux系统是可以使用键盘等按键,自然按键对于linux来说也是标配的了,所以都会带有按键相关驱动程序。

早在之前的<Linux开发>驱动开发 -之-内核定时器与中断这篇文章的第6.3小节就介绍过input相关设备的位置,对于Linux原生按键驱动内容如下:

路径:drivers/input/keyboard/gpio_keys.c
.......
static const struct of_device_id gpio_keys_of_match[] = {
	{ .compatible = "gpio-keys", },
	{ },
};
MODULE_DEVICE_TABLE(of, gpio_keys_of_match);
......
static struct platform_driver gpio_keys_device_driver = {
	.probe		= gpio_keys_probe,
	.remove		= gpio_keys_remove,
	.driver		= {
		.name	= "gpio-keys",
		.pm	= &gpio_keys_pm_ops,
		.of_match_table = of_match_ptr(gpio_keys_of_match),
	}
};
........
tatic int __init gpio_keys_init(void)
{
	return platform_driver_register(&gpio_keys_device_driver);
}

static void __exit gpio_keys_exit(void)
{
	platform_driver_unregister(&gpio_keys_device_driver);
}

late_initcall(gpio_keys_init);
module_exit(gpio_keys_exit);

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

路径:drivers/input/keyboard/gpio_keys.c
tatic int gpio_keys_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);
	struct gpio_keys_drvdata *ddata;
	struct input_dev *input;
	size_t size;
	int i, error;
	int wakeup = 0;

	if (!pdata) {
		pdata = gpio_keys_get_devtree_pdata(dev);
		if (IS_ERR(pdata))
			return PTR_ERR(pdata);
	}

	size = sizeof(struct gpio_keys_drvdata) +
			pdata->nbuttons * sizeof(struct gpio_button_data);
	ddata = devm_kzalloc(dev, size, GFP_KERNEL);
	if (!ddata) {
		dev_err(dev, "failed to allocate state\n");
		return -ENOMEM;
	}

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

	ddata->pdata = pdata;
	ddata->input = input;
	mutex_init(&ddata->disable_lock);

	platform_set_drvdata(pdev, ddata);
	input_set_drvdata(input, ddata);

	input->name = pdata->name ? : pdev->name;
	input->phys = "gpio-keys/input0";
	input->dev.parent = &pdev->dev;
	input->open = gpio_keys_open;
	input->close = gpio_keys_close;

	input->id.bustype = BUS_HOST;
	input->id.vendor = 0x0001;
	input->id.product = 0x0001;
	input->id.version = 0x0100;

	/* Enable auto repeat feature of Linux input subsystem */
	if (pdata->rep)
		__set_bit(EV_REP, input->evbit);

	for (i = 0; i < pdata->nbuttons; i++) {
		const struct gpio_keys_button *button = &pdata->buttons[i];
		struct gpio_button_data *bdata = &ddata->data[i];

		error = gpio_keys_setup_key(pdev, input, bdata, button);
		if (error)
			return error;

		if (button->wakeup)
			wakeup = 1;
	}

	error = sysfs_create_group(&pdev->dev.kobj, &gpio_keys_attr_group);
	if (error) {
		dev_err(dev, "Unable to export keys/switches, error: %d\n",
			error);
		return error;
	}

	error = input_register_device(input);
	if (error) {
		dev_err(dev, "Unable to register input device, error: %d\n",
			error);
		goto err_remove_group;
	}

	device_init_wakeup(&pdev->dev, wakeup);

	return 0;

err_remove_group:
	sysfs_remove_group(&pdev->dev.kobj, &gpio_keys_attr_group);
	return error;
}

第 13行,调用 gpio_keys_get_devtree_pdata 函数从设备树中获取到 KEY 相关的设备节点信息。

第 26行,使用 devm_input_allocate_device 函数申请 input_dev。

第 39~48,初始化 input_dev。

第 52行,设置 input_dev 事件,这里设置了 EV_REP 事件。

第 58行,调用 gpio_keys_setup_key 函数继续设置 KEY,此函数会设置 input_dev 的EV_KEY 事件已经事件码(也就是 KEY 模拟为哪个按键)。

第 73行,调用 input_register_device 函数向 Linux 系统注册 input_dev。

再来看一下 gpio_keys_setup_key 函数,此函数内容如下:

路径:drivers/input/keyboard/gpio_keys.c
static int gpio_keys_setup_key(struct platform_device *pdev,
				struct input_dev *input,
				struct gpio_button_data *bdata,
				const struct gpio_keys_button *button)
{
	const char *desc = button->desc ? button->desc : "gpio_keys";
	struct device *dev = &pdev->dev;
	irq_handler_t isr;
	unsigned long irqflags;
	int irq;
	int error;

	bdata->input = input;
	bdata->button = button;
	spin_lock_init(&bdata->lock);

	if (gpio_is_valid(button->gpio)) {

		error = devm_gpio_request_one(&pdev->dev, button->gpio,
					      GPIOF_IN, desc);
		if (error < 0) {
			dev_err(dev, "Failed to request GPIO %d, error %d\n",
				button->gpio, error);
			return error;
		}

		if (button->debounce_interval) {
			error = gpio_set_debounce(button->gpio,
					button->debounce_interval * 1000);
			/* use timer if gpiolib doesn't provide debounce */
			if (error < 0)
				bdata->software_debounce =
						button->debounce_interval;
		}

		if (button->irq) {
			bdata->irq = button->irq;
		} else {
			irq = gpio_to_irq(button->gpio);
			if (irq < 0) {
				error = irq;
				dev_err(dev,
					"Unable to get irq number for GPIO %d, error %d\n",
					button->gpio, error);
				return error;
			}
			bdata->irq = irq;
		}

		INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);

		isr = gpio_keys_gpio_isr;
		irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;

	} else {
		if (!button->irq) {
			dev_err(dev, "No IRQ specified\n");
			return -EINVAL;
		}
		bdata->irq = button->irq;

		if (button->type && button->type != EV_KEY) {
			dev_err(dev, "Only EV_KEY allowed for IRQ buttons.\n");
			return -EINVAL;
		}

		bdata->release_delay = button->debounce_interval;
		setup_timer(&bdata->release_timer,
			    gpio_keys_irq_timer, (unsigned long)bdata);

		isr = gpio_keys_irq_isr;
		irqflags = 0;
	}

	input_set_capability(input, button->type ?: EV_KEY, button->code);

	/*
	 * Install custom action to cancel release timer and
	 * workqueue item.
	 */
	error = devm_add_action(&pdev->dev, gpio_keys_quiesce_key, bdata);
	if (error) {
		dev_err(&pdev->dev,
			"failed to register quiesce action, error: %d\n",
			error);
		return error;
	}

	/*
	 * If platform has specified that the button can be disabled,
	 * we don't want it to share the interrupt line.
	 */
	if (!button->can_disable)
		irqflags |= IRQF_SHARED;

	error = devm_request_any_context_irq(&pdev->dev, bdata->irq,
					     isr, irqflags, desc, bdata);
	if (error < 0) {
		dev_err(dev, "Unable to claim irq %d; error %d\n",
			bdata->irq, error);
		return error;
	}

	return 0;
}

第 76行,调用 input_set_capability 函数设置 EV_KEY 事件以及 KEY 的按键类型,也就是 KEY 作为哪个按键?我们会在设备树里面设置指定的 KEY 作为哪个按键。

准备就绪以后剩下的就是等待按键按下,然后向 Linux 内核上报事件,事件上报是在 gpio_keys_irq_isr 函数中完成的,此函数内容如下:

路径:drivers/input/keyboard/gpio_keys.c
static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)
{
	struct gpio_button_data *bdata = dev_id;
	const struct gpio_keys_button *button = bdata->button;
	struct input_dev *input = bdata->input;
	unsigned long flags;

	BUG_ON(irq != bdata->irq);

	spin_lock_irqsave(&bdata->lock, flags);

	if (!bdata->key_pressed) {
		if (bdata->button->wakeup)
			pm_wakeup_event(bdata->input->dev.parent, 0);

		input_event(input, EV_KEY, button->code, 1);
		input_sync(input);

		if (!bdata->release_delay) {
			input_event(input, EV_KEY, button->code, 0);
			input_sync(input);
			goto out;
		}

		bdata->key_pressed = true;
	}

	if (bdata->release_delay)
		mod_timer(&bdata->release_timer,
			jiffies + msecs_to_jiffies(bdata->release_delay));
out:
	spin_unlock_irqrestore(&bdata->lock, flags);
	return IRQ_HANDLED;
}

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

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

6.2 修改按键设备树

根据驱动分析,我们知道,在设备树中需要设置一些参数,具体可参考说明文档“Documentation/devicetree/bindings/input/gpio-keys.txt”,该文档最后给出了例程节点如下:

路径:Documentation/devicetree/bindings/input/gpio-keys.txt
Example nodes:

	gpio_keys {
			compatible = "gpio-keys";
			#address-cells = <1>;
			#size-cells = <0>;
			autorepeat;
			button@21 {
				label = "GPIO Key UP";
				linux,code = <103>;
				gpios = <&gpio1 0 1>;
			};
			button@22 {
				label = "GPIO Key DOWN";
				linux,code = <108>;
				interrupts = <1 IRQ_TYPE_LEVEL_HIGH 7>;
			};
			...

根据例程节点,修改原key节点:
修改前:

路径:arch/arm/boot/dts/imx6ull-water-emmc.dts
	key {	
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "water-key";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_key>;
		key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
		interrupt-parent = <&gpio1>;
		interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
		status = "okay";
	};

修改后:

路径:arch/arm/boot/dts/imx6ull-water-emmc.dts
	gpio-keys {
		compatible = "gpio-keys";
		#address-cells = <1>;
		#size-cells = <0>;
		autorepeat;
		key0 {
				label = "GPIO Key Enter";
				linux,code = <KEY_ENTER>;
				gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
			};
	};

①、节点名字为“gpio-keys”。
②、gpio-keys 节点的 compatible 属性值一定要设置为“gpio-keys”。
③、所有的 KEY 都是 gpio-keys 的子节点,每个子节点可以用如下属性描述自己:
gpios:KEY 所连接的 GPIO 信息。
interrupts:KEY 所使用 GPIO 中断信息,不是必须的,可以不写。
label:KEY 名字
linux,code:KEY 要模拟的按键,也就是include/uapi/linux/input.h中的这些按键。
④、如果按键要支持连按的话要加入 autorepeat。

第 6 行,autorepeat 表示按键支持连按。

第 7~11 行,开发板 KEY 按键信息,
第8行:名字设置为“GPIO Key Enter”,
第9行:这里我们将开发板上的 KEY 按键设置为“EKY_ENTER”这个按键,也就是回车键,效果和键盘上的回车键一样。后面分析讲解 LCD 驱动的时候需要用到此按键,因为 Linux 内核设计的 10 分钟以后 LCD关闭,也就是黑屏,就跟我们用电脑或者手机一样,一定时间以后关闭屏幕。这里将开发板上的 KEY 按键注册为回车键,当 LCD 黑屏以后直接按一下 KEY 按键即可唤醒屏幕,就跟当电脑熄屏以后按下回车键即可重新打开屏幕一样。
第10行:最后设置 KEY 所使用的 IO 为 GPIO1_IO18,一定要检查一下设备树看看此 GPIO 有没有被用到其他外设上,如果有的话要删除掉相关代码!

重新编译设备树,然后用新编译出来的 imx6ull-alientek-emmc.dtb 启动 Linux 系统,系统启动以后查看/dev/input 目录,看看都有哪些文件,结果如下图所示:
旧设备树启动后:
在这里插入图片描述
更改设备树启动后:
在这里插入图片描述

6.3 Linux原生按键驱动配置

源码的Makefile 和Kconfig配置文件如下:
在这里插入图片描述
在这里插入图片描述

我来看一下如何配置使用。
使用“make menuconfig”命令进去kernel配置界面,配置步骤如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后保存配置即可。

重新编译kernel,使用新的DTB 和kernel启动。

6.4 Linux原生按键驱动 验证

直接使用 hexdump 命令来查看
/dev/input/event1 文件内容,输入如下命令:

hexdump   /dev/input/event1

在这里插入图片描述

 /*****************input_event 类型********************/
/* 编号 */ 	/* tv_sec */ 	/* tv_usec */ 	/* type */	/* code */ 		/* value */
0000000 	54f8 0005 		1341 0000 		0001 		001c 			0001 0000
0000010 	54f8 0005 		1341 0000 		0000 		0000 			0000 0000
0000020 	54f8 0005 		d24e 0002 		0001 		001c 			0000 0000
0000030 	54f8 0005 		d24e 0002 		0000 		0000 			0000 0000
0000040 	54f8 0005 		fe3b 0009 		0001 		001c 			0001 0000
0000050 	54f8 0005 		fe3b 0009 		0000 		0000 			0000 0000
0000060 	54f8 0005 		6f3e 000c 		0001 		001c 			0000 0000
0000070 	54f8 0005 		6f3e 000c 		0000 		0000 			0000 0000
0000080 	54f9 0005 		027d 0008 		0001 		001c 			0001 0000
0000090 	54f9 0005 		027d 0008 		0000 		0000 			0000 0000
00000a0 	54f9 0005 		2545 000a 		0001 		001c 			0000 0000
00000b0 	54f9 0005 		2545 000a 		0000 		0000 			0000 0000

可看到 code 事件码为 0x1c = 28 ,对应include/uapi/linux/input.h中定义如下:

#define KEY_ENTER		28

即为设备树中设置的回车键。

6.5 其它补充

大家如果发现按下 KEY 按键以后没有反应,那么请检查一下三方面:
①、是否使能 Linux 内核 KEY 驱动。
②、设备树中 gpio-keys 节点是否创建成功。
③、在设备树中是否有其他外设也使用了 KEY 按键对应的 GPIO,但是我们并没有删除掉
这些外设信息。检查 Linux 启动 log 信息,看看是否有类似下面这条信息:

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

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

七 总结

在前面<Linux开发>驱动开发 -之-内核定时器与中断大费周章写了很多驱动代码,虽然后面不会再用到这些驱动,写这些驱动代码的目的就是为了熟悉了解内核定时器和中断等功能;在使用Linux自带的驱动中也用到了中断,为了代码的复用性以及保持input按键设备的一致性,我们后续直接使用Linux自带的驱动。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/664075.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LNMP (Nginx网站服务)

目录 1.1 Nginx的简介 1.2 Apache与Nginx的区别 Nginx对比Apache的优势&#xff1a; 1.3 Nginx的进程 Nginx的两个进程&#xff1a; 同步&#xff0c;异步&#xff0c;阻塞&#xff0c;非阻塞的概念补充 阻塞与非阻塞 同步和异步 2.1 编译安装Nginx 2.1 .1 关闭防火墙…

mysql索引优化系列(二)

一、limit优化 之前的member会员表&#xff0c;联合索引为KEY idx_name_age_address (name,age,address)&#xff0c;表里插入了十万条数据&#xff0c;一般情况下分页查询的sql语句&#xff1a; select * from member limit 90000,10; explain select * from member limit 9…

MapReduce分布式计算(二)

MapReduce工作流程 原始数据File 1T数据被切分成块存放在HDFS上&#xff0c;每一个块有128M大小 数据块Block hdfs上数据存储的一个单元,同一个文件中块的大小都是相同的 因为数据存储到HDFS上不可变&#xff0c;所以有可能块的数量和集群的计算能力不匹配 我们需要一个动态…

ASP.NET Core MVC 从入门到精通之日志管理

随着技术的发展&#xff0c;ASP.NET Core MVC也推出了好长时间&#xff0c;经过不断的版本更新迭代&#xff0c;已经越来越完善&#xff0c;本系列文章主要讲解ASP.NET Core MVC开发B/S系统过程中所涉及到的相关内容&#xff0c;适用于初学者&#xff0c;在校毕业生&#xff0c…

向内核模块中添加新功能

一、向内核添加新功能 1.1 静态加载法&#xff1a; 即新功能源码与内核其它代码一起编译进uImage文件内 新功能源码与Linux内核源码在同一目录结构下 给新功能代码配置Kconfig #进入要添加的新功能的同级目录&#xff0c;这里用添加一个名为MY_HELLO的驱动做演示#cd到linux源…

C++基础(2)——函数高级

前言 本文主要介绍了C中函数高级相关的内容 3.1&#xff1a;函数默认参数 在函数定义的时候可以给形参赋初值&#xff0c;如果函数在调用的时候有传入参数&#xff0c;就使用传入的参数&#xff0c;如果没有就用默认的。 注意事项 1&#xff1a;如果某个参数有了默认值&…

Ubuntu下载速度过慢解决

今天用Ubuntu下载Roberta文件到本地&#xff0c;速度特别慢&#xff0c;Ubuntu 系统自带的源文件&#xff0c;都是国外的源网址&#xff0c;在国内下载安装升级源或者依赖的时候&#xff0c;都比较慢&#xff0c;更换国内的源地址&#xff0c;轻松搞定此问题。 目录 一、备份…

fecth

reducx巩固知识的传送门 1.向服务器发送数据请求的方案: 第一类:XMLHttpRequest 1.ajax:自己编写请求的逻辑和步骤 2.axios:第三方库&#xff0c;对XMLHttpRequest进行封装「基于promise进行封装」 第二类: fetchES6内置的API&#xff0c;本身即使基于promise&#xff0c;用全…

一文了解NAS协议原理

一文了解NAS协议原理 一、介绍二、NAS协议结构三、NAS协议工作原理四、NAS协议的安全机制4.1、NAS协议的认证过程4.2、NAS协议的加密过程 五、总结 一、介绍 NAS协议&#xff08;Network Attached Storage Protocol&#xff09;是一种用于网络附加存储设备&#xff08;NAS&…

学习HCIP的day.16

目录 扩展知识点 一、端口镜像 SPAN 二、DHCP 三、DHCP snooping 四、端口安全 五、SSH 六、端口隔离 扩展知识点 一、端口镜像 SPAN [r1]observe-port interface GigabitEthernet 0/0/2 监控接口[r1]interface GigabitEthernet 0/0/0[r1-GigabitEthernet0/…

浅谈单元测试的那些事

Part 01 什么是单元测试 单元测试是一种软件测试方法&#xff0c;用于测试软件系统的最小可测试单元&#xff0c;例如函数、方法或类的行为。单元测试通常由开发人员编写&#xff0c;并在编写代码时就开始执行。这样可以保证实时检测代码中的错误、缺陷和潜在的问题&#xff0…

打包Java程序为.exe文件

文章目录 将Java程序打包成Jar包打包为.exe文件效果展示文件检索联系作者 ​&#x1f451;作者主页&#xff1a;Java冰激凌 接上篇 我们还未介绍到如何打包为一个.exe文件 将Java程序打包成Jar包 1. file -> Project Structure 或者直接使用全局快捷键&#xff08;Ctrl Al…

Java调用ChatGPT实现可连续对话和流式输出

目录 1. 配置阶段1.1 依赖引入1.2 配置application.yml文件1.3 注解添加 2. 使用2.1 生成回答2.1.1 测试 2.2 生成图片2.2.1 测试 2.3 下载图片2.3.1 测试 2.4 生成流式回答2.4.1 流式回答输出到IDEA控制台2.4.2 流式回答输出到浏览器页面2.4.3 流式回答结合Vue输出到前端界面 …

Docker: 改变容器化世界的革命性技术

目录 1.1什么是虚拟化 1.2什么是Docker 1.3容器与虚拟机的比较 1.4Docker组建 2、Docker安装 2.2设置ustc的镜像 2.3Docker的启动与停止 3、docker常用命令 3.1镜像 3.2容器相关命令 1.1什么是虚拟化 在计算机中&#xff0c;虚拟化&#xff08;Vitualization&#x…

这可能是最全的Web测试各个测试点,有这一篇就够了

前言 什么是Web测试&#xff1f; Web测试测试Web或Web应用程序的潜在错误。它是在上线前对基于网络的应用程序进行完整的测试。 Web测试检查 功能测试 易用性测试 接口测试 性能测试 安全测试 兼容性测试 1、功能测试 测试网页中的所有链接、数据库连接、网页中用于提交或从…

结合具体代码理解yolov5-7.0锚框(anchor)生成机制

最近对yolov5-7.0的学习有所深入&#xff0c;感觉官方代码也比较易读&#xff0c;所以对网络结构的理解更进一步&#xff0c;其中对锚框生成这块没太看明白细节&#xff0c;也想弄明白这块&#xff0c;于是前前后后好好看了代码。现在把我的学习收获做一下记录。个人见解&#…

如何在大规模服务中迁移缓存

当您启动初始服务时&#xff0c;通常会过度设计以考虑大量流量。但是&#xff0c;当您的服务达到爆炸式增长阶段&#xff0c;或者如果您的服务请求和处理大量流量时&#xff0c;您将需要重新考虑您的架构以适应它。糟糕的系统设计导致难以扩展或无法满足处理大量流量的需求&…

第三章 decimal模块

1. decimal 模块介绍 decimal 模块是 Python 提供的用于进行十进制定点和浮点运算的内置模块。使用它可以快速正确地进行十进制定点和浮点数的舍入运算&#xff0c;并且可以控制有效数字的个数。 使用 decimal 模块主要是因为它与 Python 自带的浮点数相比&#xff0c;有以下…

关于Stream流和Lambda表达式,这些技巧你都知道吗?

&#x1f4a7; 关于 S t r e a m 流和 L a m b d a 表达式&#xff0c;这些技巧你都知道吗&#xff1f; \color{#FF1493}{关于Stream流和Lambda表达式&#xff0c;这些技巧你都知道吗&#xff1f;} 关于Stream流和Lambda表达式&#xff0c;这些技巧你都知道吗&#xff1f;&…

2014年全国硕士研究生入学统一考试管理类专业学位联考写作试题

2014年1月真题: 四、写作:第56~57小题&#xff0c;共65分。其中论证有效性分析30 分&#xff0c;论说文35分。 56.论证有效性分析: 分析下述论证中存在的缺陷和漏洞&#xff0c;选择若干要点&#xff0c;写一篇600字左右的文章&#xff0c;对该论证的有效性进行分析和评论。…