输入子系统了解,input_event结构体详解,触摸屏信息种类以及捕获信息,触摸屏生成事件流程总结,触摸屏使用例子以及单击触摸屏例子+双击退出例子

输入子系统的介绍

输入子系统不仅仅是指单纯的硬件设备,它实际上是一个完整的处理流程,涉及从硬件信号的捕获到事件的生成、分发,再到最终的事件处理。这个子系统包括了硬件设备(鼠标,键盘,摇杆,麦克风,触摸屏等)、驱动程序、内核中的处理逻辑,以及与用户空间的接口。

详细过程

  • 用户操作:用户在输入设备(如键盘、鼠标、触摸屏等)上执行一系列操作。例如,按下一个键、移动鼠标或点击触摸屏。

  • 驱动程序捕获操作:输入设备的驱动程序捕获这些操作。驱动程序是专门为特定硬件设备编写的软件,能够识别设备并且理解设备发出的信号。它将这些物理操作转换为标准化的输入信号。

  • 事件生成和封装驱动程序将捕获的信号封装成输入事件。例如,当用户按下一个键时,驱动程序生成一个“键按下”的事件,包含了按下的键码等信息。

  • 事件分发:操作系统的输入子系统核心部分接收到这些事件后,会根据事件的类型和目的,将它们分发给相应的处理程序。这可能包括内核中的其他模块,或直接发送给用户空间的应用程序。

  • 应用程序处理事件应用程序通过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 存在。

所以通过打印和观察 SYNEV_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结构体中的时间属性也用上了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值