Step to UEFI (274)EFI Application MEMTEST86 自启动研究

前面提到了如何将 MemTest86 打包为一个 EFI 文件,美中不足的是运行这个 EFI 之后无法自动跳转到 Memtest86中执行,还需要手工运行map 和执行。针对这个问题增加代码进行实验。

新增的代码如下:

1.代码从创建 RAM Disk 开始

	//
	// Register the newly created RAM disk.
	//
	Status = MyRamDisk->Register (
	             ((UINT64)(UINTN) StartingAddr),
	             FileSize,
	             &gEfiVirtualDiskGuid,
	             NULL,
	             &DevicePath
	         );
	if (EFI_ERROR (Status))
	{
		Print(L"Can't create RAM Disk!\n");
		return EFI_SUCCESS;
	}

2.使用 ShellExecute 来运行 map -r 命令

	Print(L"Running map -r command!\n");
	CHAR16	  *MapCommmand=L"map -r";
	EFI_STATUS  CmdStat;
	Status = ShellExecute( &gImageHandle, MapCommmand, FALSE, NULL, &CmdStat);
	if (EFI_ERROR (Status))
	{
		Print(L"Can't run MAP command\n");
		return EFI_SUCCESS;
	}
	Print(L"%r\n",CmdStat);

3.接下来扫描所有的 FSx: 。如果存在某个FSx::\efi\boot\mt86.png,那就说明是 MemTest86 的盘。

	UINTN i;
	CHAR16 StrBuffer[80];
	BOOLEAN Found=FALSE;

	for (i=0; i<9; i++)
	{
		UnicodeSPrint(StrBuffer,sizeof(StrBuffer),L"fs%d:\\efi\\boot\\mt86.png",i);
		Print(L"%s\n",StrBuffer);
		if (!EFI_ERROR(ShellFileExists(StrBuffer)))
		{
			UnicodeSPrint(StrBuffer,sizeof(StrBuffer),L"fs%d:\\efi\\boot\\BootX64.EFI",i);
			Found=TRUE;
			break;
		}
	}
	if (Found)
	{
		ShellExecute( &gImageHandle, StrBuffer, FALSE, NULL, NULL);
	}

	return 0;

上述设计逻辑没有问题,但是测试发现无法达到预期的目标:MemTest86 不会自动运行起来。执行之后,仍然需要手工运行 Map -r ,然后找到新生成的 FsX:再进入执行。

于是进行调试,从ShellExecute()函数入手。

这个函数定义在 \ShellPkg\Library\UefiShellLib\UefiShellLib.c 这个文件中。

/**
  Cause the shell to parse and execute a command line.

  This function creates a nested instance of the shell and executes the specified
  command (CommandLine) with the specified environment (Environment). Upon return,
  the status code returned by the specified command is placed in StatusCode.
  If Environment is NULL, then the current environment is used and all changes made
  by the commands executed will be reflected in the current environment. If the
  Environment is non-NULL, then the changes made will be discarded.
  The CommandLine is executed from the current working directory on the current
  device.

  The EnvironmentVariables pararemeter is ignored in a pre-UEFI Shell 2.0
  environment.  The values pointed to by the parameters will be unchanged by the
  ShellExecute() function.  The Output parameter has no effect in a
  UEFI Shell 2.0 environment.

  @param[in] ParentHandle         The parent image starting the operation.
  @param[in] CommandLine          The pointer to a NULL terminated command line.
  @param[in] Output               True to display debug output.  False to hide it.
  @param[in] EnvironmentVariables Optional pointer to array of environment variables
                                  in the form "x=y".  If NULL, the current set is used.
  @param[out] Status              The status of the run command line.

  @retval EFI_SUCCESS             The operation completed sucessfully.  Status
                                  contains the status code returned.
  @retval EFI_INVALID_PARAMETER   A parameter contains an invalid value.
  @retval EFI_OUT_OF_RESOURCES    Out of resources.
  @retval EFI_UNSUPPORTED         The operation is not allowed.
**/
EFI_STATUS
EFIAPI
ShellExecute (
  IN EFI_HANDLE   *ParentHandle,
  IN CHAR16       *CommandLine OPTIONAL,
  IN BOOLEAN      Output OPTIONAL,
  IN CHAR16       **EnvironmentVariables OPTIONAL,
  OUT EFI_STATUS  *Status OPTIONAL
  )

从Log可以看到执行的是下面这段代码:

  //
  // Check for UEFI Shell 2.0 protocols
  //
  if (gEfiShellProtocol != NULL) {
    //
    // Call UEFI Shell 2.0 version (not using Output parameter)
    //
    return (gEfiShellProtocol->Execute (
                                 ParentHandle,
                                 CommandLine,
                                 EnvironmentVariables,
                                 Status
                                 ));
  }

进一步追踪,执行的是 \ShellPkg\Application\Shell\ShellProtocol.c定义的如下:

// Pure FILE_HANDLE operations are passed to FileHandleLib
// these functions are indicated by the *
EFI_SHELL_PROTOCOL  mShellProtocol = {
  EfiShellExecute,
  EfiShellGetEnv,
……
};

具体实现在 \ShellPkg\Application\Shell\ShellProtocol.c文件中:

EFI_STATUS
EFIAPI
EfiShellExecute (
  IN EFI_HANDLE   *ParentImageHandle,
  IN CHAR16       *CommandLine OPTIONAL,
  IN CHAR16       **Environment OPTIONAL,
  OUT EFI_STATUS  *StatusCode OPTIONAL
  )

运行方式如下:

    Temp = NULL;
    Size = 0;
    ASSERT ((Temp == NULL && Size == 0) || (Temp != NULL));
    StrnCatGrow (&Temp, &Size, L"Shell.efi -exit ", 0);
    StrnCatGrow (&Temp, &Size, CommandLine, 0);

    Status = InternalShellExecuteDevicePath (
               ParentImageHandle,
               DevPath,
               Temp,
               (CONST CHAR16 **)Environment,
               StatusCode
               );

    Status = InternalShellExecute (
               (CONST CHAR16 *)CommandLine,
               (CONST CHAR16 **)Environment,
               StatusCode
               );

从代码上看,出现问题的原因是:默认情况下(允许 嵌套/NEST),ShellExecute()运行的 MAP 命令会通过 “shell.efi map -r ”的方式运行,这样相当于重新启动了一个 Shell ,在新启动的 Shell 中执行了 map -r,运行完成后会返回调用者处于的 shell 中,之前的 map 加载出现的 Fsx:盘符失效,所以无法看到新添加的 fsx:。

解决方法:通过 shell.efi -nonest 参数启动 Shell ,这个 Shell 禁止了 Nest ,再次运行 mrd3 调用的 map -r 就是对于当前shell。

完整代码下载:

编译后的 EFI Application 下载:

“又不是不能用”逻辑引发的问题

最近碰到了一个困扰我很久的奇怪问题问题,最终研究发现这应该算是设计上“又不是不能用”引发的悲剧。

简单的说简单的说问题是这样的,我需要在 DSC 文件中修改一个PCD 定义:问题是这样的,我需要在 DSC 文件中修改一个PCD 定义:

gChipsetPkgTokenSpaceGuid.PcdLABZBuild|FALSE

于是将代码修改为:

#LABZDebug gChipsetPkgTokenSpaceGuid.PcdLABZBuild|FALSE
gChipsetPkgTokenSpaceGuid.PcdLABZBuild|TRUE

实际测试下来问题依旧存在,检查 Build 目录生成的中间代码最终确定是上述的修改没有起效。最终翻来覆去看代码,终于发现批处理使用如下代码来识别赋值:

findstr /C:"gChipsetPkgTokenSpaceGuid.PcdLABZBuild|FALSE" %WORKSPACE%\%PROJECT_REL_PATH%\%PROJECT_PKG%\Project.dsc >nul

因为被注释掉的代码在文件位置靠前,这个语句会把它当作正式的设定,所以导致了问题。

通常来说,本科计算机系毕业生足以编写能够实现 Parser 的代码,但是如果仅仅以“又不是不能用”作为标准来编写代码将会给使用者带来极大的麻烦。当然,如果使用者仍然遵循“又不是不能用”的逻辑来处理,还可以将代码写为如下形式,只是不知道下一个接手者是否会再撞上奇怪的问题:

gChipsetPkgTokenSpaceGuid.PcdLABZBuild|TRUE
#LABZDebug gChipsetPkgTokenSpaceGuid.PcdLABZBuild|FALSE

Step to UEFI (273打包为 EFI Application 的 MEMTEST86

前面介绍了最新的 MemTest86 ,美中不足的是这个版本需要制作启动盘,这次介绍一种将它打包为一个 EFI 的方法。

基本的思路是:将完整的 MemTest86 磁盘镜像按照资源打包到一个 EFI 文件中,然后再配合之前的RamDisk 知识将这个镜像加载到内存中。这样就相当于制作的镜像文件,跳进去就可以执行了。

代码要点:

1.将之前介绍的 MemTest86 制作成一个硬件镜像,然后将这个镜像命令为MemTest2023.png

2.MyRamDisk2.inf 中给出用到的文件如下

[Sources]
  MyRamDisk2.c
  MyRamDisk2.idf
  MemTest2023.png

3. MyRamDisk2.idf 文件内容如下

#image IMG_LOGO MemTest2023.png

4.主程序

a.首先找到当前 EFI 中的资源

//
        // Retrieve HII package list from ImageHandle
        //
        Status = gBS->OpenProtocol (
                        gImageHandle,
                        &gEfiHiiPackageListProtocolGuid,
                        (VOID **) &PackageListHeader,
                        gImageHandle,
                        NULL,
                        EFI_OPEN_PROTOCOL_GET_PROTOCOL
                        );
        if (EFI_ERROR (Status)) {
          Print(L"HII Image Package with logo not found in PE/COFF resource section\n");
          return Status;
        }

b.取得资源

//Step2. Parser HII Image
        ImageHeader=(EFI_HII_IMAGE_PACKAGE_HDR*)(PackageListHeader+1);
		ImageData=(UINT8 *)(ImageHeader+1);

c.解析资源之后拷贝到分配的内存中

	// Look for Ram Disk Protocol
        Status = gBS->LocateProtocol (
                        &gEfiRamDiskProtocolGuid,
                        NULL,
                        &MyRamDisk
                 );
        if (EFI_ERROR (Status)) {
            Print(L"Couldn't find RamDiskProtocol\n");
            return EFI_ALREADY_STARTED;
        }
					
        //Allocate a memory for Image
        Status = gBS->AllocatePool (
                    EfiReservedMemoryType,
                    (UINTN)FileSize,
                    (VOID**)&StartingAddr
                    ); 
        if(EFI_ERROR (Status)) {
                Print(L"Allocate Memory failed!\n");
                return EFI_SUCCESS;
        } 
		
		CopyMem(StartingAddr,&ImageData[5],FileSize);
        
        //
        // Register the newly created RAM disk.
        //
        Status = MyRamDisk->Register (
             ((UINT64)(UINTN) StartingAddr),
             FileSize,
             &gEfiVirtualDiskGuid,
             NULL,
             &DevicePath
             );
        if (EFI_ERROR (Status)) {
                Print(L"Can't create RAM Disk!\n");
                return EFI_SUCCESS;
        }

上述代码编译运行之后,会在当前系统中生成一个新的盘符,进入这个盘符之后就可以运行 MemTest86 进行内存测试了。

文本完整代码如下:

生成的 EFI 如下:

Step2FPGA(1) 环境的搭建

一直有学习 FPGA的想法,这次下定决心花时间来学习FPGA。因为 FPGA 相关知识能够帮助更好的理解硬件知识,同时可以使用FPGA来实现验证自己的想法。我选择的开发板和教材是 “至芯携手特权同学Altera Cyclone IV EP4CE6 FPGA开发板NIOSII”:

勇敢的心 伴你玩转 Altera FPGA 书籍和开发板

教材是《勇敢的芯伴你玩转 Altera FPGA》《例说FPGA》《FPGA设计 实战演练(逻辑篇)》,作者都是吴厚航先生(特权同学)。

勇敢的心 伴你玩转 Altera FPGA 和其余两本内容上有所重复

这套书籍和套件是我在 2018年购买的,但是一直没有坚持下去。和学习单片机一样,最大的敌人并不是内容的难度,而是自己是否能够不断坚持学习。因此,这次开始新的系列。

教材上使用的软件比较老(Quartus 13),在新的操作系统上有问题必须升级,这次选择 Quartus 18 的版本。安装文件是QuartusLiteSetup-18.1.0.625-windows.exe

Quartus 18 安装界面

 推荐使用默认路径进行安装,一路Next即可

安装占用大约7G 的空间。

安装选项,推荐默认选项即可

点击 “Finish” 按钮后会自动继续安装 USB-Blaster(下载器):

下载器安装,我购买开发板也选购了配套的下载器

接下来可以从 Windows 菜单中启动 Quartus:

安装后从 Start Menu 上启动

启动后会提示目前没有安装任何 Devices ,需要安装Device Package(这个类似于 Keil 安装好之后,需要安装某一个型号的单片机的支持文件;又好比在 Arduino IDE 上使用 ESP32 需要先从 Board Manger上安装 ESP32支持包):

提示需要安装 Device

关闭上面的软件再回到 Start Menu 选择“Device Installer”

Start menu 启动 “Device Installer”

在这个页面选择存放着 Devices 支持文件的路径(文件后缀是 .qdz)

接下来需要选择要安装的功能如下:

  1. Cyclone IV (其中有开发板的FPGA型号)
  2.  ModelSim (模拟仿真工具,Starter Edition 是免费的,下面的是需要额外购买的收费版本)
这个两个功能会占用硬盘 4.5G 的空间。

再次启动 Quartus,选择 File->Open  Project 打开开发板例子 cy4ex2 项目 cy4.qpf 文件。项目中的各种设置都已经准备好了,使用 Processing -> Start Compilation 即可直接编译:

开始编译
编译成功

接下来使用 “Programmer” 功能将编译后的结果下载到开发板中:

选择这里的 programmer
第一次使用需要用 Setup进行设置
在弹出的界面下拉菜单选择 USB-Blaster
点击 Start 即可下载,右上角会提示当前进度

特别注意,烧写时需要给开发板上电。成功之后,板子上的蜂鸣器一直会发出Beep声。

至此,已经踏出了第一步开始了 FPGA之旅。

实现 Ch567 USB0 串口

上次我们在 CH567 的 USB1 上实现了 USB CDC 的功能,这一次尝试在 USB0上实现同样的同能。相比之前的程序,需要修改的位置有:

  1. \src\sys\CH56X_irq.c 中使用USB0DevIntDeal() 响应 USB 0 的中断
__attribute__( ( interrupt ( "id="XSTR(INT_ID_SATA) ) ) )void SATA_Handler(void)
{
	USB0DevIntDeal( );
}

2. \src\main\main.c 中打开 USB0 的中断

	Interrupt_init( 1<<INT_ID_USB0 );     /* 系统总中断开启 */

	USB0DeviceInit();			/* USB0Device Init */
	printf("USB0 Device Init!\n");

        while(1)
        {
                printf("Run\n");
                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;
                        while (R8_USB0_MIS_ST&bUMS_SIE_FREE==0) {}
                }
        };

