FireBeetle 系列是DFRobot出品的非常好用的开发板,我经常使用。
ESP32 S3 是 ESP32 系列较新的SoC,同时支持 蓝牙和USB,可玩性很高。非常遗憾的是 FireBeetle没有ESP32 S3 的版本,这次就制作一个FireBeetle 的兼容板,使得能够在上面使用诸如FireBeetle萤火虫OLED 12864显示屏这种扩展版。
接下来就开始电路图设计:
为了实现最大的兼容性,关键点在于排插的定义,最主要的是右侧的 SPI 和 IIC接口的定义:
板子上带有一个USB公头,连接到 IO20和 IO19,这样可以充分发挥出 ESP32 S3 的USB 能力。
设计上仍然使用串口进行代码烧写,对应接口如下,就是说使用时还需要搭配另外一款 CH343 USB转串口板卡使用【参考1】。
PCB 设计如下(可以看到省去串口芯片,这个几乎相当于 ESP32 的最小系统):
实物成品如下(选择了黑色PCB):
编写一个代码,读取 USB键盘的数据,然后显示在 OLED上:
#include <elapsedMillis.h>
#include <usb/usb_host.h>
#include "show_desc.hpp"
#include "usbhhelp.hpp"
#include "DFRobot_OLED12864.h"
const uint8_t I2C_addr = 0x3c;
const uint8_t pin_SPI_cs = 6;
DFRobot_OLED12864 OLED(I2C_addr, pin_SPI_cs);
bool isKeyboard = false;
bool isKeyboardReady = false;
uint8_t KeyboardInterval;
bool isKeyboardPolling = false;
elapsedMillis KeyboardTimer;
String Message = "";
const size_t KEYBOARD_IN_BUFFER_SIZE = 8;
usb_transfer_t *KeyboardIn = NULL;
// 键盘 ScanCode转 ASCII
uint8_t ScanCode2Ascii(uint8_t modifier , uint8_t scancode) {
switch (scancode) {
case 0x04: return 'A';
case 0x05: return 'B';
case 0x06: return 'C';
case 0x07: return 'D';
case 0x08: return 'E';
case 0x09: return 'F';
case 0x0A: return 'G';
case 0x0B: return 'H';
case 0x0C: return 'I';
case 0x0D: return 'J';
case 0x0E: return 'K';
case 0x0F: return 'L';
case 0x10: return 'M';
case 0x11: return 'N';
case 0x12: return 'O';
case 0x13: return 'P';
case 0x14: return 'Q';
case 0x15: return 'R';
case 0x16: return 'S';
case 0x17: return 'T';
case 0x18: return 'U';
case 0x19: return 'V';
case 0x1A: return 'W';
case 0x1B: return 'X';
case 0x1C: return 'Y';
case 0x1D: return 'Z';
case 0x1E: return ((modifier & 0x02) == 0x02) ? '!' : '1';
case 0x1F: return ((modifier & 0x02) == 0x02) ? '@' : '2';
case 0x20: return '3';
case 0x21: return ((modifier & 0x02) == 0x02) ? '$' : '4';
case 0x22: return ((modifier & 0x02) == 0x02) ? '%' : '5';
case 0x23: return '6';
case 0x24: return ((modifier & 0x02) == 0x02) ? '&' : '7';
case 0x25: return '8';
case 0x26: return ((modifier & 0x02) == 0x02) ? '(' : '9';
case 0x27: return ((modifier & 0x02) == 0x02) ? ')' : '0';
case 0x28: return 0x1; // 这里用0x1表示回车
case 0x2A: return 0x2; // 这里用0x2表示 BackSpace
case 0x2D: return ((modifier & 0x02) == 0x02) ? '_' : '-';
case 0x2e: return ((modifier & 0x02) == 0x02) ? '+' : '=';
case 0x33: return ((modifier & 0x02) == 0x02) ? ':' : ';';
case 0x34: return ((modifier & 0x02) == 0x02) ? '"' : '\'';
case 0x36: return ',';
case 0x37: return '.';
case 0x38: return ((modifier & 0x02) == 0x02) ? '?' : '/';
case 0x2C: return ' ';
default: return 0x0; // INVALID OR ERROR
}
}
void keyboard_transfer_cb(usb_transfer_t *transfer)
{
if (Device_Handle == transfer->device_handle) {
isKeyboardPolling = false;
if (transfer->status == 0) {
if (transfer->actual_num_bytes == 8) {
uint8_t *const p = transfer->data_buffer;
ESP_LOGI("", "HID report: %02x %02x %02x %02x %02x %02x %02x %02x",
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
uint8_t c = ScanCode2Ascii(p[0], p[2]);
if (c != 0) {
ESP_LOGI("", "%c", c);
if ((Message.length() != 0) && (c == 0x02)) {
Message = Message.substring(0, Message.length() - 1);
} else if (c == 0x01) {
Message = "";
} else if (c != 0) {
if (Message.length()>4*16) {
Message=Message.substring(1,Message.length());
}
Message = Message + (char)c;
}
ESP_LOGI("", "%s", Message);
char buf[4*16+1];
Message.toCharArray(buf,sizeof(buf));
for (int i=Message.length();i<4*16;i++) {
buf[i]=' ';
}
buf[4*16]=0;
OLED.disStr(0, 0, buf);
OLED.display();
}
}
else {
ESP_LOGI("", "Keyboard boot hid transfer too short or long");
}
}
else {
ESP_LOGI("", "transfer->status %d", transfer->status);
}
}
}
void check_interface_desc_boot_keyboard(const void *p)
{
const usb_intf_desc_t *intf = (const usb_intf_desc_t *)p;
if ((intf->bInterfaceClass == USB_CLASS_HID) &&
(intf->bInterfaceSubClass == 1) &&
(intf->bInterfaceProtocol == 1)) {
isKeyboard = true;
ESP_LOGI("", "Claiming a boot keyboard!");
esp_err_t err = usb_host_interface_claim(Client_Handle, Device_Handle,
intf->bInterfaceNumber, intf->bAlternateSetting);
if (err != ESP_OK) ESP_LOGI("", "usb_host_interface_claim failed: %x", err);
}
}
void prepare_endpoint(const void *p)
{
const usb_ep_desc_t *endpoint = (const usb_ep_desc_t *)p;
esp_err_t err;
// must be interrupt for HID
if ((endpoint->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK) != USB_BM_ATTRIBUTES_XFER_INT) {
ESP_LOGI("", "Not interrupt endpoint: 0x%02x", endpoint->bmAttributes);
return;
}
if (endpoint->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) {
err = usb_host_transfer_alloc(KEYBOARD_IN_BUFFER_SIZE, 0, &KeyboardIn);
if (err != ESP_OK) {
KeyboardIn = NULL;
ESP_LOGI("", "usb_host_transfer_alloc In fail: %x", err);
return;
}
KeyboardIn->device_handle = Device_Handle;
KeyboardIn->bEndpointAddress = endpoint->bEndpointAddress;
KeyboardIn->callback = keyboard_transfer_cb;
KeyboardIn->context = NULL;
isKeyboardReady = true;
KeyboardInterval = endpoint->bInterval;
ESP_LOGI("", "USB boot keyboard ready");
}
else {
ESP_LOGI("", "Ignoring interrupt Out endpoint");
}
}
void show_config_desc_full(const usb_config_desc_t *config_desc)
{
// Full decode of config desc.
const uint8_t *p = &config_desc->val[0];
static uint8_t USB_Class = 0;
uint8_t bLength;
for (int i = 0; i < config_desc->wTotalLength; i += bLength, p += bLength) {
bLength = *p;
if ((i + bLength) <= config_desc->wTotalLength) {
const uint8_t bDescriptorType = *(p + 1);
switch (bDescriptorType) {
case USB_B_DESCRIPTOR_TYPE_DEVICE:
ESP_LOGI("", "USB Device Descriptor should not appear in config");
break;
case USB_B_DESCRIPTOR_TYPE_CONFIGURATION:
show_config_desc(p);
break;
case USB_B_DESCRIPTOR_TYPE_STRING:
ESP_LOGI("", "USB string desc TBD");
break;
case USB_B_DESCRIPTOR_TYPE_INTERFACE:
USB_Class = show_interface_desc(p);
check_interface_desc_boot_keyboard(p);
break;
case USB_B_DESCRIPTOR_TYPE_ENDPOINT:
show_endpoint_desc(p);
if (isKeyboard && KeyboardIn == NULL) prepare_endpoint(p);
break;
case USB_B_DESCRIPTOR_TYPE_DEVICE_QUALIFIER:
// Should not be config config?
ESP_LOGI("", "USB device qual desc TBD");
break;
case USB_B_DESCRIPTOR_TYPE_OTHER_SPEED_CONFIGURATION:
// Should not be config config?
ESP_LOGI("", "USB Other Speed TBD");
break;
case USB_B_DESCRIPTOR_TYPE_INTERFACE_POWER:
// Should not be config config?
ESP_LOGI("", "USB Interface Power TBD");
break;
case 0x21:
if (USB_Class == USB_CLASS_HID) {
show_hid_desc(p);
}
break;
default:
ESP_LOGI("", "Unknown USB Descriptor Type: 0x%x", bDescriptorType);
break;
}
}
else {
ESP_LOGI("", "USB Descriptor invalid");
return;
}
}
}
void setup()
{
OLED.init();
OLED.flipScreenVertically();
usbh_setup(show_config_desc_full);
}
void loop()
{
usbh_task();
if (isKeyboardReady && !isKeyboardPolling && (KeyboardTimer > KeyboardInterval)) {
KeyboardIn->num_bytes = 8;
esp_err_t err = usb_host_transfer_submit(KeyboardIn);
if (err != ESP_OK) {
ESP_LOGI("", "usb_host_transfer_submit In fail: %x", err);
}
isKeyboardPolling = true;
KeyboardTimer = 0;
}
}
工作视频如下:
https://www.bilibili.com/video/BV1c84y1E7ob/
参考:
1. https://mc.dfrobot.com.cn/forum.php?mod=viewthread&tid=312276