正点原子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驱动篇3新字符设备驱动实验newchrled,自动创建设备节点
正点原子I.MX6U-MINI应用篇4嵌入式Linux关于GPIO的一些操作