ESP32S2 模拟USB触摸屏

参照Teensy的触摸【参考1】,在 ESP32 S2 上实现了触摸屏。最关键的步骤有2个:

  1. 正确的 HID Descriptor,下面是一个10指触摸的触摸屏幕的描述符
static const uint8_t report_descriptor[] = { // 8 TouchData
  0x05, 0x0D,
  0x09, 0x04,
  0xA1, 0x01,
  0x09, 0x22,
  0xA1, 0x02,
  0x09, 0x42,
  0x15, 0x00,
  0x25, 0x01,
  0x75, 0x01,
  0x95, 0x01,
  0x81, 0x02,
  0x09, 0x30,
  0x25, 0x7F,
  0x75, 0x07,
  0x95, 0x01,
  0x81, 0x02,
  0x09, 0x51,
  0x26, 0xFF, 0x00,
  0x75, 0x08,
  0x95, 0x01,
  0x81, 0x02,
  0x05, 0x01,
  0x09, 0x30,
  0x09, 0x31,
  0x26, 0xFF, 0x7F,
  0x65, 0x00,
  0x75, 0x10,
  0x95, 0x02,
  0x81, 0x02,
  0xC0,
  0x05, 0x0D,
  0x27, 0xFF, 0xFF, 0x00, 0x00,
  0x75, 0x10,
  0x95, 0x01,
  0x09, 0x56,
  0x81, 0x02,
  0x09, 0x54,
  0x25, 0x0A,
  0x75, 0x08,
  0x95, 0x01,
  0x81, 0x02,
  0x05, 0x0D,
  0x09, 0x55,
  0x25, 0x0A,
  0x75, 0x08,
  0x95, 0x01,
  0xB1, 0x02,
  0xC0,
};

对应发送的数据结构是:

    // touch report
    //  0: on/off + pressure
    //  1: contact id
    //  2: X lsb
    //  3: X msb
    //  4: Y lsb
    //  5: Y msb
    //  6: scan time lsb
    //  7: scan time msb
   //  8: contact count

其中 Byte0 Bit0 是按下标志,一直为1,Bit1-7 是按键压力;Byte1 是按键编号,从 0-255,可以理解为手指编号,比如:右手食指按下,编号为0;右手中指按下,编号为1; 右手抬起后再次按下,会重新分配一个编号。Byte2-3 按键的X坐标;Byte4-5 按键的Y坐标;Byte6-7 是按压发生的事件,是以 100us为单位;Byte8 是当前正在发生的按压事件中触摸点的数量。在【参考2】有一个例子:

Table 7 Report Sequence for Two Contacts with Separated Lift (Two-Finger Hybrid)

Report1234567891011
Contact Count22222211111
Contact 1 Tip Switch111110NRNRNRNRNR
Contact 1 X,YX₁,Y₁X₂,Y₂X₃,Y₃X₄,Y₄X₅,Y₅X₅,Y₅NRNRNRNRNR
Contact 2 Tip Switch11111111110
Contact 2 X,YX₁,Y₁X₂,Y₂X₃,Y₃X₄,Y₄X₅,Y₅X₆,Y₆X₇,Y₇X₈,Y₈X₉,Y₉X₁₀,Y₁₀X₁₀,Y₁₀

图中是2个手指

图中是2个手指进行触摸的例子,R1 会分别报告手指1和2移动的信息,同时 Byte8 的 Contract Count 会等于2;R6 的时候,因为手指1已经抬起,所以Contract Count会变成1.

2.另外一个重要的,容易被忽视的要求:Get Report 的处理。即使上面的描述符正确报告,然后数据也正常发送到Windows中,你的触摸屏依然无法正常工作,原因就是缺少了对Get Report的处理。更糟糕的是:你无法使用 USBlyzer 这样的工具抓到 Teensy 中的数据。

Teensy 例子中上位机发送 GET_REPORT 收到返回值0x0a

如果不在代码中特别处理,对于这个命令会 STALL

关于这个 COMMAND 的含义,目前没搞清楚【参考3】

对于我们来说,只要有一个返回值就能让它工作正常。最终一个可以工作的代码如下(这个会在屏幕上方的中间移动一个手指触摸):

