Skip to content

关于电容触摸屏的研究及坐标回报原理

手上有几块In-cell触摸屏,触控IC是新思的。查找某宝上的电容触摸驱动板,没有发现支持新思的产品。后发现Linux内核中有对应的驱动,遂尝试自行开发触摸屏驱动板。在开发过程中深入学习了触摸屏相关知识,记录于此。

提示

使用RP2040实现的USB触摸屏驱动板: GitHub

触摸屏数据回报格式

网上查找资料,发现多点触摸屏的数据回报方式分A型和B型。使用A型的触摸屏已经很少了,这里只关注B型协议。
B型,网上的说法是会将每一个触摸点都分配一个slot(槽位),回报时会将触摸点的信息按照槽位来进行回报。
这里说的比较抽象,直接看例子:

假设我们规定,一个触点由以下几个元素组成:

  • 触点的ID
  • 触点的X轴坐标
  • 触点的Y轴坐标
    转换为代码就是这样:
c
typedef struct __packed {
    uint8_t touchpoint_id; // 触点ID
    uint16_t x; // X轴坐标
    uint16_t y; // Y轴坐标
} touchpoint;

由于触屏的长宽一般都大于255,所以这里使用两个16字节的整数来存储坐标。
实际情况中触屏的回报数据格式一般还会包括更多的信息,比如触点的形状、大小、压力……这里只是提供一个最小的格式示例。
假如此时有一根手指落在了触摸屏上,则:

触摸屏:*给主机发一个中断信号*
主机:触摸数据来了?有几个点?
触摸屏:有1个点
主机:好嘞,那你把这个点的数据给我
触摸屏:okay

数据如下:

触点IDXY
0500500

这样,主机拿到这个触点的数据后,将其发给上级设备,就完成了触摸点的回报。
这时,又有一根手指落在了触屏上:

触摸屏:*给主机发一个中断信号*
主机:wc,又特么干啥?
触摸屏:有人摸我,这次是两个同时在摸
主机:行行行,把这两个点的数据都给我
触摸屏:好

数据如下:

触点IDXY
0500500
1200400

在一段时间后,第一根落在触屏上的手指抬了起来:

触摸屏:*给主机发一个中断信号*
主机:这回是什么情况?
触摸屏:好消息:摸我的人走了 坏消息:没走完,还有一个在摸
主机:...把现在正在摸的那个数据给我
触摸屏:中

数据如下:

触点IDXY
000
1200400

主机通过这三次数据回报,就完整地描述了一个"第一根手指落下——第二根手指落下——第一根手指抬起"的情景。
实际上的数据回报模式各有不同,比如GT系列是不会上报空的触摸点, 而新思的F11协议中则会上报。

HID回报

HID协议是比较复杂的,它并没有规定数据要按照某种特定的格式来回报,而是让从机发挥"主观能动性":

从机:你好,我是USB设备,我支持HID协议
主机:好,你把你的HID回报描述符拿来给我看看
从机: *发了一大坨描述符*
主机:byd这么多啊,我看看
主机: 所以你是属于数字化器(HID_USAGE_PAGE_DIGITIZER)设备,是一个触摸屏(0x04),你回报的数据格式是&%^$&%&#……
从机:*不语,只是一味的点头*
主机:*一段时间的处理*
主机:我准备好了,你收到数据就发给我吧
从机:hell yeah,上班咯

根据微软的HID触摸屏开发指南,一个触点需要有以下的元素:

  • ID
  • X
  • Y
  • Tip(指示器,后面会讲)

而以下的这些元素是可选值:

  • Confidence(可信度)
  • Width
  • Height
  • Pressure
  • Azimuth

转换成代码则是这样:

c
typedef struct __packed {
    uint8_t id;
    uint16_t x;
    uint16_t y;
    bool tip;
    /* Optional */ bool confidence;
    /* Optional */ uint8_t width;
    /* Optional */ uint8_t height;
    /* Optional */ uint8_t pressure;
    /* Optional */ uint16_t azimuth;
}hid_touchpoint;

顺带一提,由于HID回报可以修改每个值的大小,所以这里每个值的大小都是可以配置的,比如触点长宽如果大于255的话也可以在回报描述符中修改这个值所占的位长度为16。

那么这里还有一个问题:主机怎么知道从机回报了几个点呢?

从机:*发了一大坨数据*
主机: *分析中*
主机:兹收到从机发来触摸数据:内包含触点1个,扫描时间为第1秒,触点的x为100, y为100, 状态是已接触,ID为0,特此声明
从机:好好好

在从机将所有的触摸点数据收集起来之后,还要在它的外面附上这个包的大致信息,就像纸箱子上面的快递单一样,这里直接用代码来演示:

c
typedef struct {
    uint8_t report_id; // 报告的ID
    uint8_t touch_points; // 触点的数量
    uint16_t scan_time; // 扫描的时间
    hid_touchpoint touchpoint[5]; // 真实的触点数据
} hid_report;

可能这时候你就要纳闷了,这个report_id是个什么byd玩意?

