目录
一、解析触摸屏设备上报的数据
触摸屏设备是一个绝对位移设备,可以上报绝对位移事件,绝对位移事件如下:
单点触摸和多点触摸
触摸屏分为多点触摸设备和单点触摸设备。 单点触摸设备只支持单点触摸, 一轮(一个同步事件称为一轮) 完整的数据只包含一个触摸点信息; 单点触摸设备以 ABS_XXX 事件承载、上报触摸点的信息,譬如 ABS_X(value 值对应的是 X 轴坐标值)、 ABS_Y(value 值对应的是 Y 轴坐标值)等绝对位移事件,而有些设备可能还支持 Z 轴坐标(通过 ABS_Z 事件上报、 value 值对应的便是 Z 轴坐标值)、 按压力大小(通过 ABS_PRESSURE 事件上报、 value 值对应的便是按压力大小) 以及接触面积等属性。 大部分的单点触摸设备都会上报 ABS_X 和 ABS_Y 事件,而其它绝对位移事件则根据具体的设备以及驱动的实现而定!
多点触摸设备可支持多点触摸,对于多点触摸设备, 一轮完整的数据可能包含多个触摸点信息。 多点触摸设备则是以 ABS_MT_XXX(MT 是 Multi-touch, 意思为: 多点触摸)事件承载、上报触摸点的信息, 如 ABS_MT_POSITION_X(X 轴坐标) 、 ABS_MT_POSITION_Y(Y 轴坐标)等绝对位移事件
触摸屏设备除了上报绝对位移事件之外,还可以上报按键类事件和同步类事件。因为几乎每一个输入设备都会上报同步事件、告知应用层本轮数据是否完整;当手指点击触摸屏或手指从触
摸屏离开时,此时就会上报按键类事件, 用于描述按下触摸屏和松开触摸屏; 具体的按键事件为
BTN_TOUCH(code=0x14a,也就是 330) ,手指在触摸屏上滑动不会上报 BTN_TOUCH 事件。 BTN_TOUCH 事件不支持长按状态,故其 value 不会等于 2。 对于多点触摸设备来说,只有第一个点按下时上报 BTN_TOUCH 事件 value=1、当最后一个点离开触摸屏时上报 BTN_TOUCH 事件 value=0。
单点触摸设备事件上报的流程大概如下:
# 点击触摸屏时
BTN_TOUCH
ABS_X
ABS_Y
SYN_REPORT
当手指点击触摸屏时,首先上报 BTN_TOUCH 事件,此时 value=1,表示按下;接着上报 ABS_X、 ABS_Y事件将 X、 Y 轴坐标数据发送给应用层;数据上报完成接着上报一个同步事件 SYN_REPORT,表示此次触摸点信息已经完整
# 滑动
此时状态手指已经在屏幕上,是接着上面已经完成点击屏幕但手指没松开的
ABS_X
ABS_Y
SYN_REPORT
手指在触摸屏上滑动时,并不会上报 BTN_TOUCH 事件,因为滑动过程并未发生按下、松开这种动作。
# 松开
BTN_TOUCH
SYN_REPORT
当松开时,首先上报了 BTN_TOUCH 事件,此时 value=0,表示手指已经松开了触摸屏,接着上报一个同步事件 SYN_REPORT。
上面列举出只是一个大致流程,实际上对于不同的触摸屏设备,能够获取到的信息量大小是不相同的,譬如某设备只能读取到触摸点的 X 和 Y 坐标、而另一设备却能读取 X、 Y 坐标以及按压力大小、 触点面积等信息, 总之这些数据都会在 SYN_REPORT 同步事件之前上报给应用层
多点触摸设备--事件上报的顺序
多点触摸设备上报的一轮完整数据中可能包含多个触摸点的信息,譬如 5 点触摸设备,如果 5 个手指同时在触摸屏上滑动,那么硬件就会更新 5 个触摸点的信息, 内核需要把这 5 个触摸点的信息上报给应用层。
在 Linux 内核中, 多点触摸设备使用多点触摸(MT)协议上报各个触摸点的数据, MT 协议分为两种类型: Type A 和 Type B, Type A 协议几乎处于淘汰的边缘,重点来看看 Type B 协议
MT 协议之 Type B 协议
Type B 协议适用于能够追踪并区分触摸点的设备。 Type B协议的重点是通过 ABS_MT_SLOT 事件上报各个触摸点信息的更新!能够追踪并区分触摸点的设备通常在硬件上能够区分不同的触摸点, 譬如对于一个 5 点触摸设备来说,硬件能够为每一个识别到的触摸点与一个 slot 进行关联,这个 slot 就是一个编号, 触摸点 0、触摸点 1、触摸点 2 等。底层驱动向应用层上报 ABS_MT_SLOT 事件, 此事件会告诉接收者当前正在更新的是哪个触摸点的数据, ABS_MT_SLOT 事件中对应的 value 数据存放的便是一个 slot、以告知应用层当前正在更新 slot
关联的触摸点对应的信息
每个识别出来的触摸点分配一个 slot, 与该 slot 关联起来, 利用这个 slot 来传递对应触点的变化。 除了ABS_MT_SLOT 事 件 之 外 , Type B 协 议 还 会 使 用 到ABS_MT_TRACTKING_ID 事 件 ,ABS_MT_TRACTKING_ID 事件则用于触摸点的创建、替换和销毁工作,ABS_MT_TRACTKING_ID 事件携带的数据 value 表示一个 ID,一个非负数的 ID(ID>=0) 表示一个有效的触摸点,如果 ID 等于-1 表示该触摸点已经不存在、被移除了;一个以前不存在的 ID 表示这是一个新的触摸点
Type B 协议可以减少发送到用户空间的数据,只有发生了变更的数据才会上报,譬如某个触摸点发生了移动,但仅仅只改变了 X 轴坐标、而未改变 Y 轴坐标,那么内核只会将改变后的 X 坐标值通过ABS_MT_POSITION_X 事件发送给应用层
测试
用上一章读取input_event结构体数据的代码测试一下触摸屏,首先在测试触摸屏之前,需要保证开发板上已经连接了 LCD 屏,使用命令"cat /proc/bus/input/devices",确定触摸屏对应的设备节点
这里使用名字是goodix-ts,使用的是其它屏,那么查看到的名称可能不是"goodix-ts",设备文件是event1
加载APP测试一下
APP是上章使用读取input_event结构体数据的,动作是先放第一个手指在触摸屏上不松开
第一行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_TRACKING_ID(code=57)事件,并且 value 值等于 0,也就是 ID,这个 ID 是一个非负数,所以表示这是一个新的触摸点被创建,也就意味着触摸屏上产生了一个新的触摸点(手指按下)
第二行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_POSITION_X(code=53)事件,其 value对应的便是触摸点的 X 坐标;
第三行上报了 ABS_MT_POSITION_Y(code=54)事件,其 value 值对应的便是触摸点 Y 坐标,所以由此可知该触摸点的坐标为(314, 237)
第四行上报了ABS_MT_TOUCH_MAJOR(code=48)事件
第五行上报了 ABS_MT_WIDTH_MAJOR(code=50)事件
最后一行上报了同步类事件 EV_SYN(type=0)中的 SYN_REPORT(code=0)事件,表示此次触摸点的信息全部上报完毕。
在第一个触摸点的基础上,增加第二个触摸点,打印信息如下所示
1-6行与上同理,第7行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_SLOT 事件(code=47),表示目前要更新 slot=1 所关联的触摸点(也就是触摸点 1) 对应的信息
第8行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_TRACKING_ID 事(code=57), ID=1,这是之前没有出现过的 ID,表示这是一个新的触摸点
第九、十、十一十二行分别上报了 ABS_MT_POSITION_X事件 、 ABS_MT_POSITION_Y 事件、ABS_MT_TOUCH_MAJOR(code=48事件和 ABS_MT_WIDTH_MAJOR(code=50)事件
最后一行上报同步事件(type=0、 code=0) ,告知应用层数据完整
当松开时,触摸点就会被销毁,上报 ABS_MT_TRACKING_ID 事件,并将 value 设置为-1(ID)
不管是键盘也好、或者是鼠标、触摸屏,都可以像上面那样将输入设备的数据直接打印出来,然后自己再去分析,确定该输入设备上报事件的规则和流程,把这些弄懂之后再去编写程序验证结果
二、获取触摸屏的信息
先了解一下ioctl()函数,原型如下
int ioctl(int fd, unsigned long request, ...);
第一个参数 fd 对应文件描述符;第二个参数 request 与具体要操作的对象有关,没有统一值, 表示向文件描述符请求相应的操作,也就是请求指令;此函数是一个可变参函数, 第三个参数需要根据 request 参数来决定,配合 request 来使用
ioctl()是一个文件 I/O 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或设备文件
在 input.h 头文件有这样一些宏定义
每一个宏定义后面都有注释,对于 input 输入设备,对其执行 ioctl()操作需要使用这些宏, 不同的宏表示不同请求指令,EVIOCG(get)开头的表示获取信息, EVIOCS(set)开头表示设置
比如使用 EVIOCGNAME 宏获取设备名称,使用方式如下:
char name[100];
ioctl(fd, EVIOCGNAME(sizeof(name)), name);
EVIOCGNAME(len)就表示用于接收字符串数据的缓冲区大小,而此时 ioctl()函数的第三个参数需要传入一个缓冲区的地址,该缓冲区用于存放设备名称对应的字符串数据
想获取触摸屏支持的最大触摸点数,首先了解这个EVIOCGABS(abs)宏,原型如下
#define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo)
可以看到使用该宏需要传入一个 abs 参数,该参数表示为一个 ABS_XXX 绝对位移事件,譬如 EVIOCGABS(ABS_MT_SLOT)表示获取触摸屏的 slot 信息,此时 ioctl()函数的第三个参数是
一个 struct input_absinfo *的指针,指向一个 struct input_absinfo 对象,调用 ioctl()会将获取到的信息写入到struct input_absinfo 对象中
struct input_absinfo 结构体如下所示:
struct input_absinfo {
__s32 value; //最新的报告值
__s32 minimum; //最小值
__s32 maximum; //最大值
__s32 fuzz;
__s32 flat;
__s32 resolution;
};
代码编写
25行把ABS_MT_SLOT传给宏EVIOCGABS,表示获取触摸屏的 slot 信息,,后面为 struct input_absinfo 结构体,用于接收数据
31行,计算最大触摸点,因为 slot
数组下标从0开始,所以需要将 slot.maximum
加1
代码如下
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <linux/input.h>
int main(int argc, char *argv[])
{
struct input_absinfo info;
int fd = -1, max_slots;
if (argc != 2)
{
fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
exit(EXIT_FAILURE);
}
if ((fd = open(argv[1], O_RDONLY)) < 0)
{
perror("open error");
exit(EXIT_FAILURE);
}
if (ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &info) < 0)
{
perror("ioctl error");
close(fd);
exit(EXIT_FAILURE);
}
max_slots = info.maximum +1 - info.minimum;
printf("max_slots: %d\n", max_slots);
close(fd);
exit(EXIT_SUCCESS);
}
验证
所以这个屏是一个 5 点触摸屏
三、多点触摸APP
编写代码获取触摸点的坐标信息,并将其打印出来,全部代码如下
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <linux/input.h>
struct ts_mt
{
int x;
int y;
int id;
int valid;
};
struct tp_xy
{
int x;
int y;
};
static int ts_read(const int fd, const int max_slots,
struct ts_mt *mt)
{
int i;
struct input_event in_ev;
static int slot = 0;
static struct tp_xy xy[12] = {0};
memset(mt, 0x0, max_slots * sizeof(struct ts_mt));
for (i = 0; i < max_slots; i++)
mt[i].id = -2;
for (;;)
{
if (sizeof(struct input_event) !=
read(fd, &in_ev, sizeof(struct input_event)))
{
perror("read error");
return -1;
}
switch (in_ev.type)
{
case EV_ABS:
switch (in_ev.code)
{
case ABS_MT_SLOT:
slot = in_ev.value;
break;
case ABS_MT_POSITION_X:
xy[slot].x = in_ev.value;
mt[slot].valid = 1;
break;
case ABS_MT_POSITION_Y:
xy[slot].y = in_ev.value;
mt[slot].valid = 1;
break;
case ABS_MT_TRACKING_ID:
mt[slot].id = in_ev.value;
mt[slot].valid = 1;
break;
}
break;
case EV_SYN:
if (SYN_REPORT == in_ev.code)
{
for (i = 0; i < max_slots; i++)
{
mt[i].x = xy[i].x;
mt[i].y = xy[i].y;
}
}
return 0;
}
}
}
int main(int argc, char *argv[])
{
struct input_absinfo slot;
struct ts_mt *mt = NULL;
int max_slots;
int fd, i;
if (2 != argc)
{
fprintf(stderr, "usage: %s <input_dev>\n", argv[0]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
if (0 > fd)
{
perror("open error");
exit(EXIT_FAILURE);
}
if (ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot) < 0)
{
perror("ioctl error");
close(fd);
exit(EXIT_FAILURE);
}
max_slots = slot.maximum + 1 - slot.minimum;
printf("max_slots: %d\n", max_slots);
mt = calloc(max_slots, sizeof(struct ts_mt));
for (;;)
{
if (ts_read(fd, max_slots, mt) < 0)
{
break;
}
for (i = 0; i < max_slots; i++)
{
if (mt[i].valid)
{
if (mt[i].id >= 0)
printf("slot<%d>, 按下(%d, %d)\n", i, mt[i].x, mt[i].y);
else if (mt[i].id == -1)
printf("slot<%d>, 松开\n", i);
else
printf("slot<%d>, 移动(%d, %d)\n", i, mt[i].x, mt[i].y);
}
}
}
close(fd);
free(mt);
exit(EXIT_SUCCESS);
}
代码开头定义两个结构体,第一个结构体用于描述多点触摸中每一个触摸点的信息,第二个结构体就是一个触摸点中的x和y坐标
在main函数中,实例一个input_absinfo
用来描述输入设备上的一个信息的结构体,接着定义mt结构体指针,下面用if判断传参是否等于2,接收打开文件,获取触摸屏支持的最大触摸点数max_slots,然后给mt指针申请空间,calloc()在动态分配完内存后,自动初始化该内存空间为零
进入for死循环中,把文件描述符,最大触摸点传给ts_read函数。
进入ts_read函数,这个函数用于读取输入设备文件中的事件数据。首先定义一个input_event
结构体用于描述输入设备事件,定义的结构体数组xy为12个元素,主要是用于保存上一次的x和y坐标值,这里假设触摸屏支持的最大触摸点数不会超过12,然后用memset函数初始化缓冲区,全部清零,然后把触摸点的idc全部初始化为-2(因为 id=-1表示触摸点删除, id>=0表示创建),进入ts_read函数的for死循环,先用if来判断读取量是否相等,不相等则意味着读取的数据大小不正确,可能会导致错误。开始判断type类型,如果事件类型是EV_ABS(表示绝对事件),则根据事件代码来处理。例如,如果事件代码是ABS_MT_SLOT(表示触摸点的槽位),则更新当前的槽位;如果事件代码是ABS_MT_POSITION_X或ABS_MT_POSITION_Y(表示触摸点的x坐标和y坐标),则更新当前槽位对应的xy坐标,并将该触摸点的有效标志位设为1;如果事件代码是ABS_MT_TRACKING_ID(表示跟踪ID),则将该跟踪ID保存到当前槽位对应的mt结构体中,并将该触摸点的有效标志位设为1。如果事件类型是EV_SYN(表示同步事件),则根据事件代码来处理。例如,如果事件代码是SYN_REPORT(表示一组事件已经结束),则将保存在xy数组中的坐标值赋值给mt结构体,并返回0。
返回到main函数的for死循环中,下一步就是在读取事件数据成功后,程序遍历mt结构体数组,判断每一个触摸点信息是否发生更新(关注的信息发生更新),如果发生更新,则输出相应的信息,例如按下、松开或移动等。最后,程序关闭输入设备文件,释放内存,并退出。
验证
实现5点触摸的信息读取