数码之家

 找回密码
 立即注册

QQ登录

只需一步,快速开始

微信登录

微信扫一扫,快速登录

搜索
查看: 2683|回复: 52

[外设] 使用ESP32+ELAN触摸板打造Windows精确式触摸板设备

  [复制链接]
发表于 2025-12-27 15:11:30 | 显示全部楼层 |阅读模式
观前提醒:作者不是计算机专业的。所以以下有什么涉及到专业错误的请指出来,作者虚心接受一切批评。

之前开发的ESP32 HID设备都是通过HID通信控制硬盘盒控制中心什么的。但其实这些只是进行简单的通信,并没有完全掌握HID设备的相关开发。

正好最近无聊,于是打算做一些进阶的东西来打发时间。

三年前刚上大学的时候从笔记本换到台式机主力,突然从触摸板+鼠标转到纯鼠标的我在面对部分场景时显得有些吃力。

之前都是鼠标配合触摸板使用的,只有纯鼠标后在桌面上划拉半天才想起来我已经没有触摸板(悲。心想要是在台式机上能用上触摸板多好啊。

暑假开始整HID玩后,这个愿望就显得越发强烈了……

于是这个计划便也提上日程了(喜。

目前已经完成:

硬件:
  • PCB设计
  • 材料选型
软件:
  • 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堆栈
系统就会知道:

  • 同一时间存在N个指针
  • 他们的运动轨迹和生命周期

最后系统会通过非公开的手势识别引擎判断多指触控类型。




关于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报文获得了这个诱骗代码:

  1. int elan_i2c_write_payload(uint8_t addr, uint16_t reg, const uint8_t* data, size_t len) {
  2.   Wire.beginTransmission(addr);
  3.   Wire.write(reg & 0xFF);       // LSB
  4.   Wire.write((reg >> 8) & 0xFF); // MSB
  5.   for (size_t i = 0; i < len; i++) {
  6.     Wire.write(data[i]);
  7.   }
  8.   return Wire.endTransmission();
  9. }

  10. elan_i2c_write_payload(I2C_ADDR, 0x0005, abs_mode_cmd, sizeof(abs_mode_cmd));

  11.   uint8_t abs_mode_cmd[] = {
  12.   0x33, 0x03, 0x06, 0x00,
  13.   0x05, 0x00, 0x03, 0x03,
  14.   0x00
  15. };
复制代码

这个代码通过HID描述符读取让触摸板固件认为有一个完整Host,它可以作用于Vendor Feature Report,固件内部状态机识别后开启PTP Collection。

最后的效果如下:








关于HID报文与触摸事件上报:

首先是手指滑动。

我在usb descriptor中定义了五个手指,单个手指的定义如下:

  1. 0x85, REPORTID_TOUCHPAD,            //   REPORT_ID (Touch pad)
  2. 0x09, 0x22,                         //   USAGE (Finger)
  3. 0xa1, 0x02,                         //   COLLECTION (Logical)
  4. 0x15, 0x00,                         //       LOGICAL_MINIMUM (0)
  5. 0x25, 0x01,                         //       LOGICAL_MAXIMUM (1)
  6. 0x09, 0x47,                         //       USAGE (Confidence)
  7. 0x09, 0x42,                         //       USAGE (Tip switch)
  8. 0x95, 0x02,                         //       REPORT_COUNT (2)
  9. 0x75, 0x01,                         //       REPORT_SIZE (1)
  10. 0x81, 0x02,                         //       INPUT (Data,Var,Abs)
  11. 0x95, 0x01,                         //       REPORT_COUNT (1)
  12. 0x75, 0x02,                         //       REPORT_SIZE (2)
  13. 0x25, 0x02,                         //       LOGICAL_MAXIMUM (2)
  14. 0x09, 0x51,                         //       USAGE (Contact Identifier)
  15. 0x81, 0x02,                         //       INPUT (Data,Var,Abs)
  16. 0x75, 0x01,                         //       REPORT_SIZE (1)
  17. 0x95, 0x04,                         //       REPORT_COUNT (4)
  18. 0x81, 0x03,                         //       INPUT (Cnst,Var,Abs)
  19. 0x05, 0x01,                         //       USAGE_PAGE (Generic Desktop)
  20. 0x15, 0x00,                         //       LOGICAL_MINIMUM (0)
  21. 0x26, 0xff, 0x0f,                   //       LOGICAL_MAXIMUM (4095)
  22. 0x75, 0x10,                         //       REPORT_SIZE (16)
  23. 0x55, 0x0e,                         //       UNIT_EXPONENT (-2)
  24. 0x65, 0x13,                         //       UNIT(Inch,EngLinear)
  25. 0x09, 0x30,                         //       USAGE (X)
  26. 0x35, 0x00,                         //       PHYSICAL_MINIMUM (0)
  27. 0x46, 0x90, 0x01,                   //       PHYSICAL_MAXIMUM (400)
  28. 0x95, 0x02,                         //       REPORT_COUNT (2)
  29. 0x81, 0x02,                         //       INPUT (Data,Var,Abs)
  30. 0x46, 0x13, 0x01,                   //       PHYSICAL_MAXIMUM (275)
  31. 0x09, 0x31,                         //       USAGE (Y)
  32. 0x81, 0x02,                         //       INPUT (Data,Var,Abs)
  33. 0x05, 0x0d,                         //       USAGE_PAGE (Digitizers)
  34. 0x15, 0x00,                         //       LOGICAL_MINIMUM (0)
  35. 0x25, 0x64,                         //       LOGICAL_MAXIMUM (100)
  36. 0x95, 0x02,                         //       REPORT_COUNT (2)
  37. 0x09, 0x48,                         //       USAGE (Width)
  38. 0x09, 0x49,                         //       USAGE (Height)
  39. 0x81, 0x02,                         //       INPUT (Data,Var,Abs)
  40. 0xc0,                               //    END_COLLECTION
复制代码



最后通过每Frame来上报PTP,通过下面的报文格式:

  1. typedef struct __attribute__((packed)) {
  2.     finger_t fingers[5];   // 每个手指信息
  3.     uint16_t scan_time;    // 时间戳
  4.     uint8_t contact_count; // 当前活跃手指数量
  5.     uint8_t buttons;       // 按键状态
  6. } ptp_report_t;

  7. typedef struct __attribute__((packed)) {
  8.     uint8_t tip_conf_id;  // Tip switch, Confidence, Contact ID
  9.     uint16_t x;           // X 坐标
  10.     uint16_t y;           // Y 坐标
  11. } finger_t;
复制代码


通过微软官方的验证工具DigiInfo抓取相关报文后,可以看到手势由多Frame组成:




而关于触摸板的物理按键,因为在板上只有一个按键,所以如果按下的时候,报文格式是这样的:

  1. 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
复制代码

未按下的时候:

  1. 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(触控死区):

  1. DeadZone(触控死区):
  2. #define TAP_DEADZONE 20
  3. ...
  4. int dx = abs((int)rx - (int)origin_x[id]);
  5. int dy = abs((int)ry - (int)origin_y[id]);

  6. if (dx < TAP_DEADZONE && dy < TAP_DEADZONE) {
  7.     current_state.fingers[id].x = origin_x[id];
  8.     current_state.fingers[id].y = origin_y[id];
  9. } else {
  10.     current_state.fingers[id].x = fx;
  11.     current_state.fingers[id].y = fy;
  12. }
复制代码

通过触控死区判定消除轻微抖动,避免手指刚接触时Windows误判为移动。
如果手指移动距离 < DeadZone Threshold,则返回原点坐标。

  • 一阶滤波(低通滤波):

一阶滤波公式:



其中:

ynew:滤波后的当前值
xcurrent:当前输入值
ylast:上一帧滤波值
α:滤波系数(0~1),数值越大响应越快

代码实现:

  1. float dynamic_alpha = (dist > 30.0f) ? 0.8f : 0.3f;
  2. filtered_x[id] = (dynamic_alpha * (float)rx) + ((1.0f - dynamic_alpha) * filtered_x[id]);
  3. filtered_y[id] = (dynamic_alpha * (float)ry) + ((1.0f - dynamic_alpha) * filtered_y[id]);
复制代码

dynamic_alpha根据距离动态调整:
  • 当手指移动距离大(快速移动)时,α高→快速响应
  • 当手指移动距离小(微小抖动)时,α低→平滑处理

通过一阶滤波保证手指移动平滑、视觉体验好,同时避免触控板抖动导致鼠标抖动.

  • Jump Threshold(跳变抑制):

  1. if (last_raw_x[id] != 0 && abs((int)rx - (int)last_raw_x[id]) > JUMP_THRESHOLD) {
  2.     current_state.fingers[id].x = last_raw_x[id];
  3.     current_state.fingers[id].y = last_raw_y[id];
  4. }
复制代码


如果连续两次读取坐标差值超过跳变抑制阈值,则丢弃当前值,保持上一帧坐标。防止触控数据瞬间可能由芯片噪声或I2C读取错误导致的异常跳变。增加系统鲁棒性,避免鼠标瞬移或手势异常。

  • Settling(抖动稳定期):

  1. if (active_count > 0 && active_count &lt; 4 && (now - first_touch_time &lt; SETTLING_MS)) {
  2.     continue;
  3. }
复制代码

用first_touch_time记录初次触摸时间,在短时间内(SETTLING_MS)忽略不完整触控数据。
避免刚接触时触发误判多指触控,提高多指手势识别稳定性。

  • 按键锁定:

  1. static uint8_t locked_button = 0;
  2. static bool is_detecting_click = false;

  3. if (physical_pressed) {
  4.     if (!is_detecting_click) {
  5.         is_detecting_click = true;
  6.         locked_button = (raw_x > 1700) ? 0x02 : 0x01;
  7.     }
  8.     msg_out->button_mask = locked_button;
  9. } else {
  10.     is_detecting_click = false;
  11.     locked_button = 0;
  12.     msg_out->button_mask = 0;
  13. }
复制代码


实现物理按键单击锁定,锁定按钮状态直到手指抬起,避免短时间内反复触发。




实物/测试大概是这样:

请忽略图中的胶布,因为fpc买短了所以只能先用胶布固定开发,等后面画个壳子装起来。













GIF动图演示:






写在最后:

  • 因为小弟水平实在有限,所以希望大佬们可以帮忙优化一下代码,顺便请求各位大佬们做一下最后的多指tap适配,在此谢过各位大佬们~(๑˃̵ᴗ˂̵)
  • 项目仅适用于部分ELAN触摸板,Synaptic等其他厂家的触摸板需自行适配。




资料参考:





补充内容 (2026-1-1 10:31):
补充一下,多指tap手势已经做出来了,但是目前可能不稳定没办法触发

补充内容 (2026-1-24 18:56):
再补充一下,这玩意还有一个用就是替代我笔记本的触摸板
暗影精灵7的断触问题快把我逼疯了,客服也只会一句“你用鼠标不就行了”,真的无语.....

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录

x

打赏

参与人数 8家元 +204 收起 理由
zzqqzzz + 30
cbh1a + 30 優秀文章
沙漠臭屁虫 + 30
vicdoo + 30 優秀文章
hongo + 30 優秀文章

查看全部打赏

 楼主| 发表于 2026-1-26 20:11:00 | 显示全部楼层
游客请登录后查看回复内容

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录

x
回复 支持 反对

使用道具 举报

发表于 2025-12-27 15:40:38 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

 楼主| 发表于 2025-12-27 15:57:49 来自手机浏览器 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-29 17:41:31 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-29 19:30:05 来自手机浏览器 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

 楼主| 发表于 2025-12-29 23:16:32 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-30 08:23:41 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-30 09:08:32 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-30 10:45:47 来自手机浏览器 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-30 10:54:35 | 显示全部楼层
游客请登录后查看回复内容

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录

x
回复 支持 反对

使用道具 举报

 楼主| 发表于 2025-12-30 13:19:15 | 显示全部楼层
游客请登录后查看回复内容

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录

x
回复 支持 反对

使用道具 举报

发表于 2025-12-30 23:55:16 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

 楼主| 发表于 2025-12-31 00:24:05 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-31 08:59:30 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-31 09:54:15 来自手机浏览器 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-31 10:02:04 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-31 14:28:15 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-31 18:03:05 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-31 18:16:35 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

发表于 2025-12-31 18:34:49 | 显示全部楼层
游客请登录后查看回复内容
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册 微信登录

本版积分规则

APP|手机版|小黑屋|关于我们|联系我们|法律条款|数码之家-技术知识分享平台

闽公网安备35020502000485号

闽ICP备2021002735号-2

GMT+8, 2026-6-12 09:36 , Processed in 0.140400 second(s), 12 queries , Gzip On, Redis On.

Powered by Discuz!

© MyDigit.Net Since 2006

快速回复 返回顶部 返回列表