主机:*空闲*
从机: *插入USB*
主机: 哟,有新小弟啊,介绍介绍自己?
从机:各位大哥好,我叫U2HTS,我的功能是向你们提供触摸数据
主机:行,那既然大伙都在一块,就一起上班吧
主机:你能提供哪些数据?
从机:我能提供三种不同的报告:第一个报告是触摸点的信息,报告的号码是1;第二个报告是我最大能支持多少个触摸点的信息,报告号码是2;第三个报告是我老爹给我的传家宝,好像是什么证书,我也不知道有啥用,报告号码是3。
主机:*思索*
主机:那好吧,你把报告2和报告3发上来我看看
从机:*发送报告*
主机:*思索*
主机:我准备好了,你收到数据就发给我吧
从机:好

因为前面说的HID报告的"主观能动性",这里的报告号码实际上可以是任何值,取决于你喜欢哪个数字。

由于报告2和3实际上是HID_FEATURE,所以在枚举设备后,主机会立即要求从机回报这两个值。

报告2一般来说只是一个16位的数字,主机读取这个值之后就知道从机最多支持多少个触摸点了。

而报告3则是微软的某种证书,当Windows读到这个证书并验证后,会在控制面板中显示"完全支持Windows触控"。

回报模式

还是以刚才那个"第一根手指落下——第二根手指落下——第一根手指抬起"的事件为例子,下文简写为“触摸事件”。 我们假设这个触摸屏设备一次性只能回报两个触摸点,触摸数据只有触摸点id、x坐标、y坐标。
则回报结构应该是这样的:

c
typedef struct __packed {
    uint8_t id;
    uint16_t x;
    uint16_t y;
    bool tip;
}hid_touchpoint;

typedef struct {
    uint8_t report_id;
    uint8_t touch_points;
    uint16_t scan_time;
    hid_touchpoint touchpoint[2];
} hid_report;
时间点1的ID点1的x坐标点1的y坐标点2的ID点2的x坐标点2的y坐标
10512512XXX
205125121100100
30001100100

以上表格为触摸事件发生过程中单片机收到的数据。
这时候便轮到一直没有讲到的Tip值出场了: HID触摸屏协议规定,当一个点离开触摸屏之后,必须向主机报告其最后的坐标,并且设置Tip值为0。此外,还需要在外层报告中的“触摸点数”中包含该点。
则以上触摸事件的报告流程表如下:

时间点1的Tip点1的ID点1的x坐标点1的y坐标点2的Tip点2的ID点2的x坐标点2的y坐标总触摸点数
1105125120XXX1
210512512111001002
300512512111001002
40000111001001

转换为代码则是这样:

c
hid_report report;
// 第一次回报
memset(report, 0x00, sizeof(report));
report.touchpoint[0].id = 0;
report.touchpoint[0].x = 512;
report.touchpoint[0].y = 512;
report.touchpoint[0].tip = 1;
report.scan_time = 1;
report.touch_points = 1;
HID_Report(&report, sizeof(report));

// 第二次回报
memset(report, 0x00, sizeof(report));
report.touchpoint[0].id = 0;
report.touchpoint[0].x = 512;
report.touchpoint[0].y = 512;
report.touchpoint[0].tip = 1;
report.touchpoint[1].id = 1;
report.touchpoint[1].x = 100;
report.touchpoint[1].y = 100;
report.touchpoint[1].tip = 1;
report.scan_time = 2;
report.touch_points = 2;
HID_Report(&report, sizeof(report));

// 第三次回报
memset(report, 0x00, sizeof(report));
report.touchpoint[0].id = 0;
report.touchpoint[0].x = 512;
report.touchpoint[0].y = 512;
report.touchpoint[0].tip = 0; // 注意,这里将Tip值设为0。
report.touchpoint[1].id = 1;
report.touchpoint[1].x = 100;
report.touchpoint[1].y = 100;
report.touchpoint[1].tip = 1;
report.scan_time = 3;
report.touch_points = 2; // 包括已经释放掉的触摸点
HID_Report(&report, sizeof(report));

// 第四次回报
memset(report, 0x00, sizeof(report));
report.touchpoint[1].id = 1;
report.touchpoint[1].x = 100;
report.touchpoint[1].y = 100;
report.touchpoint[1].tip = 1;
report.scan_time = 4;
report.touch_points = 1;
HID_Report(&report, sizeof(report));

关于如何确定触摸点是否在屏幕上,一般来说芯片会使用一个寄存器值的某一位来表示触摸点状态,比如在FT5X06的寄存器手册中,每一个触摸点的第一个字节(即TOUCHX_XH)的第6和第7位为Event Flag,当该值为0b10(即Contact)时,则该点位于触摸屏上。

在文档中还存在一种被称为串行模式的回报模式,其具体实现为:HID报告中只声明一个触摸点,当设备检测到多个触摸点时,发送的第一个报告中将touch_points这个值设为触摸点的数量,而之后发送的报告中将该值设为0。笔者尝试使用这种模式来回报数据,但是当触摸点数量较多时,回报率会降低到10以下,所以不采用这种回报模式。