LV15-03-输入类设备-03-触摸屏设备

本文主要是输入类设备控制——触摸屏设备的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
其他网站 kernel - Linux source code (v4.15) - Bootlin linux内核源码在线查看
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
https://elixir.bootlin.com/linux/latest/source 在线阅读linux kernel源码
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)

一、绝对位移事件

触摸屏设备是一个绝对位移设备,可以上报绝对位移事件,绝对位移事件如下(input-event-codes.h - include/uapi/linux/input-event-codes.h - Linux source code v4.15 - Bootlin):

image-20240922091236961

二、单点触摸和多点触摸

1. 两个概念

触摸屏分为多点触摸设备和单点触摸设备。 单点触摸设备只支持单点触摸, 一轮(这里把一个同步事件称为一轮) 完整的数据只包含一个触摸点信息; 单点触摸设备以 ABS_XXX 事件承载、上报触摸点的信息,如 ABS_X(value 值对应的是 X 轴坐标值)、 ABS_Y(value 值对应的是 Y 轴坐标值)等绝对位移事件,而有些设备可能还支持 Z 轴坐标(通过 ABS_Z 事件上报、 value 值对应的便是 Z 轴坐标值)、 按压力大小(通过 ABS_PRESSURE 事件上报、 value 值对应的便是按压力大小) 以及接触面积等属性。 大部分的单点触摸设备都会上报 ABS_X 和 ABS_Y 事件,而其它绝对位移事件则根据具体的设备以及驱动的实现而定。

多点触摸设备可支持多点触摸,如 ALPHA 开发板配套使用的 4.3 寸、 7 寸等触摸屏均支持多点触摸,对于多点触摸设备, 一轮完整的数据可能包含多个触摸点信息。 多点触摸设备则是以 ABS_MT_XXX(MT 是 Multi-touch, 意思为: 多点触摸)事件承载、上报触摸点的信息, 如 ABS_MT_POSITION_X(X 轴坐标) 、 ABS_MT_POSITION_Y(Y 轴坐标)等绝对位移事件。

触摸屏设备除了上报绝对位移事件之外,还可以上报按键类事件和同步类事件。同步事件很好理解,因为几乎每一个输入设备都会上报同步事件、告知应用层本轮数据是否完整;当手指点击触摸屏或手指从触摸屏离开时,此时就会上报按键类事件, 用于描述按下触摸屏和松开触摸屏; 具体的按键事件为BTN_TOUCH(code=0x14a,也就是 330) ,当然,手指在触摸屏上滑动不会上报 BTN_TOUCH 事件。

TN_TOUCH 事件不支持长按状态,故其 value 不会等于 2。 对于多点触摸设备来说,只有第一个点按下时上报 BTN_TOUCH 事件 value=1、当最后一个点离开触摸屏时上报 BTN_TOUCH 事件 value=0。

2.事件上报的顺序

2.1  单点触摸设备

通过上面的了解可知,单点触摸设备事件上报的流程大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 点击触摸屏时
BTN_TOUCH
ABS_X
ABS_Y
SYN_REPORT

# 滑动
ABS_X
ABS_Y
SYN_REPORT

# 松开
BTN_TOUCH
SYN_REPORT

以上列举出只是一个大致流程,实际上对于不同的触摸屏设备,能够获取到的信息量大小是不相同的,如某设备只能读取到触摸点的 X 和 Y 坐标、而另一设备却能读取 X、 Y 坐标以及按压力大小、 触点面积等信息, 总之这些数据都会在 SYN_REPORT 同步事件之前上报给应用层。

当手指点击触摸屏时,首先上报 BTN_TOUCH 事件,此时 value=1,表示按下;接着上报 ABS_X、 ABS_Y事件将 X、 Y 轴坐标数据发送给应用层;数据上报完成接着上报一个同步事件 SYN_REPORT,表示此次触摸点信息已经完整。

当手指在触摸屏上滑动时,并不会上报 BTN_TOUCH 事件,因为滑动过程并未发生按下、松开这种动作。

当松开时,首先上报了 BTN_TOUCH 事件,此时 value=0,表示手指已经松开了触摸屏,接着上报一个同步事件 SYN_REPORT。

2.2 多点触摸设备

