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:

工作的测试视频:

C# 获得当前电池电量并保存

基本原理:通过 GetSystemPowerStatus 这个 API 获得当前系统的电池电量信息,以1秒为间隔进行查询,查询结果保存到文件中同时输出到屏幕上。收到按键后退出。

代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Threading;
using System.IO;
namespace GeetBatter
{

    class Program
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct SYSTEM_POWER_STATUS
        {
            public byte ACLineStatus;
            public byte BatteryFlag;
            public byte BatteryLifePercent;
            public byte Reserved1;
            public int BatteryLifeTime;
            public int BatteryFullLifeTime;
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern bool GetSystemPowerStatus([In, Out] ref SYSTEM_POWER_STATUS systemPowerStatus);

        static void Main(string[] args)
        {
            // The system power charger struct
            SYSTEM_POWER_STATUS status = new SYSTEM_POWER_STATUS();

            Boolean Running = true;
            DateTime currentTime;
            String Result;
            String Filename = DateTime.Now.ToString("HHmmss")+".txt";
            FileStream fs = new FileStream(Filename, FileMode.Append);
            StreamWriter wr = new StreamWriter(fs);

            while (Running)
            {
                Thread.Sleep(1000);
                while (Console.KeyAvailable)
                {
                    Console.ReadKey(true);
                    Running = false;
                }
                // Get Power status from Kernell
                GetSystemPowerStatus(ref status);

                currentTime = DateTime.Now;
                Result = currentTime.ToString("HH:mm:ss") + "," + status.BatteryLifePercent;
                Console.WriteLine(Result);
                wr.WriteLine(Result);
            }
            wr.Close();
            Console.WriteLine("Program would exit");
            Console.ReadLine();
        }
    }
}

4Pin 风扇控制器

对于风扇来说,只要供电就能工作。最开始的电脑使用的都是这种风扇。但是随着技术的发展,人们发现需要对这个风扇进行控制,因为风扇转的快噪音和功耗都会随之增加。于是,加上一根反馈线,让用户能够得知当前的转速。但是这样会遇到另外的两个问题:第一个问题是风扇电压变化,反馈线上的电压也会随之变化,范围大了读取这个反馈会很麻烦。第二个问题是:电压和风扇转速转速关系并不是线性的。比如,一个 12V 的风扇,12V时转速是 2000CPM,10V供电时转速时1000CPM,但是如果11V 供电时,转速很可能是 1100CPM。风扇的转速和风力噪音直接相关,用户想要得到一个大概的转速非常困难。最终 Intel 推出了一个方案:通过 PWM 来控制风扇转速,然后使用5V 信号作为当前转速反馈引脚。

我们最常见到的CPU的风扇通常都是 4 Pin的。供电要求12V,控制的PWM 为 5V 25KHz,转速反馈引脚为 5V  输出。

有些风扇是存在最低转速的,意思是哪怕有PWM 已经为0仍然能够旋转,有些不存在最低转速,PWM 比较低的时候就会停止转动。

了解了上述知识就可以得知我们为了实现控制 CPU 风扇,需要提供12V 电源,提供25Khz 的 PWM 信号,以及读取 5V 的转速输出。

电路设计如下,我们使用 Arduino Uno作为主控。外部12V 供电进入(DC1)之后,经过一个DC-DC 降压模块(来自DFROBOT 的DFR0831,DC-DC降压模块7~24V转5V/4A),降压为5V提供给 Arduino 作为风扇控制,然后风扇的转速反馈信号经过U3上拉后进入Arduino 即可读取。最终转速和当前的PWM设定显示在一个 1602LCD上。

PCB 设计如下,可以看到板子上带有4个按钮用于控制 PWM ,分别是-1、-10、+1、+10这样用户可以快速的改变 PWM 设置。

代码设计如下:

#include <LiquidCrystal_I2C.h>
// 保存当前设定的 PWM 值
#include<EEPROM.h>

LiquidCrystal_I2C lcd(0x3F, 16, 2);

// PWM 发生器相关定义
const byte OC1A_PIN = 9; //PWM输出引脚为 D9
// 定义 PWM 频率
const word PWM_FREQ_HZ = 25000; //Adjust this value to adjust the frequency
const word TCNT1_TOP = 16000000 / (2 * PWM_FREQ_HZ);
const int BTN1 = 13;
const int BTN2 = 12;
const int BTN3 = 11;
const int BTN4 = 10;

// 计算转速相关定义
const int TOTAL = 10; // 计算10次输出一次,这是一个简单的滤波
int InterruptPin = 2;// 接收风扇中断 D2
volatile long int counter = 0;
volatile long int t[TOTAL];
byte p = 0;
byte SavedPWM = 0;
byte lastPWM = -1;
byte pwm;
long int Elsp=0;

void setup() {
  pinMode(BTN1, INPUT_PULLUP);
  pinMode(BTN2, INPUT_PULLUP);
  pinMode(BTN3, INPUT_PULLUP);
  pinMode(BTN4, INPUT_PULLUP);

  pinMode(OC1A_PIN, OUTPUT);

  // Clear Timer1 control and count registers
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  // Set Timer1 configuration
  // COM1A(1:0) = 0b10   (Output A clear rising/set falling)
  // COM1B(1:0) = 0b00   (Output B normal operation)
  // WGM(13:10) = 0b1010 (Phase correct PWM)
  // ICNC1      = 0b0    (Input capture noise canceler disabled)
  // ICES1      = 0b0    (Input capture edge select disabled)
  // CS(12:10)  = 0b001  (Input clock select = clock/1)

  TCCR1A |= (1 << COM1A1) | (1 << WGM11);
  TCCR1B |= (1 << WGM13) | (1 << CS10);
  ICR1 = TCNT1_TOP;

  Serial.begin(115200);
  Serial.setTimeout(300);

  pinMode(InterruptPin, INPUT);
  attachInterrupt(0, speedX, FALLING );

  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("PWM Fan CNT");

  // PWM 存在地址0 上
  SavedPWM = EEPROM.read(0);

  if (SavedPWM>100) {
      SavedPWM=0;
    }
  // 将上一次保存的 PWM 设定进去
  setPwmDuty(SavedPWM);
  pwm = SavedPWM;
  lastPWM = pwm + 1;
}

void loop() {
  if (pwm != lastPWM) {
    Serial.println(pwm);
    setPwmDuty(pwm);
    lastPWM = pwm;
    Elsp=millis();
  }

  // 计算当前转速
  long int avg = 0;
  counter = 0;
  delay(1000);

  p = (p + 1) % TOTAL;
  t[p] = (long int)(counter * 60 / 2);

  for (byte i = 0; i < TOTAL; i++) {
    avg = avg + t[i];
  }

  // 输出转速
  Serial.print("Current speed: ");
  Serial.println(avg / TOTAL);

  lcd.setCursor(0, 1);
  lcd.print("Fan Speed:");
  lcd.print(avg / TOTAL);
  lcd.print("      ");

  counter = 0;
  pinMode(BTN1, INPUT_PULLUP);
  if (digitalRead(BTN1) == LOW) {
    delay(5);
    if ((digitalRead(BTN1) == LOW) && (pwm > 9)) {
      pwm = pwm - 10;
    }
  }
  if (digitalRead(BTN2) == LOW) {
    delay(5);
    if ((digitalRead(BTN2) == LOW) && (pwm > 0)) {
      pwm = pwm - 1;
    }
  }  
  if (digitalRead(BTN3) == LOW) {
    delay(5);
    if ((digitalRead(BTN3) == LOW) && (pwm <=100-10)) {
      pwm = pwm + 10;
    }
  }
  if (digitalRead(BTN4) == LOW) {
    delay(5);
    if ((digitalRead(BTN4) == LOW) && (pwm <100)) {
      pwm = pwm + 1;
    }
  }

  // 每隔5秒检查 pwm 是否已经改变,如果改变则保存
  if ((SavedPWM!=pwm)&&(millis()-Elsp>5000)) {
      Serial.println("Save current pwm");
      Elsp=millis();
      SavedPWM=pwm;
      EEPROM.write(0,pwm);
    }
}

void setPwmDuty(byte duty) {
  Serial.print("Set pwm "); Serial.println(duty);
  lcd.setCursor(0, 0);
  lcd.print("Current pwm:");
  lcd.print(duty);
  lcd.print("  ");
  OCR1A = (word) (duty * TCNT1_TOP)/ 100;
}


void speedX()//中断函数
{
  counter++;
}

4 Pin fan 的 spec:

本文提到的电路图和PCB:

FireBeetle FFT VGA显示

之前基于 FireBeetle ESP32 和全向MEMS麦克风模块(SEN0487)制作过一个在OLED 屏幕上显示当前环境声音频谱的装置【参考1】。这次制作的是能够输出 VGA 信号的频谱装置,这样,用户能够在显示器或者电视机上看到实时频谱输出。

具体的VGA 显示原理,可以在之前的介绍中看到【参考2】,这次的设计硬件部分与之类似。电路图如下:

其中主控和VGA 部分如下:VGA本质上还是模拟信号,这里使用电阻能够输出不同电平的模拟信号,三根GPIO能够实现2^3=16种组合,因此也意味着能够实现16种颜色.

下面是用于连接全向MEMS麦克风模块的接口:

板子上带有一个 USB 公头用于取电,另外还有一个 USB母头,如果你的显示设备没有 VGA接口只有HDMI接口,那么需要一个VGA转HDMI线,而这种线通常使用USB公头取电,这种情况可以直接将它连接到这个 USB母头取电。

同样的,为了便于从充电宝取电,还设计了一个负载消耗电路。

PCB设计如下:

3D预览如下:

焊接后的实物如下:

接下来就可以进行软件的设计了,。基本原理是:首先通过ADC进行采样,然后将采样结果进行 FFT ,最终得到的是采样期间每个频率的能量。我们将这个数值显示在 VGA上就得到了期望的结果:

#include <arduinoFFT.h>
#include "fabgl.h"

// VGA 显示
fabgl::VGA16Controller DisplayController;
Canvas cv(&DisplayController);

//ZivDebug #define SAMPLES         1024          // Must be a power of 2
#define SAMPLES         256          // Must be a power of 2
#define SAMPLING_FREQ   40000         // Hz, must be 40000 or less due to ADC conversion time. Determines maximum frequency that can be analysed by the FFT Fmax=sampleF/2.
#define AMPLITUDE       1000          // Depending on your audio source level, you may need to alter this value. Can be used as a 'sensitivity' control.
#define AUDIO_IN_PIN    A0            // Signal in on this pin

#define NOISE           500           // Used as a crude noise filter, values below this are ignored
const uint8_t kMatrixWidth = 16;      // Matrix width
const uint8_t kMatrixHeight = 16;     // Matrix height

#define NUM_BANDS       16            // To change this, you will need to change the bunch of if statements describing the mapping from bins to bands

#define BAR_WIDTH      (kMatrixWidth  / (NUM_BANDS - 1))  // If width >= 8 light 1 LED width per bar, >= 16 light 2 LEDs width bar etc
#define TOP            (kMatrixHeight - 0)                // Don't allow the bars to go offscreen

// Sampling and FFT stuff
unsigned int sampling_period_us;
byte peak[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // The length of these arrays must be >= NUM_BANDS
int oldBarHeights[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int bandValues[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
double vReal[SAMPLES];
double vImag[SAMPLES];
unsigned long newTime;

arduinoFFT FFT = arduinoFFT(vReal, vImag, SAMPLES, SAMPLING_FREQ);

unsigned long int Elsp1;
int h[16];
int Height,Width;

void setup() {
  Serial.begin(115200);
  sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQ));
  DisplayController.begin();
  // 设定分辨率
  DisplayController.setResolution(VGA_640x400_60Hz);
  Height=cv.getHeight();
  Width=cv.getWidth();

  cv.setBrushColor(Color::Red    );
  
  // get a font for about 40x14 text screen
  cv.selectFont(&fabgl::FONT_8x8);

  cv.setGlyphOptions(GlyphOptions().FillBackground(true));
  
}

void loop() {
  static int64_t stime  = esp_timer_get_time();
  static int FPS        = 0;
  static int FPSCounter = 0;
  

  // Reset bandValues[]
  for (int i = 0; i < NUM_BANDS; i++) {
    bandValues[i] = 0;
  }

  // Sample the audio pin
  for (int i = 0; i < SAMPLES; i++) {
    newTime = micros();
    vReal[i] = analogRead(AUDIO_IN_PIN); // A conversion takes about 9.7uS on an ESP32
    vImag[i] = 0;
    while ((micros() - newTime) < sampling_period_us) {
      /* chill */
    }
  }

  // Compute FFT
  FFT.DCRemoval();
  FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
  FFT.Compute(FFT_FORWARD);
  FFT.ComplexToMagnitude();

  // Analyse FFT results
  for (int i = 2; i < (SAMPLES / 2); i++) {    // Don't use sample 0 and only first SAMPLES/2 are usable. Each array element represents a frequency bin and its value the amplitude.
    if (vReal[i] > NOISE) {                    // Add a crude noise filter

      //16 bands, 12kHz top band
      if (i <= 2 )           bandValues[0]  += (int)vReal[i];
      if (i > 2   && i <= 3  ) bandValues[1]  += (int)vReal[i];
      if (i > 3   && i <= 5  ) bandValues[2]  += (int)vReal[i];
      if (i > 5   && i <= 7  ) bandValues[3]  += (int)vReal[i];
      if (i > 7   && i <= 9  ) bandValues[4]  += (int)vReal[i];
      if (i > 9   && i <= 13 ) bandValues[5]  += (int)vReal[i];
      if (i > 13  && i <= 18 ) bandValues[6]  += (int)vReal[i];
      if (i > 18  && i <= 25 ) bandValues[7]  += (int)vReal[i];
      if (i > 25  && i <= 36 ) bandValues[8]  += (int)vReal[i];
      if (i > 36  && i <= 50 ) bandValues[9]  += (int)vReal[i];
      if (i > 50  && i <= 69 ) bandValues[10] += (int)vReal[i];
      if (i > 69  && i <= 97 ) bandValues[11] += (int)vReal[i];
      if (i > 97  && i <= 135) bandValues[12] += (int)vReal[i];
      if (i > 135 && i <= 189) bandValues[13] += (int)vReal[i];
      if (i > 189 && i <= 264) bandValues[14] += (int)vReal[i];
      if (i > 264          ) bandValues[15] += (int)vReal[i];
    }
  }

  // Process the FFT data into bar heights
  for (byte band = 0; band < NUM_BANDS; band++) {

    // Scale the bars for the display
    int barHeight = bandValues[band] / AMPLITUDE;
    if (barHeight > TOP) barHeight = TOP;

    // Small amount of averaging between frames
    barHeight = ((oldBarHeights[band] * 1) + barHeight) / 2;

    // Move peak up
    if (barHeight > peak[band]) {
      peak[band] = min(TOP, barHeight);
    }
    h[band] = barHeight;

    // Save oldBarHeights for averaging later
    oldBarHeights[band] = barHeight;

  }

  if (millis() - Elsp1 > 10) {
    for (byte band = 0; band < NUM_BANDS; band++)
      if (peak[band] > 0) peak[band] -= 1;


    cv.setBrushColor(Color::Black    );
    cv.clear();
    cv.setBrushColor(Color::Red    );
    for (int i = 0; i < 16; i++) {
      if (h[i] != 0) {
        //cv.fillRectangle(cv.getWidth()*i / 16, 0, cv.getWidth() * (i + 1) / 16, cv.getHeight() *h[i] / 16);
        cv.fillRectangle(Width*i / 16, Height -1 , Width * (i + 1) / 16-1, (Height-1) *(16-h[i]) / 16);
        
      }
      Serial.print(h[i], HEX);
      Serial.print("");
    }
    Serial.println("");

    Elsp1 = millis();
  }
  
      if (esp_timer_get_time() - stime > 1000000) {
    // calculate FPS
    FPS = FPSCounter;
    stime = esp_timer_get_time();
    FPSCounter = 0;
  }
  ++FPSCounter;

  // display test state and FPS
  cv.setPenColor(Color::Blue);
  cv.setBrushColor(Color::Yellow);
  cv.drawTextFmt(80, 5, "%d FPS ",  FPS);
}

参考:

  1. https://mc.dfrobot.com.cn/thread-314320-1-1.html
  2. https://mc.dfrobot.com.cn/thread-311156-1-1.html?fromuid=70205

本文提到的电路图下载:

本文提到的完整代码:

Step to UEFI (278)Progra message 的使用

Visual Studio 的 C 支持 #pragma message() 宏可以用来输出一些信息。于是编写一个代码进行测试:

#include  &lt;Uefi.h>
#include  &lt;Library/UefiLib.h>
#include  &lt;Library/ShellCEntryLib.h>

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  #pragma message (__FILE__)
    return(0);
}

唯一的问题是:我在 EDK2 中编译的时候,无法看到输出的结果。经过研究,编译C代码是通过下面这个指令:

"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Tools\MSVC\14.29.30133\bin\Hostx86\x64\cl.exe" /Foc:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\OUTPUT\.\ /showIncludes /nologo /c /WX /GS- /W4 /Gs32768 /D UNICODE /O1b2s /GL /Gy /FIAutoGen.h /EHs-c- /GR- /GF /Z7 /Gw /X /Zc:wchar_t /D UEFI_C_SOURCE /Wv:11 /Ic:\buildbs\edk2-edk2-stable202205\AppPkg\Applications\HelloMacro  /Ic:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\DEBUG  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Include  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Test\UnitTest\Include  /Ic:\buildbs\edk2-edk2-stable202205\MdePkg\Include\X64  /Ic:\buildbs\edk2-edk2-stable202205\ShellPkg  /Ic:\buildbs\edk2-edk2-stable202205\ShellPkg\Include @c:\buildbs\edk2-edk2-stable202205\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\HelloMacro\HelloMacro\OUTPUT\cc_resp_1.txt

于是尝试去掉其中的/nologo 指令,运行结果如下:

再同时去掉 /showIncludes 运行结果:

可以看到,其中出现了当前的文件名信息。

总结:在使用VC 编写代码的时候,如果需要输出一些编译期的数据,可以考虑使用 #pragma message() 来实现。

Step to UEFI (277)QEMU 增加自定义的 FFS和读取

这次实验的是在 OVMF 生成的BIOS中插入一个Binary ,然后在代码中将这个Binary 读取出来。

第一个目标:在 OVMF 中插入 Binary。

1.我们准备一个 message.txt,其中内容是简单的字符串:

This is a test message comes from 
www.lab-z.com

2.在\OvmfPkg\OvmfPkgX64.fdf 文件中,加入下面的代码

!if $(E1000_ENABLE)
  FILE DRIVER = 5D695E11-9B3F-4b83-B25F-4A8D5D69BE07 {
    SECTION PE32 = Intel3.5/EFIX64/E3522X2.EFI
  }
!endif

#LABZDebug_Start
FILE FREEFORM = C3E36D09-2023-0829-A857-D5288FE33E28 Align=4K {
  SECTION RAW = OvmfPkg/LabzBin/message.txt
}
#LABZDebug_End

!include NetworkPkg/Network.fdf.inc
  INF  OvmfPkg/VirtioNetDxe/VirtioNet.inf

3.使用工具查看放置的FFS,可以看到正确的增加到 BIOS 中

这样,第一个目标已经完成,我们成功的生成了一个FFS文件。

第二个目标,将这个 FFS文件从FV中读取出来。之前我们做过类似的实验,在【参考1】中有介绍。这次我们编写一个 UEFI Shell Application ,显示前面插入的 FFS文件的内容。测试代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include  "PiFirmwareVolume.h"
#include  "PiFirmwareFile.h"
#include  "FirmwareVolume2.h"

INTN
EFIAPI
ShellAppMain (
    IN UINTN Argc,
    IN CHAR16 **Argv
)
{
	CONST EFI_GUID    NameGuid= { 0xC3E36D09, 0x2023, 0x0829,
		{ 0xA8, 0x57, 0xD5, 0x28, 0x8F, 0xE3, 0x3E, 0x28 }
	};
	EFI_SECTION_TYPE  SectionType=EFI_SECTION_RAW;

	VOID             *Buffer=NULL;
	UINTN             Size=0;
	UINT32            AuthenticationStatus=0;
	EFI_STATUS  	  Status;
	EFI_FIRMWARE_VOLUME2_PROTOCOL *Fv;

	Status = gBS->LocateProtocol (
	             &gEfiFirmwareVolume2ProtocolGuid,
	             NULL,
	             (VOID **) &Fv
	         );
	if (EFI_ERROR (Status))
	{
		Print(L"[EFI_FIRMWARE_VOLUME2_PROTOCOL not found]\n");
		return EFI_NOT_FOUND;
	}
	//
	// Read desired section content in NameGuid file
	//
	Status      = Fv->ReadSection (
	                  Fv,
	                  &NameGuid,
	                  SectionType,
	                  0,
	                  &Buffer,
	                  &Size,
	                  &AuthenticationStatus);
	UINT8 *P=(UINT8 *)Buffer;
	Print(L"[EFI_FIRMWARE_VOLUME2_PROTOCOL %r]\n",Status);

	for (UINTN i=0; i<Size; i++)
	{
		Print(L"%c",P[i]);
	}
	Print(L"\n");

	return(0);
}

运行的结果如下图所示,可以看到正确的读取出我们存放的内容:

完整的代码下载:

参考:

1. https://www.lab-z.com/getffs/  代码读取一个 FFS

Step to UEFI (276)宏和结构体初始化表格

在 EDK2 中有一种比较有趣的定义和初始化Table 的方法,主要是基于 __VA_ARGS__ 这个宏。

“__VA_ARGS__是一个预处理宏,用于表示可变数量的参数。当在宏定义中使用__VA_ARGS__,它会自动展开为传递给宏的实际参数。以下是一个示例使用__VA_ARGS__的宏定义代码:
#include &lt;stdio.h>
 
#define PRINT_ARGS(...) printf(__VA_ARGS__)
 
int main() {
    PRINT_ARGS("Hello, %s!\n", "World");
    return 0;
}
上述代码中,宏定义PRINT_ARGS使用__VA_ARGS__来表示可变数量的参数,并通过printf函数打印参数。在main函数中,我们调用PRINT_ARGS宏来打印字符串"Hello, World!"。运行结果为输出"Hello, World!"。
总结:__VA_ARGS__是一个用于表示可变数量参数的预处理宏,在宏定义中使用它可以方便地处理不定数量的参数。“----来自百度

很多时候,我们定义一个 Table 用来传递一些常量,Table需要给出具体的长度,通过这个宏可以实现自动给出Table 的长度,避免用户手工计数的麻烦。

下面是一个示例代码:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

#define MY_TABLE_INIT(Vid,Did,...) \
{ \
  { Vid, Did, (sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) }, \
  { __VA_ARGS__ } \
}

typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  DataDwords;
} MY_TABLE_HEADER;

typedef struct  {
  MY_TABLE_HEADER  Header;
  UINT32 Data[];
} ONE_TABLE;

ONE_TABLE Table = MY_TABLE_INIT (
  0x1234, 0x5678,
  
  // Raw Data
  0x01234567,
  0x89ABCDEF,
  0xFEDCBA98,
  0x76543210
);

INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  Print(L"Table:\n");
  Print(L"   VendorId:[%X]\n",Table.Header.VendorId);
  Print(L"   DeviceId:[%X]\n",Table.Header.DeviceId);
  Print(L"   Size    :[%X]\n",Table.Header.DataDwords);  
  
  for (int i=0;i<Table.Header.DataDwords;i++) {
	  Print(L"[%04X]",Table.Data[i]);  
  }
  Print(L"\n");
  return(0);
}

运行结果如下:

上面代码的解释如下:

1.首先我们定义一个 ONE_TABLE 结构体用来“携带”数据。

typedef struct  {
  MY_TABLE_HEADER  Header;
  UINT32 Data[];
} ONE_TABLE;

从定义可以看到,这个结构体包含了一个头,还有一个变长的数据段。头可以实现用于识别判断这个Table 是否为我们需要的目的,例如,其中有DID和VID 信息。具体定义如下,特别注意 DataDwords 给出了后面变长数据段的长度:

typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  DataDwords;
} MY_TABLE_HEADER;

对于DataDwords 就是我们前面提到的“Table需要给出具体的长度”的问题。

2.为了解决上述问题,通过下面的宏来解决:

#define MY_TABLE_INIT(Vid,Did,...) \
{ \
  { Vid, Did, (sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) }, \
  { __VA_ARGS__ } \
}

其中(sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) 就是计算长度的代码。最终的结果是以 UINT32(DWORD)给出的。

3.初始化定义如下,可以看到DataDwords的计算是宏直接完成的,并不需要我们直接提供

ONE_TABLE Table = MY_TABLE_INIT (
  0x1234, 0x5678,
  
  // Raw Data
  0x01234567,
  0x89ABCDEF,
  0xFEDCBA98,
  0x76543210
);

可以看到,通过上面的方法可以帮助我们方便的实现可变数据的长度定义,有兴趣的朋友不妨尝试一下。

完整的代码下载:

批处理延时和计算经过时间

首先介绍一下批处理中延时的实现:下面代码实现延时3秒

CHOICE /T 3 /C ync /CS /D y

计算经过时间,以秒为单位:

@echo off
set "t=%time%"
::You code start here

::You code end here
set "t1=%time%"

if "%t1:~,2%" lss "%t:~,2%" set "add=+24"
set /a "times=(%t1:~,2%-%t:~,2%%add%)*3600+(1%t1:~3,2%%%100-1%t:~3,2%%%100)*60+(1%t1:~6,2%%%100-1%t:~6,2%%%100)" 
echo Time Used %times% Seconds
pause

上述代码合在一起进行测试:

@echo off
set "t=%time%"
::You code start here
CHOICE /T 3 /C ync /CS /D y
::You code end here
set "t1=%time%"

if "%t1:~,2%" lss "%t:~,2%" set "add=+24"
set /a "times=(%t1:~,2%-%t:~,2%%add%)*3600+(1%t1:~3,2%%%100-1%t:~3,2%%%100)*60+(1%t1:~6,2%%%100-1%t:~6,2%%%100)" 
echo Time Used %times% Seconds
pause

Step to UEFI (275)UEFI 创建内存虚拟盘

前面介绍过在 UEFI 下创建内存虚拟盘的操作,这次介绍如何创建包含需要内容的内存虚拟盘。生成的虚拟盘可以在没有硬盘的情况下充当临时文件的存放位置。当然如果关机或者断电后,盘中内容会消失。

第一步,根据【参考1】制作一个磁盘镜像VHD,这里出于体积考虑,制作了一个 64MB 的FAT32 空盘;

第二步,上面制作磁盘镜像大部分内容都是 0x00(可以看作是以512Bytes为单位的稀疏矩阵),因此我们还需要一个工具提取文件中的非零内容保存起来,这样能够有效降低镜像尺寸。使用 C# 编写代码如下。简单的说就是以512Bytes为单位读取文件,然后检查512 bytes 中是否为全0.如果不是就记录下来保存到文件中。每一个项目是由 4 Bytes 记录加一个 512Byte的数组构成。特别注意 VHD 镜像文件末尾包含了一个文件头,我们这里会对文件大小取整,对于末尾的文件头不会进行处理。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace ConsoleApp7
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Count() == 0) {
                Console.WriteLine("Please input file name!");
                Console.ReadKey();
                Environment.Exit(0);
            }

            if (File.Exists((args[0] + ".raw"))) {
                File.Delete(args[0] + ".raw");
            }
            FileStream fs = new FileStream(args[0], FileMode.Open);
            FileStream RawFs = new FileStream(args[0]+".raw", FileMode.CreateNew);
            byte[] array = new byte[512];
            UInt32 Data;
            Boolean EmptyMark;

            // 写入 Disk Image 的Size (以 1MB 为单位)
            Data = (UInt32) fs.Length / 1024 / 1024;
            RawFs.Write(BitConverter.GetBytes(Data), 0, 4);
            
            // 处理整数以内的磁盘,例如:扫描 64MB 以内的内容生成及镜像
            while (fs.Position < fs.Length / 1024 / 1024 * 1024 * 1024) {
                EmptyMark = true;
                fs.Read(array, 0, array.Length);
                for (int i = 0; i < array.Length; i++)
                {
                    if (array[i] != 0x00)
                    {
                        EmptyMark = false;
                        break;
                    }
                }
                if (EmptyMark==false)
                {
                    Data = (UInt32)(fs.Position / 512 - 1);
                    RawFs.Write(BitConverter.GetBytes(Data),0,4);
                    RawFs.Write(array, 0, array.Length);
                    Console.WriteLine("{0}", Data);
                }
            }
            RawFs.Close();
            Console.WriteLine("Done!");

        }
    }
}

例如:64MB的FAT32 空盘,经过上述操作后会变成6K 大小。

第三步,编写Shell 下的 UEFI Application,创建内存虚拟盘,然后读取前述生成的文件,将内容放在虚拟盘中。这样就得到了一个带有文件系统的内存虚拟盘。代码如下:

#include <Library/BaseLib.h>
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/PrintLib.h>
#include <Library/ShellCEntryLib.h>
#include <Protocol/RamDisk.h>
#include <Protocol/DevicePathToText.h>
#include <Protocol/HiiDatabase.h>
#include <Protocol/HiiPackageList.h>
#include <Protocol/HiiImageEx.h>
#include <Protocol/PlatformLogo.h>
#include <Protocol/GraphicsOutput.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/ShellLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>

//DO NOT REMOVE IMAGE_TOKEN (IMG_LOGO)

extern EFI_BOOT_SERVICES         *gBS;

EFI_STATUS
EFIAPI
UefiMain (
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE  *SystemTable
)
{
	EFI_STATUS               Status;
	EFI_RAM_DISK_PROTOCOL    *MyRamDisk;
	EFI_FILE_HANDLE   		FileHandle;
	UINTN	tmp, DiskSize,SectorIndex;
	UINT64                   *StartingAddr,Position;
	UINT8                    Sector[512];
	UINTN					 ImageFileSize;
	EFI_DEVICE_PATH_PROTOCOL *DevicePath;

	// 磁盘镜像文件
	CHAR16	*DiskImage=L"DiskImage.BIN";
	// 如果磁盘镜像不存在,报错退出
	if (ShellFileExists(DiskImage)!=EFI_SUCCESS)
	{
		Print(L"Couldn't find 'DiskImage.bin'\n");
		return EFI_INVALID_PARAMETER;
	}

	// 打开磁盘镜像文件
	Status = ShellOpenFileByName(
	             DiskImage,
	             (SHELL_FILE_HANDLE *)&FileHandle,
	             EFI_FILE_MODE_READ,
	             0);
	if(Status != RETURN_SUCCESS)
	{
		Print(L"OpenFile failed!\n");
		return EFI_INVALID_PARAMETER;
	}


	// 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;
	}

	tmp=4;
	Status = ShellReadFile(FileHandle,&tmp,&DiskSize);
	Print(L"Disk size %dMB\n",DiskSize);
	DiskSize=DiskSize*1024*1024;

	//Allocate a memory for Image
	StartingAddr = AllocateReservedZeroPool	((UINTN)DiskSize);	 
	if(StartingAddr==0)
	{
		Print(L"Allocate Memory failed!\n");
		return EFI_SUCCESS;
	}

	ShellGetFileSize(FileHandle,&ImageFileSize);
	Position=0;
	Print(L"File size %d\n",ImageFileSize);

	while (Position<ImageFileSize)
	{
		tmp=4;
		Status = ShellReadFile(FileHandle,&tmp,&SectorIndex);
		if (Status!=EFI_SUCCESS)
		{
			break;
		}
		Print(L"Sector index %d\n",SectorIndex);
		tmp=512;
		Status = ShellReadFile(FileHandle,&tmp,&Sector);
		if (Status==EFI_SUCCESS)
		{
			//Print(L"Read success %d\n",(UINT8*)StartingAddr+SectorIndex*512);
			CopyMem((UINT8*)StartingAddr+SectorIndex*512,&Sector,tmp);
		}
		ShellGetFilePosition(FileHandle,&Position);
		//Print(L"postion %d\n",Position);
	}

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

	return EFI_SUCCESS;
}

基本思路就是:打开镜像文件读取4 Bytes,然后创建这个大小的 Memory Disk。接下来读取 4 Bytes的扇区位置,然后再读取512字节的扇区内容,将这个内容存放在内存中的对应位置。

在Shell 下执行这个程序后,使用 map -r 即可看到新生成的内存盘。

UEFI 代码和编译后的EFI 文件,同时内置了一个64MB的磁盘镜像。

前面提到的 Windows 下的磁盘镜像扫描工具。内置了一个 64MB的空磁盘镜像。

参考:

1. https://www.lab-z.com/vt/

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&lt;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( &amp;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 下载: