站内搜索
发作品签到
专业版

基于ESP32的三模精确式压感触摸板

工程标签

659
0
0
4

简介

基于ESP32的Surface Laptop Studio 1964压感触摸板破解项目。支持有线、2.4G和Bluetooth三模连接。

简介:基于ESP32的Surface Laptop Studio 1964压感触摸板破解项目。支持有线、2.4G和Bluetooth三模连接。

开源协议

CERN-OHL-S-2.0

(未经作者授权,禁止转载)
创建时间:2026-03-07 22:12:53更新时间:2026-06-09 17:13:22

描述

image.png

这是什么?

又一个触摸板破解项目。基于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支持

无线模式

压感与触觉

  • 压感调整原理破解
    • CS40L25 SDK适配
    • ROM模式下触发振动
    • 特定waveform固件优化触觉反馈 (实验性)
  • 压感支持
    • 单击敏感度 (实验性)
    • 触觉点击和强度调整 (实验性)
    • 触觉反馈和强度控制 (后续可能支持)

目前仍然存在的问题:

  • 在蓝牙模式下, Windows接受到并正确解析了HID报文, 但是手势处于不可用的状态。
    同时, 在蓝牙模式下目前Windows依旧不支持PTP设置。
  • 蓝牙模式下暂时不支持PTP/Mouse Mode切换, 所以目前默认只使用BLE Mouse模式。
  • 由于CS40L25的固件问题, 按下的振动反馈比较奇怪。

使用说明

以下操作均基于默认sdkconfig设置。

初始上电,只有USB2513B、指纹模块和CP2102工作。

通过背面的电源开关,向上拨动即可打开ESP32-S3和触摸板的电源。

image.png


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

Func. Key在PCB的左侧:

image.png

Func. Key行为对应如下:

按下时间/行为行为
3s切换运行模式 (有线/2.4G/BLE依次顺序)
5s切换到默认有线模式
按下Func. Key后通电ESP32进入Download Mode,可通过DFU Mode刷新固件

Func. Key的下方有三颗LED指示灯:

image.png

每颗指示灯的行为如下:

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方案。

image.png

image.png

  • 指纹方案采用戴尔G7 7500/7700 Goodix指纹解决方案。

  • 主控采用ESP32-S3FH4R2方案, 集成2.4 GHz Wi-FiBluetooth 5 (LE) 的MCU芯片。

  • 接收器主控采用ESP32-S2FN4R2方案, 集成2.4 GHz Wi-Fi的MCU芯片。

  • 板载一颗CP2102 USB到UART桥接控制器 (QFN-24封装) , 与CP2104-GMRCH9102F封装兼容。

  • ESP32-S3CP2102和指纹模块通过USB2513B连接到上游HOST, 3-Port USB 2.0 Hi-Speed Hub控制器。

  • 电源管理系统采用来自Texas InstrumentsBQ24195。具有5.1V、2.1A同步升压操作的I2C控制型4.5A单节电池充电器。

  • CS40L25采用的电源驱动方案基于MP28167GQ-A-Z, 2.8V-22V输入、3A电流输出、4通道Buck-Boost转换器。

  • 板载设备采用了两颗TLV62585 DCDC降压3.3V输出方案, 2.5V-5.5V输入、3A降压转换器。ESP32-S3+触摸板共享一路3.3V, USB2513B指纹CP2102共享另外一路3.3V。

  • 其中一路TLV62585MP28167GQ-A-Z上游通过BQ24195供电。


编译设置

TouchPad sdkconfig

触摸板sdkconfig默认设置已保存在sdkconfig.defaults。以下值可手动调整。


USB Descriptor Options (sdkconfig / TouchPad Configuration / USB Descriptor Options)

image.png

TouchPad ManufacturerTouchPad Product StringTouchPad Serial Number (TOUCHPAD_MANUFACTURER_STRINGTOUCHPAD_PRODUCT_STRINGTOUCHPAD_SERIAL_NUMBER_STRING)

设置触摸板面向Host端显示的USB制造商、产品名和序列号。


Receiver Configuration (sdkconfig / TouchPad Configuration / Receiver Configuration)

设置触摸板端的2.4G Receiver相关选项。

image.png

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)

设置触摸板端的特殊功能。

image.png

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)

image.png

TouchPad ManufacturerTouchPad Product StringTouchPad Serial Number (TOUCHPAD_MANUFACTURER_STRINGTOUCHPAD_PRODUCT_STRINGTOUCHPAD_SERIAL_NUMBER_STRING)

设置触摸板面向Host端显示的USB制造商、产品名和序列号。

Using Goodix Descriptor from ESP32-Haptic-Touchpad for better capability (LAST_GEN_DESC)

使用来自上一代项目ESP32 Precision TouchpadGoodix HID Descriptor以获得更好的兼容性。

这个值仅会影响Legacy TouchPad模式。


PTP Simulated Mouse Mode说明

因为原生鼠标模式的功能很少(只有光标滑动和左键),所以设计了一个PTP Simulated Mouse Mode,同时通过计算定义了一些手势, 用于模拟正常鼠标的使用行为手势。

轻触手势:

手势名称说明备注
image.png单指轻触鼠标左键/
image.png双指轻触鼠标右键/
image.png三指轻触鼠标中键/

拖动手势:

手势名称说明备注
image.png + image.png单点并拖动点击两次后再拖动即可多选/
image.png + image.png双指滑动鼠标滚轮可以上下平移, 也可以左右平移

技术方案

1️⃣ 触摸板破解

HID Descriptor破解

自第四代Surface产品开始, Surface开始采用自研的EC控制器SAM (Surface Aggregator Module) 。

在Surface Laptop Studio产品中, 触摸板挂在EC控制器下。

