输入设备应用编程——通过设备文件读取按键事件

        本章学习输入设备的应用编程,首先要知道什么是输入设备?输入设备其实就是能够产生输入事件的设备就称为输入设备,常见的输入设备包括鼠标、键盘、触摸屏、按钮等等,它们都能够产生输入事件,产生输入数据给计算机系统。

        对于输入设备的应用编程其主要是获取输入设备上报的数据、输入设备当前状态等,譬如获取触摸屏当前触摸点的 X、Y 轴位置信息以及触摸屏当前处于按下还是松开状态。

输入类设备编程介绍

什么是输入设备?

        先来了解什么是输入设备(也称为 input 设备),常见的输入设备有鼠标、键盘、手柄、触摸屏、遥控器、电脑画图板等,用户通过输入设备与系统进行交互。

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 结构体类型数据,该结构体定义在头文件中,它的定义如下:

struct input_event {
 struct timeval time;
 __u16 type;
 __u16 code;
 __s32 value;
};

        结构体中的 time 成员变量是一个 struct timeval 类型的变量,该结构体在前面给大家介绍过,内核会记录每个上报的事件其发生的时间,并通过变量 time 返回给应用程序。时间参数通常不是那么重要,而其它 3 个成员变量 type、code、value 更为重要。

        type:type 用于描述发生了哪一种类型的事件(对事件的分类),Linux 系统所支持的输入事件类型如下所示:

/*
* 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
#define EV_SND 0x12
#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)

        以上这些宏定义也是在头文件中,所以在应用程序中需要包含该头文件;一种输入设备通常可以产生多种不同类型的事件,譬如点击鼠标按键(左键、右键,或鼠标上的其它按键)时会上报按键类事件,移动鼠标时则会上报相对位移类事件。

        code:code 表示该类事件中的哪一个具体事件,以上列举的每一种事件类型中都包含了一系列具体事件,譬如一个键盘上通常有很多按键,譬如字母 A、B、C、D 或者数字 1、2、3、4 等,而 code 变量则告知应用程序是哪一个按键发生了输入事件。每一种事件类型都包含多种不同的事件,譬如按键类事件:

#define KEY_RESERVED 0
#define KEY_ESC 1 //ESC 键
#define KEY_1 2 //数字 1 键
#define KEY_2 3 //数字 2 键
#define KEY_TAB 15 //TAB 键
#define KEY_Q 16 //字母 Q 键
#define KEY_W 17 //字母 W 键
#define KEY_E 18 //字母 E 键
#define KEY_R 19 //字母 R 键
……

        相对位移事件

#define REL_X 0x00 //X 轴
#define REL_Y 0x01 //Y 轴
#define REL_Z 0x02 //Z 轴
#define REL_RX 0x03
#define REL_RY 0x04
#define REL_RZ 0x05
#define REL_HWHEEL 0x06
#define REL_DIAL 0x07
#define REL_WHEEL 0x08
#define REL_MISC 0x09
#define REL_MAX 0x0f
#define REL_CNT (REL_MAX+1)

        绝对位移事件

        触摸屏设备是一种绝对位移设备,它能够产生绝对位移事件;譬如对于触摸屏来说,一个触摸点所包含的信息可能有多种,譬如触摸点的 X 轴坐标、Y 轴坐标、Z 轴坐标、按压力大小以及接触面积等,所以 code 变量告知应用程序当前上报的是触摸点的哪一种信息(X 坐标还是 Y 坐标、亦或者其它);绝对位移事件如下:

#define ABS_X 0x00 //X 轴
#define ABS_Y 0x01 //Y 轴
#define ABS_Z 0x02 //Z 轴
#define ABS_RX 0x03
#define ABS_RY 0x04
#define ABS_RZ 0x05
#define ABS_THROTTLE 0x06
#define ABS_RUDDER 0x07
#define ABS_WHEEL 0x08
#define ABS_GAS 0x09
#define ABS_BRAKE 0x0a
#define ABS_HAT0X 0x10
#define ABS_HAT0Y 0x11
#define ABS_HAT1X 0x12
#define ABS_HAT1Y 0x13
#define ABS_HAT2X 0x14
#define ABS_HAT2Y 0x15
#define ABS_HAT3X 0x16
#define ABS_HAT3Y 0x17
#define ABS_PRESSURE 0x18
#define ABS_DISTANCE 0x19
#define ABS_TILT_X 0x1a
#define ABS_TILT_Y 0x1b
#define ABS_TOOL_WIDTH 0x1c
......

        除了以上列举出来的之外,还有很多,大家可以自己浏览头文件(这些宏其实是定义在 input-event-codes.h 头文件中,该头文件被所包含了)关于这些具体的事件,后面再给大家进行介绍。

        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 值而定。

数据同步

        上面我们提到了同步事件类型 EV_SYN,同步事件用于实现同步操作、告知接收者本轮上报的数据已经完整。应用程序读取输入设备上报的数据时,一次 read 操作只能读取一个 struct input_event 类型数据,譬如对于触摸屏来说,一个触摸点的信息包含了 X 坐标、Y 坐标以及其它信息,对于这样情况,应用程序需要执行多次 read 操作才能把一个触摸点的信息全部读取出来,这样才能得到触摸点的完整信息。

        那么应用程序如何得知本轮已经读取到完整的数据了呢?其实这就是通过同步事件来实现的,内核将本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据已经完整、可以进行同步了。

        同步类事件中也包含了多种不同的事件,如下所示:

/*
* Synchronization events.
*/
#define SYN_REPORT 0
#define SYN_CONFIG 1
#define SYN_MT_REPORT 2
#define SYN_DROPPED 3
#define SYN_MAX 0xf
#define SYN_CNT (SYN_MAX+1)

        所以的输入设备都需要上报同步事件,上报的同步事件通常是 SYN_REPORT,而 value 值通常为 0。

读取 struct input_event 数据

        根据前面的介绍可知,对输入设备调用 read()会读取到一个 struct input_event 类型数据,接下来编写应用程序,将读取到的 struct input_event 类型数据中的每一个元素打印出来、并对它们进行解析。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[]){
    struct input_event in_ev = {0};
    int fd = -1;
    /* 校验传参 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s \n", argv[0]);
        exit(-1);
    }
 
    /* 打开文件 */
    if (0 > (fd = open(argv[1], O_RDONLY))) {
        perror("open error");
        exit(-1);
    }
    for ( ; ; ) {
        /* 循环读取数据 */
        if (sizeof(struct input_event) !=
            read(fd, &in_ev, sizeof(struct input_event))) {
            error("read error");
            exit(-1);
        }
    printf("type:%d code:%d value:%d\n",in_ev.type, in_ev.code, in_ev.value);
    }
}

        执行程序时需要传入参数,这个参数就是对应的输入设备的设备节点(设备文件),程序中会对传参进行校验。程序中首先调用 open()函数打开设备文件,之后在 for 循环中调用 read()函数读取文件,将读取到的数据存放在 struct input_event 结构体对象中,之后将结构体对象中的各个成员变量打印出来。注意,程序中使用了阻塞式 I/O 方式读取设备文件,所以当无数据可读时 read 调用会被阻塞,知道有数据可读时才会被唤醒!

        Tips:设备文件不同于普通文件,读写设备文件之前无需设置读写位置偏移量。 使用交叉编译工具编译上述代码得到可执行文件 testApp:

        作者使用的是正点原子ALPHA IXM6ULL开发板,板载按键KEY_0提供给用户一个 GPIO 按键,在出厂系统中,该按键驱动基于 input 子系统而实现,所以在/dev/input 目录下存在 KEY0 的设备节点,可以通过查看/proc/bus/input/devices 文件得知,查看该文件可以获取到系统中注册的所有输入设备相关的信息,如下所示:

输入设备应用编程——通过设备文件读取按键事件_第1张图片

        接下来我们使用这个按键进行测试,执行下面的命令: 输入设备应用编程——通过设备文件读取按键事件_第2张图片

        程序运行后,执行按下 KEY0、松开 KEY0 等操作终端将会打印出相应的信息,如上所示。

        打开 input-event-codes.h 头文件进行查找,有

        type = 1 code = 114 value =1            KEY_VOLUMEDOWN事件按键按下

        type = 0 code = 0                               SYN_REPORT 事件

        type = 1 code = 114 value =0             KEY_VOLUMEDOWN事件按键松开

         所以整个上面 4 行的打印信息就是开发板上的 KEY0 按键被按下以及松开这个过程,内核所上报的事件以及发送给应用层的数据 value。 

应用程序编写

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[]){
    struct input_event in_ev = {0};
    int fd = -1;
    int value = -1;
 
    /* 校验传参 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s \n", argv[0]);
        exit(-1);
    }
 
    /* 打开文件 */
    if (0 > (fd = open(argv[1], O_RDONLY))) {
        perror("open error");
        exit(-1);
    }
    for ( ; ; ) {
        /* 循环读取数据 */
        if (sizeof(struct input_event) != read(fd, &in_ev, sizeof(struct input_event))) {
            perror("read error");
            exit(-1);
        }
        if (EV_KEY == in_ev.type) { //按键事件
            switch (in_ev.value) {
            case 0:
                printf("code<%d>: 松开\n", in_ev.code);
                break;
            case 1:
                printf("code<%d>: 按下\n", in_ev.code);
                break;
            case 2:
                printf("code<%d>: 长按\n", in_ev.code);
                break;
            }
        }
    }
}

 测试结果

输入设备应用编程——通过设备文件读取按键事件_第3张图片

你可能感兴趣的:(Linux,驱动开发)