Ch9350 控制键盘 LED

CH9350 提供了控制键盘LED(就是 Caps Lock、Scroll Lock、 Num Lock) 的方法。不过非常遗憾的是对应的 DataSheet 语言不详,查阅了网上资料【参考1】【参考2】之后我感觉CH9350 可能是不断升级,所以这部分不是很确定。

最终经过实验,我手上的可以通过对CH9350 发送如下11 Bytes 长度的命令来实现:

0x57, 0xAB, 0x12, 0x00,  0x00, 0x00, 0x00, 0x00,  0x00, 0xAC, 0x20

修改其中的 Byte[7] ,可以实现更改Led的目标。对应的,在 DataSheet有如下介绍:

编写一个测试程序,运行在 ESP32 C3 上:

#include <Arduino.h>

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200, SERIAL_8N1, RX, TX);
}
char LEDbuffer[11] =
{ 0x57, 0xAB, 0x12, 0x00,
  0x00, 0x00, 0x00, 0x00,
  0x00, 0xAC, 0x20
};

byte Index = 0;

void loop() {
  // 预留调试使用
  while (Serial.available()) {
    char c = Serial.read();
    // 简单返回
    if (c == '1') {
      Serial.printf("RX:%d TX:%d\n", RX, TX);
      Serial.println("Test Message");
    }
    //重启
    if (c == '2') {
      ESP.restart();
    }
  }
  LEDbuffer[7]=Index;
  Serial1.write(LEDbuffer, sizeof(LEDbuffer));
  Index++;
  if (Index == 8) {
    Index = 0;
  }
  delay(1000);
}

工作的测试视频在下面的链接可以看到:

https://www.bilibili.com/video/BV1K94y1r731/?share_source=copy_web&vd_source=5ca375392c3dd819bfc37d4672cb6d54

有兴趣的朋友可以试试。

参考:

  1. https://www.wch.cn/bbs/thread-69476-1.html “CH9350 如何設置鍵盤的NumLock/CapsLock/ScrollLock” WCH 官方论坛
  2. https://www.cnblogs.com/gooutlook/p/16805870.html 单片机读取键鼠数据串口

ESP32 IDF SDMMC 测试

官方的 sd_card_example_main.c 代码,在末尾添加如下代码:

	int64_t StartTime=esp_timer_get_time()/1000;
	
	char buffer[64];
	int  filesize,total=0,PicIndex=0;
	FILE *fd = NULL;
	
	for (PicIndex=0;PicIndex<100;PicIndex++) {
		sprintf(buffer,MOUNT_POINT"/m/%04d.jpg",PicIndex);	
		fd = fopen(buffer, "r");
		fseek(fd, 0, SEEK_END);  
		filesize = ftell(fd);  
		rewind(fd);
		fread(Buffer, 1, filesize, fd);
		fclose(fd);
		total=total+filesize;
	}
	ESP_LOGI(TAG, "Read %dKB in %llums",
				total/1024,
				(esp_timer_get_time()/1000-StartTime));

        StartTime=esp_timer_get_time()/1000;
	for (size_t i=0;i<100;i++) {
		sdmmc_read_sectors(card, Buffer, i*128, 128);
	}
	ESP_LOGI(TAG, "Read %dKB in %llums",
				64*100,
				(esp_timer_get_time()/1000-StartTime));

一段是从 m 目录下读取从 0000.jpg 到 0100.jpg ;另外一段是读取从0扇区开始的 100个64KB 扇区。最终运行结果如下:

前者花了 1789ms 读取 1.5MB 的内容;后者711ms能够读取 6.4MB 左右的内容。从这里可以看出文件系统的开销比较大。

几个常见的 ACPI Debug 相关 Table

HEST: Hardware Error Source Table

用于报告系统平台能够产生的错误来源。比如,对于 X86 架构的 MCE和CMC,以及PCIe AER, OS 能够处理的 MSI 和 PCI INTs 错误。

The HEST table enables host firmware to declare all errors that platform component can generate and error signaling for those. The host firmware shall create Error source entries in HEST for each component (such as, processor, PCIe device, PCIe bridge, etc) and each type of error with corresponding error notification mechanism (singling) to OS. These error entries include x86 architectural errors, industry standard errors and generic hardware error source for platform errors. The x86 architectural errors, MCE and CMC, and standard errors PCIe AER, MSI and PCI INTx can be handled by OS natively. The generic hardware error source can be used for all firmware 1st errors and platform errors (such as memory, board logic) that do not have OS native signaling, so they have to use platform signaling SCI or NMI


ERST:Error Record Serialization table

通过这个 Table 给 OS 提供一个能够存放错误的接口,通过这个接口,OS可以将一些信息存放在不受断电影响的存储器上,比如: NVRAM。