#include "USB.h"
#include "USBHID.h"
USBHID HID;

static const uint8_t report_descriptor[] = { // 8 TouchData
  0x05, 0x0D,
  0x09, 0x04,
  0xA1, 0x01,
  0x09, 0x22,
  0xA1, 0x02,
  0x09, 0x42,
  0x15, 0x00,
  0x25, 0x01,
  0x75, 0x01,
  0x95, 0x01,
  0x81, 0x02,
  0x09, 0x30,
  0x25, 0x7F,
  0x75, 0x07,
  0x95, 0x01,
  0x81, 0x02,
  0x09, 0x51,
  0x26, 0xFF, 0x00,
  0x75, 0x08,
  0x95, 0x01,
  0x81, 0x02,
  0x05, 0x01,
  0x09, 0x30,
  0x09, 0x31,
  0x26, 0xFF, 0x7F,
  0x65, 0x00,
  0x75, 0x10,
  0x95, 0x02,
  0x81, 0x02,
  0xC0,
  0x05, 0x0D,
  0x27, 0xFF, 0xFF, 0x00, 0x00,
  0x75, 0x10,
  0x95, 0x01,
  0x09, 0x56,
  0x81, 0x02,
  0x09, 0x54,
  0x25, 0x0A,
  0x75, 0x08,
  0x95, 0x01,
  0x81, 0x02,
  0x05, 0x0D,
  0x09, 0x55,
  0x25, 0x0A,
  0x75, 0x08,
  0x95, 0x01,
  0xB1, 0x02,
  0xC0,
};

class CustomHIDDevice: public USBHIDDevice {
  public:
    CustomHIDDevice(void) {
      static bool initialized = false;
      if (!initialized) {
        initialized = true;
        HID.addDevice(this, sizeof(report_descriptor));
      }
    }
    uint16_t _onGetFeature(uint8_t report_id, uint8_t* buffer, uint16_t len)
      {
        buffer[0]=0x0A;
        return 0x01;
      }
    void begin(void) {
      HID.begin();
    }

    uint16_t _onGetDescriptor(uint8_t* buffer) {
      memcpy(buffer, report_descriptor, sizeof(report_descriptor));
      return sizeof(report_descriptor);
    }

    bool send(uint8_t * value) {
      return HID.SendReport(0, value, 9);
    }
};

CustomHIDDevice Device;

const int buttonPin = 0;
int previousButtonState = HIGH;
uint8_t TouchData[9];

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Device.begin();
  USB.begin();
}

void loop() {
  if (HID.ready()) {
    Serial.println("Finger");
    // touch report
    //  0: on/off + pressure
    //  1: contact id
    //  2: X lsb
    //  3: X msb
    //  4: Y lsb
    //  5: Y msb
    //  6: scan time lsb
    //  7: scan time msb
    //  8: contact count
    for (int i=0;i<200;i+=100) {
    TouchData[0] = 0x81; TouchData[1] = 0x08;
    TouchData[2] = (16000)&0xFF; TouchData[3] = ((16000)>>8)&0xFF;
    TouchData[4] = (4000+i)&0xFF; TouchData[5] = ((4000+i)>>8)&0xFF;
    TouchData[6] = (millis()*10)&0xFF; TouchData[7] = (millis()*10>>8)&0xFF;
    TouchData[8] = 0x01; 
    Device.send(TouchData);
    delay(10);
    TouchData[0] = 0x81; TouchData[1] = 0x08;
    TouchData[2] = (16000)&0xFF; TouchData[3] = ((16000)>>8)&0xFF;
    TouchData[4] = (4000+i)&0xFF; TouchData[5] = ((4000+i)>>8)&0xFF;
    TouchData[6] = (millis()*10)&0xFF; TouchData[7] = (millis()*10>>8)&0xFF;
    TouchData[8] = 0x01; 
    delay(10);
    }
    //每隔10秒
    delay(5000);
  }
}

再复杂一点,做一个画圆的:

#include "USB.h"
#include "USBHID.h"
USBHID HID;

int STARTX=16000;
int STARTY=12000;
int STARTR=2000;
static const uint8_t report_descriptor[] = { // 8 TouchData
  0x05, 0x0D,
  0x09, 0x04,
  0xA1, 0x01,
  0x09, 0x22,
  0xA1, 0x02,
  0x09, 0x42,
  0x15, 0x00,
  0x25, 0x01,
  0x75, 0x01,
  0x95, 0x01,
  0x81, 0x02,
  0x09, 0x30,
  0x25, 0x7F,
  0x75, 0x07,
  0x95, 0x01,
  0x81, 0x02,
  0x09, 0x51,
  0x26, 0xFF, 0x00,
  0x75, 0x08,
  0x95, 0x01,
  0x81, 0x02,
  0x05, 0x01,
  0x09, 0x30,
  0x09, 0x31,
  0x26, 0xFF, 0x7F,
  0x65, 0x00,
  0x75, 0x10,
  0x95, 0x02,
  0x81, 0x02,
  0xC0,
  0x05, 0x0D,
  0x27, 0xFF, 0xFF, 0x00, 0x00,
  0x75, 0x10,
  0x95, 0x01,
  0x09, 0x56,
  0x81, 0x02,
  0x09, 0x54,
  0x25, 0x0A,
  0x75, 0x08,
  0x95, 0x01,
  0x81, 0x02,
  0x05, 0x0D,
  0x09, 0x55,
  0x25, 0x0A,
  0x75, 0x08,
  0x95, 0x01,
  0xB1, 0x02,
  0xC0,
};

class CustomHIDDevice: public USBHIDDevice {
  public:
    CustomHIDDevice(void) {
      static bool initialized = false;
      if (!initialized) {
        initialized = true;
        HID.addDevice(this, sizeof(report_descriptor));
      }
    }
    uint16_t _onGetFeature(uint8_t report_id, uint8_t* buffer, uint16_t len)
      {
        buffer[0]=0x0a;
        return 1;
      }
    void begin(void) {
      HID.begin();
    }

    uint16_t _onGetDescriptor(uint8_t* buffer) {
      memcpy(buffer, report_descriptor, sizeof(report_descriptor));
      return sizeof(report_descriptor);
    }

    bool send(uint8_t * value) {
      return HID.SendReport(0, value, 9);
    }
};

CustomHIDDevice Device;

const int buttonPin = 0;
int previousButtonState = HIGH;
uint8_t TouchData[9];

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Device.begin();
  USB.begin();
}

void loop() {
  if (HID.ready()) {
    Serial.println("Finger");
    // touch report
    //  0: on/off + pressure
    //  1: contact id
    //  2: X lsb
    //  3: X msb
    //  4: Y lsb
    //  5: Y msb
    //  6: scan time lsb
    //  7: scan time msb
    //  8: contact count
    for (int i=0;i<101;i++) {
    TouchData[0] = 0x81; TouchData[1] = 0x08;
    TouchData[2] = ((int)(STARTX+STARTR*sin(2*PI*i/100)))&0xFF; 
    TouchData[3] = ((int)(STARTX+STARTR*sin(2*PI*i/100)))>>8&0xFF;
    TouchData[4] = ((int)(STARTY+STARTR*cos(2*PI*i/100)))&0xFF; 
    TouchData[5] = ((int)(STARTY+STARTR*cos(2*PI*i/100)))>>8&0xFF;
    TouchData[6] = (millis()*10)&0xFF; TouchData[7] = (millis()*10>>8)&0xFF;
    TouchData[8] = 0x01; 
    Device.send(TouchData);
    delay(10);
    }
    //每隔10秒
    delay(5000);
    STARTX=STARTX+300;
    STARTY=STARTY+300;
    
  }
}

工作的视频

https://www.bilibili.com/video/BV1e3411n7hm?share_source=copy_web

参考:

  1. https://www.arduino.cn/thread-107925-1-1.html
  2. https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-precision-touchpad-required-hid-top-level-collections
  3. https://download.microsoft.com/download/7/d/d/7dd44bb7-2a7a-4505-ac1c-7227d3d96d5b/hid-over-i2c-protocol-spec-v1-0.docx

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

CAPTCHAis initialing...