
基于ESP32的三模精确式压感触摸板
简介
基于ESP32的Surface Laptop Studio 1964压感触摸板破解项目。支持有线、2.4G和Bluetooth三模连接。
简介:基于ESP32的Surface Laptop Studio 1964压感触摸板破解项目。支持有线、2.4G和Bluetooth三模连接。开源协议
:CERN-OHL-S-2.0
(未经作者授权,禁止转载)描述

这是什么?
又一个触摸板破解项目。基于ESP32-S3 + Surface Laptop Studio 1964 Synaptics TouchPad。
- 兼容Microsoft精确式触摸板规范。
- 支持Windows触摸板手势。
- 支持压感引擎。
- 支持反馈调整。
- 支持USB有线/2.4G/蓝牙连接。
同时包含了一个Dell Goodix指纹模块, 用于Windows Hello登录。
TODO List
硬件
- PCB设计
- 外观设计 (SOLIDWORKS建模)
软件
基础功能
- Microsoft精确式触摸板握手
- 从Mouse Mode (仅单指) 切换到Absolute Mode (支持多指)
触摸支持
- 单指触摸
- 多指触摸
物理按键
- 左键 & 右键支持
兼容性
- 添加HID端口以支持Mouse Mode兼容 (适用于不支持PTP的老系统/BIOS, 如Windows 7/XP)
- PTP模拟Mouse Mode支持
无线模式
- 2.4G无线
- 蓝牙(目前仅支持Mouse Mode)
压感与触觉
- 压感调整原理破解
- CS40L25 SDK适配
- ROM模式下触发振动
- 特定waveform固件优化触觉反馈 (实验性)
- 压感支持
- 单击敏感度 (实验性)
- 触觉点击和强度调整 (实验性)
- 触觉反馈和强度控制 (后续可能支持)
目前仍然存在的问题:
- 在蓝牙模式下, Windows接受到并正确解析了HID报文, 但是手势处于不可用的状态。
同时, 在蓝牙模式下目前Windows依旧不支持PTP设置。 - 蓝牙模式下暂时不支持PTP/Mouse Mode切换, 所以目前默认只使用BLE Mouse模式。
- 由于CS40L25的固件问题, 按下的振动反馈比较奇怪。
使用说明
以下操作均基于默认sdkconfig设置。
初始上电,只有USB2513B、指纹模块和CP2102工作。
通过背面的电源开关,向上拨动即可打开ESP32-S3和触摸板的电源。

初始上电运行,默认运行在USB有线连接模式。可通过Func. Key切换模式。
Func. Key在PCB的左侧:

Func. Key行为对应如下:
| 按下时间/行为 | 行为 |
|---|---|
| 3s | 切换运行模式 (有线/2.4G/BLE依次顺序) |
| 5s | 切换到默认有线模式 |
| 按下Func. Key后通电 | ESP32进入Download Mode,可通过DFU Mode刷新固件 |
Func. Key的下方有三颗LED指示灯:

每颗指示灯的行为如下:
| LED | 状态 | 含义 |
|---|---|---|
| LED1 | 常亮 | 放电模式,电量 > 20% |
| 闪烁 | 电量 < 20%,请充电 | |
| LED2 | 常亮 | 电池已充满 |
| 闪烁 | 电池正在充电 | |
| LED3 | 长亮1次(2s) | 有线连接 |
| 长亮2次(0.5s) | 2.4G连接 | |
| 短亮2次循环 | BLE连接,设备未连接 | |
| 长亮3次(0.5s) | BLE连接,设备已连接 |
硬件说明
所有PCB设计板厚1.0mm,阻抗管控JLC04161H-7628,USB差分阻抗90Ω,单端阻抗50Ω。
- 触摸板采用
Microsoft Surface Laptop Studio 1964的触摸板备件。触摸板采用新思Synaptics S96U7方案。
板载振动马达方案采用CirrusLogic CS40L25方案。


-
指纹方案采用戴尔
G7 7500/7700 Goodix指纹解决方案。 -
主控采用
ESP32-S3FH4R2方案, 集成2.4 GHz Wi-Fi和Bluetooth 5 (LE)的MCU芯片。 -
接收器主控采用
ESP32-S2FN4R2方案, 集成2.4 GHz Wi-Fi的MCU芯片。 -
板载一颗
CP2102USB到UART桥接控制器 (QFN-24封装) , 与CP2104-GMR和CH9102F封装兼容。 -
ESP32-S3、CP2102和指纹模块通过USB2513B连接到上游HOST, 3-Port USB 2.0 Hi-Speed Hub控制器。 -
电源管理系统采用来自
Texas Instruments的BQ24195。具有5.1V、2.1A同步升压操作的I2C控制型4.5A单节电池充电器。 -
CS40L25采用的电源驱动方案基于MP28167GQ-A-Z, 2.8V-22V输入、3A电流输出、4通道Buck-Boost转换器。 -
板载设备采用了两颗
TLV62585DCDC降压3.3V输出方案, 2.5V-5.5V输入、3A降压转换器。ESP32-S3+触摸板共享一路3.3V,USB2513B、指纹和CP2102共享另外一路3.3V。 -
其中一路
TLV62585和MP28167GQ-A-Z上游通过BQ24195供电。
编译设置
TouchPad sdkconfig
触摸板sdkconfig默认设置已保存在sdkconfig.defaults。以下值可手动调整。
USB Descriptor Options (sdkconfig / TouchPad Configuration / USB Descriptor Options)

TouchPad Manufacturer、TouchPad Product String、TouchPad Serial Number (TOUCHPAD_MANUFACTURER_STRING、TOUCHPAD_PRODUCT_STRING、TOUCHPAD_SERIAL_NUMBER_STRING)
设置触摸板面向Host端显示的USB制造商、产品名和序列号。
Receiver Configuration (sdkconfig / TouchPad Configuration / Receiver Configuration)
设置触摸板端的2.4G Receiver相关选项。

Receiver MAC Address (RECEIVER_MAC_ADDR)
填写2.4G Receiver物理Mac地址, 格式为XX:XX:XX:XX:XX:XX, 默认为FF:FF:FF:FF:FF:FF。
Feature Options (sdkconfig / TouchPad Configuration / Feature Options)
设置触摸板端的特殊功能。

TouchPad Rotation (TP_ROTATION_LANDSCAPE)
设置触摸板朝向, 该项有4个选项:
Landscape- 横向Portrait- 竖向Landscape (flipped)- 横向 (翻转)Portrait (flipped)- 竖向 (翻转)
这个选项只会在有线模式和BLE模式下生效, 2.4G模式下不会生效。
Press FUNC key to switch mode timeout (ms) (FUNC_TIMEOUT_MS)
设置三模模式切换的Func. key按下时间。
Press FUNC key to reset mode timeout (ms) (FUNC_RESET_MS)
设置三模模式重置的Func. key按下时间。当超过该值时默认重置回USB有线连接。
Led blink when FUNC key is pressed if reaching FUNC_TIMEOUT_MS (LED_FLASH_FUNC_TIMEOUT)
当Func. key按下时间达到FUNC_TIMEOUT_MS时则会闪烁进行提示。
BLE HID Mode Select
选择BLE模式下的声明模式。该项有两个选项:
- Mouse Mode (默认) : 鼠标模式。
- PTP Mode: 精确式触摸板模式, 但当前不可用, 系统不支持。
Mouse Mode Select
选择鼠标模式下的计算模式。该项有两个选项:
- Original Mouse Mode
ORI_MOUSE_MODE(默认) : 原生鼠标模式, 拥有最好的兼容性, 但是功能最少。 - PTP Simulated Mouse Mode
PTP_SIMULATED_MOUSE_MODE: PTP模拟鼠标模式, 拥有自定义手势, 但是bug可能较多。
2.4G Receiver sdkconfig
2.4G Receiver sdkconfig默认设置已保存在sdkconfig.defaults。以下值可手动调整。
USB Descriptor Options (sdkconfig / TouchPad Configuration / USB Descriptor Options)

TouchPad Manufacturer、TouchPad Product String、TouchPad Serial Number (TOUCHPAD_MANUFACTURER_STRING、TOUCHPAD_PRODUCT_STRING、TOUCHPAD_SERIAL_NUMBER_STRING)
设置触摸板面向Host端显示的USB制造商、产品名和序列号。
Using Goodix Descriptor from ESP32-Haptic-Touchpad for better capability (LAST_GEN_DESC)
使用来自上一代项目ESP32 Precision Touchpad的Goodix HID Descriptor以获得更好的兼容性。
这个值仅会影响Legacy TouchPad模式。
PTP Simulated Mouse Mode说明
因为原生鼠标模式的功能很少(只有光标滑动和左键),所以设计了一个PTP Simulated Mouse Mode,同时通过计算定义了一些手势, 用于模拟正常鼠标的使用行为手势。
轻触手势:
| 手势 | 名称 | 说明 | 备注 |
|---|---|---|---|
![]() | 单指轻触 | 鼠标左键 | / |
![]() | 双指轻触 | 鼠标右键 | / |
![]() | 三指轻触 | 鼠标中键 | / |
拖动手势:
| 手势 | 名称 | 说明 | 备注 |
|---|---|---|---|
+ ![]() | 单点并拖动 | 点击两次后再拖动即可多选 | / |
+ ![]() | 双指滑动 | 鼠标滚轮 | 可以上下平移, 也可以左右平移 |
技术方案
1️⃣ 触摸板破解
HID Descriptor破解
自第四代Surface产品开始, Surface开始采用自研的EC控制器SAM (Surface Aggregator Module) 。
在Surface Laptop Studio产品中, 触摸板挂在EC控制器下。
但触摸板本身依旧采用I2C HID协议与设备通信。
首先通过I2C地址扫描程序确定其HID控制器地址:

结合原厂图纸中的注释可得知:
7-bit I2C Address TP Touch IC = 0x48
7-bit I2C Address Haptic Motor Dr = 0x43
7-bit I2C Address Synaptic Touch Controller = 0x2C
最后可以基本确定可以破解的地址是0x2c或者0x48。
参考其他项目源码后尝试写入Synaptics常见的HID描述符寄存器地址, 最后在0x2c下0x0021处获取到了HID Descriptor:

获取到的HID Descriptor如下:
05 01 09 02 A1 01 85 02 09 01 A1 00 05 09 19 01 29 02 15 00 25 01 75 01 95 02 81 02 95 06 81 01 05 01 09 30 09 31 15 81 25 7F 75 08 95 02 81 06 C0 C0 05 0D 09 05 A1 01 85 03 05 0D 09 22 A1 02 15 00 25 01 09 47 09 42 95 02 75 01 81 02 95 01 75 03 25 05 09 51 81 02 75 01 95 03 81 03 05 01 15 00 26 FA 08 75 10 55 0E 65 11 09 30 35 00 46 7D 04 95 01 81 02 46 FE 02 26 FC 05 09 31 81 02 05 0D 15 00 27 FF FF 00 00 75 10 65 12 95 01 09 30 81 02 C0 05 0D 09 22 A1 02 15 00 25 01 09 47 09 42 95 02 75 01 81 02 95 01 75 03 25 05 09 51 81 02 75 01 95 03 81 03 05 01 15 00 26 FA 08 75 10 55 0E 65 11 09 30 35 00 46 7D 04 95 01 81 02 46 FE 02 26 FC 05 09 31 81 02 05 0D 15 00 27 FF FF 00 00 75 10 65 12 95 01 09 30 81 02 C0 05 0D 09 22 A1 02 15 00 25 01 09 47 09 42 95 02 75 01 81 02 95 01 75 03 25 05 09 51 81 02 75 01 95 03 81 03 05 01 15 00 26 FA 08 75 10 55 0E 65 11 09 30 35 00 46 7D 04 95 01 81 02 46 FE 02 26 FC 05 09 31 81 02 05 0D 15 00 27 FF FF 00 00 75 10 65 12 95 01 09 30 81 02 C0 05 0D 09 22 A1 02 15 00 25 01 09 47 09 42 95 02 75 01 81 02 95 01 75 03 25 05 09 51 81 02 75 01 95 03 81 03 05 01 15 00 26 FA 08 75 10 55 0E 65 11 09 30 35 00 46 7D 04 95 01 81 02 46 FE 02 26 FC 05 09 31 81 02 05 0D 15 00 27 FF FF 00 00 75 10 65 12 95 01 09 30 81 02 C0 05 0D 09 22 A1 02 15 00 25 01 09 47 09 42 95 02 75 01 81 02 95 01 75 03 25 05 09 51 81 02 75 01 95 03 81 03 05 01 15 00 26 FA 08 75 10 55 0E 65 11 09 30 35 00 46 7D 04 95 01 81 02 46 FE 02 26 FC 05 09 31 81 02 05 0D 15 00 27 FF FF 00 00 75 10 65 12 95 01 09 30 81 02 C0 05 0D 55 0C 66 01 10 47 FF FF 00 00 27 FF FF 00 00 75 10 95 01 09 56 81 02 09 54 25 7F 95 01 75 08 81 02 05 09 09 01 25 01 75 01 95 01 81 02 95 07 81 03 06 01 FF 09 01 15 00 25 FF 75 08 95 01 81 02 09 02 75 08 95 01 81 02 09 03 15 00 27 FF FF 00 00 75 10 95 01 81 02 09 04 15 00 27 FF FF 00 00 75 10 95 06 81 02 05 0D 85 08 09 55 09 59 75 04 95 02 25 0F B1 02 85 0D 09 60 75 01 95 01 15 00 25 01 B1 02 95 07 B1 03 85 07 06 00 FF 09 C5 15 00 26 FF 00 75 08 96 00 01 B1 02 C0 05 0D 09 0E A1 01 85 04 09 22 A1 02 09 52 15 00 25 0A 75 08 95 01 B1 02 C0 09 22 A1 00 85 06 09 57 09 58 75 01 95 02 25 01 B1 02 95 06 B1 03 C0 C0 06 00 FF 09 01 A1 01 85 09 09 02 15 00 26 FF 00 75 08 95 14 91 02 85 0A 09 03 15 00 26 FF 00 75 08 95 14 91 02 85 0B 09 04 15 00 26 FF 00 75 08 95 3D 81 02 85 0C 09 05 15 00 26 FF 00 75 08 95 3D 81 02 85 0F 09 06 15 00 26 FF 00 75 08 95 03 B1 02 85 0E 09 07 15 00 26 FF 00 75 08 95 01 B1 02 85 22 09 08 15 00 25 FF 75 08 95 01 B1 02 85 23 09 16 15 00 25 FF 75 08 95 0F B1 02 85 24 09 17 15 00 25 FF 75 08 95 0C B1 02 85 25 09 18 15 00 25 FF 75 08 95 01 B1 02 C0
HID报文&触摸板运行模式破解
触摸板的触摸报文同样通过0x2c发送。
与上一个项目的触摸板相同, 在RESET后触摸板默认为鼠标模式:
Raw Data: 06 00 02 00 09 ef 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
Raw Data: 06 00 02 00 17 d8 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
Raw Data: 06 00 02 00 09 ea 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
Raw Data: 06 00 02 00 11 d7 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
Raw Data: 06 00 02 00 0a ea 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
Raw Data: 06 00 02 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
通过以下代码读取:

Synaptics的激活方法与上一个项目的触摸板类似, 都是需要一个Magic Pack来激活PTP模式。
与ELAN和Goodix触摸板不同的是, Synaptics需要对对应的寄存器写入正确的激活指令。
如果寄存器不对, Synaptics会默认无视该指令并保持当前模式不变。
如果寄存器正确但激活指令不对, Synaptics会进入失效模式, 即INT引脚强制拉低并只有00。
Magic Pack和对应的寄存器地址我最后在crostouchpad4-synaptics项目的rmi.c找到了。

由前面的HID Descriptor可得知RMI_SET_RMI_MODE_REPORT_ID为0x0f, 最后构造一下得到了这个:

通过这个函数可以成功的使触摸板进入Mouse Mode/PTP Mode。
激活PTP Mode后报文如下 (长度约64字节) :
Raw Data: 40 00 0c 18 01 ba 03 8c 02 48 06 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Raw Data: 40 00 0c 18 01 ba 03 8c 02 4b 06 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Raw Data: 40 00 0c 18 01 be 03 8c 02 4c 06 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Raw Data: 40 00 0c 18 01 d6 03 8b 02 4d 06 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Raw Data: 40 00 0c 18 01 e4 03 8c 02 4e 07 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Raw Data: 40 00 0c 18 01 f8 03 8c 02 4e 07 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
HID报文解析
Mouse Mode报文范例:
Raw Data: 06 00 02 00 09 ef 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
Raw Data: 06 00 02 00 17 d8 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
Raw Data: 06 00 02 00 09 ea 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
Mouse Mode的报文非常简单, 第三位为Mouse Button, 第4位为MOUSE_X, 第5位为MOUSE_Y:
mouse_msg.buttons = tp_packet[3];
mouse_msg.x = (int8_t)tp_packet[4];
mouse_msg.y = (int8_t)tp_packet[5];
PTP Mode报文范例:
Raw Data: 40 00 0c 18 01 ba 03 8c 02 48 06 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Raw Data: 40 00 0c 18 01 ba 03 8c 02 4b 06 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Raw Data: 40 00 0c 18 01 be 03 8c 02 4c 06 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
PTP Mode的报文由前四个Report相关字节+手指组成, 每个手指占8字节。
解析方式如下:
int offset = 4 + (id * 8);
offset + 0 : status
offset + 1 : X low
offset + 2 : X high
offset + 3 : Y low
offset + 4 : Y high
offset + 5 : Z low (pressure)
offset + 6 : Touching area major (猜测)
offset + 7 : Touching area minor (猜测)
status可以用于Tip标志位处理。
2️⃣ 转译层构建
HID描述符与上一个项目类似, 但是单个手指定义如下:
// -------- Finger 0 --------
0x09, 0x22, // USAGE (Finger)
0xA1, 0x02, // COLLECTION (Logical)
0x05, 0x0D, // USAGE_PAGE (Digitizers)
0x09, 0x47, // USAGE (Confidence)
0x09, 0x42, // USAGE (Tip Switch)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x02, // REPORT_COUNT (2)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x09, 0x51, // USAGE (Contact Identifier)
0x25, 0x3F, // LOGICAL_MAXIMUM (63)
0x75, 0x06, // REPORT_SIZE (6)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
// ---- X Axis ----
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFA, 0x08, // LOGICAL_MAXIMUM
0x35, 0x00, // PHYSICAL_MINIMUM (0)
0x46, 0x7D, 0x04, // PHYSICAL_MAXIMUM
0x55, 0x0E, // UNIT_EXPONENT (-3)
0x65, 0x11, // UNIT (Centimeter)
0x75, 0x10, // REPORT_SIZE (16)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
// ---- Y Axis ----
0x09, 0x31, // USAGE (Y)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFC, 0x05, // LOGICAL_MAXIMUM
0x35, 0x00, // PHYSICAL_MINIMUM (0)
0x46, 0xFE, 0x02, // PHYSICAL_MAXIMUM
0x75, 0x10, // REPORT_SIZE (16)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
// ---- Pressure ----
0x05, 0x0D, // USAGE_PAGE (Digitizers)
0x09, 0x30, // USAGE (Tip Pressure)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0xC0, // END_COLLECTION
其中Tip Pressure为压力字段定义。
同时还要定义触觉反馈描述符, 描述符内容如下:
0x85, 0x41, // ReportId(65)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x01, // UsageId(Simple Haptic Controller)
0xA1, 0x02, // Collection(Logical)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x23, // UsageId(Intensity)
0x35, 0x00, // PhysicalMinimum(0)
0x45, 0x00, // PhysicalMaximum(0)
0x65, 0x00, // Unit(None)
0x55, 0x00, // UnitExponent(0)
0x15, 0x00, // LogicalMinimum(0)
0x25, 0x04, // LogicalMaximum(4)
0x95, 0x01, // ReportCount(1)
0x75, 0x08, // ReportSize(8)
0xB1, 0x02, // Feature(Data,Var,Abs)
0xC0, // EndCollection
0x85, 0x42, // ReportId(66)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x01, // UsageId(Simple Haptic Controller)
0xA1, 0x02, // Collection(Logical)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x10, // UsageId(Waveform List)
0xA1, 0x02, // Collection(Logical)
0x05, 0x0A, // UsagePage(Ordinal)
0x19, 0x03, // UsageIdMin(Instance 3)
0x29, 0x07, // UsageIdMax(Instance 7)
0x35, 0x00, // PhysicalMinimum(0)
0x45, 0x00, // PhysicalMaximum(0)
0x65, 0x00, // Unit(None)
0x55, 0x00, // UnitExponent(0)
0x16, 0x01, 0x10, // LogicalMinimum(4097)
0x26, 0xFF, 0x2F, // LogicalMaximum(12287)
0x95, 0x05, // ReportCount(5)
0x75, 0x10, // ReportSize(16)
0xB1, 0x02, // Feature(Data,Var,Abs)
0xC0, // EndCollection
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x11, // UsageId(Duration List)
0xA1, 0x02, // Collection(Logical)
0x05, 0x0A, // UsagePage(Ordinal)
0x19, 0x03, // UsageIdMin(Instance 3)
0x29, 0x07, // UsageIdMax(Instance 7)
0x35, 0x00, // PhysicalMinimum(0)
0x45, 0x32, // PhysicalMaximum(50)
0x66, 0x01, 0x10, // UNIT (Milliseconds)
0x55, 0x0D, // UNIT_EXPONENT (-3)
0x15, 0x00, // LogicalMinimum(0)
0x25, 0x32, // LogicalMaximum(50)
0x95, 0x05, // ReportCount(5)
0x75, 0x08, // ReportSize(8)
0xB1, 0x02, // Feature(Data,Var,Abs)
0xC0, // EndCollection
0xC0, // EndCollection
0x85, 0x43, // ReportId(67)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x01, // UsageId(Simple Haptic Controller)
0xA1, 0x02, // Collection(Logical)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x21, // UsageId(Manual Trigger)
0x35, 0x00, // PhysicalMinimum(0)
0x45, 0x00, // PhysicalMaximum(0)
0x65, 0x00, // Unit(None)
0x55, 0x00, // UnitExponent(0)
0x15, 0x01, // LogicalMinimum(1)
0x25, 0x07, // LogicalMaximum(7)
0x95, 0x01, // ReportCount(1)
0x75, 0x08, // ReportSize(8)
0x91, 0x02, // Output(Data,Var,Abs)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x23, // UsageId(Intensity)
0x35, 0x00, // PhysicalMinimum(0)
0x45, 0x00, // PhysicalMaximum(0)
0x65, 0x00, // Unit(None)
0x55, 0x00, // UnitExponent(0)
0x15, 0x00, // LogicalMinimum(0)
0x25, 0x04, // LogicalMaximum(4)
0x95, 0x01, // ReportCount(1)
0x75, 0x08, // ReportSize(8)
0x91, 0x02, // Output(Data,Var,Abs)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x24, // UsageId(Repeat Count)
0x35, 0x00, // PhysicalMinimum(0)
0x45, 0x00, // PhysicalMaximum(0)
0x65, 0x00, // Unit(None)
0x55, 0x00, // UnitExponent(0)
0x15, 0x00, // LogicalMinimum(0)
0x25, 0x05, // LogicalMaximum(5)
0x95, 0x01, // ReportCount(1)
0x75, 0x08, // ReportSize(8)
0x91, 0x02, // Output(Data,Var,Abs)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x25, // UsageId(Retrigger Period)
0x35, 0x00, // PhysicalMinimum(0)
0x46, 0xE8, 0x03, // PhysicalMaximum(1000)
0x66, 0x01, 0x10, // UNIT (Milliseconds)
0x55, 0x0D, // UNIT_EXPONENT (-3)
0x15, 0x00, // LogicalMinimum(0)
0x26, 0xE8, 0x03, // LogicalMaximum(1000)
0x95, 0x01, // ReportCount(1)
0x75, 0x10, // ReportSize(16)
0x91, 0x02, // Output(Data,Var,Abs)
0x05, 0x0E, // UsagePage(Haptics)
0x09, 0x28, // UsageId(Waveform Cutoff Time)
0x36, 0xE8, 0x03, // PhysicalMinimum(1000)
0x46, 0x88, 0x13, // PhysicalMaximum(5000)
0x66, 0x01, 0x10, // UNIT (Milliseconds)
0x55, 0x0D, // UNIT_EXPONENT (-3)
0x16, 0xE8, 0x03, // LogicalMinimum(1000)
0x26, 0x88, 0x13, // LogicalMaximum(5000)
0x95, 0x01, // ReportCount(1)
0x75, 0x10, // ReportSize(16)
0x91, 0x02, // Output(Data,Var,Abs)
0xC0, // EndCollection
0xC0, // END_COLLECTION
触觉报告结构如下:
| Report ID | 类型 | 大小 (byte) | 功能 |
|---|---|---|---|
0x40 | Feature | 1 | Button Press Threshold |
0x41 | Feature | 1 | Haptic Intensity (0-4) |
0x42 | Feature | 15 | Waveform List (5×16bit) + Duration List (5×8bit) |
0x43 | Output | 7 | Manual Trigger + Intensity + Repeat Count + Period + Cutoff |
在Haptic项目中, button的触发与上一代项目不同。上一代项目有单独的button标志位, 这一代项目需要根据pressure阈值计算button标志位。
button开关描述符如下:
0x85, REPORTID_FUNCTION_SWITCH, // REPORT_ID (0x06)
0x09, 0x57, // USAGE (Surface switch)
0x09, 0x58, // USAGE (Button switch)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x02, // REPORT_COUNT (2)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0xb1, 0x02, // FEATURE (Data,Var,Abs)
这一个项目的USB与上一代采用了同样的方案, 即HID多端口 (自定义设备+触摸板+鼠标) 。
所以直接集合起来给tinyusb:

REPORTID也要添加对应的回调:
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) {
if (report_type == HID_REPORT_TYPE_FEATURE) {
if (report_id == REPORTID_FEATURE) {
buffer[0] = 0x03;
return 1;
}
if (report_id == REPORTID_MAX_COUNT) {
buffer[0] = 0x15;
return 1;
}
if (report_id == REPORTID_PTPHQA) {
memset(buffer, 0, 256);
return 256;
}
}
return 0;
}
蓝牙HID大部分沿用了ESP-IDF中的Bluedroid范例代码, 在此不再过多阐述。
HID 报文逻辑
Mouse HID报文直接按照标准格式来就行, 但是Original Mouse Mode只有X、Y和Button, 没有Wheel位。触摸板在Original Mouse Mode下不支持滚轮。
PTP Simulated Mouse Mode定义了滚轮和笔用于模拟鼠标滚轮操作。
PTP HID报文定义了5个手指, 其中一个手指如下:
// -------- Finger 0 --------
0x09, 0x22, // USAGE (Finger)
0xA1, 0x02, // COLLECTION (Logical)
0x05, 0x0D, // USAGE_PAGE (Digitizers)
0x09, 0x47, // USAGE (Confidence)
0x09, 0x42, // USAGE (Tip Switch)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x02, // REPORT_COUNT (2)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x09, 0x51, // USAGE (Contact Identifier)
0x25, 0x3F, // LOGICAL_MAXIMUM (63)
0x75, 0x06, // REPORT_SIZE (6)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
// ---- X Axis ----
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFA, 0x08, // LOGICAL_MAXIMUM
0x35, 0x00, // PHYSICAL_MINIMUM (0)
0x46, 0x7D, 0x04, // PHYSICAL_MAXIMUM
0x55, 0x0E, // UNIT_EXPONENT (-3)
0x65, 0x11, // UNIT (Centimeter)
0x75, 0x10, // REPORT_SIZE (16)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
// ---- Y Axis ----
0x09, 0x31, // USAGE (Y)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFC, 0x05, // LOGICAL_MAXIMUM
0x35, 0x00, // PHYSICAL_MINIMUM (0)
0x46, 0xFE, 0x02, // PHYSICAL_MAXIMUM
0x75, 0x10, // REPORT_SIZE (16)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
// ---- Pressure ----
0x05, 0x0D, // USAGE_PAGE (Digitizers)
0x09, 0x30, // USAGE (Tip Pressure)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x01, // REPORT_COUNT (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0xC0, // END_COLLECTION
通过下面的信息结构体格式构造并发送:
typedef struct {
uint16_t x;
uint16_t y;
uint8_t tip_switch;
uint8_t contact_id;
uint8_t confidence;
uint8_t pressure_z;
} tp_finger_t;
typedef struct {
tp_finger_t fingers[5];
uint8_t actual_count;
uint8_t button_mask;
uint16_t scan_time;
} tp_multi_msg_t;
typedef struct {
uint16_t last_x[5];
uint16_t last_y[5];
float remainder_x;
float remainder_y;
uint32_t last_click_time;
bool is_scrolling;
} ptp_simulated_mouse_msg_t;
typedef struct __attribute__((packed)) {
uint8_t buttons;
int8_t x;
int8_t y;
int8_t wheel;
int8_t pan;
} mouse_msg_t;
关于XY坐标轴计算, 有一点需要注意就是触摸板的Y轴是翻转的, 需要通过计算将Y轴再次翻转为正常状态。

标志位处理
标志位处理主要是Tip&Confidence标志位的处理。对应关系如下:
| Confidence | Tip | Config ID | 表现 |
|---|---|---|---|
| 0 | 0 | 0x00 | 非法报文,被系统抛弃处理,保持之前的手指状态不变 |
| 1 | 0 | 0x01 | 应用于手指抬起,坐标被上报,系统会认为手指已抬起,手势完成 |
| 0 | 1 | 0x02 | 不可信接触,一般应用于例如手掌而非手指接触等情况,坐标会被上报但不会处理 |
| 1 | 1 | 0x03 | 可信接触,上报正常报文,系统会认为手指已接触 |
由前面HID报文解析获取到的单个手指报文格式:
int offset = 4 + (id * 8);
offset + 0 : status
offset + 1 : X low
offset + 2 : X high
offset + 3 : Y low
offset + 4 : Y high
offset + 5 : Z low (或 pressure)
offset + 6 : Z high / major
offset + 7 : minor
status可以用于Tip标志位上报, 但是单个手指里面并没有Confidence标志位的处理。
后面通过网上的多数公开项目和资料大概推测, major和minor应该是Synaptic RMI4里规定的手指接触面“形状尺寸”。
Major→ 接触区域的“最长直径”Minor→ 接触区域的“最短直径”
画个图的话大概就是这个样:
↑ minor (短轴)
┌─────────┐
│ │
│ ● │ → major (长轴)
│ │
└─────────┘
所以在判断Confidence标志位时, 我用的逻辑是这样的:
- 手指: major ≈ minor (接近圆)
- 手掌: major 很大, minor 也大
转成代码就是:

后面的测试证明这个确实很有效。当手指接触时是Confidence&Tip, 抬起就是Confidence:

当我整个手掌去摸触摸板时, 标志位变成了Tip:

优化算法
三点中值滤波 (Median Filter)
取最近三帧的中间值, 用于滤除单点噪声。
代码实现:
uint16_t mx = get_median(raw_x_history[id][HISTORY_LEN-3],
raw_x_history[id][HISTORY_LEN-2],
raw_x_history[id][HISTORY_LEN-1]);
突跳抑制 (Outlier Rejection)
计算当前点与上一帧过滤坐标的位移平方和。若位移量超过物理极限阈值, 则视为干扰并拦截, 除非该跳变连续出现。
代码实现:

动态自适应指数滤波 (Dynamic EMA)
基于瞬时速度 (两帧原始坐标差的绝对值之和) 动态调整滤波系数 。

代码实现:

死区控制
在手指移动距离未突破死区 (Deadzone) 前, 锁定输出为起始点坐标, 防止点击操作演变为微小拖拽。

3️⃣ 压感原理
压力获取
根据HID报文解析拿到pressure标志位, pressure标志位就是压力。
Haptic马达原理
Haptic马达原理图:
这张图由Gemini生成, 最终以实物为准。

磁铁在下面作为永磁定子提供振动所需的力, 线圈在触摸板PCB上, 通电后带动触摸板振动, 从而达到振动->按键模拟的效果。
Waveform Firmware配置&破解
根据CS40L25 MCU Driver User Guide可知, ROM模式下虽然能振动但是能调的参数有限, 如果需要详细调参, 需要通过firmware_converter.py将waveform firmware (.wmfw)和配套的.bin固件转为cs40l25_fw_img.c和cs40l25_fw_img.h, 后续通过ESP32将fw_img写入CS40L25后才能获得更好的振动反馈行为。
由于CirrusLogic表示固件为不可公开的NDA内容, 所以项目中的CS40L25 Firmware是由Surface Haptic Firmware逆向而来的。
逆向的方法可能不完全正确, 因为触发的Click手感与实际体验的不同。
这部分篇幅较多,可前往Github原项目Waveform Firmware配置&破解和原始内容与新增包装的对应关系查看。
压感系统端适配
单击敏感度适配
其实就是设置button触发的pressure值:
#define CLICK_LIGHT_WEIGHT_DEFAULT 80
#define CLICK_MIDIUM_WEIGHT_DEFAULT 100
#define CLICK_STRONG_WEIGHT_DEFAULT 130
触觉点击适配
通过对逆向Surface WaveForm Firmware进行测试发现, cp_dig_scale越大, 则振动感越小。
static uint16_t ptp_map_haptic_cp_dig_scale(uint8_t intensity_level) {
switch (ptp_haptic_click_intensity_clamp(intensity_level)) {
case 4:
return 3;
case 3:
return 33;
case 2:
return 66;
case 1:
return 100;
default:
return UINT16_MAX;
}
}
触觉信号适配
由于CS40L25的开发资料非常少, 再加上对应waveform片段的缺失, 所以这个功能暂时没有适配。
微软原机好像也没有这个功能。
以上的threshold value都通过USB端的Set Report行为调整。
关于外壳
外壳由Solidworks 2024建模,采用两块CNC金属板上下包夹式结构。
同时为了最大程度配合原来的设计, 需要加一个自定义底板, 并在底板上安装N55磁铁。


CNC金属板可通过JLC CNC特价下单。
成品展示
实物组装+外壳展示



接收器展示

系统层面识别



项目和参考资料
Github项目地址: barryblueice - ESP32 Haptic Precision TouchPad
演示视频: Bilibili - 基于ESP32的三模精确式压感触摸板
参考资料:
- crostouchpad4-synaptics by coolstar——https://github.com/coolstar/crostouchpad4-synaptic
- surface-aggregator-module——https://github.com/linux-surface/surface-aggregator-module
- Kernel by linux-surface——https://github.com/linux-surface/linux-surface
- ESP32-Precision-TouchPad——https://github.com/barryblueice/ESP32-Precision-TouchPad
- mcu-drivers by CirrusLogic——https://github.com/CirrusLogic/mcu-drivers
- Windows I2C Over I2C HID——https://learn.microsoft.com/zh-cn/windows-hardware/drivers/hid/hid-over-i2c-guide
- Windows精确式触摸板实现指南——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/touchpad-implementation-guide
- Windows精确式触摸板设备实现——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/touchpad-protocol-implementation
- Windows精确式触摸板设备总线连接——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/touchpad-device-bus-connectivity
- Windows精确式触摸板优化 (触摸板优化指南) ——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/touchpad-tuning-guidelines
- Windows输入设备触觉实现指南——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/input-haptics-implementation-guide
设计图
未生成预览图,请在编辑器重新保存一次BOM
暂无BOM
克隆工程知识产权声明&复刻说明
本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。
请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。




+ 
+ 

评论