The ERST table provides a generic interface for the OS to store and retrieve error records in the platform persistent storage, such as NVRAM (Non-volatile RAM). The error records stored through this interface shall persist across system resets till OS clears it. The OS will use this interface store error information during critical error handling for later extensive error analysis. Host firmware shall provide serialization instruction using ACPI specification defined actions to facilitate read, write and clear error records


EINJ:Error Injection Table
通过这个 Table OS 可以给错误处理函数增加限定条件(具体使用场景我没有碰到过,对此不理解,有知道的朋友可以在评论指教一下)。

One of the important functions required in implementing the error is the ability to inject error conditions by the OS to evaluate the correct functionality of the entire error handling in the platform hardware, host firmware and the OS. The EINJ table interface facilitates error injection from the OS. The host firmware shall provide at minimum one error injection method per error type supported in the platform. The host firmware will provide the generic error serialization instructions to trigger the error in the hardware


BERT:Boot Error Record Table

记录启动时的错误信息并报告给OS,对于我们平常使用的X86来说一般是放在内存中的。

The BERT table provides the interface to report errors that occurred system boot time. In
addition BERT also can be used to report fatal errors that resulted in surprise system reset or shutdown before OS had the chance to see the error. The host firmware shall build this table with error record entries for each error that occurred

上述介绍来自下面这个文档

Step to UEFI (279)介绍一个最小的UEFI Application 编译器

