正点原子I.MX6U-MINI应用篇7输入设备(鼠标键盘触摸屏按钮)的应用编程和tslib库

Posted 果果小师弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了正点原子I.MX6U-MINI应用篇7输入设备(鼠标键盘触摸屏按钮)的应用编程和tslib库相关的知识,希望对你有一定的参考价值。

什么是输入设备?输入设备其实就是能够产生输入事件的设备就称为输入设备,常见的输入设备包括鼠标、键盘、触摸屏、按钮等等,它们都能够产生输入事件,产生输入数据给计算机系统。

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

一、输入类设备编程介绍

1.1 什么是输入设备

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

1.2 input子系统

由上面的介绍可知,输入设备种类非常多,每种设备上报的数据类型又不一样,那么 Linux 系统如何管理呢?Linux 系统为了统一管理这些输入设备,实现了一套能够兼容所有输入设备的框架,那么这个框架就是 input 子系统。驱动开发人员基于 input 子系统开发输入设备的驱动程序,input 子系统可以屏蔽硬件的差异,向应用层提供一套统一的接口。
基于 input 子系统注册成功的输入设备,都会在/dev/input 目录下生成对应的设备节点(设备文件),设备节点名称通常为 eventX(X 表示一个数字编号 0、1、2、3 等),譬如/dev/input/event0、/dev/input/event1、/dev/input/event2 等,通过读取这些设备节点可以获取输入设备上报的数据。

1.3 读取数据的流程

如果我们要读取触摸屏的数据,假设触摸屏设备对应的设备节点为/dev/input/event0,那么数据读取流程如下:

  • ①、应用程序打开/dev/input/event0设备文件;
  • ②、应用程序发起读操作(譬如调用read),如果没有数据可读则会进入休眠(阻塞 I/O 情况下);
  • ③、当有数据可读时,应用程序会被唤醒,读操作获取到数据返回;
  • ④、应用程序对读取到的数据进行解析。

当无数据可读时,程序会进入休眠状态(也就是阻塞),譬如应用程序读触摸屏数据,如果当前并没有去触碰触摸屏,自然是无数据可读;当我们用手指触摸触摸屏或者在屏上滑动时,此时就会产生触摸数据、应用程序就有数据可读了,应用程序会被唤醒,成功读取到数据。那么对于其它输入设备亦是如此,无数据可读时应用程序会进入休眠状态(阻塞式 I/O 方式下),当有数据可读时才会被唤醒。

1.4 应用程序如何解析数据

首先我们要知道,应用程序打开输入设备对应的设备文件,向其发起读操作,那么这个读操作获取到的是什么样的数据呢?其实每一次 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 用于描述发生了哪一种类型的事件(对事件的分类),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)

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

  • 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
......

