ESP32S3 FireBeetle 高仿板

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

发表回复

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