最近接触到了 TinyCC (https://bellard.org/tcc/), 这是一个小巧的、开源、编译速度快的C编译器。

https://github.com/andreiw/tinycc/tree/mob 这里,有一个基于 TinyCC 支持编译UEFI Application的项目。这里介绍如何使用这个编译器编译生成 UEFI Shell Application。

第一步,下载上述代码。

第二步,生成 TinyCC  UEFI 编译器。下载的 Package 中只有源代码,没有二进制的 EXE ,所以需要先进行编译。我这边使用 VS2019 进行编译,进入源代码 Win32 目录下:

特别注意需要对 build-tcc.bat进行修改,其中的-DONE_SOURCE=0需要使用引号:

%CC% -o tcc.exe ..\tcc.c libtcc.dll %D% “-DONE_SOURCE=0”

运行 build-tcc.bat -c cl -t 64_ 命令:

生成的x86_64-win32-tcc.exe 就是我们需要的编译器:

第三步,在 EDK2 中编译生成一个 EFI Application。

  1. 拷贝 x86_64-win32-tcc.exe 到edk2-stable202308根目录下
  2. 将如下代码命名为 Hello.c
#include <Uefi.h>

CHAR16 *gHello = L"Hello from a TinyCC compiled UEFI binary!\r\n";

EFI_STATUS EFIAPI
_start(EFI_HANDLE Handle,
       EFI_SYSTEM_TABLE *SystemTable)
{
  SystemTable->ConOut->OutputString(SystemTable->ConOut, gHello);
  return EFI_SUCCESS;
}

3.直接打开一个 cmd 窗口(不需要 Vs2019)

4.编译命令如下

x86_64-win32-tcc -I MdePkg/Include -I MdePkg/Include/X64  hello.c  -Wl,-subsystem=efiapp -nostdlib -o efitest.x64.efi -DMDE_CPU_EBC

5.编译后直接生成 efitest.x64.efi

第四步,在模拟器中测试生成的 EFI 文件,可以看到能够正常工作。

本文提到的完整代码可以在下面下载:

10月 nVidia 工作机会

GPU Server- Firmware 应用工程师(上海/北京/深圳)

NVIDIA GPU Application Engineering 是为 NVIDIA 全球客户提供技术支持的部门。我们的主要任务是向我们的客户提供 NVIDIA GPU 技术信息,培训我们的客户设计 NVIDIA GPU 相关产品,并协助我们的客户成功地将产品推向市场。我们正在寻找软件应用工程师加入我们的应用工程团队!您将负责 GPU 软件设计支持和使用工具进行问题分析解决。

[工作内容]这是您即将从事的工作:

• 为客户提供 NVIDIA GPU 软件设计技术支持。

• 为客户在 GPU 的软件功能实现和调试提供支持。

• 在技术层面与客户硬件和软件工程师进行交流,以提供设计协助和解决问题。

• 协助內部测试部門复现客戶的问题并进行相关测试和验证解决方案。

• 与客户和总部进行沟通与协同工作,为客户的问题提供解决方案

[职位要求]• 电机工程,电子工程,计算机科学相关领域, 学士学位或以上学历。

• x86 系统 BIOS 或 GPU VBIOS/驱动程序经验。

• 有 x86 计算机系统结构, C, C++的背景。

• 良好的英文听说读写能力。• 有 BIOS/firmware 开发的经验

JR1967728 GPU Enterprise Firmware Application Engineer – 深圳/北京/上海

同样的还有Server 系统硬件设计应用工程师、数据中心 Server 硬件应用工程师 等等职位,有兴趣的朋友可以直接扫描如下链接进行查看:

DFRobot ESP32C3 USB HID Shield

这次带来的作品是 DFRobot Beetle ESP32-C3的扩展版,通过这个扩展版能够让 Beetle ESP32-C3 取得 USB 键盘鼠标这种 HID 设备的输入数据。

这个扩展版的核心是 WCH 出品的CH9350芯片,CH9350是USB键盘鼠标转串口通讯控制芯片。就是说USB 键盘鼠标连接到这个芯片之后,数据会转化为串口输出。关于这个芯片的功能介绍如下:

  • 支持12Mbps全速USB传输和1.5Mbps低速USB传输,兼容USB V2.0。
  • 上位机端USB端口符合标准HID类协议,不需要额外安装驱动程序,支持内置HID类设备驱动的Windows、Linux、macOS等操作系统。
  • 同一芯片可配置为上位机模式和下位机模式,分别连接USB-Host主机和USB键盘、鼠标。
  • 支持USB键盘鼠标在BIOS界面使用,支持多媒体功能键,支持不同分辨率USB鼠标。
  • 支持各种品牌的USB键盘鼠标、USB无线键盘鼠标、USB转PS2线等。
  • 上位机端和下位机端支持热插拔。
  • 提供发送状态引脚,支持485通讯。
  • 串口支持115200/57600/38400串口通信波特率。
  • 内置晶振和上电复位电路,外围电路简单。
  • 支持5V、3.3V电源电压。
  • 提供LQFP-48无铅封装,兼容RoHS。

电路图设计如下:

从DataSheet可以知道,芯片支持 3.3V h和5V供电,为了方便电路设计这里我们直接使用5V供电。PCB 设计如下:

3D渲染结果如下:

黑色PCB 风格非常接近DFRobot

为了Beetle 配合板子只使用了一个 USB 母头,实际芯片同时支持2个USB接口,让客户可以同时使用键盘和鼠标。

下面带来一个USB键盘转蓝牙例子,展示这个板子的能力。

从原理上来说,首先使用 CH9350取得鼠标数据,之后通过 Beetle ESP32-C3 的蓝牙功能将这个数据发送出去。

#include <Arduino.h>
#include <BleKeyboard.h>

BleKeyboard bleKeyboard;

#define DEBUGMODE 1

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200, SERIAL_8N1, RX, TX);

  bleKeyboard.begin();

}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '1') {
      Serial.println("get1");
    }
    if (c == '3') {
      ESP.restart();
    }
  }

  //根据 CH9350 Spec 每次最多输出 72Bytes
  byte Data[72];
  unsigned int CounterLast = Serial1.available();
  unsigned int CounterCurrent = 0;


  // 如果当前串口有数据
  if (CounterLast != 0) {
    // 进行简单测试,如果当前还在传输数据那么持续接收
    while (CounterCurrent != CounterLast) {
      CounterLast = Serial1.available();
      delayMicroseconds(500);
      CounterCurrent = Serial1.available();
    }
  }

  if (CounterCurrent > 0) {
    // 一次性将数据收取下来
    Serial1.readBytes(Data, CounterCurrent);
    unsigned int i = 0;
    unsigned int Length;
    while (i < CounterCurrent) {
      // 识别帧头
      if ((Data[i] == 0x57) && (Data[i + 1] == 0xAB)) {
        // 有效键值帧
        if (Data[i + 2] == 0x88) {
          // 获得数据长度
          Length = Data[i + 3];
          if (DEBUGMODE) {
            //Serial.print("Ln:");Serial.print(Length);
            for (int j = 1; j < Length + 1; j++) {
              if (Data[i + 3  + j] < 16) {
                Serial.print("0");
              }
              Serial.print(Data[i + 3  + j], HEX);
              Serial.print(" ");
            }
            Serial.println(" ");
          }

          //如果是键盘
          if (Data[i + 4] == 0x10) {
            if (DEBUGMODE) {
              Serial.print("Key");
              for (int j = 1; j < Length + 1; j++) {
                Serial.print(Data[i + 3  + j], HEX);
                Serial.print(" ");
              }
              Serial.println(" ");
            }
            
            //判断为Dostyle键盘
            if (Data[i + 3  + 1] == 0x10) { 
              if (bleKeyboard.isConnected() == true) {
                bleKeyboard.sendReport((KeyReport*)(&Data[i + 3  + 2]));
              }
            }
          }
          i = i + 3 + Length;
        } else if (Data[i + 2] == 0x82) {
          i = i + 3; // 跳过
        }
      }
      i++;
    } // while (i < Counter)
  }

}

首先,调用了 ESP32 的 BLE 键盘库(第三方),创建一个蓝牙键盘;之后就是分析串口数据。CH9350 能够获得USB 键盘鼠标数据,但是获得这个数据每家会有差别。正确的做法是使用工具(USBlyzer)读取键盘鼠标数据,然后编写分析代码。或者是使用工具读取这个设备的 HID Report Descriptor。再进一步解释就是,例如:市面上有2中鼠标,他们发送出来的数据格式可能是:

A鼠标:

Byte0:  Button

Byte1: X

Byte2: Y

Byte3: Wheel

B鼠标:

Byte0:  Button

Byte1: X 低8位

Byte2: X 高8位

Byte3: X 低8位

Byte4: X 高8位

Byte5: Wheel

具体的格式数据是设备通过HID Report Descriptor报告给系统的,而对于我们来说只能通过人工识别然后在代码中分析的方式来实现解析。相比USB鼠标,键盘的情况要好得多,通常都是使用8 Bytes 来报告按键信息。我这次测试使用的是一款机械键盘,使用的同样是 8 Bytes的格式,因此代码中直接将对应的数据以KeyReport结构体直接发送出去。

if (bleKeyboard.isConnected() == true) {
                bleKeyboard.sendReport((KeyReport*)(&Data[i + 3  + 2]));
 }

本文提到的电路图和PCB:

本文提到的完整代码:

本文使用的库:

EDK2 Stable202308来了

上个月edk2 202308 正式发布在:

https://github.com/tianocore/edk2/releases/tag/edk2-stable202308

从 History 来看,改动并不大:

和之前类似,这里放上一个完整版,补全了所有的三方库,大小是107MB 左右。

https://pan.baidu.com/s/1rQf19nHbpxDdwB5DJkVi0w?pwd=LABZ

提取码: LABZ

此外,为了方便初学者,这里提供一个配置好的 Win10+VS2019 EDK2 环境,导入即可上手:

https://pan.baidu.com/s/1B9aFEcRur8xY4g1X6Fdgcg?pwd=labz

提取码: labz

ESP32 C3 双USB 手柄转蓝牙

这次带来的项目是一个能够将两个USB 手柄转为蓝牙手柄的项目,这样玩家可以不受距离的限制使用手柄进行游戏。

项目基于 ESP32 C3 作为主控,使用 Max3421e 芯片作为 USB Host ,经过 WCH 的CH334 USB HUB 芯片扩展出2个USB 接口,这样就能同时连接2个USB手柄(CH334 支持一转四,因此最多同时可以连接4个USB手柄)。获得数据之后, C3 将自身模拟为USB 手柄设备,将按键数据通过蓝牙上传给主机,这样就实现了将两个USB 手柄转为蓝牙手柄。

下面首先介绍硬件设计。

完整电路图如下:

电路1 是 ESP32 C3 模块。ESP32-C3是一款安全稳定、低功耗、低成本的物联网芯片,搭载 RISC-V 32 位单核处理器,时钟频率高达 160 MHz。具有 22 个可编程 GPIO 管脚、内置 400 KB SRA、支持 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE)。从图中也可以看到其所需外围元件很少,方便应用;

电路2是我们之前设计的 Super Micro USB Host,它的核心是 Max3421e 芯片,这个方案我们多次使用,是非常优秀的USB Host 方案;

电路3 是 USB Hub 部分,这里使用了CH334芯片.这款芯片符合 USB2.0 协议规范的4 端口 USB HUB 控制器芯片,上行端口支持 USB2.0高速和全速,下行端口支持 USB2.0 高速480Mbps、全速 12Mbps 和低速1.5Mbps。不但支持低成本的的 STT 模式(单个TT分时调度4个下行端口),还支持高性能的 MTT 模式 (4个TT各对应1个端口,并发处理)。需要注意的是,在此之前我实验过价格低廉的 FE1.2 芯片,但是FE1.2对 Max3421e 存在兼容性问题,无法正常工作。最终选择了CH334这个。如果有需要对 Max3421e 扩展USB 来使用的朋友,请注意USB Hub 这个坑。

上面就是这个设计最主要的三个部分,其他的都是辅助部分。

PCB 设计如下:

3D 预览如下:

焊接安装之后如下:

硬件完成后就要开始着手软件的设计了。

中所周知,USB HID的一个特点是:无需驱动即可使用。实现这个功能的秘密在于:在开始工作后设备会对主机发送一个HID Report Descriptor。在这个 Descriptor中描述了数据格式。这样主机收到之后能够清晰的了解收到的数据包中数据的含义,比如:Report Descriptor 内容是第一个字节是按键信息,第二个字节表示 X 坐标,第三个字节表示Y坐标。这样,当主机收到 01 00 00 ,主机知道有按键按下;当主机收到 00 0A 00 ,它能够知道鼠标向右移动了10 像素。蓝牙HID 也有类似的设计,其中的Report Descriptor 在蓝牙上成为Report Map 。其中的条目定义和 USB HID 的完全相同。因此,这里我们通过很小的改动就能将USB 手柄迁移到蓝牙上,他们使用相同的 Report Descriptor也能够避免数据解析的麻烦。仍然以前面的鼠标为例,当蓝牙设备使用和USB 设备相同的Report Descriptor时,主机对于 00 0A 00 这种数据,都能解析为鼠标向右移动了10 像素。

下面是我使用的 USB 手柄的 Report Descriptor:

Interface 0 HID Report Descriptor Joystick
Item Tag (Value)	Raw Data
Usage Page (Generic Desktop)	05 01 
Usage (Joystick)	09 04 
Collection (Application)	A1 01 
    Report ID (1)	85 01 
    Collection (Logical)	A1 02 
        Report Size (8)	75 08 
        Report Count (4)	95 04 
        Logical Minimum (0)	15 00 
        Logical Maximum (255)	26 FF 00 
        Physical Minimum (0)	35 00 
        Physical Maximum (255)	46 FF 00 
        Usage (Rz)	09 35 
        Usage (Z)	09 32 
        Usage (X)	09 30 
        Usage (Y)	09 31 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)	81 02 
        Report Size (4)	75 04 
        Report Count (1)	95 01 
        Logical Maximum (7)	25 07 
        Physical Maximum (315)	46 3B 01 
        Unit (Eng Rot: Degree)	65 14 
        Usage (Hat Switch)	09 39 
        Input (Data,Var,Abs,NWrp,Lin,Pref,Null,Bit)	81 42 
        Unit (None)	65 00 
        Report Size (1)	75 01 
        Report Count (12)	95 0C 
        Logical Maximum (1)	25 01 
        Physical Maximum (1)	45 01 
        Usage Page (Button)	05 09 
        Usage Minimum (Button 1)	19 01 
        Usage Maximum (Button 12)	29 0C 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)	81 02 
        Usage Page (Vendor-Defined 1)	06 00 FF 
        Report Size (1)	75 01 
        Report Count (8)	95 08 
        Logical Maximum (1)	25 01 
        Physical Maximum (1)	45 01 
        Usage (Vendor-Defined 1)	09 01 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)	81 02 
    End Collection	C0 
    Collection (Logical)	A1 02 
        Report Size (8)	75 08 
        Report Count (4)	95 04 
        Physical Maximum (255)	46 FF 00 
        Logical Maximum (255)	26 FF 00 
        Usage (Vendor-Defined 2)	09 02 
        Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)	91 02 
    End Collection	C0 
End Collection	C0 
Usage Page (Generic Desktop)	05 01 
Usage (Joystick)	09 04 
Collection (Application)	A1 01 
    Report ID (2)	85 02 
    Collection (Logical)	A1 02 
        Report Size (8)	75 08 
        Report Count (4)	95 04 
        Logical Minimum (0)	15 00 
        Logical Maximum (255)	26 FF 00 
        Physical Minimum (0)	35 00 
        Physical Maximum (255)	46 FF 00 
        Usage (Rz)	09 35 
        Usage (Z)	09 32 
        Usage (X)	09 30 
        Usage (Y)	09 31 
        Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)	81 02 
        Report Size (4)	75 04 
        Report Count (1)	95 01 
        Logical Maximum (7)	25 07 
        Physical Maximum (315)	46 3B 01 
        Unit (Eng Rot: Degree)	65 14 
        Usage (Hat Switch)	09 39 
        Input (Data,Var,Abs,NWrp,Lin,Pref,Null,Bit)	81 42 
        Unit (None)	65 00 
        Report Size (1)	75 01 
        Report Count (12)	95 0C 
        Logical Maximum (1)	25 01 
        Physical Maximum (1)	45 01 
        Usage Page (Button)	05 09 
        Usage Minimum (Button 1)	19 01 

上面红色标记出来的是这次使用的USB手柄的有效部分。除了红色还有黑色的字体,从实验来看,在手柄时没有使用。我们将有效部分取出,合成一个蓝牙使用的Report Map如下:

  // 1st Gamepad
  0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  0x09, 0x04,        // Usage (Joystick)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x01,        //   Report ID (1)
  0xA1, 0x02,        //   Collection (Logical)
  0x75, 0x08,        //     Report Size (8)
  0x95, 0x04,        //     Report Count (4)
  0x15, 0x00,        //     Logical Minimum (0)
  0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  0x35, 0x00,        //     Physical Minimum (0)
  0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  0x09, 0x35,        //     Usage (Rz)
  0x09, 0x32,        //     Usage (Z)
  0x09, 0x30,        //     Usage (X)
  0x09, 0x31,        //     Usage (Y)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x75, 0x04,        //     Report Size (4)
  0x95, 0x01,        //     Report Count (1)
  0x25, 0x07,        //     Logical Maximum (7)
  0x46, 0x3B, 0x01,  //     Physical Maximum (315)
  0x65, 0x14,        //     Unit (System: English Rotation, Length: Centimeter)
  0x09, 0x39,        //     Usage (Hat switch)
  0x81, 0x42,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)
  0x65, 0x00,        //     Unit (None)
  0x75, 0x01,        //     Report Size (1)
  0x95, 0x0C,        //     Report Count (12)
  0x25, 0x01,        //     Logical Maximum (1)
  0x45, 0x01,        //     Physical Maximum (1)
  0x05, 0x09,        //     Usage Page (Button)
  0x19, 0x01,        //     Usage Minimum (0x01)
  0x29, 0x0C,        //     Usage Maximum (0x0C)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x06, 0x00, 0xFF,  //     Usage Page (Vendor Defined 0xFF00)
  0x75, 0x01,        //     Report Size (1)
  0x95, 0x08,        //     Report Count (8)
  0x25, 0x01,        //     Logical Maximum (1)
  0x45, 0x01,        //     Physical Maximum (1)
  0x09, 0x01,        //     Usage (0x01)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,              //   End Collection
  0xA1, 0x02,        //   Collection (Logical)
  0x75, 0x08,        //     Report Size (8)
  0x95, 0x04,        //     Report Count (4)
  0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  0x09, 0x02,        //     Usage (0x02)
  0x91, 0x02,        //     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  0xC0,              //   End Collection
  0xC0,              // End Collection

  // 2nd Gamepad
  0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  0x09, 0x04,        // Usage (Joystick)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x02,        //   Report ID (2)
  0xA1, 0x02,        //   Collection (Logical)
  0x75, 0x08,        //     Report Size (8)
  0x95, 0x04,        //     Report Count (4)
  0x15, 0x00,        //     Logical Minimum (0)
  0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  0x35, 0x00,        //     Physical Minimum (0)
  0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  0x09, 0x35,        //     Usage (Rz)
  0x09, 0x32,        //     Usage (Z)
  0x09, 0x30,        //     Usage (X)
  0x09, 0x31,        //     Usage (Y)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x75, 0x04,        //     Report Size (4)
  0x95, 0x01,        //     Report Count (1)
  0x25, 0x07,        //     Logical Maximum (7)
  0x46, 0x3B, 0x01,  //     Physical Maximum (315)
  0x65, 0x14,        //     Unit (System: English Rotation, Length: Centimeter)
  0x09, 0x39,        //     Usage (Hat switch)
  0x81, 0x42,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)
  0x65, 0x00,        //     Unit (None)
  0x75, 0x01,        //     Report Size (1)
  0x95, 0x0C,        //     Report Count (12)
  0x25, 0x01,        //     Logical Maximum (1)
  0x45, 0x01,        //     Physical Maximum (1)
  0x05, 0x09,        //     Usage Page (Button)
  0x19, 0x01,        //     Usage Minimum (0x01)
  0x29, 0x0C,        //     Usage Maximum (0x0C)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x06, 0x00, 0xFF,  //     Usage Page (Vendor Defined 0xFF00)
  0x75, 0x01,        //     Report Size (1)
  0x95, 0x08,        //     Report Count (8)
  0x25, 0x01,        //     Logical Maximum (1)
  0x45, 0x01,        //     Physical Maximum (1)
  0x09, 0x01,        //     Usage (0x01)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,              //   End Collection
  0xA1, 0x02,        //   Collection (Logical)
  0x75, 0x08,        //     Report Size (8)
  0x95, 0x04,        //     Report Count (4)
  0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  0x09, 0x02,        //     Usage (0x02)
  0x91, 0x02,        //     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  0xC0,              //   End Collection
  0xC0,              // End Collection

可以看到,上面的 Descriptor 红色部分和其余部分的区别只是 Report ID 不同。 Report ID 是USB HID 设备用来区分功能的一个设计。例如,我们经常遇到同时带有键盘和鼠标的混合设备。通常它的Descriptor 写法就是:

Report ID 1

鼠标数据[0], 鼠标数据[1], ……鼠标数据[M-1],

Report ID 2 

键盘数据[0], 键盘数据[1],……键盘数据[N-1],

当设备上有鼠标动作发生时,鼠标动作描述为 :M 长度的鼠标数据;设备对主机的报告是:

 1, 鼠标数据[0], 鼠标数据[1],…… 鼠标数据[M-1]

当设备上有键盘动作发生时,键盘动作描述为 :N 长度的键盘数据;设备对主机的报告是:

 2,键盘数据[0], 键盘数据[1],…… 键盘数据[N-1]

当然,你还可以继续给这个设备加入功能。比如:Report ID 3 , 键盘数据[0], 键盘数据[1],……键盘数据[P-1]  这样就又加入了一个键盘的功能(实际上这样做是有意义的,比如,通常的USB键盘数据包只有8个按键信息,如果我们同时按下9个按键就会超出它的运载能力。这就是一种“键位冲突”。如果你的设备同时声明了2个键盘,那么可以用将第九个按键的信息放置在第二个键盘的数据中发出去。如果你肯声明三个键盘设备,每个带有8个按键,理论上对于人类是完全够用的)。显而易见,通过 Report ID 主机能够分清楚当收到的数据属于哪个设备。

这次的设计,USB 手柄产生数据如下:

1, 手柄数据[0], 手柄数据[1],…… 手柄数据[M-1]

(第一个1是来自USB 手柄 Descriptor 的 Report ID 1)

我们无需解析了解每一位的含义,去掉最前面的 Report ID 然后将剩余数据转发出去即可。具体代码如下:

  if (DataBuffer[0] == 0) {
    input->setValue(&DataBuffer[2], RPT_GEMEPAD_LEN - 2);
    input->notify();
  }
  if (DataBuffer[0] == 1) {
    input2->setValue(&DataBuffer[2], RPT_GEMEPAD_LEN - 2);
    input2->notify();
  }

主机收到数据之后,能够分清楚来源,然后将数据通过蓝牙转发出去即可。完整代码如下:

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

#include "BLE2902.h"
#include "BLEHIDDevice.h"
#include "HIDTypes.h"

#include <usbhid.h>
#include <hiduniversal.h>
#include <usbhub.h>

#define DEBUGMODE 0

// Satisfy IDE, which only needs to see the include statment in the ino.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

#include "hidjoystickrptparser.h"

USB Usb;
USBHub Hub(&Usb);
HIDUniversal Hid1(&Usb);
HIDUniversal Hid2(&Usb);
JoystickEvents JoyEvents;
JoystickReportParser Joy(&JoyEvents);

BLEHIDDevice*       hid;
BLECharacteristic*  input;

BLECharacteristic*  input2;
BLECharacteristic*  output2;
bool      connected = false;


const uint8_t report[] = {
  // 1st Gamepad
  0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  0x09, 0x04,        // Usage (Joystick)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x01,        //   Report ID (1)
  0xA1, 0x02,        //   Collection (Logical)
  0x75, 0x08,        //     Report Size (8)
  0x95, 0x04,        //     Report Count (4)
  0x15, 0x00,        //     Logical Minimum (0)
  0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  0x35, 0x00,        //     Physical Minimum (0)
  0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  0x09, 0x35,        //     Usage (Rz)
  0x09, 0x32,        //     Usage (Z)
  0x09, 0x30,        //     Usage (X)
  0x09, 0x31,        //     Usage (Y)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x75, 0x04,        //     Report Size (4)
  0x95, 0x01,        //     Report Count (1)
  0x25, 0x07,        //     Logical Maximum (7)
  0x46, 0x3B, 0x01,  //     Physical Maximum (315)
  0x65, 0x14,        //     Unit (System: English Rotation, Length: Centimeter)
  0x09, 0x39,        //     Usage (Hat switch)
  0x81, 0x42,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)
  0x65, 0x00,        //     Unit (None)
  0x75, 0x01,        //     Report Size (1)
  0x95, 0x0C,        //     Report Count (12)
  0x25, 0x01,        //     Logical Maximum (1)
  0x45, 0x01,        //     Physical Maximum (1)
  0x05, 0x09,        //     Usage Page (Button)
  0x19, 0x01,        //     Usage Minimum (0x01)
  0x29, 0x0C,        //     Usage Maximum (0x0C)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x06, 0x00, 0xFF,  //     Usage Page (Vendor Defined 0xFF00)
  0x75, 0x01,        //     Report Size (1)
  0x95, 0x08,        //     Report Count (8)
  0x25, 0x01,        //     Logical Maximum (1)
  0x45, 0x01,        //     Physical Maximum (1)
  0x09, 0x01,        //     Usage (0x01)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,              //   End Collection
  0xA1, 0x02,        //   Collection (Logical)
  0x75, 0x08,        //     Report Size (8)
  0x95, 0x04,        //     Report Count (4)
  0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  0x09, 0x02,        //     Usage (0x02)
  0x91, 0x02,        //     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  0xC0,              //   End Collection
  0xC0,              // End Collection


  // 2nd Gamepad
  0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  0x09, 0x04,        // Usage (Joystick)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x02,        //   Report ID (2)
  0xA1, 0x02,        //   Collection (Logical)
  0x75, 0x08,        //     Report Size (8)
  0x95, 0x04,        //     Report Count (4)
  0x15, 0x00,        //     Logical Minimum (0)
  0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  0x35, 0x00,        //     Physical Minimum (0)
  0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  0x09, 0x35,        //     Usage (Rz)
  0x09, 0x32,        //     Usage (Z)
  0x09, 0x30,        //     Usage (X)
  0x09, 0x31,        //     Usage (Y)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x75, 0x04,        //     Report Size (4)
  0x95, 0x01,        //     Report Count (1)
  0x25, 0x07,        //     Logical Maximum (7)
  0x46, 0x3B, 0x01,  //     Physical Maximum (315)
  0x65, 0x14,        //     Unit (System: English Rotation, Length: Centimeter)
  0x09, 0x39,        //     Usage (Hat switch)
  0x81, 0x42,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)
  0x65, 0x00,        //     Unit (None)
  0x75, 0x01,        //     Report Size (1)
  0x95, 0x0C,        //     Report Count (12)
  0x25, 0x01,        //     Logical Maximum (1)
  0x45, 0x01,        //     Physical Maximum (1)
  0x05, 0x09,        //     Usage Page (Button)
  0x19, 0x01,        //     Usage Minimum (0x01)
  0x29, 0x0C,        //     Usage Maximum (0x0C)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x06, 0x00, 0xFF,  //     Usage Page (Vendor Defined 0xFF00)
  0x75, 0x01,        //     Report Size (1)
  0x95, 0x08,        //     Report Count (8)
  0x25, 0x01,        //     Logical Maximum (1)
  0x45, 0x01,        //     Physical Maximum (1)
  0x09, 0x01,        //     Usage (0x01)
  0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,              //   End Collection
  0xA1, 0x02,        //   Collection (Logical)
  0x75, 0x08,        //     Report Size (8)
  0x95, 0x04,        //     Report Count (4)
  0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  0x09, 0x02,        //     Usage (0x02)
  0x91, 0x02,        //     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  0xC0,              //   End Collection
  0xC0,              // End Collection
};

class ServerCallbacks: public BLEServerCallbacks
{
    void onConnect(BLEServer* pServer)
    {
      connected = true;
      //digitalWrite(CONNECT_LED_PIN, HIGH);
      BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
      desc->setNotifications(true);
    }

    void onDisconnect(BLEServer* pServer)
    {
      connected = false;
      // digitalWrite(CONNECT_LED_PIN, LOW);
      BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
      desc->setNotifications(false);
    }
};

void JoystickEvents::OnGamePadChanged(int Index) {

  if (DEBUGMODE) {
    /*  for (uint8_t i = 0; i < RPT_GEMEPAD_LEN; i++) {
        Serial.print(Joy.oldPad[Index][i]); Serial.print(" ");
      }
      Serial.println(" ");
    */
  }

  uint8_t DataBuffer[RPT_GEMEPAD_LEN];
  memcpy(DataBuffer, &Joy.oldPad[Index][0], RPT_GEMEPAD_LEN);
  DataBuffer[0] = Index;

  // Send message via ESP-NOW
  //esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) DataBuffer, RPT_GEMEPAD_LEN);

  if (DEBUGMODE) {
    Serial.print("BT send");
    for (int i = 0; i < RPT_GEMEPAD_LEN; i++) {
      if (DataBuffer[i] < 0x10) {
        Serial.print("0");
      }
      Serial.print(DataBuffer[i], HEX); Serial.print(" ");
    }
    Serial.println(" ");
  }

  if (DataBuffer[0] == 0) {
    input->setValue(&DataBuffer[2], RPT_GEMEPAD_LEN - 2);
    input->notify();
  }
  if (DataBuffer[0] == 1) {
    input2->setValue(&DataBuffer[2], RPT_GEMEPAD_LEN - 2);
    input2->notify();
  }

}
void setup() {
  if (DEBUGMODE) {
    Serial.begin(1000000);
    printf("Starting BLE Gamepad device\n!");
  }

  BLEDevice::init("ESP32-Gamepad");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());

  hid = new BLEHIDDevice(pServer);
  input = hid->inputReport(1); // <-- input REPORTID 1 from report map

  input2 = hid->inputReport(2); // <-- input REPORTID 2 from report map
  output2 = hid->outputReport(2); // <-- output REPORTID 2 from report map
  //output2->setCallbacks(new OutputCallbacks());

  BLESecurity *pSecurity = new BLESecurity();
  pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);

  std::string name = "DeonVanDerWesthuysen";
  hid->manufacturer()->setValue(name);
  hid->pnp(0x02, 0xADDE, 0xEFBE, 0x0100);    // High and low bytes of words get swapped
  hid->hidInfo(0x00, 0x02);
  hid->reportMap((uint8_t*)report, sizeof(report));
  hid->startServices();

  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->setAppearance(HID_GAMEPAD);
  pAdvertising->addServiceUUID(hid->hidService()->getUUID());
  pAdvertising->start();
  hid->setBatteryLevel(100);

  if (DEBUGMODE) {
    Serial.println("Init complete!\n");
  }

  if (!Hid1.SetReportParser(0, &Joy))
    ErrorMessage<uint8_t > (PSTR("SetReportParser"), 1);
  if (!Hid2.SetReportParser(0, &Joy))
    ErrorMessage<uint8_t > (PSTR("SetReportParser"), 1);

  if (Usb.Init() == -1)
    Serial.println("OSC did not start.");

  delay(200);

}

void loop() {
  Usb.Task();
}

上面就是双USB 转蓝牙的代码,实际上它这个框架可以根据需求改造成各种蓝牙HID 设备,比如:蓝牙鼠标加蓝牙键盘,蓝牙键盘加蓝家键盘,蓝牙手柄加蓝牙键盘。有兴趣的朋友不妨动手尝试。

上述代码完整版:

本文提到的电路图和 PCB:

工作的测试视频: