CH567 实现MIDI 设备

使用 Lufa 的示例,作为 MIDI 的参考:

USB Composite Device

  Connection Status    Device connected  
  Current Configuration    1  
  Speed    Full (12 Mbit/s)  
  Device Address    4  
  Number Of Open Pipes    2  

Device Descriptor LUFAMIDI Demo

  Offset    Field    Size    Value    Description  
  0    bLength    1    12h  
  1    bDescriptorType    1    01h    Device  
  2    bcdUSB    2    0110h    USB Spec 1.1  
  4    bDeviceClass    1    00h    Class info in Ifc  Descriptors  
  5    bDeviceSubClass    1    00h  
  6    bDeviceProtocol    1    00h  
  7    bMaxPacketSize0    1    08h    8 bytes  
  8    idVendor    2    03EBh  
  10    idProduct    2    2048h  
  12    bcdDevice    2    0001h    0.01  
  14    iManufacturer    1    01h    “Dean  Camera”  
  15    iProduct    1    02h    “LUFA MIDI  Demo”  
  16    iSerialNumber    1    00h  
  17    bNumConfigurations    1    01h  

Configuration Descriptor1

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    02h    Configuration  
  2    wTotalLength    2    0065h  
  4    bNumInterfaces    1    02h  
  5    bConfigurationValue    1    01h  
  6    iConfiguration    1    00h  
  7    bmAttributes    1    C0h    Self Powered  
  4..0: Reserved    …00000  
  5: Remote Wakeup    ..0…..    No  
  6: Self Powered    .1……    Yes  
  7: Reserved (set to  one)
  (bus-powered for 1.0)  
  1…….  
  8    bMaxPower    1    32h    100 mA  

Interface Descriptor 0/0 Audio,0 Endpoints

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    04h    Interface  
  2    bInterfaceNumber    1    00h  
  3    bAlternateSetting    1    00h  
  4    bNumEndpoints    1    00h  
  5    bInterfaceClass    1    01h    Audio  
  6    bInterfaceSubClass    1    01h    Audio Control  
  7    bInterfaceProtocol    1    00h  
  8    iInterface    1    00h  

Audio Control InterfaceHeader Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    24h    Audio Control  Interface Header  
  2    7    01 00 01 09 00 01 01  

Interface Descriptor 1/0 Audio,2 Endpoints

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    04h    Interface  
  2    bInterfaceNumber    1    01h  
  3    bAlternateSetting    1    00h  
  4    bNumEndpoints    1    02h  
  5    bInterfaceClass    1    01h    Audio  
  6    bInterfaceSubClass    1    03h    MIDI Streaming  
  7    bInterfaceProtocol    1    00h  
  8    iInterface    1    00h  

MIDI Streaming InterfaceHeader Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    07h  
  1    bDescriptorType    1    24h    MIDI Streaming  Interface Header  
  2    5    01 00 01 41 00  

MIDI In Jack Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    06h  
  1    bDescriptorType    1    24h    MIDI In Jack  
  2    4    02 01 01 00  

MIDI In Jack Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    06h  
  1    bDescriptorType    1    24h    MIDI In Jack  
  2    4    02 02 02 00  

MIDI Out Jack Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    24h    MIDI Out Jack  
  2    7    03 01 03 01 02 01 00  

MIDI Out Jack Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    24h    MIDI Out Jack  
  2    7    03 02 04 01 01 01 00  

Endpoint Descriptor 01 1Out, Bulk, 64 bytes

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    05h    Endpoint  
  2    bEndpointAddress    1    01h    1 Out  
  3    bmAttributes    1    02h    Bulk  
  1..0: Transfer Type    ……10    Bulk  
  7..2: Reserved    000000..  
  4    wMaxPacketSize    2    0040h    64 bytes  
  6    bInterval    1    05h  
  7    bRefresh    1    00h  
  8    bSynchAddress    1    00h  

Unrecognized AudioClass-Specific Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    05h  
  1    bDescriptorType    1    25h    Unrecognized Audio  Class-Specific  
  2    3    01 01 01  

Endpoint Descriptor 82 2In, Bulk, 64 bytes

  Offset    Field    Size    Value    Description  
  0    bLength    1    09h  
  1    bDescriptorType    1    05h    Endpoint  
  2    bEndpointAddress    1    82h    2 In  
  3    bmAttributes    1    02h    Bulk  
  1..0: Transfer Type    ……10    Bulk  
  7..2: Reserved    000000..  
  4    wMaxPacketSize    2    0040h    64 bytes  
  6    bInterval    1    05h  
  7    bRefresh    1    00h  
  8    bSynchAddress    1    00h  

Unrecognized AudioClass-Specific Descriptor

  Offset    Field    Size    Value    Description  
  0    bLength    1    05h  
  1    bDescriptorType    1    25h    Unrecognized Audio  Class-Specific  
  2    3    01 01 03  
This report was generated by USBlyzer

代码上和之前的串口非常类似(MIDI 可以看作是波特率特殊的串口):

使用 CH567 实现 USB1 串口

这次的目标是实现一个 USB 转串口的设备,参考的是Arduino Leonardo 的 USB CDC。这个串口是标准USB串口,在Windows 下无需驱动。首先抓取描述符如下:

USB Composite Device

Connection StatusDevice connected
Current Configuration1
SpeedFull (12 Mbit/s)
Device Address4
Number Of Open Pipes3

Device Descriptor Arduino Leonardo

