一、知识点
1. 什么是输入子系统?
Input 子系统是 Linux 内核中专门为输入设备设计的一个子系统,它提供了一个通用的框架来管理各种输入设备,如键盘、鼠标、触摸屏、游戏手柄等。这个子系统的主要目的是简化和规范化输入设备驱动的开发,同时提高驱动的通用性和兼容性。
●具体来说:
(1)输入设备:如键盘、鼠标、触摸屏、游戏手柄等,它们是用户与计算机进行交互的主要方式。
(2)输入设备驱动的复杂性:传统上,为每种输入设备编写驱动都需要创建文件、进行硬件初始化、实现文件操作函数集和处理中断等。每种设备的驱动可能都有许多重复性的工作。
(3)Input 子系统的目的:Linux 的 Input 子系统就是为了简化这种重复性的开发工作。它将输入设备驱动中的共性部分提取出来,形成一个通用的框架,开发者只需关注差异化的部分。这样,不仅降低了驱动开发的难度,也提高了驱动的通用性和兼容性。
●通俗讲:
我们在编写输入设备驱动时:假设我们写一个键盘驱动,需要创建文件,硬件初始化,实现文件操作集函数,中断等等,非常麻烦。假设我们再写一个鼠标驱动,同样需要创建文件,硬件初始化,实现文件操作集函数,中断等等操作,非常麻烦。如果我们再写一个输入设备驱动呢?
这些输入设备驱动当中是有共同点的,共同点就是获取数据,上报给用户。所以Linux就将通用的代码编写好,将差异化的代码留给驱动开发工程师来编写。从而出现了input子系统。因此,Input 子系统极大地简化了输入设备驱动的开发,使得开发者能够专注于设备特有的功能,而不必重复编写通用的代码。
2. 输入子系统的作用
(1)兼容所有的输入设备:Input 子系统为各种输入设备(如键盘、鼠标、触摸屏等)提供了统一的接口,使得这些设备能够通过标准化的方式与系统进行交互。不同厂家的设备,只要遵循 Input 子系统的规范,就能保证在 Linux 系统中正常工作。
(2)统一的驱动编程方式:Input 子系统提供了一套标准化的框架,使得开发者只需按照这个框架编写驱动,而不需要为每种设备编写完全不同的代码。这避免了因为不同开发者使用不同的编码方式导致的兼容性问题,也降低了驱动开发的复杂性。
(3)统一的应用操作接口:Input 子系统提供了一致的设备节点(通常位于 /dev/input
目录下),应用程序可以通过这些统一的接口来访问和操作输入设备,而不需要关心设备的具体实现。这大大简化了应用程序的开发和维护。
通过这些特性,Input 子系统不仅提高了输入设备驱动的兼容性和移植性,也简化了应用程序对这些设备的访问方式,增强了系统的稳定性和一致性。
3. 输入子系统框架
(1)Input 子系统包括三个层次,分别是设备驱动层、核心层、事件处理层。
设备驱动层:设备驱动层可以通过获取设备树中硬件的信息,对硬件各寄存器的读写访问和将底层硬件的状态变化转换为标准的输入事件,将相应事件上报,再通过核心层提交给事件处理层。
核心层:用于将设备驱动层和事件处理层进行匹配,处理输入事件的分发和管理,是输入子系统的核心部分。这部分由内核工程师来编写,不需要我们自己编写。
事件处理层:这一层是直接与应用程序交互的部分。事件处理层负责将核心层生成的输入事件传递给系统的高层应用,并确保这些事件被正确处理。应用程序通过这一层接收用户输入(如点击、键盘按键等),并据此进行相应的操作。这部分由内核或设备厂商来实现,也不需要我们编写。
那么对于一个输入子系统驱动程序,我们只需要调用内核实现的接口来编写设备驱动层即可。核心层,和事件处理层的代码不需要我们来编写。
(2)重要结构体
●设备驱动层结构体:struct input_dev
struct input_dev {
const char *name; // 设备名称,例如 "Keyboard" 或 "Mouse"
const char *phys; // 设备在系统中的物理路径,例如 "usb-0000:00:14.0-1/input0"
const char *uniq; // 设备的唯一标识符,通常用于匹配特定硬件
struct input_id id; // 包含设备识别信息的结构体(例如供应商ID、产品ID、版本号)
// 属性位图,用于表示设备支持的属性类型
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
// 事件位图,用于表示设备支持的事件类型
unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
// 键位图,用于表示设备支持的按键类型
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];
// 相对位图,用于表示设备支持的相对轴事件。 例如鼠标
unsigned long relbit[BITS_TO_LONGS(REL_CNT)];
// 绝对位图,用于表示设备支持的绝对轴事件,例如触摸屏
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];
// 杂项位图,用于表示设备支持的其他事件类型
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
// 指示灯位图,用于表示设备支持的LED灯类型
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
// 声音位图,用于表示设备支持的声音类型
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; // 每个数据包中的建议事件数量
unsigned int keycodemax; // 最大按键码数量
unsigned int keycodesize; // 每个按键码的大小
void *keycode; // 指向按键码数据的指针
// 设置按键码的函数指针
int (*setkeycode)(struct input_dev *dev,
const struct input_keymap_entry *ke,
unsigned int *old_keycode);
// 获取按键码的函数指针
int (*getkeycode)(struct input_dev *dev,
struct input_keymap_entry *ke);
struct ff_device *ff; // 力反馈设备的指针
unsigned int repeat_key; // 重复按键
struct timer_list timer; // 用于处理重复按键的定时器
int rep[REP_CNT]; // 用于存储重复延迟和重复率
struct input_mt *mt; // 多点触控相关信息的指针
struct input_absinfo *absinfo; // 绝对轴相关信息的指针
// 当前设备状态的位图(按键、指示灯、声音、开关)
unsigned long key[BITS_TO_LONGS(KEY_CNT)];
unsigned long led[BITS_TO_LONGS(LED_CNT)];
unsigned long snd[BITS_TO_LONGS(SND_CNT)];
unsigned long sw[BITS_TO_LONGS(SW_CNT)];
// 打开设备的函数指针
int (*open)(struct input_dev *dev);
// 关闭设备的函数指针
void (*close)(struct input_dev *dev);
// 刷新设备的函数指针
int (*flush)(struct input_dev *dev, struct file *file);
// 处理事件的函数指针
int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
struct input_handle __rcu *grab; // 用于处理独占设备的指针
spinlock_t event_lock; // 用于保护事件处理的自旋锁
struct mutex mutex; // 设备访问的互斥锁
unsigned int users; // 使用该设备的用户数量
bool going_away; // 标志设备是否正在关闭
struct device dev; // 设备的基础信息
struct list_head h_list; // 处理句柄的链表
struct list_head node; // 设备的链表节点
unsigned int num_vals; // 当前输入值的数量
unsigned int max_vals; // 最大输入值的数量
struct input_value *vals; // 输入值数组的指针
bool devres_managed; // 标志设备资源是否由设备资源管理器管理
};
其中evbit
成员可选事件类型如下:
EV_SYN //同步事件
EV_KEY //按键事件
EV_REL //相对坐标事件:比如说鼠标
EV_ABS //绝对坐标事件:比如触摸屏
EV_MSC //杂项事件
EV_SW //开关事件
EV_LED //指示灯事件
EV_SND //音效事件
EV_REP //重复按键事件
EV_FF //力反馈事件
EV_PWR //电源事件
EV_FF_STATUS //力反馈状态事件
●事件处理层结构体:input_handler
struct input_handler {
void *private; // 私有数据指针,用于存储与特定处理程序相关的上下文信息
// 用于向应用层上报数据
// 处理单个输入事件的函数指针
void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
// 处理多个输入事件的函数指针
void (*events)(struct input_handle *handle,
const struct input_value *vals, unsigned int count);
// 过滤输入事件的函数指针,返回值决定事件是否应传递给上层
bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
//用于和设备驱动层进行匹配
// 匹配输入设备的函数指针,判断处理程序是否与设备匹配
bool (*match)(struct input_handler *handler, struct input_dev *dev);
//与设备驱动层匹配后,执行的函数
// 连接输入设备的函数指针,当设备被识别并准备好处理时调用
int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
// 断开输入设备的函数指针,当设备不再被使用时调用
void (*disconnect)(struct input_handle *handle);
// 启动输入设备的函数指针,在设备连接之后调用
void (*start)(struct input_handle *handle);
bool legacy_minors; // 标志处理程序是否使用传统的次设备号
int minor; // 处理程序的次设备号,用于设备编号
const char *name; // 处理程序名称,用于标识该输入处理程序
const struct input_device_id *id_table; // 设备ID表,定义处理程序支持的设备类型
struct list_head h_list; // 句柄链表节点,用于将处理程序的所有句柄连接到链表中
struct list_head node; // 处理程序链表节点,用于将处理程序挂载到处理程序链表中
};
●struct input_handle结构体
作用:当事件处理层和设备驱动层匹配成功后,会在内核空间中产生该结构体对象。用于记录匹配的双方的信息。在设备驱动层上报事件时,就会通过该结构体找到与之匹配的事件处理层对象,并完成数据的上报。
struct input_handle {
void *private; // 私有数据指针,通常用于存储与具体设备相关的上下文信息
int open; // 设备是否已打开的标志(0 表示关闭,非 0 表示打开)
const char *name; // 句柄名称,用于标识输入句柄的字符串
struct input_dev *dev; // 指向输入设备结构体的指针,用于关联该句柄所对应的设备
struct input_handler *handler; // 指向输入处理程序结构体的指针,用于关联该句柄的处理程序
struct list_head d_node; // 设备链表节点,用于将该句柄挂载到设备链表中
struct list_head h_node; // 处理程序链表节点,用于将该句柄挂载到处理程序的链表中
};
(3)重要链表
●管理输入设备链表:input_dev_list
,该链表中包含input输入子系统中的所有输入设备,当使用input_register_device() 向内核注册input设备时,所注册的设备会被添加到该链表中。
●管理输入处理事件链表:input_handler_list
,该链表中包含input输入子系统的handler事件处理层,当使用input_register_handler() 注册一个handler时,会将该handler添加到该链表中。
使用下面的代码定义这两个链表。
static LIST_HEAD(input_dev_list); //dev链表
static LIST_HEAD(input_handler_list); //handler链表
在调用注册函数向内核注册输入设备或处理事件时,在注册函数的内部会调用下面的函数来添加到链表中。
list_add_tail(&dev->node, &input_dev_list);
list_add_tail(&handler->node, &input_handler_list);
4. 设备驱动层和事件处理层匹配过程
设备驱动层和事件处理层进行匹配时,会遍历对方的链表来进行匹配。即当一个输入设备注册时,系统会遍历 input_handler_list 链表,查看哪些事件处理器可以匹配这个输入设备。当一个事件处理器注册时,系统会遍历 input_dev_list 链表,查看有哪些输入设备适合这个事件处理器。
当双方匹配成功后,会在内核空间生成一个新的对象,即input_handle
。它会记录匹配的双方,并且内核将 input_handle 添加到 input_dev 和 input_handler 各自的链表中。这使得一个事件处理器可以处理多个输入设备,一个输入设备也可以与多个事件处理器关联。
5. 如何确定输入设备与设备节点的对应关系
我们进入/dev/input
目录下,查看当前存在的设备节点,如下图所示:
(1)设备节点名称
input 子系统的设备节点名称是有规律的。可以分为通用设备名和专用设备名。
专用设备名:从名字上可以看出设备是什么。比如mouse0、mouse1是鼠标的设备节点。
通用设备名:从名字上看不出设备是什么,例如event0、event1、event2等。
(2)如何判断哪个设备对应哪个节点呢?
答:可以通过下面两种方法。
①试探法:通过cat
命令来查看设备节点,当我们操作输入设备时,如果某个设备节点有内容输出,则该设备对应该设备节点。
具体操作如下所示,我们查看鼠标设备节点:当我们使用cat命令来查看mouse1设备节点时,我们移动鼠标,发现并没有任何信息输出,则说明操作的鼠标不对应该设备节点。当我们cat命令查看mouse0设备节点时,发现有内容输出,则该鼠标对应mouse0设备节点。
解决上述乱码问题,我们可以使用hexdump
命令,将二进制文件内容转为其他任意格式输出,当不添加参数时,默认输出为十六进制格式,具体使用查看其他博客。
②查看输入设备信息法:cat /proc/bus/input/devices
,该文件记录了当前系统的所有输入设备的信息。如下图所示:
二、设备驱动层代码编写及流程
1. 代码编写流程
(1)创建input_dev结构体变量。
(2)填充结构体内容,例如name、事件类型、具体事件等。
(3)设备注册:通过 input_register_device()
将设备注册到 Input 子系统。
(4)事件上报:输入事件通过 input_event()
上报给用户空间。
(5)设备注销:在驱动出口函数中,注销设备。
2. 事件设置框架图
在设备驱动层代码中,需要描述输入设备能产生什么类型的数据,这个类型的数据要产生什么具体的事件,然后值是什么。因为不同的输入设备产生的数据类型不同,比如键盘,触摸屏。具体可选事件类型查看本文上述结构体中的内容。
在代码中可以通过 __set_bit()
设置相应的位,可以表示设备支持的事件类型。
3. 编写一个最简单的设备驱动层代码(不包含上报事件)
input_dev.c
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
struct input_dev *myinput_dev; //输入设备
static int __init myinput_dev_init(void)
{
int ret;
myinput_dev = input_allocate_device(); //用于为即将注册的输入设备分配内存和资源。
if (myinput_dev == NULL) {
printk(KERN_ERR "input_allocate_device failed\n");
return -ENOMEM;
}
// 设置设备名称
myinput_dev->name ="myinput_dev";
// 设置支持的事件类型
set_bit(EV_KEY, myinput_dev->evbit); // 设置支持按键事件
set_bit(KEY_1, myinput_dev->keybit); // 设置支持的按键(KEY_1 是一个示例按键)
// 注册设备
ret = input_register_device(myinput_dev);
if (ret < 0) {
printk(KERN_ERR "input_register_device failed: %d\n", ret);
return ret;
}
return 0;
}
static void __exit myinput_dev_exit(void)
{
input_unregister_device(myinput_dev); // 当设备成功注销时,使用该函数注销设备
input_free_device(myinput_dev); //使用该函数释放输入设备分配的内存。
}
// 模块初始化和退出函数
module_init(myinput_dev_init);
module_exit(myinput_dev_exit);
MODULE_LICENSE("GPL");
●问题:如何理解 __set_bit(EV_KEY, myinput_dev->evbit);
答:evbit为事件类型。evbit是一个长度为 EV_CNT 的数组,可以通过将对应的位设置为1表示支持该事件类型。
●问题:如何理解 __set_bit(KEY_1, myinput_dev->keybit);
答:设置完支持的事件类型后,我们就需要设置该事件类型的具体事件。例如按键1事件:keybit 是一个长度为 KEY_CNT的数组,可以通过将对应的位设置为1,表示支持具体事件。
4. 完整设备驱动层代码(包含内核上报事件)
例如:在开发板上我们可以通过按键按下来触发中断,在中断处理函数中上报相应的事件,但是由于博主身边没有开发板,所以本节,我们使用内核定时器来进行定时上报事件。文章:内核定时器知识点。在驱动程序中,当我们上报完数据后,要上报一个“同步”事件,表示数据上报完毕。
★问题:为什么我们要上报一个“同步事件”作为数据上报结束的标志呢?
答:上报“同步事件”的目的是为了确保数据能够及时从内核空间传递到应用层。在我们内核上报数据时,上报的数据包会存储在内核空间的一个buf缓冲区(f数组)中,而不是立即传递到应用层。如果没有“同步事件”,应用层无法立即读取这些数据。只有当缓冲区被填满时,内核才会将所有数据一次性传递到应用层。这会导致数据延迟,甚至可能导致应用层无法实时处理事件。例如,我们按下按键1时,在应用层读取不到数据包。按下按键2时,也读取不到…当我们按下一堆按键时,这时buff被存满,之前被存储的数据包一次性被读了出来,这是我们不想看到的结果,所以我们需要每上报完一次数据,就发送一次同步事件。
●上报事件函数:
void input_event(struct input_dev *dev, unsigned int type,
unsigned int code, int value)
//struct input_dev *dev :输入设备结构体。
//unsigned int type:事件类型。
// unsigned int code :具体事件。
//int value :事件的值。
具体代码: input_dev.c
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
struct input_dev *myinput_dev; //输入设备结构体
static struct timer_list my_timer;
void my_timer_callback(unsigned long data) //定时器超时函数
{
static int key_value=0; //变量定义必须使用static描述
input_event(myinput_dev, EV_KEY, KEY_L, key_value); //上报按键类型的按键L事件,以及键值
input_event(myinput_dev, EV_KEY, KEY_S, key_value); //上报按键类型的按键S事件,以及键值
input_event(myinput_dev, EV_KEY, KEY_ENTER, key_value); //上报按键类型的按键ENTER事件,以及键值.
input_event(myinput_dev, EV_SYN, SYN_REPORT, 0); //上报完事件后,要使用该命令表示上报结束。
key_value=!key_value; //模拟按键按下,电平转换。
mod_timer(&my_timer, jiffies + msecs_to_jiffies(2000)); // 重新设置定时器,定时器将在1秒后再次触发
}
static int __init myinput_dev_init(void)
{
int ret;
myinput_dev = input_allocate_device(); //用于为即将注册的输入设备分配内存和资源。
if (myinput_dev == NULL) {
printk(KERN_ERR "input_allocate_device failed\n");
return -ENOMEM;
}
// 设置设备名称
myinput_dev->name ="myinput_dev";
// 设置支持的事件类型
set_bit(EV_KEY, myinput_dev->evbit); // 设置支持按键事件
set_bit(EV_SYN, myinput_dev->evbit); // 设置支持同步事件
set_bit(KEY_L, myinput_dev->keybit); // 设置支持的按键(KEY_L是一个示例按键)
set_bit(KEY_S, myinput_dev->keybit); // 设置支持的按键(KEY_S 是一个示例按键)
set_bit(KEY_ENTER, myinput_dev->keybit); // 设置支持的按键(KEY_ENTER 是一个示例按键)
// 注册设备
ret = input_register_device(myinput_dev);
if (ret < 0) {
printk(KERN_ERR "input_register_device failed: %d\n", ret);
return ret;
}
//初始化定时器内容
my_timer.function = my_timer_callback;
my_timer.expires = jiffies + msecs_to_jiffies(2000);
init_timer(&my_timer); // 初始化定时器
add_timer(&my_timer); // 添加定时器
return 0;
}
static void __exit myinput_dev_exit(void)
{
input_unregister_device(myinput_dev); // 当设备成功注销时,使用该函数注销设备
input_free_device(myinput_dev); //使用该函数释放输入设备分配的内存。
del_timer(&my_timer); // 删除定时器。
}
// 模块初始化和退出函数
module_init(myinput_dev_init);
module_exit(myinput_dev_exit);
MODULE_LICENSE("GPL");
将代码编译为模块加载到内核,我们可以通过hexdump
命令来查看设备节点的输出信息来判断是否上报了事件。如果将模块加载到Ubuntu_x86系统上,我们可以使用Ctrl+Alt+F1
进入虚拟终端查看输出,最后使用Ctrl+Alt+F7
退出虚拟终端。如果是在开发板上,我们可以使用exec 0</dev/tty1
来查看效果。
虚拟终端效果如下:
三、上报数据格式分析
要想分析上报数据的内容,我们首先要明白每次上传数据包的大小。即分析struct input_event
结构体所占字节大小。不同位的操作系统下,结构体所占字节数大小不一样,具体大小要根据实际的操作系统而定。本文使用结构体如下:
根据下面的分析,可知该结构体数据包在64位操作系统下所占大小为24个字节。
struct input_event {
struct timeval time; // 事件发生的时间,在64位系统上总占16字节。
__u16 type; // 事件类型 ,u16无论多少位的系统都占 2字节
__u16 code; // 事件代码 ,u16无论多少位的系统都占 2字节
__s32 value; // 事件值 ,u32无论多少位的系统都占 4字节
};
/*
struct timeval {
__kernel_time_t tv_sec; //long类型,64位系统占8字节,32位系统占4字节。
__kernel_suseconds_t tv_usec; //long类型,64位系统占8字节,32位系统占4字节。
};
*/
根据上报数据,拆解数据包。每使用input_event()
上报一次数据,就会发送一次数据包,我们可以根据数据包的内容来分析、调试代码,查看代码上传数据是否正确。
四、应用层获取上报事件
APP读取数据时,可以得到一个或多个数据,比如一个触摸屏的一个触点会上报X、Y位置信息,也可能会上报压力值。
APP怎么知道它已经读到了完整的数据?
驱动程序上报完一系列事件后,会上报一个“同步”事件,表示数据上报完毕。APP读到“同步事件”时,就知道已经读完了当前的数据。同步事件也是一个input_event结构体,它的type、code、value三项都是0。
app.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
int main()
{
int fd;
int ret;
struct input_event my_event;
fd = open("/dev/input/event5", O_RDWR);
if (fd < 0) {
perror("open error");
return -1;
}
while (1) {
ret = read(fd, &my_event, sizeof(struct input_event)); //获取数据包
if (ret < 0) {
perror("read error");
close(fd);
return -2;
}
if (my_event.type == EV_KEY && my_event.code == KEY_1) { //解析数据包完成相应动作
if (my_event.value == 1) {
printf("value=1\n");
} else if (my_event.value == 0) {
printf("value=0\n");
}
}
}
close(fd); // 关闭文件描述符
return 0;
}
我们要使用管理员来运行此程序,因为打开的节点为根目录下的节点。