除了以上列举出来的之外,还有很多,大家可以自己浏览<linux/input.h>头文件(这些宏其实是定义在input-event-codes.h 头文件中,该头文件被<linux/input.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。

1.5 读取struct input_event数据

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

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>

int main(int argc, char *argv[])

    struct input_event in_ev = 0;
    int fd = -1;

    /* 校验传参 */
    if (2 != argc) 
        fprintf(stderr, "usage: %s <input-dev>\\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);
        

        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:设备文件不同于普通文件,读写设备文件之前无需设置读写位置偏移量。

使用交叉编译工具编译上述代码得到可执行文件,在使用交叉编译链之前要使能环境变量。

source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
$CC -o read_input read_input.c

1.6 上传程序到开发板执行

开发板启动后通过nfs挂载Ubuntu目录的方式,将相应的文件拷贝到开发板上。简单来说,就是通过NFS在开发板上通过网络直接访问ubuntu虚拟机上的文件,并且就相当于自己本地的文件一样。

开发板想访问/home/zhiguoxin/myproject/alientek_app_development_source这个目录中的文件,就要把/home/zhiguoxin/myproject/alientek_app_development_source挂载到开发板的mnt目录,这样就可以通过nfs来访问/home/zhiguoxin/myproject/alientek_app_development_source了。

因为我的代码都放在/home/zhiguoxin/myproject/alientek_app_development_source这个目录下,所以我们将这个目录作为NFS共享文件夹。设置方法参考移植SQLite3、OpenCV到RV1126开发板上开发人脸识别项目第一章。

Ubuntu IP为192.168.10.100,然后一般都是挂载在开发板的mnt目录下,这个目录是专门用来给我们作为临时挂载的目录。

然后使用MobaXterm软件通过SSH访问开发板。

ubuntu ip:192.168.10.100
windows ip:192.168.10.200
开发板ip:192.168.10.50

在开发板上执行以下命令:

mount -t nfs -o nolock,vers=3 192.168.10.100:/home/zhiguoxin/myproject/alientek_app_development_source /mnt

就将开饭的mnt目录挂载在ubuntu的/home/zhiguoxin/myproject/alientek_app_development_source目录下了。这样我们就可以在Ubuntu下修改文件,然后可以直接在开发板上执行可执行文件了。当然我这里的/home/zhiguoxin/myproject/windows之间是一个共享目录,我也可以直接在windows上面修改文件,然后ubuntu和开发板直接进行文件同步了。

1.7 在开发板上验证

Mini 开发板上都有一个用户按键 KEY0,它就是一个典型的输入设备,如下图所示:

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

cat /proc/bus/input/devices


接下来我们使用这个按键进行测试,执行下面的命令:



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

第一行中type等于 1,表示上报的是按键事件EV_KEY,code=114,打开 input-event-codes.h 头文件进行查找,可以发现code=114对应的是键盘上的KEY_VOLUMEDOWN按键,这个是Mini开发板出厂系统已经配置好的。而value=1表示按键按下,所以整个第一行的意思就是按键KEY_VOLUMEDOWN被按下。

第二行,表示上报了EV_SYN同步类事件(type=0)中的SYN_REPORT事件(code=0),表示本轮数据已经完整、报告同步。

第三行,type等于1,表示按键类事件,code等于 114、value 等于0,所以表示按键KEY_VOLUMEDOWN被松开。

第四行,又上报了同步事件。

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

我们试试长按按键 KEY0,按住不放,如下所示:


可以看到上报按键事件时,对应的value等于 2,表示长按状态。

1.8 按键应用编程

本小节编写一个应用程序,获取按键状态,判断按键当前是按下、松开或长按状态。从上面打印的信息可知,对于按键来说,它的事件上报流程如下所示:

# 以字母 A 键为例
KEY_A //上报 KEY_A 事件
SYN_REPORT  //同步

如果是按下,则上报KEY_A事件时,value=1;如果是松开,则value=0;如果是长按,则value=2。
接下来编写按键应用程序,读取按键状态并将结果打印出来,代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>

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 <input-dev>\\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;
            
        
    

在for循环中,调用read()读取输入设备上报的数据,当按键按下或松开(以及长按)动作发生时,read()会读取到输入设备上报的数据,首先判断此次上报的事件是否是按键类事件(EV_KEY),如果是按键类事件、接着根据value值来判断按键当前的状态是松开、按下还是长按。

将上述代码进行编译:

然后在开发板中测试

./read_key /dev/input/event2

运行程序之后,按下 KEY0 或松开 KEY0 以及长按情况下,终端会打印出相应的信息,如上图所示。code=114(KEY_VOLUMEDOWN 按键)。

除了测试开发板上的 KEY0 按键之外,我们还可以测试键盘上的按键,首先找到一个 USB 键盘连接到开发板的 USB HOST接口上,驱动加载成功之后,可以查看下该键盘设备对应的设备节点,使用命令cat /proc/bus/input/devices,在打印信息中找到键盘设备的信息:


我使用的是一个Ducky的USB键盘"Ducky Akko Keyboard",对应的设备节点为/dev/input/event3,
运行测试程序并按下、松开键盘上的按键:


大家可以根据code值查询到对应的按键(通过input-event-codes.h头文件),譬如code=30对应的是键盘上的字母A键,code=48对应的是字母B键。

二、使用tslib库

上面介绍了如何编写触摸屏应用程序,包括单点触摸和多点触摸,主要是对读取到的struct input_event类型数据进行剖析,得到各个触摸点的坐标。tslib 库,这是 Linux 系统下,专门为触摸屏开发的应用层函数库。

2.1 tslib简介

tslib是专门为触摸屏设备所开发的 Linux 应用层函数库,并且是开源,也就意味着我们可以直接获取到tslib 的源代码。

tslib为触摸屏驱动和应用层之间的适配层,它把应用程序中读取触摸屏struct input_event类型数据(这是输入设备上报给应用层的原始数据)并进行解析的操作过程进行了封装,向使用者提供了封装好的 API 接口。tslib 从触摸屏中获得原始的坐标数据,并通过一系列的去噪、去抖、坐标变换等操作,来去除噪声并将原始的触摸屏坐标转换为相应的屏幕坐标。

tslib 有一个配置文件ts.conf,该配置文件中提供了一些配置参数、用户可以对其进行修改,具体的配置信息稍后介绍!

tslib可以作为Qt的触摸屏输入插件,为Qt提供触摸输入支持,如果在嵌入式 Linux 硬件平台下开发过
Qt 应用程序的读者应该知道;当然,并不是只有 tslib 才能作为 Qt 的插件、为其提供触摸输入支持,还有很多插件都可以,只不过大部分都会选择使用 tslib。

2.2 tslib下载和安装

2.2.1 下载tslib源码

首先下载tslib源码包,进入到tslib的 git 仓库下载源码https://github.com/libts/tslib/releases,如下:

所以为了统一,我们也下载 1.16 版本的 tslib,往下翻找到 1.16 版本的下载链接:

将其解压到当前目录下:

tar -xzf tslib-1.16.tar.gz

解压之后会生成tslib-1.16目录,在/home/zhiguoxin/linux/tool目录下创建 tslib 目录,等会编译tslib库的时候将安装目录指定到这里,如下所示:


进入到 tslib-1.16 目录,准备进行编译 tslib 源码:

接下来进行编译,整个源码的编译分为3个步骤:

  • 首先第一步是配置工程;
  • 第二步是编译工程;
  • 第三步是安装,将编译得到的库文件、可执行文件等安装到一个指定的目录下。首先在配置工程之前,先对交叉编译工具的环境进行设置,使用source执行交叉编译工具安装目录下的environment-setup-cortexa7hf-neon-poky-linux-gnueabi脚本文件:
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

执行下面这条命令对 tslib 源码工程进行配置:

./configure --host=arm-poky-linux-gnueabi --prefix=/home/zhiguoxin/linux/tool/tslib/

至于工程是如何配置的,大家可以执行./configure --help查看它的配置选项以及含义,--host选项用于指定交叉编译得到的库文件是运行在哪个平台,通常将--host设置为交叉编译器名称的前缀,譬如 arm-poky-linux-gnueabi-gcc前缀就是arm-poky-linux-gnueabi--prefix选项则用于指定库文件的安装路径,我们将安装路径设置为之前创建的/home/zhiguoxin/linux/tool/tslib目录。

接着编译工程,直接执行 make:

make

最后执行 make install 安装:

make install

2.2.2 tslib安装目录下的文件夹介绍

进入到tslib安装目录下:

cd /home/zhiguoxin/linux/tool/tslib

bin目录

bin目录下有一些 tslib 提供的小工具,可以用于测试触摸屏,如下所示:

etc目录

etc目录下有一个配置文件 ts.conf,前面给大家提到过,

打开ts.conf文件看看它有哪些配置选项:

这里的配置不需要去改动了,直接使用默认的配置就行了。

  • module_raw input:取消注释,使能支持 input 输入事件;
  • module pthres pmin=1:如果我们的设备支持按压力大小测试,那么可以把它的注释取消,pmin 用于调节按压力灵敏度,默认就是等于 1。
  • module dejitter delta=100:tslib 提供了触摸屏去噪算法插件,如果需要过滤噪声样本,取消注释,默认参数delta=100。
  • module linear:tslib 提供了触摸屏坐标变换的功能,譬如将 X、Y 坐标互换、坐标旋转等之类,如果我们需要实现坐标变换,可以把注释去掉。

include目录

include 目录下只有一个头文件tslib.h,该头文件中包含了一些结构体数据结构以及 API 接口的申明,使用tslib提供的API就需要包含该头文件。

lib目录

lib目录下包含了编译tslib源码所得到的库文件,默认这些都是动态库文件,也可以通过配置tslib工程使其生成静态库文件;ts目录下存放的是一些插件库。

share目录

可以忽略!

2.3 在开发板上测试 tslib

移植的最后一步就是把tslib安装目录下的库文件、etc下的配置文件以及编译得到的测试工具拷贝到开发板Linux系统目录下,由于开发板出厂系统中已经移植了tslib库,所以我们这里就不用拷贝了。但如果大家是自己做的根文件系统,并没有移植tslib,那么就需要把这些库、可执行文件以及配置文件拷贝到根文件系统中,只需要下面四步,这里就不做过多的演示了:

  • 将安装目录bin/目录下的所有可执行文件拷贝到开发板/usr/bin 目录下;
  • 将安装目录etc/目录下的配置文件 ts.conf 拷贝到开发板/etc 目录下;
  • 将安装目录lib/目录下的所有库文件拷贝到开发板/usr/lib 目录下。
  • 将安装目录下的测试工具、库文件以及配置文件拷贝到开发板之后,接着需要配置一些环境变量,因为tslib工作的时候它需要依赖于一些环境变量,譬如它会通过读取环境变量来得知ts.conf配置文件、库文件的路径以及我们要测试的触摸屏对应的设备节点等。
export TSLIB_CONSOLEDEVICE正点原子I.MX6U-MINI驱动篇4Linux设备树详解

正点原子I.MX6U-MINI应用篇2点亮开发板的LED

正点原子I.MX6U-MINI驱动篇3新字符设备驱动实验newchrled,自动创建设备节点

正点原子I.MX6U-MINI应用篇4嵌入式Linux关于GPIO的一些操作

正点原子I.MX6U-MINI应用篇1编写第一个应用App程序helloworld

正点原子I.MX6U-MINI应用篇3Framebuffer应用编程,操作屏幕