多点触摸设备上报的一轮完整数据中可能包含多个触摸点的信息,如 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 事件发送给应用层。

Type B 协议下多点触摸设备上报数据的流程大致如下:

1
2
3
4
5
6
7
8
9
ABS_MT_SLOT 0
ABS_MT_TRACKING_ID 10
ABS_MT_POSITION_X
ABS_MT_POSITION_Y
ABS_MT_SLOT 1
ABS_MT_TRACKING_ID 11
ABS_MT_POSITION_X
ABS_MT_POSITION_Y
SYN_REPORT

这里看完应该还是不会很明白,可以在下一节的笔记中根据现象来分析。

Tips:

slot 和 ID 这两个概念好像有些混乱,这里再简单解释一下:

slot 是硬件上的一个概念、而 ID 则可认为是软件上的一个概念;对于一个多点触摸设备来说,它最大支持的触摸点数是确定的,如 5 点触摸设备,最多支持 5 个触摸点;每一个触摸点在硬件上它有一个区分的编号,如触摸点0、触摸点 1、触摸点 2 等,这个编号就是一个 slot(通常从 0 开始) ; 如何给识别到的触点分配一个 slot 呢(触点与 slot 关联)?通常是按照时间先后顺序来的,如第一根手指先触碰到触摸屏,那第一根手指就对应触摸点 0(slot=0),接着第二根手指触碰到触摸屏则对应触摸点 1(slot=1)以此类推。这个通常是硬件所支持的。

而 ID 可认为是软件上的一个概念,它也用于区分不同的触摸点,但是它跟 slot 不同, 不是同一层级的概念;举个例子,如一根手指触碰到触摸屏之后拿开,然后再次触碰触摸屏,这个过程中,假设只有这一根手指进行触碰操作,那么两次触碰对应都是触摸点 0(slot=0),这个无疑义。但从触摸点的生命周期来看, 它们是同一个触摸点吗?答案肯定不是,为啥?手指从触摸屏上离开后,该触摸点就消失了、被删除了, 该触摸点的生命周期也就到此结束了,所以它们自然是不同的触摸点, 所以它们的 ID 是不同的。

3. 触摸屏上报数据分析

首先在测试触摸屏之前,需要保证开发板上已经连接了 LCD 屏, ALPHA I.MX6U 开发板出厂系统配套支持多种不同分辨率的 LCD 屏,包括 4.3 寸 480*272、 4.3 寸 800*480、 7 寸 800*480、 7 寸 1024*600以及 10.1 寸 1280*800,在启动开发板之前需要将 LCD 屏通过软排线连接到开发板的 LCD 接口,开发板连接好 LCD 屏之后上电启动开发板、运行出厂系统。

image-20240922101009736

不过我用的没上图那么大,我用的实4.3寸800*480那个。触摸屏与 LCD 液晶屏面板是粘合在一起的,也就是说触摸屏是直接贴在了 LCD 液晶屏上面,直接在LCD 屏上触摸、滑动操作即可。为了测试方便,可以将出厂系统的 GUI 应用程序退出,如何退出呢?点击屏幕进入设置页面,可以看到在该页面下有一个退出按钮选项,直接点击即可。退出后如下所示:

1726971289291

那触摸屏输入的时候是哪个事件节点?我们还是通过以下命令来查看:

1
cat /proc/bus/input/devices

可以看到如下打印信息:

image-20240922094313045

其中这个goodix-ts就是我所使用的触摸屏的名称。我们使用这个测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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);
}

while(1)
{

/* 循环读取数据 */
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);
}
}

编译后执行以下命令:

1
./app_demo /dev/input/event1

然后一个手指点击触摸屏先不松开,终端将会打印如下信息:

image-20240922102341563

我不知道是不是因为我这个是5点触摸屏的缘故,和正点原子官方提供的文档中的差别还挺大,这里按文档中进行学习吧,毕竟我用的是另一块屏幕。文档中现象是这样的:

image-20240922102435847

第一行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_TRACKING|_ID(code=57)事件,并且 value 值等于 78,也就是 ID,这个 ID 是一个非负数,所以表示这是一个新的触摸点被创建,也就意味着触摸屏上产生了一个新的触摸点(手指按下) 。