3. ch56x_usb0dev372.h 中全部 USB1 替换为 USB0

4. ch56x_usb0dev372.c 中全部 USB1 替换为 USB0

CopperCube 配合 FireBeetle 改变球体颜色

这次使用 CopperCube 制作2个球体,然后可以通过 FireBeetle 控制这两个球体的颜色。
1.创建一个新的场景,删除场景中自带的立方体,然后创建一个球体(Sphere)

2.新建的球体是自带贴图的,这个贴图来自前面立方

3.选中球体,在Textures 中选择第一个空贴图,然后在属性的 Materials 中点击更换贴图

4.之后球体上面的贴图就为空了

5.为了便于观察,我们给图赋予一个颜色,选中物体后右键,在弹出菜单中 选择 “Modify Selection”->”Set vertex Colors”。 在弹出的调色板上选择你喜欢的颜色

6.球体变成了红色,选中球体后再使用右键调出菜单进行: clone

7.现在场景中有2个红色球体了,为了便于观察,改成动态光照,在Materials 中选择 Dynamic

8.在场景中创景一个光源

9.让光源动起来,具体方法在上次的文章中介绍过

10.之后保存场景为FBTest.ccb文件

11.编写一个响应键盘的JavaScripe 文档,当收到不同的按键时,改变球体的颜色。文件命名为 FBTest.js 放到和上面 FBTest.ccb 同一个目录下

// register key events
ccbRegisterKeyDownEvent("keyPressedDown");

function keyPressedDown(keyCode)
{
	//z
	if (keyCode == 90)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh1");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x00ff0000);
		} 
	}

	//x
	if (keyCode == 88)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh1");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x0000FF00);
		} 

	}


	//c
	if (keyCode == 67)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh2");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x0000FF00);
		} 
	}


	//v
	if (keyCode == 86)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh2");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x000000FF);
		} 

	}
	
	print(keyCode );
}
  1. 编写FireBeetle 代码,我们需要使用 FireBeetle 的蓝牙功能,将其模拟为一个蓝牙键盘,当有不同按键按下后,发送按键信息。这样当CopperCube 生成的 EXE 收到后,会改变颜色
  2. 编译之后就能看到最终的结果了。
/**
   This example turns the ESP32 into a Bluetooth LE keyboard that writes the words, presses Enter, presses a media key and then Ctrl+Alt+Delete
*/
#include <BleKeyboard.h>
#define PINA 12
#define PINB  4
#define PINC 16
#define PIND 17

BleKeyboard bleKeyboard;

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE work!");
  pinMode(PINA, INPUT_PULLUP);
  pinMode(PINB, INPUT_PULLUP);
  pinMode(PINC, INPUT_PULLUP);
  pinMode(PIND, INPUT_PULLUP);
  bleKeyboard.begin();
}

void loop() {
  if (bleKeyboard.isConnected()) {
    if (digitalRead(PINA) == LOW) {
      Serial.println("Sending 'z'");
      bleKeyboard.print("z");
      delay(200);
    }
    if (digitalRead(PINB) == LOW) {
      Serial.println("Sending 'x'");
      bleKeyboard.print("x");
      delay(200);
    }

    if (digitalRead(PINC) == LOW) {
      Serial.println("Sending 'c'");
      bleKeyboard.print("c");
      delay(200);
    }
    if (digitalRead(PIND) == LOW) {
      Serial.println("Sending 'v'");
      bleKeyboard.print("v");
      delay(200);
    }
  }
}

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