关于电容触摸屏的研究及坐标回报原理
手上有几块In-cell触摸屏,触控IC是新思的。查找某宝上的电容触摸驱动板,没有发现支持新思的产品。后发现Linux内核中有对应的驱动,遂尝试自行开发触摸屏驱动板。在开发过程中深入学习了触摸屏相关知识,记录于此。
提示
使用RP2040实现的USB触摸屏驱动板: GitHub
触摸屏数据回报格式
网上查找资料,发现多点触摸屏的数据回报方式分A型和B型。使用A型的触摸屏已经很少了,这里只关注B型协议。
B型,网上的说法是会将每一个触摸点都分配一个slot(槽位),回报时会将触摸点的信息按照槽位来进行回报。
这里说的比较抽象,直接看例子:
假设我们规定,一个触点由以下几个元素组成:
- 触点的ID
- 触点的X轴坐标
- 触点的Y轴坐标
转换为代码就是这样:
typedef struct __packed {
uint8_t touchpoint_id; // 触点ID
uint16_t x; // X轴坐标
uint16_t y; // Y轴坐标
} touchpoint;
由于触屏的长宽一般都大于255,所以这里使用两个16字节的整数来存储坐标。
实际情况中触屏的回报数据格式一般还会包括更多的信息,比如触点的形状、大小、压力……这里只是提供一个最小的格式示例。
假如此时有一根手指落在了触摸屏上,则:
触摸屏:*给主机发一个中断信号*
主机:触摸数据来了?有几个点?
触摸屏:有1个点
主机:好嘞,那你把这个点的数据给我
触摸屏:okay
数据如下:
触点ID | X | Y |
---|---|---|
0 | 500 | 500 |
这样,主机拿到这个触点的数据后,将其发给上级设备,就完成了触摸点的回报。
这时,又有一根手指落在了触屏上:
触摸屏:*给主机发一个中断信号*
主机:wc,又特么干啥?
触摸屏:有人摸我,这次是两个同时在摸
主机:行行行,把这两个点的数据都给我
触摸屏:好
数据如下:
触点ID | X | Y |
---|---|---|
0 | 500 | 500 |
1 | 200 | 400 |
在一段时间后,第一根落在触屏上的手指抬了起来:
触摸屏:*给主机发一个中断信号*
主机:这回是什么情况?
触摸屏:好消息:摸我的人走了 坏消息:没走完,还有一个在摸
主机:...把现在正在摸的那个数据给我
触摸屏:中
数据如下:
触点ID | X | Y |
---|---|---|
0 | 0 | 0 |
1 | 200 | 400 |
主机通过这三次数据回报,就完整地描述了一个"第一根手指落下——第二根手指落下——第一根手指抬起"的情景。
实际上的数据回报模式各有不同,比如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
转换成代码则是这样:
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,特此声明
从机:好好好
在从机将所有的触摸点数据收集起来之后,还要在它的外面附上这个包的大致信息,就像纸箱子上面的快递单一样,这里直接用代码来演示:
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坐标。
则回报结构应该是这样的:
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坐标 |
---|---|---|---|---|---|---|
1 | 0 | 512 | 512 | X | X | X |
2 | 0 | 512 | 512 | 1 | 100 | 100 |
3 | 0 | 0 | 0 | 1 | 100 | 100 |
以上表格为触摸事件发生过程中单片机
收到的数据。
这时候便轮到一直没有讲到的Tip
值出场了: HID触摸屏协议规定,当一个点离开触摸屏之后,必须向主机报告其最后的坐标,并且设置Tip值为0。此外,还需要在外层报告中的“触摸点数”中包含该点。
则以上触摸事件的报告流程表如下:
时间 | 点1的Tip | 点1的ID | 点1的x坐标 | 点1的y坐标 | 点2的Tip | 点2的ID | 点2的x坐标 | 点2的y坐标 | 总触摸点数 |
---|---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 512 | 512 | 0 | X | X | X | 1 |
2 | 1 | 0 | 512 | 512 | 1 | 1 | 100 | 100 | 2 |
3 | 0 | 0 | 512 | 512 | 1 | 1 | 100 | 100 | 2 |
4 | 0 | 0 | 0 | 0 | 1 | 1 | 100 | 100 | 1 |
转换为代码则是这样:
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以下,所以不采用这种回报模式。