第二行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_POSITION_X(code=53)事件,其 value对应的便是触摸点的 X 坐标;第三行上报了 ABS_MT_POSITION_Y(code=54)事件,其 value 值对应的便是触摸点 Y 坐标,所以由此可知该触摸点的坐标为(372, 381)。

第四行上报了按键类事件 EV_KEY(type=1)中的 BTN_TOUCH(code=330) , value 值等于 1,表示这是触摸屏上最先产生的触摸点(slot=0、也就是触摸点 0) 。

第五行和第六行分别上报了绝对位移事件 EV_ABS(type=3)中的 ABS_X(code=0)和 ABS_Y(code=1),其 value 分别对应的是触摸点的 X 坐标和 Y 坐标。 多点触摸设备也会通过 ABS_X、 ABS_Y 事件上报触摸点的 X、 Y 坐标, 但通常只有触摸点 0 支持,所以可以把多点触摸设备当成单点触摸设备来使用。

最后一行上报了同步类事件 EV_SYN(type=0)中的 SYN_REPORT(code=0)事件,表示此次触摸点的信息全部上报完毕。

在第一个触摸点的基础上,增加第二个触摸点,打印信息如下所示(还是按文档来学习):

image-20240922103226855

1-7 行和上面一样;

第八行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_SLOT 事件(code=47),表示目前要更新 slot=1 所关联的触摸点(也就是触摸点 1) 对应的信息。

第九行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_TRACKING_ID 事件(code=57), ID=79,这是之前没有出现过的 ID,表示这是一个新的触摸点。

第十、十一行分别上报了 ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 事件。

最后一行上报同步事件(type=0、 code=0) ,告知应用层数据完整。

当手指松开时,触摸点就会被销毁,上报 ABS_MT_TRACKING_ID 事件,并将 value 设置为-1(ID),如下所示:

image-20240922103702265

不管是键盘也好、或者是鼠标、触摸屏,都可以像上面那样将输入设备的数据直接打印出来,然后再去分析,确定该输入设备上报事件的规则和流程,把这些弄懂之后再去编写程序验证结果。

三、获取触摸屏信息

我们怎么知道这个触摸屏支持几个点?触摸点的坐标在什么范围内?我们其实可以通过 ioctl()函数可以获取到这些信息

1. 函数简介

ioctl()是一个文件 I/O 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或设备文件,函数的原型如下:

1
2
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

第一个参数 fd 对应文件描述符;

第二个参数 request 与具体要操作的对象有关,没有统一值, 表示向文件描述符请求相应的操作,也就是请求指令;此函数是一个可变参函数, 第三个参数需要根据 request 参数来决定,配合 request 来使用。

2. 如何使用?

input.h 头文件有这样一些宏定义,如下所示 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#define EVIOCGVERSION		_IOR('E', 0x01, int)			/* get driver version */
#define EVIOCGID _IOR('E', 0x02, struct input_id) /* get device ID */
#define EVIOCGREP _IOR('E', 0x03, unsigned int[2]) /* get repeat settings */
#define EVIOCSREP _IOW('E', 0x03, unsigned int[2]) /* set repeat settings */

#define EVIOCGKEYCODE _IOR('E', 0x04, unsigned int[2]) /* get keycode */
#define EVIOCGKEYCODE_V2 _IOR('E', 0x04, struct input_keymap_entry)
#define EVIOCSKEYCODE _IOW('E', 0x04, unsigned int[2]) /* set keycode */
#define EVIOCSKEYCODE_V2 _IOW('E', 0x04, struct input_keymap_entry)

#define EVIOCGNAME(len) _IOC(_IOC_READ, 'E', 0x06, len) /* get device name */
#define EVIOCGPHYS(len) _IOC(_IOC_READ, 'E', 0x07, len) /* get physical location */
#define EVIOCGUNIQ(len) _IOC(_IOC_READ, 'E', 0x08, len) /* get unique identifier */
#define EVIOCGPROP(len) _IOC(_IOC_READ, 'E', 0x09, len) /* get device properties */

/**
* EVIOCGMTSLOTS(len) - get MT slot values
* @len: size of the data buffer in bytes
*
* The ioctl buffer argument should be binary equivalent to
*
* struct input_mt_request_layout {
* __u32 code;
* __s32 values[num_slots];
* };
*
* where num_slots is the (arbitrary) number of MT slots to extract.
*
* The ioctl size argument (len) is the size of the buffer, which
* should satisfy len = (num_slots + 1) * sizeof(__s32). If len is
* too small to fit all available slots, the first num_slots are
* returned.
*
* Before the call, code is set to the wanted ABS_MT event type. On
* return, values[] is filled with the slot values for the specified
* ABS_MT code.
*
* If the request code is not an ABS_MT value, -EINVAL is returned.
*/
#define EVIOCGMTSLOTS(len) _IOC(_IOC_READ, 'E', 0x0a, len)

#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len) /* get global key state */
#define EVIOCGLED(len) _IOC(_IOC_READ, 'E', 0x19, len) /* get all LEDs */
#define EVIOCGSND(len) _IOC(_IOC_READ, 'E', 0x1a, len) /* get all sounds status */
#define EVIOCGSW(len) _IOC(_IOC_READ, 'E', 0x1b, len) /* get all switch states */

#define EVIOCGBIT(ev,len) _IOC(_IOC_READ, 'E', 0x20 + (ev), len) /* get event bits */
#define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo) /* get abs value/limits */
#define EVIOCSABS(abs) _IOW('E', 0xc0 + (abs), struct input_absinfo) /* set abs value/limits */

#define EVIOCSFF _IOW('E', 0x80, struct ff_effect) /* send a force effect to a force feedback device */
#define EVIOCRMFF _IOW('E', 0x81, int) /* Erase a force effect */
#define EVIOCGEFFECTS _IOR('E', 0x84, int) /* Report number of effects playable at the same time */

#define EVIOCGRAB _IOW('E', 0x90, int) /* Grab/Release device */
#define EVIOCREVOKE _IOW('E', 0x91, int) /* Revoke device access */

每一个宏定义后面都有相应的注释,对于 input 输入设备,对其执行 ioctl()操作需要使用这些宏, 不同的宏表示不同请求指令; 如使用 EVIOCGNAME 宏获取设备名称,使用方式如下:

1
2
char name[100];
ioctl(fd, EVIOCGNAME(sizeof(name)), name);

EVIOCGNAME(len)就表示用于接收字符串数据的缓冲区大小,而此时 ioctl()函数的第三个参数需要传入一个缓冲区的地址,该缓冲区用于存放设备名称对应的字符串数据。

EVIOCG(get)开头的表示获取信息, EVIOCS(set)开头表示设置;这里暂且不管其它宏,重点来看看 EVIOCGABS(abs)宏, 这个宏也是通常使用最多的, 如下所示:

1
#define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo)

通过这个宏可以获取到触摸屏 slot(slot<0>表示触摸点 0、 slot<1>表示触摸点 1、 slot<2>表示触摸点 2,以此类推)的取值范围, 可以看到使用该宏需要传入一个 abs 参数,该参数表示为一个 ABS_XXX 绝对位移事件,如 EVIOCGABS(ABS_MT_SLOT)表示获取触摸屏的 slot 信息,此时 ioctl()函数的第三个参数是一个 struct input_absinfo *的指针,指向一个 struct input_absinfo 对象,调用 ioctl()会将获取到的信息写入到struct input_absinfo 对象中。 struct input_absinfo 结构体(input.h - include/uapi/linux/input.h - Linux source code v4.15 - Bootlin)如下所示:

1
2
3
4
5
6
7
8
struct input_absinfo {
__s32 value; //最新的报告值
__s32 minimum; //最小值
__s32 maximum; //最大值
__s32 fuzz;
__s32 flat;
__s32 resolution;
};

获取触摸屏支持的最大触摸点数:

1
2
3
4
5
6
7
struct input_absinfo info;
int max_slots; // 最大触摸点数
if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &info))
{
perror("ioctl error");
}
max_slots = info.maximum + 1 - info.minimum;

四、查看触摸屏节点

其实前面已经说过了,我们还是通过以下命令来查看:

1
cat /proc/bus/input/devices

可以看到如下打印信息:

image-20240922094313045

其中这个goodix-ts就是我所使用的触摸屏的名称。