输入事件子系统
我们下面来考虑一下输入事件的子系统。在这里笔者的输出如下:
cat /proc/bus/input/devices
I: Bus=0019 Vendor=0000 Product=0000 Version=0000 N: Name="20cc000.snvs:snvs-powerkey" P: Phys=snvs-pwrkey/input0 S: Sysfs=/devices/platform/soc/2000000.aips-bus/20cc000.snvs/20cc000.snvs:snvs-powerkey/input/input0 U: Uniq= H: Handlers=kbd event0 B: PROP=0 B: EV=3 B: KEY=100000 0 0 0 I: Bus=0018 Vendor=dead Product=beef Version=28bb N: Name="goodix-ts" P: Phys=input/ts S: Sysfs=/devices/virtual/input/input2 U: Uniq= H: Handlers=event1 B: PROP=3 B: EV=b B: KEY=e520 0 0 0 0 0 0 0 0 0 0 B: ABS=2658000 0 I: Bus=0019 Vendor=0001 Product=0001 Version=0100 N: Name="gpio_keys@0" P: Phys=gpio-keys/input0 S: Sysfs=/devices/platform/gpio_keys@0/input/input3 U: Uniq= H: Handlers=kbd event2 B: PROP=0 B: EV=100003 B: KEY=40000 0 0 0
目前对于我们而言比较好操作的就是Key0这个摁扭,在我们的开发板上就是key0,对应于kbd event2。
下面我们尝试做简单的编程。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <linux/input.h> #include <fcntl.h> int main(int argc, char* argv[]) { struct input_event in_env = {0}; if(argc != 2){ fprintf(stderr, "usage: %s <input-device>\n", argv[0]); return 0; } int fd = open(argv[1], O_RDONLY); if(0 > fd){ perror("open error"); exit(-1); } for(;;) { if(sizeof(struct input_event) != read(fd, &in_env, sizeof(struct input_event))) { perror("read error"); exit(-1); } printf("type: %d code %d value: %d\n", in_env.type, in_env.code, in_env.value); } }
我们把结果放到板子里跑一下:
./input /dev/input/event2
type: 1 code 114 value: 1 type: 0 code 0 value: 0 type: 1 code 114 value: 0 type: 0 code 0 value: 0 type: 1 code 114 value: 1 type: 0 code 0 value: 0 type: 1 code 114 value: 0 type: 0 code 0 value: 0 type: 1 code 114 value: 1 type: 0 code 0 value: 0 type: 1 code 114 value: 2 type: 0 code 0 value: 1
现在来看看概念
常见的输入设备有鼠标、键盘、触摸屏、遥控器、电脑画图板等,用户通过输入设备与系统进行交互。
input 子系统
由上面的介绍可知,输入设备种类非常多,每种设备上报的数据类型又不一样,那么Linux 系统如何管理呢?Linux 系统为了统一管理这些输入设备,实现了一套能够兼容所有输入设备的框架,那么这个框架就是 input 子系统。驱动开发人员基于 input 子系统开发输入设备的驱动程序,input 子系统可以屏蔽硬件的差异,向应用层提供一套统一的接口。 基于input 子系统注册成功的输入设备,都会在/dev/input 目录下生成对应的设备节点(设备文件),设备节点名称通常为 eventX(X 表示一个数字编号0、1、2、3 等),譬如/dev/input/event0、/dev/input/event1、/dev/input/event2 等,通过读取这些设备节点可以获取输入设备上报的数据。
读取数据的流程
如果我们要读取触摸屏的数据,假设触摸屏设备对应的设备节点为/dev/input/event0,那么数据读取流程 如下:
-
应用程序打开对应的设备文件,比如说在这块板子上是/dev/input/event0 设备文件;
-
应用程序发起读操作(譬如调用read),如果没有数据可读则会进入休眠(阻塞 I/O 情况下);
-
当有数据可读时,应用程序会被唤醒,读操作获取到数据返回;
-
应用程序对读取到的数据进行解析。
当无数据可读时,程序会进入休眠状态(也就是阻塞),譬如应用程序读触摸屏数据,如果当前并没有去触碰触摸屏,自然是无数据可读;当我们用手指触摸触摸屏或者在屏上滑动时,此时就会产生触摸数据、应用程序就有数据可读了,应用程序会被唤醒,成功读取到数据。那么对于其它输入设备亦是如此,无数据可读时应用程序会进入休眠状态(阻塞式I/O 方式下),当有数据可读时才会被唤醒。
应用程序如何解析数据
首先我们要知道,应用程序打开输入设备对应的设备文件,向其发起读操作,那么这个读操作获取到的是什么样的数据呢?其实每一次 read 操作获取的都是一个 struct input_event 结构体类型数据,该结构体定义在<linux/input.h>头文件中,它的定义如下:
struct input_event { struct timeval time; __u16 type; __u16 code; __s32 value; };
结构体中的 time 成员变量是一个 struct timeval 类型的变量,该结构体在前面给大家介绍过,内核会记录每个上报的事件其发生的时间,并通过变量 time 返回给应用程序。时间参数通常不是那么重要,而其它3 个成员变量type、code、value 更为重要。
type:type 用于描述发生了哪一种类型的事件(对事件的分类)
value:内核每次上报事件都会向应用层发送一个数据 value,对value 值的解释随着code 的变化而变化。譬如对于按键事件(type=1)来说,如果 code=2(键盘上的数字键 1,也就是KEY_1),那么如果value 等于1,则表示KEY_1 键按下;value 等于0 表示KEY_1 键松开,如果value 等于2则表示KEY_1 键长按。再比如,在绝对位移事件中(type=3),如果code=0(触摸点X 坐标ABS_X),那么 value 值就等于触摸点的 X 轴坐标值;同理,如果 code=1(触摸点 Y 坐标 ABS_Y),此时value 值便等于触摸点的Y 轴坐标值;所以对 value 值的解释需要根据不同的 code 值而定!
数据同步
同步事件用于实现同步操作、告知接收者本轮上报的数据已经完整。应用程序读取输入设备上报的数据时,一次read 操作只能读取一个struct input_event 类型数据,譬如对于触摸屏来说,一个触摸点的信息包含了 X 坐标、Y 坐标以及其它信息,对于这样情况,应用程序需要执行多次read 操作才能把一个触摸点的信息全部读取出来,这样才能得到触摸点的完整信息。 那么应用程序如何得知本轮已经读取到完整的数据了呢?其实这就是通过同步事件来实现的,内核将本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据已经完整、可以进行同步了。
下一篇我们来尝试一下对触摸屏进行编程!