OffsetFieldSizeValueDescription
0bLength112h
1bDescriptorType101hDevice
2bcdUSB20200hUSB Spec 2.0
4bDeviceClass1EFhMiscellaneous
5bDeviceSubClass102hCommon Class
6bDeviceProtocol101hInterface Association Descriptor
7bMaxPacketSize0140h64 bytes
8idVendor22341h
10idProduct28036h
12bcdDevice20100h1.00
14iManufacturer101h“Arduino LLC”
15iProduct102h“Arduino Leonardo”
16iSerialNumber103h
17bNumConfigurations101h

Configuration Descriptor 1 Bus Powered, 500 mA

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType102hConfiguration
2wTotalLength2004Bh
4bNumInterfaces102h
5bConfigurationValue101h
6iConfiguration100h
7bmAttributes1A0hBus Powered, Remote Wakeup
4..0: Reserved…00000 
5: Remote Wakeup..1….. Yes
6: Self Powered.0…… No, Bus Powered
7: Reserved (set to one)
(bus-powered for 1.0)
1……. 
8bMaxPower1FAh500 mA

Interface Association Descriptor Abstract Control Model

OffsetFieldSizeValueDescription
0bLength108h
1bDescriptorType10BhInterface Association
2bFirstInterface100h
3bInterfaceCount102h
4bFunctionClass102hCDC Control
5bFunctionSubClass102hAbstract Control Model
6bFunctionProtocol100h
7iFunction100h

Interface Descriptor 0/0 CDC Control, 1 Endpoint

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber100h
3bAlternateSetting100h
4bNumEndpoints101h
5bInterfaceClass102hCDC Control
6bInterfaceSubClass102hAbstract Control Model
7bInterfaceProtocol100h
8iInterface100h

Header Functional Descriptor

OffsetFieldSizeValueDescription
0bFunctionLength105h
1bDescriptorType124hCS Interface
2bDescriptorSubtype100hHeader
3bcdCDC20110h1.10

Call Management Functional Descriptor

OffsetFieldSizeValueDescription
0bFunctionLength105h
1bDescriptorType124hCS Interface
2bDescriptorSubtype101hCall Management
3bmCapabilities101h
7..2: Reserved000000.. 
1: Data Ifc Usage……0. Call management only over Comm Ifc
0: Call Management…….1 Handles call management itself
4bDataInterface101h

Abstract Control Management Functional Descriptor

OffsetFieldSizeValueDescription
0bFunctionLength104h
1bDescriptorType124hCS Interface
2bDescriptorSubtype102hAbstract Control Management
3bmCapabilities106h
7..4: Reserved0000…. 
3: Connection….0… 
2: Send Break…..1.. Send Break request supported
1: Line Coding……1. Line Coding requests and Serial State notification supported
0: Comm Features…….0 

Union Functional Descriptor

OffsetFieldSizeValueDescription
0bFunctionLength105h
1bDescriptorType124hCS Interface
2bDescriptorSubtype106hUnion
3bControlInterface100h
4bSubordinateInterface0101hCDC Data

Endpoint Descriptor 81 1 In, Interrupt, 64 ms

OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress181h1 In
3bmAttributes103hInterrupt
1..0: Transfer Type……11 Interrupt
7..2: Reserved000000.. 
4wMaxPacketSize20010h16 bytes
6bInterval140h64 ms

Interface Descriptor 1/0 CDC Data, 2 Endpoints

OffsetFieldSizeValueDescription
0bLength109h
1bDescriptorType104hInterface
2bInterfaceNumber101h
3bAlternateSetting100h
4bNumEndpoints102h
5bInterfaceClass10AhCDC Data
6bInterfaceSubClass100h
7bInterfaceProtocol100h
8iInterface100h

Endpoint Descriptor 02 2 Out, Bulk, 64 bytes

OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress102h2 Out
3bmAttributes102hBulk
1..0: Transfer Type……10 Bulk
7..2: Reserved000000.. 
4wMaxPacketSize20040h64 bytes
6bInterval100h

Endpoint Descriptor 83 3 In, Bulk, 64 bytes

OffsetFieldSizeValueDescription
0bLength107h
1bDescriptorType105hEndpoint
2bEndpointAddress183h3 In
3bmAttributes102hBulk
1..0: Transfer Type……10 Bulk
7..2: Reserved000000.. 
4wMaxPacketSize20040h64 bytes
6bInterval100h

This report was generated by USBlyzer

实现了上面的描述符之后,就能保证插入系统后 Windows 设备管理器上不会出现惊叹号。Windows 支持的标准 CDC 动作有下面8个【参考1】

  1. SET_LINE_CODING  用于主机对设备设置波特率,停止位,奇偶校验和位数
  2. GET_LINE_CODING用于主机取得设备当前波特率,停止位,奇偶校验和位数
  3. SET_CONTROL_LINE_STATE 用于产生 RS-232/V.24 标准的控制信号
  4. SEND_BREAK
  5. SERIAL_STATE  返回状态信息,比如:奇偶校验错误
  6. SEND_ENCAPSULATED_COMMAND
  7. GET_ENCAPSULATED_RESPONSE
  8. RESPONSE_AVAILABLE

从实际验证的结果看起来(就是前面提到的使用 Arduino Leonardo 作为验证对象),实现 1-3 的支持外加 2个Endpoint Bulk传输即可实现通讯。

 1.SET_LINE_CODING  的实现。收到 bRequestType ==0x21, bRequest== SET_LINE_CODING  即可判定这个操作;之后用 ENDPOINT 0 的OUT 中返回当前的LineInfo;最后再通过 ENDPOINT 0 的 IN 返回0字节