但触摸板本身依旧采用I2C HID协议与设备通信。

首先通过I2C地址扫描程序确定其HID控制器地址:

image.png

结合原厂图纸中的注释可得知:

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描述符寄存器地址, 最后在0x2c0x0021处获取到了HID Descriptor:

image.png

获取到的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

通过以下代码读取:

image.png

Synaptics的激活方法与上一个项目的触摸板类似, 都是需要一个Magic Pack来激活PTP模式。

与ELAN和Goodix触摸板不同的是, Synaptics需要对对应的寄存器写入正确的激活指令。

如果寄存器不对, Synaptics会默认无视该指令并保持当前模式不变。

如果寄存器正确但激活指令不对, Synaptics会进入失效模式, 即INT引脚强制拉低并只有00。

Magic Pack和对应的寄存器地址我最后在crostouchpad4-synaptics项目的rmi.c找到了。

image.png

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

image.png

通过这个函数可以成功的使触摸板进入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)功能
0x40Feature1Button Press Threshold
0x41Feature1Haptic Intensity (0-4)
0x42Feature15Waveform List (5×16bit) + Duration List (5×8bit)
0x43Output7Manual 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:

image.png

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轴再次翻转为正常状态。

image.png


标志位处理

标志位处理主要是Tip&Confidence标志位的处理。对应关系如下:

ConfidenceTipConfig ID表现
000x00非法报文,被系统抛弃处理,保持之前的手指状态不变
100x01应用于手指抬起,坐标被上报,系统会认为手指已抬起,手势完成
010x02不可信接触,一般应用于例如手掌而非手指接触等情况,坐标会被上报但不会处理
110x03可信接触,上报正常报文,系统会认为手指已接触

由前面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标志位的处理。

后面通过网上的多数公开项目和资料大概推测, majorminor应该是Synaptic RMI4里规定的手指接触面“形状尺寸”。

  • Major → 接触区域的“最长直径”
  • Minor → 接触区域的“最短直径”

画个图的话大概就是这个样:

        ↑ minor (短轴)
     ┌─────────┐
     │         │
     │   ●     │ → major (长轴)
     │         │
     └─────────┘

所以在判断Confidence标志位时, 我用的逻辑是这样的:

  • 手指: major ≈ minor (接近圆)
  • 手掌: major 很大, minor 也大

转成代码就是:

image.png

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

image.png

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

image.png


优化算法

三点中值滤波 (Median Filter)

取最近三帧的中间值, 用于滤除单点噪声。

InputX(t)=Median(RawX[t],RawX[t1],RawX[t2])Input_X(t) = Median(Raw_X[t], Raw_X[t-1], Raw_X[t-2])

代码实现:

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)

计算当前点与上一帧过滤坐标的位移平方和。若位移量超过物理极限阈值, 则视为干扰并拦截, 除非该跳变连续出现。

代码实现:

image.png


动态自适应指数滤波 (Dynamic EMA)

基于瞬时速度 (两帧原始坐标差的绝对值之和) 动态调整滤波系数 /alpha/alpha

image.png

代码实现:

image.png


死区控制

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

image.png


3️⃣ 压感原理

压力获取

根据HID报文解析拿到pressure标志位, pressure标志位就是压力。


Haptic马达原理

Haptic马达原理图:

这张图由Gemini生成, 最终以实物为准。

image.png

磁铁在下面作为永磁定子提供振动所需的力, 线圈在触摸板PCB上, 通电后带动触摸板振动, 从而达到振动->按键模拟的效果。


Waveform Firmware配置&破解

根据CS40L25 MCU Driver User Guide可知, ROM模式下虽然能振动但是能调的参数有限, 如果需要详细调参, 需要通过firmware_converter.pywaveform firmware (.wmfw)和配套的.bin固件转为cs40l25_fw_img.ccs40l25_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磁铁。

image.png

image.png

CNC金属板可通过JLC CNC特价下单。


成品展示

实物组装+外壳展示

image.png

image.png

image.png

接收器展示

image.png

系统层面识别

image.png

image.png

image.png


项目和参考资料

Github项目地址: barryblueice - ESP32 Haptic Precision TouchPad

演示视频: Bilibili - 基于ESP32的三模精确式压感触摸板

参考资料:

  1. crostouchpad4-synaptics by coolstar——https://github.com/coolstar/crostouchpad4-synaptic
  2. surface-aggregator-module——https://github.com/linux-surface/surface-aggregator-module
  3. Kernel by linux-surface——https://github.com/linux-surface/linux-surface
  4. ESP32-Precision-TouchPad——https://github.com/barryblueice/ESP32-Precision-TouchPad
  5. mcu-drivers by CirrusLogic——https://github.com/CirrusLogic/mcu-drivers
  6. Windows I2C Over I2C HID——https://learn.microsoft.com/zh-cn/windows-hardware/drivers/hid/hid-over-i2c-guide
  7. Windows精确式触摸板实现指南——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/touchpad-implementation-guide
  8. Windows精确式触摸板设备实现——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/touchpad-protocol-implementation
  9. Windows精确式触摸板设备总线连接——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/touchpad-device-bus-connectivity
  10. Windows精确式触摸板优化 (触摸板优化指南) ——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/touchpad-tuning-guidelines
  11. Windows输入设备触觉实现指南——https://learn.microsoft.com/zh-cn/windows-hardware/design/component-guidelines/input-haptics-implementation-guide

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

3D模型

序号文件名称下载次数
1
轻量外壳-顶.STEP
5
2
轻量外壳-底.STEP
5

附件

序号文件名称下载次数
暂无数据
克隆工程
添加到专辑
0
0
分享
Logo GIF0
侵权投诉
知识产权声明&复刻说明

本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。

请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。

评论

全部评论(1
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航