DCode/DMU

DMU/DCode

前面提到了 PCode/Punit,这次介绍另外的 DCode/ DMU(Die Management Unit)。 这个 IP 是负责 CPU Die 的功耗的(包括大核和小核)。它会控制休眠和工作时的功耗,温度管理,以及 IccMax。同时会参与 Reset 动作。

和前面的类似,PUnit 上面跑的 Firmware 叫做 PCode。

PCode/PUnit

现代处理器变得越来越复杂,唯一不变的是:性能越强需要的功耗越大。为此,Intel 处理器专门引入了一个控制CPU电力消耗的部件:P-Unit。

P-Unit 是 “P’ower Management ‘Unit’ for the SOC-N(North)”的缩写。主要功能是负责 SoC-N 上面的 IP 供电/温度。这里的 SoC-N 可以理解为之前的 North Bridge , 包括 Memory Controller ,但是不包括 Graphic(目前 Intel 平台这部分独立成一个 Die)。

P-Uint 不会负责 SoC-S(South,相当于 之前的 South Bridge)上面的设备,这个是 PMC 的工作。此外 PUinit 还负责各种重启,MCA和Crashlog流程,能够帮助解决 HW 的Bug。

Intel 处理器上的P-Unit 核心是一个 Xtensa 处理器,负责运行 PCode (Power Managerment Firmware)。 PCode 是通过 mFIT 集成在 IFWI 中的固件。

USB 键盘整蛊专家

这是一个能够让你整蛊别人的设备,将它串联到对方的USB 键盘和主机之间后,你可以用过手机上的 Blinker蓝牙连接到这个设备,然后在 Blinker中输出的信息就会出现在对方的电脑上。

硬件设计如下:

  1. 左上角是预留的调试烧录接口,通过这个接口可以进行烧录,同时  Debug信息也可以通过这个接口发送到 PC端;
  2. 左下角是这个的设计核心,它是一个 ESP32-S3 芯片,通过它实现USB Host 和蓝牙通讯的功能;
  3. ESP32-S3工作电压是 3.3V,这里使用 TLV1117 来实现,这个芯片外围只需要2个 1uf电容
  4. 右下角是 Ch9329 芯片,它是一个 HID 转串口芯片,在这个设计中用于实现USB键盘的功能。

CH9326是一款HID转串口免驱芯片。CH9326支持双向数据传输,用于接收串口数据,并按照HID类设备规范,将数据打包通过USB口上传给计算机,或者从计算机接收符合HID类设备的USB数据包,并从串口进行发送。通过提供的上位机软件,用户也可自行配置芯片的VID、PID,以及各种字符串描述符。芯片是 SOP16 封装,容易焊接。

设计的基本思路是:ESP32-S3 负责解析USB键盘数据,用这种方法来获得按键信息。之后,将获得的信息通过串口发送给CH9326, 然后 Ch9326会实现PC端的模拟按键。可以看到,这个设备对于PC端来说是透明的。之后,可以使用  Blinker 的蓝牙功能连接手机和这个设备,之后就可以从手机端发送字符给PC。

PCB 设计如下:

成品如下(彩色丝印,镀金工艺,背面是设计的一个二维码):

编写 Arduino 代码如下:

#include <elapsedMillis.h>
#include <usb/usb_host.h>
#include "show_desc.hpp"
#include "usbhhelp.hpp"

#define BLINKER_PRINT Serial
#define BLINKER_BLE
#include <Blinker.h>

//键盘数据
char keypress[]  = {0x57, 0xAB, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10};

bool isKeyboard = false;
bool isKeyboardReady = false;
uint8_t KeyboardInterval;
bool isKeyboardPolling = false;
elapsedMillis KeyboardTimer;

const size_t KEYBOARD_IN_BUFFER_SIZE = 8;
usb_transfer_t *KeyboardIn = NULL;

// 将 Buffer 指向的内容,size 长度,计算 checksum 之后发送到Serial2
void SendData(byte *Buffer, byte size) {
  byte sum = 0;
  for (int i = 0; i < size - 1; i++) {
    Serial2.write(*Buffer);
    sum = sum + *Buffer;
    Buffer++;
  }
  *Buffer = sum;
  Serial2.write(sum);
}
// 将ASCII 字符转化为 HID Scancode值
byte Asc2Scancode(byte Asc, boolean *shift) {
  if ((Asc >= 'a') && (Asc <= 'z')) {
    *shift = false;
    return (Asc - 'a' + 0x04);
  }
  if ((Asc >= 'A') && (Asc <= 'Z')) {
    *shift = true;
    return (Asc - 'A' + 0x04);
  }
  if ((Asc >= '1') && (Asc <= '0')) {
    *shift = false;
    return (Asc - '0' + 0x1E);
  }
  if (Asc == '>') {
    *shift = true;
    return (0x37);
  }
  if (Asc == '.') {
    *shift = false;
    return (0x37);
  }
  if (Asc == '_') {
    *shift = true;
    return (0x2D);
  }
  if (Asc == '-') {
    *shift = false;
    return (0x2D);
  }
  return 0;
}

// 如果未绑定的组件被触发,则会执行其中内容
// 输入框输入都会在这里处理
void dataRead(const String & data)
{
  BLINKER_LOG("Blinker readString: ", data);
  boolean shift;
  byte scanCode;
  for (int i = 0; i < data.length(); i++) {
    BLINKER_LOG("Key In", data.charAt(1));
    // 将收到的 ASCII 转为 ScanCode
    scanCode = Asc2Scancode(data.charAt(i), &shift);
    // 一些按键当有 Shift 按下时会发生转义
    if (scanCode != 0) {
      if (shift == true) {
        keypress[5] = 0x02;
      }
      BLINKER_LOG("Scancode", scanCode);
      // 填写要发送的 ScanCode
      keypress[7] = scanCode;
      SendData((byte*)keypress, sizeof(keypress));
      delay(10);
      keypress[5] = 0x00; keypress[7] = 0;
      SendData((byte*)keypress, sizeof(keypress));
      delay(10);
    }
  }
}

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]);
        // USB Host 解析得到的数据,传输给PC
        //
        memcpy(&keypress[5],p,transfer->actual_num_bytes);
        SendData((byte*)keypress, sizeof(keypress));
      }
      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()
{
  // 初始化调试串口
  Serial.begin(115200);
  // 初始 CH9329 串口
  Serial2.begin(9600, SERIAL_8N1, 14, 13, false, 1000, 112);

  //Serial2.begin(9600);
#if defined(BLINKER_PRINT)
  BLINKER_DEBUG.stream(BLINKER_PRINT);
#endif

  // 初始化blinker
  Blinker.begin();
  Blinker.attachData(dataRead);
  usbh_setup(show_config_desc_full);
}

void loop()
{
  usbh_task();
  Blinker.run();

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

  while (Serial.available()) {
    char c = Serial.read();
    if (c == 'q') {
      boolean shift = false;
      // 填写要发送的 ScanCode
      keypress[5] = 0x08;
      SendData((byte*)keypress, sizeof(keypress));
      delay(20);
      keypress[5] = 0;
      SendData((byte*)keypress, sizeof(keypress));
    }
    Serial.print(c);
  }
}

将板卡装入外壳后的照片:

完整的代码:

电路图和PCB 下载:

Step to UEFI (290)Cpp UEFI 006 抄一个 Print

前面编写测试代码的过程中,总感觉没有 Print 直接输出来的顺手,于是研究了一下 Print 的实现。基本原理是,对变量格式化后输出到一个 字符串Buffer 中,然后直接输出Buffer。

首先,编写一个测试的 CPP:

#include <UEFI/UEFI.h>
#include <type_traits>
#include "print.h"

EFI_SYSTEM_TABLE* gST;

EFI_STATUS
efi_main(EFI_HANDLE /*image*/, EFI_SYSTEM_TABLE* systemTable)
{
	gST=systemTable;
	Print(u"%d\n",2024);
	return EFI_SUCCESS;
}

其中使用了 Print.h 头文件,定义如下:

UINTN
EFIAPI
Print (
  IN const CHAR16 *Format,
  ...
);

接下来编写Print.cpp,关键代码来自\MdePkg\Library\UefiLib\UefiLibPrint.c

UINTN
EFIAPI
Print (
  IN CONST CHAR16  *Format,
  ...
  )
{
  VA_LIST  Marker;
  UINTN    Return;

  VA_START (Marker, Format);

  Return = InternalPrint (Format, gST->ConOut, Marker);

  VA_END (Marker);

  return Return;
}

其中的InternalPrint() 函数有较大改动,直接在函数中开了一个内存用于当作 Buffer (CharBuffer[]),不需要AllocatePool()动态分配。

UINTN
InternalPrint (
  IN  CONST CHAR16                     *Format,
  IN  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *Console,
  IN  VA_LIST                          Marker
  )
{
  EFI_STATUS  Status;
  UINTN       Return;
  CHAR16      *Buffer;
  UINTN       BufferSize;
  CHAR16      CharBuffer[320];

  ASSERT (Format != NULL);
  ASSERT (((UINTN)Format & BIT0) == 0);
  ASSERT (Console != NULL);

  BufferSize = 320;

  Buffer = &CharBuffer[0];
  ASSERT (Buffer != NULL);

  Return = UnicodeVSPrint (Buffer, BufferSize, Format, Marker);

  if ((Console != NULL) && (Return > 0)) {
    //
    // To be extra safe make sure Console has been initialized
    //
    Status = Console->OutputString (Console, Buffer);
  }

  return Return;
}

接下来编写编译的批处理,可以看到最主要是编译生成 test8.obj 和 print.obj ,最后将二者Link 在一起即可:

set Target=test8
cl /c /I"C:\\BuildBs\\CppStudy\\Cpp\\UEFI-CPP-headers" /Zc:wchar_t- /Zi /W4 /WX- /diagnostics:column /Od /D _UNICODE /D UNICODE /D HAVE_USE_MS_ABI /D GNU_EFI_USE_EXTERNAL_STDARG /D _UNICODE /D UNICODE /Gm- /MDd /GS- /fp:precise /permissive- /Zc:wchar_t /Zc:forScope /Zc:inline /std:c++17 /Fo"C:\\BuildBs\\CppStudy\\Cpp\\" /FAsc /Fd"C:\\BuildBs\\CppStudy\\Cpp\\vc142.pdb" /external:W4 /Gd /TP /wd4229 /FC /errorReport:prompt /Oi- %Target%.cpp
cl /c /I"C:\\BuildBs\\CppStudy\\Cpp\\UEFI-CPP-headers" /Zc:wchar_t- /Zi /W4 /WX- /diagnostics:column /Od /D _UNICODE /D UNICODE /D HAVE_USE_MS_ABI /D GNU_EFI_USE_EXTERNAL_STDARG /D _UNICODE /D UNICODE /Gm- /MDd /GS- /fp:precise /permissive- /Zc:wchar_t /Zc:forScope /Zc:inline /std:c++17 /Fo"C:\\BuildBs\\CppStudy\\Cpp\\" /FAsc /Fd"C:\\BuildBs\\CppStudy\\Cpp\\vc142.pdb" /external:W4 /Gd /TP /wd4229 /FC /errorReport:prompt /Oi- print.cpp

if %errorlevel% NEQ 0 goto EndError

link "/OUT:C:\\BuildBs\\CppStudy\\Cpp\\%Target%.efi" /VERBOSE /INCREMENTAL:NO "/LIBPATH:C:\\BuildBs\\CppStudy\\Cpp\\" libcmtd.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /NODEFAULTLIB /MANIFEST:NO /DEBUG:FULL "/PDB:C:\\BuildBs\\CppStudy\\Cpp\\bootx64.pdb" /SUBSYSTEM:EFI_APPLICATION /OPT:REF /TLBID:1 "/ENTRY:efi_main" /NXCOMPAT:NO "/IMPLIB:C:\\BuildBs\\CppStudy\\Cpp\\bootx64.lib" /MACHINE:X64 "C:\\BuildBs\\CppStudy\\Cpp\\%Target%.obj" "C:\\BuildBs\\CppStudy\\Cpp\\print.obj"
copy /y %Target%.efi Emulator\
:EndError

最后将Print.cpp、Test8.CPP和g8.bat 放在一起,即可编译。

模拟器运行结果如下:

完整代码如下,需要注意的是编译批处理内部使用了绝对路径,如果想实验,最好按照之前的文章架设同样名称的目录测试。

基于Ch554 制作的USB喇叭

很早之前使用 Arduino Pro Micro 实现过USB耳机转接器,这次尝试使用 WCH 的 Ch554 来实现(实际上可以使用 更见偏移的 Ch552 来实现,但是因为 Ch552 有烧写次数限制,所以最终是在 Ch554上进行开发)。

无需过多了解 USB Audio的相关知识,所作的工作基本上只有:通过描述符报告自己是一个USB Audio 设备。之后 Windows 就会发送 48Khz 16位双声道的采样数据给设备(如果想了解更多,推荐去USB中文网阅读相关内容)。我们在设备响应的 OUTPUT 端点上即可收到数据。

需要特别注意的是,代码中有一个向HOST 汇报当前支持采样率的描述符。这里申明了2个采样率:22,050Hz和48000Hz。

  0x0E,  	//Size of the descriptor, in bytes  
  0x24,  	//CS_INTERFACE Descriptor Type  
  0x02,  	//FORMAT_TYPE descriptor subtype  
  0x01,  	//FORMAT_TYPE_I  
  0x02,  	//Indicates the number of physical channels in the audio data stream.  
  0x02,  	//The number of bytes occupied by one audio subframe. Can be 1, 2, 3 or 4.  
  0x10,  	//The number of effectively used bits from the available bits in an audio subframe.  
  0x02,  	//Indicates how the sampling frequency can be programmed:  
  0x22,0x56,0x00, //	  Sampling frequency 1 in Hz for this isochronous data endpoint.  
  0x80,0xBB,0x00, //	  Sampling frequency 2 in Hz for this isochronous data endpoint.  

在工作过程中,Windows会通知当前使用的采样率。

需要注意的是:

  1. 如果你只报告支持 48000的采样率,那么Windows就不会发送 SET_CUR
  2. Windows不支持所有的采样率,我这边实验只支持48000Hz的采样率,换句话,你申明支持 24000或者22050,实际上并不会使用。

具体的数据是下面这样的,可以看到这种同步传输/等时传输的数据和通常的最大区别在于不会有 ACK 信号,相当于HOST 直接丢出来不管对错。

从上面可以看到每个数据之间间隔是1ms,每笔数据 192字节。

对应在代码中会在USBAudioSpeaker.c文件中的Mass_Storage_Out函数进行处理:

void Mass_Storage_Out (void) {
    PWM_CTRL |= bPWM2_OUT_EN;
	for (uint8_t i = 0; i < BOT_EP_Rx_Length; i=i+4){		
		PWM_DATA2 = BOT_Rx_Buf[i+1];
		// Delay for 20833ns
		for (uint16_t j=0;j<51;j++) {
		   __asm__ ("nop\n");
		}
    }
	PWM_CTRL &= (~bPWM2_OUT_EN);
    //Serial0_println("Ending");
	BOT_EP_Tx_ISO_Valid();
}

经过前面的工作,现在能够拿到PC输出的音频数据,接下来的问题就是如何将收到的数据通过喇叭播放出去。这个过程相当于一个 DAC (数字到模拟)的过程。这次选择的方法是:通过 PWM 进行模拟。这是使用的是CH554 芯片,它支持PWM:2 组 PWM 输出,PWM1/PWM2 为 2 路 8 位 。在下图可以看到 P1.5/P3.1/P3.0/P3.4都是可以选择的引脚。代码使用了 P3.4这个引脚。

PWM初始化代码如下,特别注意使用了1分频产生 PWM 信号,我们使用的主频为 24Mhz 5V,因此频率是 24000000/256=93750Hz

  // 打开 PWM2 功能
  PIN_FUNC &= ~(bPWM2_PIN_X);
  // PWM 分频设置
  // 1 分频,这样 PWM 频率为 Fsys/256
  PWM_CK_SE=1;

上述设置之后,直接在 PWM_DATA2 寄存器中填写你要生成的高电平比例即可产生对应的 PWM 信号。对应的代码就在前面提到的void Mass_Storage_Out (void) {} 函数中。此外,使用NOP 指令制作了一个简单的延时,延时 1/48000=20833ns:

// Delay for 20833ns
		for (uint16_t j=0;j<51;j++) {
		   __asm__ ("nop\n");
		}

在编译时,还需要对项目进行如下设置:

  1. 选择 Ch552 Board,前面提到了Ch552和 Ch554 在代码方面是完全兼容的;
  2. 设置USB 为“U148B USB Ram”
  3. Clock Source 为 “24Mhz(internal),5V”

硬件方面非常简单,Ch554最小系统,喇叭接到对应引脚即可:

这是我设计的用于测试 Ch554 和 Ch559 最小开发板。Ch554和Ch559 最小系统外围只需要2个电容即可,两颗芯片相互独立:

完整代码:

电路图:

从上面可以看到,Ch55xduino提供的 USB 框架扩展性不错。Ch554 可以方便的通过 Ch55xduino 实现一个USB Speaker 的功能。目前美中不足的只是音频质量较差(所有看到的人都怀疑这个是一个收音机),后续会持续进行改进。

CH559 Arduino 使用 PWM2 的例子

代码非常简单,根据官方例子移植到 Arduino 完成。使用 P2.5 引脚,Ch55xduino 编译:

#define SetPWMClk(CK_SE) (PWM_CK_SE = CK_SE)                                  //分频,默认时钟Fsys            
#define SetPWMCycle(Cycle) (PWM_CYCLE = Cycle)                                //设置循环周期
#define SetPWM1Dat(dat) (PWM_DATA = dat)                                      //设置PWM输出占空比
#define SetPWM2Dat(dat) (PWM_DATA2 = dat)

/*******************************************************************************
* Function Name  : InitPWM2(UINT8 polar)
* Description    : PWM初始化函数
* Input          : polar=0选择默认低电平,高电平输出有效;
                   polar=1选择默认高电平,低电平输出有效;
* Output         : None
* Return         : None
*******************************************************************************/
void  InitPWM2(uint8_t polar)
{
    PWM_CTRL &= ~bPWM_CLR_ALL;                                                //清空FIFO和计数                                                      
    PWM_CTRL &= ~bPWM_MOD_MFM;
    PWM_CTRL |=  bPWM_IE_END;                                                  //使能PWM计数周期完成中断
    PWM_CTRL |= bPWM2_OUT_EN;                                                 //PWM2输出使能  
    PWM_CTRL  |= bPWM_IF_END;                                                 //清除所有的中断标志
    if(polar){
        PWM_CTRL |= bPWM2_POLAR;                                              //低电平有效
    }
    else{
        PWM_CTRL &= ~bPWM2_POLAR;                                             //高电平有效  
    }     
}

void setup() {
  // put your setup code here, to run once:
    SetPWMClk(12);          //设置PWM1&2的时钟分频系数为12
    InitPWM2(0);            //PWM2初始化,高电平有效
    SetPWMCycle(1000);      //设置循环周期100
    SetPWM2Dat(50);         //PWM1占空比设置50/100
}

void loop() {
  // put your main code here, to run repeatedly:
}

Step to UEFI (289)Cpp UEFI 005 C++函数默认参数

C++ 定义函数时可以直接给形参指定默认值,如果调用函数没有给形参赋值,那就直接使用默认值。这个功能非常容易理解。编写如下代码进行验证:

#include <UEFI/UEFI.h>
#include <type_traits>

EFI_SYSTEM_TABLE* gSystemTable;

void printInt(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* conOut, int value) {
	CHAR16 out[32];
	CHAR16* ptr = out;
	static_assert(std::is_unsigned_v<char16_t>);
	if (value == 0)
	{
		conOut->OutputString(conOut, u"0");
		return;
	}

	ptr += 31;
	*--ptr = 0;
	int tmp = value;// >= 0 ? value : -value; 

	while (tmp)
	{
		*--ptr = '0' + tmp % 10;
		tmp /= 10;
	}

	if (value < 0) *--ptr = '-';
	conOut->OutputString(conOut, ptr);
}

void func(int a, int b=2, int c=3){
    printInt(gSystemTable->ConOut,a);
	gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"\r\n");
	printInt(gSystemTable->ConOut,b);
	gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"\r\n");
	printInt(gSystemTable->ConOut,c);
	gSystemTable->ConOut->OutputString(gSystemTable->ConOut, u"\r\n");
}

EFI_STATUS
efi_main(EFI_HANDLE /*image*/, EFI_SYSTEM_TABLE* systemTable)
{
	gSystemTable=systemTable;
	func(30);
	return EFI_SUCCESS;
}

上面定义了 void func(int a, int b=2, int c=3) 这个函数,当通过func(30)调用时,相当于只给 a 赋值 30,其余的直接使用了默认值。

需要注意的是,在使用时有一些限制。比如:C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。

参考:

1. https://c.biancheng.net/view/2204.html C++函数的默认参数详解

UEFI TIPS: Warning C4305

当我们在代码中直接定义浮点数如下所示时,会遇到 Warning C4305: ‘initializing’: truncation from ‘double’ to ‘float’

  float f1=0.12,f2=0.34;

这个警告的意思是:你定义的是一个 double 而非 float 类型。

解决方法有如下2种:

1.更换类型为 double

2.在数值后面加上 “f” 例如:

  float f1=0.12f,f2=0.34f;

ESP32 S3 虚拟摄像头播放 SD 卡内容

前面介绍了使用 ESP32 S3 播放 SPINOR 中的内容,美中不足的是 SPI 容量有限无法播放长视频。这次的作品能够实现读取和发送SD卡中的JPG 图片,从而实现长时间的播放。

实验是基于DFRobot 的ESP32-S3-WROOM-1-N4模组(DFR0896)【参考1】来实现的,需要注意的是:这个模组没有 PSRAM,项目中需要关闭PSRAM。为了读取 SD 卡,需要使用上一次设计的 OV2640 Shield,其中的 SD 卡是4线模式。

插入SD卡,板子堆叠起来即可工作。接下来着手代码设计。

和之前相比,代码改动较大,主要修改有:

  1. 去掉了 LVGL模块和One Button 模块,这样帮助减小代码体积和内存的占用;
  2. 添加了SD 卡初始化代码:
    // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz)
    // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 40MHz for SDMMC)
    // Example: for fixed frequency of 10MHz, use host.max_freq_khz = 10000;
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();
	host.max_freq_khz = 20000;

    // This initializes the slot without card detect (CD) and write protect (WP) signals.
    // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();

    slot_config.width = 4;

    // On chips where the GPIOs used for SD card can be configured, set them in
    // the slot_config structure:
	//ZivDebug_Start
	slot_config.clk = 48;
	slot_config.cmd = 37;
	slot_config.d0 = 10;
	slot_config.d1 = 14;
	slot_config.d2 = 35;
	slot_config.d3 = 36;
	//ZivDebug_End

主要是指定工作频率为 20Mhz (如果你发现读取的时候会出错,不妨尝试降低这个频率);工作模式为4线;另外指定了使用的SD 信号控制线和数据线。

3.接下来,我们修改之前 camera_fb_get_cb() 函数中访问 SPI 的代码,修改为访问SD 卡

char buffer[64];
	struct stat file_stat;
	int  filesize;
	FILE *fd = NULL;
	sprintf(buffer,MOUNT_POINT"/m/%04d.jpg",PicIndex);
	ESP_LOGI(TAG, "p1 %s %d",buffer,PicIndex);
		
	if (stat(buffer, &amp;file_stat) == -1) {
		ESP_LOGI(TAG, "%d frame in %llums",
				PicIndex,
				(esp_timer_get_time()/1000-Elsp));
		Elsp=esp_timer_get_time()/1000;
		PicIndex=0;
		sprintf(buffer,MOUNT_POINT"/m/%04d.jpg",PicIndex);
	} else {PicIndex++;}
	fd = fopen(buffer, "r");
	
    fseek(fd, 0, SEEK_END);  
    filesize = ftell(fd);  
    rewind(fd);
	ESP_LOGI(TAG, "send %d",filesize);

	fread(&amp;PicBuffer, 1, filesize, fd);
	s_fb.uvc_fb.buf = PicBuffer;
	s_fb.uvc_fb.len=filesize;
	fclose(fd); 

基本思路是:尝试访问 m\NNNN.jpg 这样的文件,如果文件存在,那么取得他的大小,如果该文件不存在,说明最后一帧处理完成需要从第一张再开始。之后将文件内容读取到PicBuffer作为返回值返回给调用者。

目前测试的是 320X240 的内容,速度上完全没有问题。

参考:

  1. https://www.dfrobot.com.cn/goods-3536.html

ESP32 S3 虚拟摄像头播放 SPIFFS 内容

这次带来一个好玩的 ESP32 项目:虚拟摄像头,就是将ESP32 S3 的板子烧录之后,系统中会出现一个USB摄像头,打开Camera后能够看到播放出来的视频。

下面介绍具体的实现方式。

目前 Arduino ESP32 尚不支持 USB Camera,因此,这次的项目是基于IDF 来完成的。特别注意:对于硬件有如下要求:

1.必须是 ESP32 S2或者 S3,其他型号的ESP32 目前不支持原生USB编程,所以只能使用 S2 或者 S3;

2.必须带有 PSRAM,因为这个项目是根据Demo 修改而来,Demo 要求带有 PSRAM。我对编译环境不熟悉,这部分没有修改, 理论上移除对于 Camera 的支持即可在没有 PSRAM 的板子上使用;

3.必须是 16MB 的 ESP32 模块,如果想在更小容量的板子上使用,可以删除项目中的JPEG素材缩减体积,同时修改项目配置为 4MB 或者8MB.

如果你对ESP32 IDF环境比较熟悉,可以修改去掉上面提到的2的限制;同样的,可以删除部分图片使得4MB的ESP32 也可以支持。如果你无法做到这两点,可以像我一样使用 ESP32 S3 EYS 兼容版。

先介绍一下如何使用我的代码:

安装 ESP32 IDF 编译环境

2.下载安装 esp-iot-solution,解压后放在c: 根目录下

3.尝试编译C:\esp-iot-solution\examples\usb\device\usb_webcam 确保编译环境无误

4.基本的命令有

               a. 编译命令 idf.py build (特别注意编译时需要联网)

b.烧录 idf.py -p COM端口 flash

c.串口监视器 idf.py -p COM端口 monitor

d.上述指令可以放在一起,例如:

                               idf.py -p com6 build flash monitor

e.监视器可以使用 ctrl+] 退出

f.项目配置 idf.py menuconfig ()

5.将usb_webcam1 解压到C:\esp-iot-solution\examples\usb\device目录下

使用 idf.py -p com6 build flash monitor 编译后会自动烧录然后打开串口监视器。

6.打开系统自带的相机程序,切换到ESP32 摄像头即可看到播放内容

上面介绍了如何直接使用代码,接下来介绍一下项目基本实现原理。

  1. esp-iot-solution 提供了一个USB摄像头的例子。它将自己报告为一个USB相投设备,从板载的摄像头读取数据,然后从USB端口输出;
  2. 我代码的修改是在上报数据中使用SPIFFS存放的数进行替换了摄像头的数据
  3. 下面介绍一下 SPIFFS中存放的内容是如何制作的
  4. 使用 Easy2Convert GIF to JPG 工具将GIF 每一帧转化为 JPG 格式

5. 使用 XnView 处理上面的 JPG 文件。需要将所有的图片名为为 0000、0001…..0XXX 这种名称;同样使用这个软件将所有的图片都修改为 320*240 大小。

修改的代码主要部分在动作就是按照孙旭检查 SPIFFS 中,storage 下面是否有XXXX.jpg 这样的文件,如果有就读取出来作为摄像头数据上报,如果XXXX.JPG 不存在,那么就说明读取完毕,再从 0000开始。

static uvc_fb_t* camera_fb_get_cb(void *cb_ctx)
{
	s_fb.uvc_fb.timestamp.tv_usec++;
	
	char buffer[64];
	struct stat file_stat;
	int  filesize;
	FILE *fd = NULL;
	sprintf(buffer,"/storage/%04d.jpg",PicIndex);
	ESP_LOGI(TAG, "p1 %s %d",buffer,PicIndex);
		
	if (stat(buffer, &amp;file_stat) == -1) {
		PicIndex=0;
		ESP_LOGI(TAG, "ZivHer2");
		sprintf(buffer,"/storage/%04d.jpg",PicIndex);
	} else {PicIndex++;}
	fd = fopen(buffer, "rb");
	ESP_LOGI(TAG, "ZivHer3");
    fseek(fd, 0, SEEK_END);  
    filesize = ftell(fd);  
    rewind(fd);
	ESP_LOGI(TAG, "send %d",filesize);

	fread(&amp;PicBuffer, 1, filesize, fd);
	s_fb.uvc_fb.buf = PicBuffer;
	s_fb.uvc_fb.len=filesize;
	fclose(fd);
	vTaskDelay(pdMS_TO_TICKS(100));
    return &amp;s_fb.uvc_fb;
}