2. GET_LINE_CODING  的实现。收到 bRequestType ==0xA1, bRequest== GET_LINE_CODING  即可判定这个操作;之后直接返回当前的LineInfo;最后再通过 ENDPOINT 0 的 IN 返回0字节

3. SET_CONTROL_LINE_STATE 的实现。收到 bRequestType ==0x21, bRequest== 0x22  即可判定这个操作;之后直接通过ENDPOINT 0 的 IN 返回0字节。

实现上面的操作之后,即可使用串口工具打开设备产生的串口了。接下来实现串口传输的模拟:

  1. 从Windows(HOST) 对CH567通过串口工具发送数据。数据会出现在 endpoint2 OUT上,我们将收到的数据送到CH567的串口上,然后再通过一个额外的串口转USB即可看到。具体代码是:
                        if(intstatus == (UIS_TOKEN_OUT|2))             /* endpoint 2 下传 */
                        {
                                if(R8_USB1_INT_ST&bUIS_TOG_OK)
                                {

                                        // 下传是 HOST -> DEVICE
                                        // 用串口工具打开设备对应的串口,然输入的内容可以在 Debug 串口上看到
                                        for (i=0; i<R16_USB1_RX_LEN; i++)
                                        {
                                                printf("%X ",UsbEp2OUTBuf[i]);
                                        }
                                        printf("\n");
                                }
                        }

2.从CH567定时对 Windows 发送字符串,使用串口工具打开CH567端口后可以看到这个字符串。修改有2处,第一个是发送的代码,在main.c 中每隔5秒发送一次:

        while(1)
        {
                mDelaymS(5000);
                if (UsbConfig!=0)
                {
                        memcpy( UsbEp3INBuf, &Msg[0], sizeof( Msg ));
                        R16_UEP3_T_LEN1 =  sizeof( Msg );
                        R8_UEP3_TX_CTRL1 = (R8_UEP3_TX_CTRL1 & ~ MASK_UEP_T_RES) | UEP_T_RES_ACK;
                }
        };

另外一处是当CH567收到 Endpoint3 IN 中断时,使用0字节来回复给主机

 if(intstatus == (UIS_TOKEN_IN|3))             /* endpoint 3 上传 */
                        {
                                R16_UEP3_T_LEN1 =  0;
                                R8_UEP3_TX_CTRL1 = (R8_UEP3_TX_CTRL1 & ~ MASK_UEP_T_RES) | UEP_T_RES_ACK;
                        }

此外,还有一处需要特别注意的是:必须使用高波特率用于 printf 的串口输出(>1Mhz),实验中我使用的是 CH343 6Mhz的波特率,否则会发生丢失log的情况(实际上有跑到代码,但是对应那句话的 Log 不出现,这个问题我调试了2天,在USB 逻辑分析仪上看到了发送的数据包,但是串口 Log说没有)。

运行结果如下,左侧是用于调试的CH343产生的串口,右边是CH567模拟出来的串口。当我们对CH567发送”1234567”时,CH567收到后会从UART再次送出,因此我们在左侧能看到;此外,CH567每隔5秒发送一次”www.lab-z.com”字符串在右侧窗口可以看到。

完整代码下载:

参考:

1. https://www.silabs.com/documents/public/application-notes/AN758.pdf

ESP32S2 USB触摸屏作图

这次实验使用 ESP32 S2 模拟触摸屏的方式绘制一个心形和渐开线。

首先介绍的是“笛卡尔的爱情坐标公式”:心形函数r=a(1-sinθ),常被人当做表达爱和浪漫的一种方法。并且关于这个函数的由来有一个传播很广的故事。

笛卡尔在52岁时邂逅了当时瑞典的公主,当时他是公主的数学老师,不久公主就对笛卡尔产生了爱慕之情。然而,国王知道后,非常愤怒,将他流放回法国。在那里,笛卡尔给公主写的信都会被拦截。

在笛卡尔寄出第十三封信后,笛卡尔永远离开了这个世界。在最后的一封信上,笛卡尔只写了一个公式:r=a(1-sinΘ)

国王也看不懂,于是把这封信交给了公主。这就是我们知道的极坐标下的心型函数。

这封情书至今保存在欧洲笛卡尔纪念馆里。【这一段是“读者”体,真实情况如果用震惊体来描述的话就是“天才数学家竟被女王惨无人道的折磨”参考1】

在这个公式中有2个变量:a和θ。我们首先使用网页版的绘图工具【参考2】验证一下这个公式:

上述公式的参数方程形式为:

参数方程形式:

x= a*(2*sin(t)-sin(2*t))
y= a*(2*cos(t)-cos(2*t))
0&lt;=t&lt;=2*pi

根据上述公式,设计代码如下:

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

#define STEP 600
int STARTX=5000;
int STARTY=13000;
int STARTR=5000;
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()) {
    delay(1000);
    Serial.println("Finger");
    int iX,iY;
    iX=STARTX+(STARTR*(2*sin(0)-sin(0)))/3;
    iY=STARTY-(STARTR*(2*cos(0)-cos(0)));
    TouchData[0] = 0x81; 
    TouchData[1] = 0x02;
    TouchData[2] = (iX)&0xFF; 
    TouchData[3] = (iX)>>8&0xFF;
    TouchData[4] = (iY)&0xFF; 
    TouchData[5] = (iY)>>8&0xFF;
    TouchData[6] = (millis()*10)&0xFF; TouchData[7] = (millis()*10>>8)&0xFF;
    TouchData[8] = 0x01; 
    Device.send(TouchData);
    delay(20);
    TouchData[0] = 0x81; 
    TouchData[1] = 0x02;
    TouchData[2] = (iX+1)&0xFF; 
    TouchData[3] = (iX+1)>>8&0xFF;
    TouchData[4] = (iY)&0xFF; 
    TouchData[5] = (iY)>>8&0xFF;
    TouchData[6] = (millis()*10)&0xFF; TouchData[7] = (millis()*10>>8)&0xFF;
    TouchData[8] = 0x01; 
    Device.send(TouchData);
    delay(40);

    // 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<STEP+1;i++) {
    TouchData[0] = 0x81; TouchData[1] = 0x01;
    iX=STARTX+(STARTR*(2*sin(2*PI*i/STEP)-sin(2*2*PI*i/STEP)))/3;
    iY=STARTY-(STARTR*(2*cos(2*PI*i/STEP)-cos(2*2*PI*i/STEP)));
    Serial.print(iX);Serial.print("  ");Serial.println(iY);
    TouchData[2] = ((int)(iX))&0xFF; 
    TouchData[3] = ((int)(iX))>>8&0xFF;
    TouchData[4] = ((int)(iY))&0xFF; 
    TouchData[5] = ((int)(iY))>>8&0xFF;
    TouchData[6] = (millis()*10)&0xFF; TouchData[7] = (millis()*10>>8)&0xFF;
    TouchData[8] = 0x01; 
    Device.send(TouchData);
    delay(20);
    }
    //每隔10秒
    delay(2000);
    STARTX=STARTX+6000;
    STARTY=STARTY;
    
  }

}

渐开线的参数方程:

iX=r*cos(b)+r*b*sin(b)
iY=r*sin(b)-r*b*cos(b)

完整代码:

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

#define STEP 100
int STARTX = 18000;
int STARTY = 18000;
int STARTR = 100;
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()) {
    delay(1000);
    Serial.println("Finger");
    int iX, iY;
    /*
    iX = STARTX + (STARTR * (2 * sin(0) - sin(0))) / 3;
    iY = STARTY - (STARTR * (2 * cos(0) - cos(0)));
    TouchData[0] = 0x81;
    TouchData[1] = 0x02;
    TouchData[2] = (iX) & 0xFF;
    TouchData[3] = (iX) >> 8 & 0xFF;
    TouchData[4] = (iY) & 0xFF;
    TouchData[5] = (iY) >> 8 & 0xFF;
    TouchData[6] = (millis() * 10) & 0xFF; TouchData[7] = (millis() * 10 >> 8) & 0xFF;
    TouchData[8] = 0x01;
    Device.send(TouchData);
    delay(20);
    TouchData[0] = 0x81;
    TouchData[1] = 0x02;
    TouchData[2] = (iX + 1) & 0xFF;
    TouchData[3] = (iX + 1) >> 8 & 0xFF;
    TouchData[4] = (iY) & 0xFF;
    TouchData[5] = (iY) >> 8 & 0xFF;
    TouchData[6] = (millis() * 10) & 0xFF; TouchData[7] = (millis() * 10 >> 8) & 0xFF;
    TouchData[8] = 0x01;
    Device.send(TouchData);
    delay(40);
*/
    // 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 < STEP*18; i++) {
      TouchData[0] = 0x81; TouchData[1] = 0x01;
      iX = STARTX+(STARTR * cos(2*PI*i/STEP) + STARTR * (2*PI*i/STEP) * sin(2*PI*i/STEP))*3/4;
      iY = STARTY+STARTR * sin(2*PI*i/STEP) - STARTR * (2*PI*i/STEP) * cos(2*PI*i/STEP);
      Serial.print(iX); Serial.print("  "); Serial.println(iY);
      TouchData[2] = ((int)(iX)) & 0xFF;
      TouchData[3] = ((int)(iX)) >> 8 & 0xFF;
      TouchData[4] = ((int)(iY)) & 0xFF;
      TouchData[5] = ((int)(iY)) >> 8 & 0xFF;
      TouchData[6] = (millis() * 10) & 0xFF; TouchData[7] = (millis() * 10 >> 8) & 0xFF;
      TouchData[8] = 0x01;
      Device.send(TouchData);
      delay(20);
    }
    //每隔10秒
    delay(2000);
    STARTX = STARTX + 3;
    STARTY = STARTY;

  }

}

运行结果:

参考:

1. https://baijiahao.baidu.com/s?id=1721580028060990553&wfr=spider&for=pc

2. https://zuotu.91maths.com/#W3sidHlwZSI6MSwiZXEiOiIyKigxLXNpbih0aGV0YSkpIiwiY29sb3IiOiIjMDA4MGNjIiwidGhldGFtaW4iOiIwIiwidGhldGFtYXgiOiIycGkiLCJ0aGV0YXN0ZXAiOiIwLjAxIn0seyJ0eXBlIjoxMDAwLCJ3aW5kb3ciOlsiLTguMjE1MTExOTk5OTk5OTkiLCI4LjAzNDg4Nzk5OTk5OTk5MiIsIi01LjI3ODUyNzk5OTk5OTk5NSIsIjQuNzIxNDcxOTk5OTk5OTk2Il0sImdyaWQiOlsiMSIsIjEiXX1d

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

CH567 上USB0 HOST 实现

这次实现 CH567 USB0 的 USB Host 功能。基于 \EXAM\USB1_HOST 的代码进行修改。其中已经实现了 USB0 HOST HID 的枚举,我们只需要添加针对 ENDPOINT 的读取即可。

