输入子系统的介绍
输入子系统不仅仅是指单纯的硬件设备,它实际上是一个完整的处理流程,涉及从硬件信号的捕获到事件的生成、分发,再到最终的事件处理。这个子系统包括了硬件设备(鼠标,键盘,摇杆,麦克风,触摸屏等)、驱动程序、内核中的处理逻辑,以及与用户空间的接口。
详细过程
-
用户操作:用户在输入设备(如键盘、鼠标、触摸屏等)上执行一系列操作。例如,按下一个键、移动鼠标或点击触摸屏。
-
驱动程序捕获操作:输入设备的驱动程序捕获这些操作。驱动程序是专门为特定硬件设备编写的软件,能够识别设备并且理解设备发出的信号。它将这些物理操作转换为标准化的输入信号。
-
事件生成和封装:驱动程序将捕获的信号封装成输入事件。例如,当用户按下一个键时,驱动程序生成一个“键按下”的事件,包含了按下的键码等信息。
-
事件分发:操作系统的输入子系统核心部分接收到这些事件后,会根据事件的类型和目的,将它们分发给相应的处理程序。这可能包括内核中的其他模块,或直接发送给用户空间的应用程序。
-
应用程序处理事件:应用程序通过API或系统调用来获取和处理这些输入事件。应用程序根据事件的内容,执行相应的操作。例如,当应用程序检测到用户按下某个键时,可能会在屏幕上显示对应的字符,或者触发某个功能。
-
响应用户操作:最终,应用程序根据输入事件的处理结果,对用户的操作做出响应,这可能是更新用户界面、执行某个任务,或触发更复杂的逻辑。
以触摸屏为例,当手指在屏幕上滑动的时候,数据流大致是这样的:驱动层中的触摸屏驱动会源源不断地产生触摸屏相关数据,并向上递送给内核输入子系统,输入子系统进一步将这些信息规整为统一的结构体,并借助事件触发层发往对应的设备节点,至此,应用程序即可从这些设备节点读取相关信息。
作为嵌入式软件工程师我们要做什么事情???????
嵌入式软件开发的重点通常放在事件触发层及其上层逻辑上,而更底层的事件生成、封装和捕获通常由驱动工程师来处理。我们更加关注的是访问到设备节点,利用操作系统提供给我们的接口来获取设备对应的事件,并对其进行处理。
输入信息input_event
结构体
在最靠近应用程序的事件触发层上,内核所获知的各类输入事件,比如键盘被按了一下,触摸屏被滑了一下等,都将被统一封装在一个叫做 input_event
的输入信息结构体当中,这个结构体定义如下:
struct input_event {
struct timeval time; /* 事件发生的时间戳 */
__u16 type; /* 事件的类型,例如EV_KEY, EV_REL, EV_ABS等 */
__u16 code; /* 事件的代码,具体取决于事件类型 */
__s32 value; /* 事件的值,取决于事件类型和代码 */
};
time字段:表示事件发生的时间
struct timeval
{
__time_t tv_sec; // 秒
long int tv_usec; // 微秒(1微秒 = 10-3毫秒 = 10-6秒)
};
type字段:事件的类型。类型字段指示事件的种类,常见的类型包括
code字段:事件的代码。代码字段具体化了事件的类型。例如:
这个 事件代码 用于对事件的类型作进一步的描述。比如:当发生EV_KEY事件时,则可能是键盘被按下了,那么究竟是哪个按键被按下了呢?此时查看code就知道了。当发生EV_REL事件时,也许是鼠标动了,也许是滚轮动了。这时可以用code的值来加以区分。
value字段:事件的值。值字段包含与事件相关的数据,例如:
用途:提供事件的实际数据,帮助应用程序做出响应
确定触摸屏对应的字符设备文件
/dev目录的介绍
我们要想捕获触摸屏上的一系列动作,肯定首先要做的事情就是打开触摸屏对应的字符设备文件。
我们都知道/dev目录是一个虚拟文件系统,用于提供设备节点。这些设备节点是特殊的文件,用于与系统中的硬件设备进行交互。/dev
目录中的文件和目录实际上并不占用物理存储空间,而是代表了设备文件,允许用户空间程序和内核驱动程序之间进行数据交换和控制。
/dev
目录下的文件大多数都是一些设备文件,目的是提供与系统中各种硬件设备的接口,使得用户空间程序可以方便地与这些设备进行数据交流和控制。
确定设备对应的设备文件
/dev/input
目录下的文件通常是输入设备文件,用于表示系统中的各种输入设备,里面对应有鼠标,键盘,触摸屏等设备文件。我们的触摸屏对应设备文件就在这里,但是当我们打开这个目录就会发现:
里面有很多的设备文件,我们应该如何确定我们的触摸屏具体对应哪个设备文件呢?答案是:我们可以挨个尝试,比如cat /dev/input/event0 查看这个文件,然后手指在触摸屏上面滑动,此时如果触摸屏正好对应这个设备文件,那么就会产生一系列的输出(此时输出的是乱码,后面需要将数据解析成 input_event
结构体并以可读的方式显示才能知道输出的含义)。
可以这样检测的原因是字符设备文件拥有实时性的特点,因为字符设备文件直接与硬件设备交互,cat
命令会实时显示设备产生的数据,提供了一种快速查看设备事件的方式。
介绍触摸屏传递的信息
对于触摸屏而言,该设备会产生三种数据:
- X轴坐标值
- Y轴坐标值
- P压力值
理论上来说,从手指放上屏幕开始,到滑动一段距离,离开屏幕结束,会产生如下所示的一系列数据:
(X Y P1) SYN (X Y) SYN (X Y) SYN (X Y) ... ... (X Y) SYN (P2) SYN
有如下地方需要注意:
- 只有在刚开始的第一个坐标值和最后一个坐标值后面,会读到压力值P,P1是手指刚落下时,产生压力值大于0的数据,P2是手指离开时,产生压力值等于0的数据
- 应用层并不能保证能严格交替读取
(X,Y)
坐标值,有时它们会由于异步等原因出现断续,例如:
... (X) (X) (X Y) SYN (Y)
- 若手指滑动的方向刚好垂直与坐标轴,会导致其中一个维度的坐标值不变,那么也可能会导致只出现一个维度坐标值的情形,例如:
... (X) SYN (X) SYN (X Y) SYN (Y) ... ... (X Y) SYN ...
这种情况下如果我们很想要将x和y都完整地打印出来,我们可以每次都保存最近的x,y坐标对,如果后面检测到x,y不成对情况而是只出现x或只出现y,那么我们就可以手动地将最近保存的完整成对坐标的x或者y补充打印,从而依然实现x,y成对出现。
区分非严格交替的坐标读取和滑动方向与坐标轴平行两种情况:
由于非严格交替由于异步原因,连续几组事件打印的坐标之间通常没有 SYN(
同步事件)存在,但滑动方向与坐标轴平行,中每组事件之间会有SYN
存在。
所以通过打印和观察 SYN
(EV_SYN
)事件,你可以有效区分是因为非严格交替的坐标读取导致的连续坐标事件,还是因为滑动方向与坐标轴平行导致的单一坐标变化。在调试阶段,我们通常会选择将EV_SYN
事件也打印出来。
读取触摸屏上的信息(代码)
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <linux/input.h> // 系统定义输入设备操作的API
int main(int argc, char const *argv[])
{
int tp = open("/dev/input/event0", O_RDWR);
struct input_event buf;
while(1)
{
bzero(&buf, sizeof(buf));
read(tp, &buf, sizeof(buf));
if(buf.type==EV_SYN)
{
printf("-------- SYN --------\n");
}
if(buf.type == EV_KEY && buf.code == BTN_TOUCH && buf.value > 0)
{
printf("%s\n","press on");
}
if(buf.type == EV_KEY && buf.code == BTN_TOUCH && buf.value == 0)
{
printf("%s\n","press off");
}
if(buf.type == EV_ABS && buf.code == ABS_X)
{
printf("x: %d\n", buf.value);
}
if(buf.type == EV_ABS && buf.code == ABS_Y)
{
printf("y: %d\n", buf.value);
}
}
return 0;
}
这里x坐标,y坐标,检测按下这三个事件都是单独的事件,不会同时进行检测的,这是三个不同的事件,依次进行的检测,我们根据上面的结果也可以看出来事件的处理方向也是 下面这样的,先输出了一对下x,y坐标,后面再输出了press on( P1)。
(X Y P1) SYN (X Y) SYN (X Y) SYN (X Y) ... ... (X Y) SYN (P2) SYN
需要注意的一点是:我们每次只能读取到一个值,x坐标的值或者y坐标的值,不能一次性将这一对坐标x和y值都读出来,X 和 Y 坐标的变化会被分别记录为独立的事件,这是由驱动来决定的。
从我们的input_event结构体中,我们也能看出来一个事件对应一个结构体,因为一个结构体变量也只能表示一个坐标的变化。
-
事件缓存:通常,最上层应用程序会在内存中缓存读取到的坐标值,然后在接收到
EV_SYN
事件时,将最新的 X 和 Y 值组合成一个坐标对进行处理。这样可以确保在应用层能够同时处理一对(X, Y)
坐标值。 -
同步机制:
EV_SYN
事件用于标识一组事件的结束。在接收到EV_SYN
后,应用程序可以认为所有的 X 和 Y 坐标值已经更新,并进行相应的处理。
这是事件驱动系统的一种常见机制,用于确保输入处理的灵活性和实时性。
触摸屏生成事件流程总结
首先说一下我们刚刚跑程序的触摸屏事件生成概述
按下触摸屏的时候设备会产生两个EV_ABS
事件分别来报告 X 和 Y 坐标,以及一个EV_KEY
事件(如 BTN_TOUCH
),表示触摸点的按下。这几个事件会被 EV_SYN
事件标记为一个完整的事件组。
在滑动的时候设备会一直产生EV_ABS事件来报告坐标位置的改变,并且一组EV_ABS事件(1个/2个)会被 EV_SYN
事件标记为一个完整的事件组。
在松开触摸屏幕的时候设备会产生一个 EV_KEY
事件(如 BTN_TOUCH
)来表示触摸点的释放,且被 EV_SYN
事件标记为一个完整的事件组。
不同触摸屏类型不一样,驱动的程序实现也不一样,就可能导致总体的触摸屏事件生成过程和我们上面介绍的不一样,可能有的触摸屏在按下的时候不会生成EV_KEY
事件,有的触摸屏在松开的时候也会产生两个EV_ABS
事件分别来报告 X 和 Y 坐标。由于这些不同,我们在实际操作不同触摸屏的时候,要先了解触摸屏事件生成过程。
根据我查阅的相关资料,对于标准触摸屏的操作过程中,设备生成的事件可以总结如下:
- 按下时,生成
EV_ABS
事件报告初始坐标,并生成EV_KEY
事件表示触摸点按下。 - 移动时,生成
EV_ABS
事件报告更新的坐标。 - 松开时,生成
EV_KEY
事件表示触摸点抬起。
每个事件组都会以 EV_SYN
事件结束,以通知系统完成了一次事件的处理。这一系列事件的生成使得系统能够精确地跟踪触摸屏的操作,并将这些信息传递给应用层进行处理。
封装触摸屏单机事件(练习)
既然要封装单击事件,我们肯定要首先定义一下什么叫做单击事件,这里我们定义的单击事件是按压屏幕,然后再松开屏幕,并且按下和松开屏幕的地方必须十分接近。对于按下屏幕,然后滑动手指在另一个地方松开屏幕是不算做一次单击的。这种类型单击在我们的日常生活中运用的应该是最多的。
整体代码思路:保存按压屏幕的第一个坐标,然后一直更新记录手指滑动时的坐标,在手指离开屏幕的时候比较这两个坐标是否接近,从而判断是否发生了一次单击。判断后不论是否发生了单击都重置flag_first变量的值,程序接着运行。
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <linux/input.h> // 系统定义输入设备操作的API
int main(int argc, char const *argv[])
{
int tp = open("/dev/input/event0", O_RDWR);
struct input_event buf;
int first_x , first_y , second_x , second_y;
bool flag_first=false ;
while(1)
{
bzero(&buf, sizeof(buf));
read(tp, &buf, sizeof(buf));
//只保留手指的第一次触点
if(buf.type == EV_ABS && buf.code == ABS_X && !flag_first)
{
second_x=first_x=buf.value;
}
if(buf.type == EV_ABS && buf.code == ABS_Y && !flag_first)
{
second_y=first_y=buf.value;
flag_first=true;
}
//移动过程不断保存最新手指落点
if(buf.type == EV_ABS && buf.code == ABS_X)
{
second_x=buf.value;
}
if(buf.type == EV_ABS && buf.code == ABS_Y)
{
second_y=buf.value;
}
if(buf.type == EV_KEY && buf.code == BTN_TOUCH && buf.value > 0)
{
printf("%s\n","press on");
}
//手指拿开的时候检测最新落点和第一次落点之间的差距,进而判断是否属于单击
if(buf.type == EV_KEY && buf.code == BTN_TOUCH && buf.value == 0)
{
printf("%s\n","press off");
if(abs(first_x-second_x) < 10 && abs(first_y-second_y) < 10){
printf("%s\n","单击");
}
flag_first=false;
}
}
return 0;
}
双击退出程序(拓展)
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <linux/input.h> // 系统定义输入设备操作的API
int main(int argc, char const *argv[])
{
int tp = open("/dev/input/event0", O_RDWR);
struct input_event buf;
int first_x , first_y , second_x , second_y;
int num=0;//记录已经单击次数
bool flag_first=false ;
struct timeval first_click_time, second_click_time;//保存两次单击的时间
while(1)
{
bzero(&buf, sizeof(buf));
read(tp, &buf, sizeof(buf));
//只保留手指的第一次触点
if(buf.type == EV_ABS && buf.code == ABS_X && !flag_first)
{
second_x=first_x=buf.value;
}
if(buf.type == EV_ABS && buf.code == ABS_Y && !flag_first)
{
second_y=first_y=buf.value;
flag_first=true;
}
//移动过程不断保存最新手指落点
if(buf.type == EV_ABS && buf.code == ABS_X)
{
second_x=buf.value;
}
if(buf.type == EV_ABS && buf.code == ABS_Y)
{
second_y=buf.value;
}
//手指拿开的时候检测最新落点和第一次落点之间的差距,进而判断是否属于单击
if(buf.type == EV_KEY && buf.code == BTN_TOUCH && buf.value == 0)
{
if(abs(first_x-second_x) < 10 && abs(first_y-second_y) < 10){
printf("%s\n","单击");
num++;
if(num==1)
first_click_time=buf.time;
if(num==2){
second_click_time=buf.time;
double first_click_seconds = first_click_time.tv_sec + first_click_time.tv_usec / 1000000.0;
double second_click_seconds = second_click_time.tv_sec + second_click_time.tv_usec / 1000000.0;
double time_diff = second_click_seconds - first_click_seconds;
if(time_diff < 0.5)
break;
else
num=0;
}
}
else{
num=0;
}
flag_first=false;
}
}
printf("%s\n","已双击退出");
return 0;
}
代码有点长,将输出press on和press off去掉了,然后我们用num来表示当前单击的次数,单击两次的话才可能触发退出逻辑,由于双击退出一般是短时间内退出,所以记录了第一次单击和第二次单击的时间,第二次单击的时候进行检测,只有两次单击间隔小于0.5s才被认为是双击退出。这就让我们将每次读取到的input_event结构体中的时间属性也用上了。