|
|
观前提醒:作者不是计算机专业的。所以以下有什么涉及到专业错误的请指出来,作者虚心接受一切批评。
之前开发的ESP32 HID设备都是通过HID通信控制硬盘盒控制中心什么的。但其实这些只是进行简单的通信,并没有完全掌握HID设备的相关开发。
正好最近无聊,于是打算做一些进阶的东西来打发时间。
三年前刚上大学的时候从笔记本换到台式机主力,突然从触摸板+鼠标转到纯鼠标的我在面对部分场景时显得有些吃力。
之前都是鼠标配合触摸板使用的,只有纯鼠标后在桌面上划拉半天才想起来我已经没有触摸板(悲。心想要是在台式机上能用上触摸板多好啊。
暑假开始整HID玩后,这个愿望就显得越发强烈了……
于是这个计划便也提上日程了(喜。
目前已经完成:
硬件:
软件:
- Microsoft精确式触摸板握手
- ELAN TouchPad驱动
- 从Mouse Mode (仅支持单指) 切换到Absolute Mode (支持多指), 感谢@ApprehensiveAnt9858
- 单指触摸支持
- 多指滑动手势支持
- 物理按键支持(左键 & 右键)
未完成的:
- 外壳设计 (SOLIDWORKS建模)
- 为后续的蓝牙支持添加电池(蓝牙无线模式下指纹模块不可用)
- 多指Tap手势支持(技术力有限暂时做不出来 ╥﹏╥)
网上已经有老哥尝试过了,他参考了外国大神的支持,使用的是ESP32+Synatics触摸板(P/S2协议)。
但是P/S2总感觉有一点小膈应……说不上来为什么,并且他的支持最高只有3指,最重要的是,他的多指手势并不是原生的,缺失了很多我想要的手势(要知道Windows原生支持的精确式触摸板功能是很完善的)。当然他用的也不是我们文章所使用到的USB HID技术栈。
叠个甲。这位老哥的思路是很好的,我没有批判这位老哥的意思,我只是觉得他的方案有太多局限性,如果再深入一点的话表现会更好(
所以我打算基于原生的HID触摸板,通过ESP32,造一个移动精确式触摸板设备,来满足自己的日常需要。
首先之一当然是要挑一块合适的触摸板啦。
根据Windows文档可以确定,主要的Windows精确式触摸板可以通过四种总线类型与主机进行连接:
- USB HID
- I2C HID
- SPI HID
- 第三方私有HID设备
无论是USB HID还是第三方私有HID设备,都好像不符合我的DIY之魂……
那么接下来就只剩下I2C HID和SPI HID类型的精确式触摸板了。
其实这两类在笔记本上很常见,但找到一块合适的触摸板有点难,需要找到合适的。
通过一篇老文章,我得知了来自ELAN Technology的触摸板能够支持标准的Microsoft HID over I2C Protocol:
选择范围瞬间缩小了不少。
查阅了大量包括笔记本图纸在内的资料后,我最终选择了板号Rev.A S8974A的触摸板:
这个触控板使用了ELAN 33370A作为触摸板主控,适用于R9000X 2021R/2022、小新Pro14、ThinkBook14 G4+,采用I2C HID协议。
与笔记本连接的定义如下:
插个题外话:感觉如果纯触摸板的话有点浪费了,于是我又购入了一个HP的指纹模块FM3483/3633,它可以通过USB总线提供Fingerprint for Windows Hello支持:
沟通触摸板和电脑USB HID的硬件依旧是我们的老熟人,ESP32-S2FN4R2。
ESP32-S2和指纹模块通过来自Microchip的USB2513B连接在同一块板子上:
硬件讲完了接下来就要到软件实现了,首先是多指识别。
在Windows体系里,多指手势并不是由应用或者触摸板硬件直接“声明”出来的,而是系统基于底层触点数据自动识别的。
也就是说,精确式触摸板只会上报原始触控数据:
- 当前有多少个手指
- 每个手指的唯一ID、X/Y坐标、是否接触、是否抬起
- 时间连续的多帧数据
假如我做了一个三指触摸,从逻辑层面来说是这样的:
第一帧:3个触点,位置A/B/C
第二帧:3个触点,整体向上移动
第三帧:3个触点,继续向上
第四帧:全部抬起
当原始触控数据上报到系统后,Windows的精确式触摸板驱动会:
- 解析HID报告
- 把每个触点转换成内部的指针对象
- 将这些指针数据送入到Input堆栈
系统就会知道:
最后系统会通过非公开的手势识别引擎判断多指触控类型。
关于I2C Over HID、USB HID:
HID通信在网上有很多文章,基本原理就是大差不差。所以这里主要分析的是I2C通信。
根据Windows精确式触摸板——设备总线连接定义,如果设备通过I2C连接到Host,至少需要5个引脚:
- 数据脚(SDA)
- 时钟脚(SCL)
- 中断脚(INT)
- 电源脚(VCC)
- 接地脚(GND)
同时手册中建议至少使用400KHz的I2C始终速度且应独享一个I2C控制器,不然可能会导致I2C总线端超量。最后在设备开发中我使用了800KHZ作为通信基准速度。
另外手册中也提到了Touchpad I2C Over USB这一情况:
如果决定使用某个 USB 桥将 I²C Windows 精确式触摸板连接到其 Windows 主机,则该桥应使用设备的独特属性(wVendorID、wProductID、wVersionID)将触摸板公开为独特的设备节点。
所以最后开发的时候,我们需要在USB描述符中声明HID TouchPad和Multi-Touch。
这里要注意的是不要尝试去伪装原有的ELAN触摸板,不然的话可能会加载错误的驱动,最后进入错误的设备路径导致设备无法被识别。
我们需要让Windows相信,这是一个USB HID Touchpad,不是笔记本内建的触摸板,以此来防止驱动栈误判。
感谢前辈们的辛勤付出,在开发中间层的时候,我们在开发的时候可以参考Linux源码部分hid-elan.c来开发中间层。
所以我们在开发的时候可以根据源码中的定义开发。
关于ESP32:
让触摸板进入PTP Mode后就简单的多了,剩下的两个大坑,一个是逆向ELAN I2C HID数据转译到USB,另一个是严格按照Microsoft Precision Touchpad HID格式去写转译层就行。
最后的可以成功模拟出精确触摸板:
关于ELAN TouchPad:
由于在这方面“几乎”没有人做过(我基本没查到),所以相关资料很少。
首先是地址确认,随便写了个地址程序,确定其ADDR为0x15(0x78那个不知道是什么,后续都是基于0x15开发的):
理论上来说这时候直接读接收内容直接转译就行,但是这里开始就有大坑了。
根据一篇很老的参考资料可以得知,ELAN触摸板有两种运行方式,一种是Mouse Mode(仅支持单指,多指没数据,Report ID为0x01),另外一种是Absolute Mode(支持多指,Report ID为0x04)。
但是如果没有经过BIOS/EC的私有协商后,ELAN触摸板触摸板固件对MCU暴露的是Mouse Mode而不是Absolute Mode。
我们可以从raw data看到,它传来的数据基本就和鼠标没区别(xy位移而不是绝对坐标值),Report ID也是0x01:
后面多方查找后,我在Reddit论坛的一篇帖子上找到了一名网友 @ApprehensiveAnt9858 的解决方法,他通过抓取HID报文获得了这个诱骗代码:
- int elan_i2c_write_payload(uint8_t addr, uint16_t reg, const uint8_t* data, size_t len) {
- Wire.beginTransmission(addr);
- Wire.write(reg & 0xFF); // LSB
- Wire.write((reg >> 8) & 0xFF); // MSB
- for (size_t i = 0; i < len; i++) {
- Wire.write(data[i]);
- }
- return Wire.endTransmission();
- }
- elan_i2c_write_payload(I2C_ADDR, 0x0005, abs_mode_cmd, sizeof(abs_mode_cmd));
- uint8_t abs_mode_cmd[] = {
- 0x33, 0x03, 0x06, 0x00,
- 0x05, 0x00, 0x03, 0x03,
- 0x00
- };
复制代码
这个代码通过HID描述符读取让触摸板固件认为有一个完整Host,它可以作用于Vendor Feature Report,固件内部状态机识别后开启PTP Collection。
最后的效果如下:
关于HID报文与触摸事件上报:
首先是手指滑动。
我在usb descriptor中定义了五个手指,单个手指的定义如下:
- 0x85, REPORTID_TOUCHPAD, // REPORT_ID (Touch pad)
- 0x09, 0x22, // USAGE (Finger)
- 0xa1, 0x02, // COLLECTION (Logical)
- 0x15, 0x00, // LOGICAL_MINIMUM (0)
- 0x25, 0x01, // LOGICAL_MAXIMUM (1)
- 0x09, 0x47, // USAGE (Confidence)
- 0x09, 0x42, // USAGE (Tip switch)
- 0x95, 0x02, // REPORT_COUNT (2)
- 0x75, 0x01, // REPORT_SIZE (1)
- 0x81, 0x02, // INPUT (Data,Var,Abs)
- 0x95, 0x01, // REPORT_COUNT (1)
- 0x75, 0x02, // REPORT_SIZE (2)
- 0x25, 0x02, // LOGICAL_MAXIMUM (2)
- 0x09, 0x51, // USAGE (Contact Identifier)
- 0x81, 0x02, // INPUT (Data,Var,Abs)
- 0x75, 0x01, // REPORT_SIZE (1)
- 0x95, 0x04, // REPORT_COUNT (4)
- 0x81, 0x03, // INPUT (Cnst,Var,Abs)
- 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
- 0x15, 0x00, // LOGICAL_MINIMUM (0)
- 0x26, 0xff, 0x0f, // LOGICAL_MAXIMUM (4095)
- 0x75, 0x10, // REPORT_SIZE (16)
- 0x55, 0x0e, // UNIT_EXPONENT (-2)
- 0x65, 0x13, // UNIT(Inch,EngLinear)
- 0x09, 0x30, // USAGE (X)
- 0x35, 0x00, // PHYSICAL_MINIMUM (0)
- 0x46, 0x90, 0x01, // PHYSICAL_MAXIMUM (400)
- 0x95, 0x02, // REPORT_COUNT (2)
- 0x81, 0x02, // INPUT (Data,Var,Abs)
- 0x46, 0x13, 0x01, // PHYSICAL_MAXIMUM (275)
- 0x09, 0x31, // USAGE (Y)
- 0x81, 0x02, // INPUT (Data,Var,Abs)
- 0x05, 0x0d, // USAGE_PAGE (Digitizers)
- 0x15, 0x00, // LOGICAL_MINIMUM (0)
- 0x25, 0x64, // LOGICAL_MAXIMUM (100)
- 0x95, 0x02, // REPORT_COUNT (2)
- 0x09, 0x48, // USAGE (Width)
- 0x09, 0x49, // USAGE (Height)
- 0x81, 0x02, // INPUT (Data,Var,Abs)
- 0xc0, // END_COLLECTION
复制代码
最后通过每Frame来上报PTP,通过下面的报文格式:
- typedef struct __attribute__((packed)) {
- finger_t fingers[5]; // 每个手指信息
- uint16_t scan_time; // 时间戳
- uint8_t contact_count; // 当前活跃手指数量
- uint8_t buttons; // 按键状态
- } ptp_report_t;
- typedef struct __attribute__((packed)) {
- uint8_t tip_conf_id; // Tip switch, Confidence, Contact ID
- uint16_t x; // X 坐标
- uint16_t y; // Y 坐标
- } finger_t;
复制代码
通过微软官方的验证工具DigiInfo抓取相关报文后,可以看到手势由多Frame组成:
而关于触摸板的物理按键,因为在板上只有一个按键,所以如果按下的时候,报文格式是这样的:
- 0E 00 04 03 60 02 93 08 E6 38 01 81 1C 63 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
复制代码
未按下的时候:
- 0E 00 04 03 60 02 93 08 E6 38 01 80 1C 63 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
复制代码
根据报文可知,第11位字节发生了变化(0x80→0x81)
所以最后我根据第11位字节向电脑发送触摸板button按下事件,同时发送XY坐标,让系统判断当前按下的是左键还是右键。
关于优化:
- DeadZone(触控死区):
- #define TAP_DEADZONE 20
- ...
- int dx = abs((int)rx - (int)origin_x[id]);
- int dy = abs((int)ry - (int)origin_y[id]);
- if (dx < TAP_DEADZONE && dy < TAP_DEADZONE) {
- current_state.fingers[id].x = origin_x[id];
- current_state.fingers[id].y = origin_y[id];
- } else {
- current_state.fingers[id].x = fx;
- current_state.fingers[id].y = fy;
- }
复制代码
通过触控死区判定消除轻微抖动,避免手指刚接触时Windows误判为移动。
如果手指移动距离 < DeadZone Threshold,则返回原点坐标。
一阶滤波公式:
其中:
ynew:滤波后的当前值
xcurrent:当前输入值
ylast:上一帧滤波值
α:滤波系数(0~1),数值越大响应越快
代码实现:
- float dynamic_alpha = (dist > 30.0f) ? 0.8f : 0.3f;
- filtered_x[id] = (dynamic_alpha * (float)rx) + ((1.0f - dynamic_alpha) * filtered_x[id]);
- filtered_y[id] = (dynamic_alpha * (float)ry) + ((1.0f - dynamic_alpha) * filtered_y[id]);
复制代码
dynamic_alpha根据距离动态调整:
- 当手指移动距离大(快速移动)时,α高→快速响应
- 当手指移动距离小(微小抖动)时,α低→平滑处理
通过一阶滤波保证手指移动平滑、视觉体验好,同时避免触控板抖动导致鼠标抖动.
- if (last_raw_x[id] != 0 && abs((int)rx - (int)last_raw_x[id]) > JUMP_THRESHOLD) {
- current_state.fingers[id].x = last_raw_x[id];
- current_state.fingers[id].y = last_raw_y[id];
- }
复制代码
如果连续两次读取坐标差值超过跳变抑制阈值,则丢弃当前值,保持上一帧坐标。防止触控数据瞬间可能由芯片噪声或I2C读取错误导致的异常跳变。增加系统鲁棒性,避免鼠标瞬移或手势异常。
- if (active_count > 0 && active_count < 4 && (now - first_touch_time < SETTLING_MS)) {
- continue;
- }
复制代码
用first_touch_time记录初次触摸时间,在短时间内(SETTLING_MS)忽略不完整触控数据。
避免刚接触时触发误判多指触控,提高多指手势识别稳定性。
- static uint8_t locked_button = 0;
- static bool is_detecting_click = false;
- if (physical_pressed) {
- if (!is_detecting_click) {
- is_detecting_click = true;
- locked_button = (raw_x > 1700) ? 0x02 : 0x01;
- }
- msg_out->button_mask = locked_button;
- } else {
- is_detecting_click = false;
- locked_button = 0;
- msg_out->button_mask = 0;
- }
复制代码
实现物理按键单击锁定,锁定按钮状态直到手指抬起,避免短时间内反复触发。
实物/测试大概是这样:
请忽略图中的胶布,因为fpc买短了所以只能先用胶布固定开发,等后面画个壳子装起来。
GIF动图演示:
写在最后:
- 因为小弟水平实在有限,所以希望大佬们可以帮忙优化一下代码,顺便请求各位大佬们做一下最后的多指tap适配,在此谢过各位大佬们~(๑˃̵ᴗ˂̵)
- 项目仅适用于部分ELAN触摸板,Synaptic等其他厂家的触摸板需自行适配。
资料参考:
补充内容 (2026-1-1 10:31):
补充一下,多指tap手势已经做出来了,但是目前可能不稳定没办法触发
补充内容 (2026-1-24 18:56):
再补充一下,这玩意还有一个用就是替代我笔记本的触摸板
暗影精灵7的断触问题快把我逼疯了,客服也只会一句“你用鼠标不就行了”,真的无语..... |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
打赏
-
查看全部打赏
|