在Main中添加如下代码:

                        s=WaitU0HTransactTimes(1,USB_PID_IN,ctrltog,6000);
                        if( s == USB_INT_SUCCESS ) {
                                ctrltog  = ctrltog ? 0 : 1;
                                printf("in: ");   
                                for(i=0; i<R16_USB0_RX_LEN; i++){
                                printf("%02x ", UHBuffer1[i]);}
                                printf("\n");
                        }
                        mDelaymS(1);

其中的 mDelayms() 数值应该根据描述符中的数值进行填写,这里我偷懒了直接使用 1ms。这对于功能没有 影响,USB KB 如果没有数据会直接 NAK 这次的请求。

完整代码:

FireBeetle 帮你把手机变成键盘鼠标

这次做的项目能够帮你把手上的手机变成能够控制电脑的键盘和鼠标。

基本原理上是:用户通过手机应用程序经由BLE蓝牙和FireBeetle 进行通讯,FireBeetle收到之后再通过USB接口将数据发送到电脑上。从原理上看整体分作三部分:硬件的选择和设计,Arduino 代码的编写和手机端程序设计。

首先介绍硬件的选择和设计。FireBeetle是DFRobot 出品的基于 ESP32 的开发板,它能够支持蓝牙和 WIFI 的通讯,它带有USB转串口芯片但是无法将自身模拟为USB键盘鼠标设备,为了实现这个功能,还需要设计一个USB键盘鼠标Shield。经过研究,最终选择了 WCH 出品的CH9329芯片进行实现。CH9329是一款串口转标准 USB HID 设备(键盘、鼠标、自定义 HID)芯片,根据不同的工作模式,在电脑上可被识别为标准的 USB 键盘设备、 USB 鼠标设备或自定义 HID 类设备。该芯片接收客户端发送过来的串口数据,并按照 HID 类设备规范,将数据先进行打包再通过 USB 口上传给计算机。这款芯片基本特性如下:

●支持 12Mbps 全速 USB 传输,兼容 USB V2.0,内置晶振。
● 默认串口通信波特率为 9600bps,支持各种常见波特率。
● 支持 5V 电源电压和 3.3V 电源电压。
● 多种芯片工作模式, 适应不同应用需求。
● 多种串口通信模式,灵活切换。
● 支持普通键盘和多媒体键盘功能,支持全键盘功能。
● 支持相对鼠标和绝对鼠标功能。
● 支持自定义 HID 类设备功能,可用于单纯数据传输。
● 支持 ASCII 码字符输入和区位码汉字输入。
● 支持远程唤醒电脑功能。
● 支持串口或 USB 口配置芯片参数。
● 可自行配置芯片的 VID、 PID,以及芯片各种字符串描述符。
● 可自行配置芯片的默认波特率。
● 可自行配置芯片通信地址,实现同一个串口下挂载多个芯片。
● 可自行配置回车字符。
● 可自行配置过滤字符串,以便进行无效字符过滤。
● 符合 USB 相关规范,符合 HID 类设备相关规范。
● 采用小体积的 SOP-16 无铅封装,兼容 RoHS。

对于这次的设计来说,通过串口就能实现USB键盘鼠标,非常方便。确定了芯片之后,接下来即可着手Shield设计了。电路设计如下:

左侧和中间是 FireBeetle 的接口,右侧是USB 公头,右下是CH9329芯片。

下面是CH9329 的最小系统电路,芯片内置了晶振,外部只需要一个0.1uf(C1)的电容即可正常工作。

图中的 Pin1 是用来标志芯片配置完成的引脚(#ACT),Pin2、3、4、5是用来配置芯片功能的引脚,通过组合可以在上电的时候实现芯片的功能选择。

工作模式MODE1电平MODE0 电平功能说明
模式011模拟标准USB键盘+USB鼠标设备+USB自定义HID类设备(默认)
该模式下CH9329芯片在电脑上识别为USB键盘、USB鼠标和自定义HID类设备的多功能复合设备,USB键盘包含普通键和多媒体键, USB鼠标包含相对鼠标和绝对鼠标。
该模式功能最全,可以实现USB键盘和USB鼠标的全部功能。 MODE0引脚和MODE1引脚内置了上拉电阻,当这两个引脚悬空时,芯片处于本模式。
模式110模拟标准USB键盘设备
该模式下CH9329芯片在电脑上识别为单一USB键盘设备, USB键盘只包含普通键,不包含多媒体键,支持全键盘模式,适用于部分不支持复合设备的系统。
模式201模拟标准USB键盘+USB鼠标设备
该模式下CH9329芯片在电脑上识别为USB键盘和USB鼠标的多功能复合设备, USB键盘包含普通键和多媒体键, USB鼠标包含相对鼠标和绝对鼠标。
注: Linux/Android/苹果等操作系统下, 出于兼容性考虑,建议使用该模式。  
模式300模拟标准USB自定义HID类设 该模式下CH9329芯片在电脑上识别为单一USB自定义HID类设备,具有上传和下传2个通道,可以实现串口和HID数据透传功能。CH9329芯片如果接收到串口数据,则打包通过USB上传,如果接收到USB下传数据,则通过串口进行发送。 这个模式可以方便用户实现串口转HID。  
串口通信模式CFG1电平CFG0电平功能
模式011协议传输模式(默认)
该模式一般适用于既需要使用USB键盘功能,又
需要使用USB鼠标功能的应用。如果需要使用全
键盘功能,也建议采用该模式。 CFG0引脚和CFG1引脚内置了上拉电阻,当这两个引脚悬空时,芯片处于本模式。
模式110ASCII模式
该模式下客户串口设备向CH9329芯片发送串口
数据时,可以发送ASCII码字符数据,也可以发
送区位码汉字数据。
该模式适用于只需要使用USB键盘中可见ASCII
字符的应用。
模式201透传模式
该模式下客户串口设备向CH9329芯片发送串口
数据时,可以是任意16进制数据。
该模式适用于CH9329芯片处于芯片工作模式3的
应用。  

PCB 设计如下:

3D预览:

接下来开始手机端程序的设计。经过考察,选择了点灯科技出品的 Blinker,这是一套专业且易用物联网解决方案,提供了服务器、应用、设备端SDK支持。简单便捷的应用配合多设备支持的SDK,可以让开发者在3分钟内实现设备的接入。 点灯服务有三个版本,社区版开源且免费,让大家可以体验到点灯方案的特点和优势;云服务版提供更多增值服务与功能,且有效降低客户的项目实施成本,让客户更快的进行物联网升级;商业版可进行独立部署,可以满足客户更多样的需求。这次我们使用它提供的ESP32 支持通过蓝牙连接FireBeetle 开发板。首先,安装 Arduino 的库,在https://diandeng.tech/dev 页面下载 Arduino 库。之后解压放到 Arduino 的 Library目录下。

之后,烧写示例文件:

\blinker-library\examples\Blinker_Widgets\Blinker_Button\Button_BLE\Button_BLE.ino

打开手机上的“点灯 Blinker”程序之后开始创建控制设备的应用:

1.创建一个新设备:

2.添加一个独立设备

3.选择蓝牙接入

4.这时手机会执行一个搜索蓝牙设备的动作,这也是为什么要提前刷上一个示例代码的原因

5.在界面上放置一个输入框(当作键盘用于输入字符),一个摇杆组件(用于控制鼠标)和六个按钮(分别用于实现鼠标左键单击,左键双击,中键单击,右键单击,以及输入键盘回车键)

6.每个组件可以进行属性的调整,包括显示的文字和名称。

设置好了之后,在界面上操作数据可以在Arduino 的串口监视器中看到当有事件信息,其中有摇杆的动作、按钮事件和文本框的输入内容。

接下来就可以进行 Arduino 代码的编写了, 关键代码有:

  1. 代码首部加入#define BLINKER_BLE这个定义后, Blinker 库能够帮助用户完成大部分的蓝牙操作,使用者只需要专注于“收到数据如何处理”而不必关心“如何收到数据”。
  2. Setup函数中通过Button1.attach(button1_callback); 绑定按键和处理函数,当按键发生后会自动调用 button1_callback() 函数来处理;
  3.  Setup函数中通过Blinker.attachData(dataRead);绑定数据处理函数, dataRead() 函数能够收到输入框和摇杆的数据;
  4. 收到的输入框数据是 ASCII 码,通过Asc2Scancode() 函数转化为HID Scancode 再发送给 CH9329 芯片;
#define BLINKER_PRINT Serial
#define BLINKER_BLE
#include <Blinker.h>

//键盘数据
char keypress[]  = {0x57, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10};
//鼠标数据
char mousemove[] = {0x57, 0xAB, 0x00, 0x05, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};

// 左键单击
BlinkerButton Button1("btn-l1");
// 左键双击
BlinkerButton Button2("btn-l2");
// 右键单击
BlinkerButton Button3("btn-r1");
// 中键单击
BlinkerButton Button4("btn-m1");
// 回车
BlinkerButton Button5("btn-rtn");

// 左键单击
void button1_callback(const String & state) {
  BLINKER_LOG("Left Click ", state);
  // 触发鼠标左键
  mousemove[6] = 0x01;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(10);
  // 鼠标左键抬起
  mousemove[6] = 0x00;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(10);
}
// 左键双击
void button2_callback(const String & state) {
  BLINKER_LOG("Left Double Click ", state);
  // 触发鼠标左键
  mousemove[6] = 0x01;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(10);
  // 鼠标左键抬起
  mousemove[6] = 0x00;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(20);
  // 再来一次
  mousemove[6] = 0x01;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(10);
  mousemove[6] = 0x00;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(20);
}
// 右键单击
void button3_callback(const String & state) {
  BLINKER_LOG("Right Click ", state);
  // 触发鼠标右键
  mousemove[6] = 0x02;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(10);
  mousemove[6] = 0x00;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(10);
}
// 中键双击
void button4_callback(const String & state) {
  BLINKER_LOG("Middle Click ", state);
  // 触发鼠标中键
  mousemove[6] = 0x04;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(10);
  mousemove[6] = 0x00;
  SendData((byte*)mousemove, sizeof(mousemove));
  delay(10);
}
// 回车
void button5_callback(const String & state) {
  BLINKER_LOG("Enter ", state);
  // 键盘回车
  keypress[7] = 0x28;
  SendData((byte*)keypress, sizeof(keypress));
  delay(10);
  keypress[7] = 0;
  SendData((byte*)keypress, sizeof(keypress));
  delay(10);
}
// 将 Buffer 指向的内容,size 长度,计算 checksum 之后发送到Serial2
void SendData(byte *Buffer, byte size) {
  byte sum = 0;
  for (int i = 0; i < size - 1; i++) {
    Serial2.write(*Buffer);
    sum = sum + *Buffer;
    Buffer++;
  }
  *Buffer = sum;
  Serial2.write(sum);
}
// 将ASCII 字符转化为 HID Scancode值
byte Asc2Scancode(byte Asc, boolean *shift) {
  if ((Asc >= 'a') && (Asc <= 'z')) {
    *shift = false;
    return (Asc - 'a' + 0x04);
  }
  if ((Asc >= 'A') && (Asc <= 'Z')) {
    *shift = true;
    return (Asc - 'A' + 0x04);
  }
  if ((Asc >= '1') && (Asc <= '0')) {
    *shift = false;
    return (Asc - '0' + 0x1E);
  }
  if (Asc == '>') {
    *shift = true;
    return (0x37);
  }
  if (Asc == '.') {
    *shift = false;
    return (0x37);
  }
  if (Asc == '_') {
    *shift = true;
    return (0x2D);
  }    
  if (Asc == '-') {
    *shift = false;
    return (0x2D);
  }    
  return 0;
}
// 如果未绑定的组件被触发,则会执行其中内容
// 这里的游戏摇杆和输入框都会在这里处理
void dataRead(const String & data)
{
  BLINKER_LOG("Blinker readString: ", data);
  // 判断是否为游戏摇杆
  if (data.indexOf("joy") != -1) {
    BLINKER_LOG("Joy Move");
    String StrX, StrY;
    // 将摇杆坐标从输入中分离出来
    StrX = data.substring(data.indexOf("[") + 1, data.indexOf(","));
    StrY = data.substring(data.indexOf(",") + 1, data.indexOf("]"));
    BLINKER_LOG("", StrX); BLINKER_LOG("", StrY);
    // 摇杆数据按照鼠标发送出去
    mousemove[7] = map(StrX.toInt(), 0, 255, -127, 127);
    mousemove[8] = map(StrY.toInt(), 0, 255, -127, 127);
    SendData((byte*)mousemove, sizeof(mousemove));
    delay(10);
    mousemove[7] = 0;
    mousemove[8] = 0;
  } else {
    boolean shift;
    byte scanCode;
    for (int i = 0; i < data.length(); i++) {
      BLINKER_LOG("Key In", data.charAt(1));
      // 将收到的 ASCII 转为 ScanCode
      scanCode = Asc2Scancode(data.charAt(i), &shift);
      // 一些按键当有 Shift 按下时会发生转义
      if (scanCode != 0) {
        if (shift == true) {
          keypress[5] = 0x02;
        }
        BLINKER_LOG("Scancode", scanCode);
        // 填写要发送的 ScanCode
        keypress[7] = scanCode;
        SendData((byte*)keypress, sizeof(keypress));
        delay(10);
        keypress[5] = 0x00; keypress[7] = 0;
        SendData((byte*)keypress, sizeof(keypress));
        delay(10);
      }
    }
  }
}

void setup() {
  // 初始化调试串口
  Serial.begin(115200);
  // 初始 CH9329 串口
  Serial2.begin(9600);
#if defined(BLINKER_PRINT)
  BLINKER_DEBUG.stream(BLINKER_PRINT);
#endif

  // 初始化blinker
  Blinker.begin();
  Blinker.attachData(dataRead);
  Button1.attach(button1_callback);
  Button2.attach(button2_callback);
  Button3.attach(button3_callback);
  Button4.attach(button4_callback);
  Button5.attach(button5_callback);
}

void loop() {
  Blinker.run();
}

工作的视频

https://mc.dfrobot.com.cn/thread-312785-1-1.html#pid511208

Intel平台调试的几个概念

在这个页面 https://www.intel.com/content/www/us/en/developer/articles/technical/software-security-guidance/secure-coding/intel-debug-technology.html有几个关于 Intel 平台调试基本概念。

首先,如同使用工具进行测量一定会有误差一样,产品天生会有Bug,一些用户要求的产品特性在另外一些用户严重就是Bug。比如,当Setup 中设置 USB Controller 为 Disable 之后,有些用户会发现仍然能够通过USB键盘进入 BIOS Setup界面,而有的用户则会抱怨修改之后USB键盘完全失效,甚至无法进去BIOS Setup 修改选项。芯片产品更有芯片级调试的需求。但是调试不可避免的碰到用户的数据,比如:反编译追踪,另外这种技术还可能会被用于破解产品,比如,经过研究让客户设计只能运行 Windows的平台能够运行 Linux这种。因此,只能尽量在二者之间寻找平衡点。以我的经验而言,市面上大多数XX产品留有后门的新闻中提到的所谓的“后门”通常都只是预留的调试接口。

对于这种平衡,Intel 采用的策略主要是:区分开发者/调试者和最终用户的策略。比如,开发者的权限更高,能够读写一些最终用户无法看到的寄存器这种。

下面是产品声明周期的示意图,可以看到包含4个阶段:

  1. 芯片的生产制造à2.交给厂商制造电路(比如:主板)à3.厂商制造机器(比如: 笔记本、平板等等)à4.返修。其中的1 2 过程是明显需要有调试能力的,作为BIOS工程师通常工作在这两个阶段。在阶段3之后就要交付给客户了。

为了解决调试和安全之间的矛盾,提出了“Protection Classes”的概念。它是用来控制当前能进行的系统级别调试能力的。它分为三个级别:

  • Public (之前也被称作“Green”或者“Locked”)。这个级别是普通用户能够接触到的级别,安全等级最高,调试能力最低
  • “OxM(之前也被称作“Orange”或者“OEM UnLocked”)。这个级别在 Public 级别上增加了更多的调试能力。这个级别假定的用户是 OxM 厂商。
  • “Intel”(之前也被称为“Read”或者 “Intel Unlocked”)。比前一个 OxM具有更高的调试能力。因为这个级别需要Intel 授权,假定的用户是 Intel。

个人感觉,诸如 BIOS  Debug Log 或者 EC log 是 OxM 级别的调试手段,特别是EC 的数据也只有 OxM 能够获取(需要特别版本的 EC Firmware),同时也只有 OxM才能解读。

为了更好的平衡调试和用户隐私的关系,Intel 引入了Hardware-based Policy Protection 用于管理调试能力。在特殊情况下,可以通过左侧的 Hardware-based Authentication Logic 和 Security Processor 打开目标机的调试能力,对应的右侧的Block X 是SoC 内部的IP,每一个IP 都有预留的用于调试的功能(Debug Capability)。

在上电过程中,会有一个窗口期,如果在这个事件段内有Intel授权,那么可以打开调试功能。(There is a limited time during the boot for which this feature can be used by an Intel entity (i.e., an entity authenticated by a per-part Intel key hash stored in the product).  The entity must perform the authentication (and unlocking) through this hardware-based mechanism before any secure assets are distributed from its rest location (note: “rest location” refers to where an asset is stored)

但是注意,前面提到默认情况下是一个“窗口期”,如果过了这个时间点,那就不行(除非再次上电)。如果想让再任何时候都能够进行调试,那么需要打开 Delayed Authentication 功能,这个可以在 FIT 中设置,也可以在BIOS Setup界面设置。二者是等效的,BIOS 的设置也是告知 CSME 进行调整。所以,Delayed Authentication的作用是:让芯片处于正常模式,当有调试需要的时候再打开调试功能进行 Dbc或者CCA 的连接。

Teensy 3.6 触摸屏功能

Teensy 3.6 支持触摸屏,10指触摸,具体的库在\hardware\teensy\avr\cores\teensy3\usb_touch.c 文件中,下面是一个示例代码,使用了2个手指绘制直线:

#include <Bounce.h>

int yoffset = 4000;

void setup() {
  pinMode(A1, INPUT_PULLUP);
  TouchscreenUSB.begin();
}

void drawline(int x, int y) {
 for (int i=0; i < 6000; i += 100) {
   TouchscreenUSB.press(0, x + i, y + i/13);
   TouchscreenUSB.press(1, x + i+400, y + i/13+400);
   delay(10);
 }
 TouchscreenUSB.release(0);
 TouchscreenUSB.release(1); 
}

void loop() {
  if (digitalRead(A1)==LOW) {
    Serial.println("press");
    drawline(16000, yoffset);
    yoffset += 1200;
    if (yoffset > 24000) yoffset = 4000;
  }
}

特别的,需要在菜单中打开 Touch Screen

另外,如果你使用Windows 10 下面的画板进行测试,需要选中 Brushes,只有这个才支持多点触摸绘图:

内存的奇怪问题

最近遇到了一个奇怪的问题,经过化简得代码表示如下:

#include "stdafx.h"
#include <malloc.h>

void foo2() {
	int *Handle;
	Handle = (int *)alloca(100);
	memset(Handle, 0x11, 100);
	return;
}
void foo1(int **Handle){
	*Handle = (int *)alloca(100);
	memset(*Handle,0xAA,100);
	return;
}

int main()
{
	int *Value=NULL;
	foo1(&Value);
	foo2();
	getchar();
    return 0;
}

简单的说,在 foo1() 中分配100bytes的内存空间,0然后在foo2() 中在分配100Bytes,但是实践发现,前面分配的内存空间被“冲掉”了。更具体的说:

1.运行 foo1(), 之后查看到 value 的内存地址已经赋值为 0xaa

2.接下来执行 foo2(),但是运行之后,Value 对应的内存空间被覆盖为0x11。

有兴趣的朋友可以自己先琢磨五分钟看看能否找到问题。

最终,这个问题是分配内存的 alloca()导致的:alloca分配的是栈区(stack)内存,程序自动释放;(注意,栈空间有限仅几kb左右,堆空间远大于栈空间)。当 foo1 执行完成,这个区域已经被释放;当执行 foo2 的时候,程序会再次使用这个内存【参考1】。

解决方法:改成 malloc,它是在堆上进行分配内存的。

参考:

  1. https://zhuanlan.zhihu.com/p/449165315
  2. https://cloud.tencent.com/developer/article/1729074

一个让你轻松配置ESP32 WIFI的库

ESP32 带有 WIFI 功能,而众所周知要想让一个设备连接WIFI AP 需要告知设备对应 AP 的名称和密码,简单实验的话,可以直接在代码中写死这两个参数,但这种情况下烧写之后设备只能在固定的环境下使用。https://github.com/tzapu/wifimanager 这个项目可以解决上述问题。先说一下这个东西如何使用:

  1. 编译下载Arduino\libraries\WiFiManager-master\examples\OnDemand下面的代码
  2. 上电运行之后短接 Pin0
  3. 用手机查找OnDemandAP 这个 AP
  4. 连接之后自动打开下面的界面

5.选择 Configure WIFI 会显示当前能搜索到的WIFI AP名称,选择你要的

6.输入对应的密码设备即可连接

我在 DFRobot的 FireBeetle 上实验(需要注意他自带的WIFI库太老,运行期AP 无法启动,需要用ESP32 Arduino库中的 FireBeetle),工作正常。