DMU/DCode
前面提到了 PCode/Punit,这次介绍另外的 DCode/ DMU(Die Management Unit)。 这个 IP 是负责 CPU Die 的功耗的(包括大核和小核)。它会控制休眠和工作时的功耗,温度管理,以及 IccMax。同时会参与 Reset 动作。
和前面的类似,PUnit 上面跑的 Firmware 叫做 PCode。
DMU/DCode
前面提到了 PCode/Punit,这次介绍另外的 DCode/ DMU(Die Management Unit)。 这个 IP 是负责 CPU Die 的功耗的(包括大核和小核)。它会控制休眠和工作时的功耗,温度管理,以及 IccMax。同时会参与 Reset 动作。
和前面的类似,PUnit 上面跑的 Firmware 叫做 PCode。
现代处理器变得越来越复杂,唯一不变的是:性能越强需要的功耗越大。为此,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 键盘和主机之间后,你可以用过手机上的 Blinker蓝牙连接到这个设备,然后在 Blinker中输出的信息就会出现在对方的电脑上。
硬件设计如下:
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 下载:
前面编写测试代码的过程中,总感觉没有 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 放在一起,即可编译。
模拟器运行结果如下:
完整代码如下,需要注意的是编译批处理内部使用了绝对路径,如果想实验,最好按照之前的文章架设同样名称的目录测试。
很早之前使用 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会通知当前使用的采样率。
需要注意的是:
具体的数据是下面这样的,可以看到这种同步传输/等时传输的数据和通常的最大区别在于不会有 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");
}
在编译时,还需要对项目进行如下设置:
硬件方面非常简单,Ch554最小系统,喇叭接到对应引脚即可:
这是我设计的用于测试 Ch554 和 Ch559 最小开发板。Ch554和Ch559 最小系统外围只需要2个电容即可,两颗芯片相互独立:
完整代码:
电路图:
从上面可以看到,Ch55xduino提供的 USB 框架扩展性不错。Ch554 可以方便的通过 Ch55xduino 实现一个USB Speaker 的功能。目前美中不足的只是音频质量较差(所有看到的人都怀疑这个是一个收音机),后续会持续进行改进。
代码非常简单,根据官方例子移植到 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:
}
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++函数的默认参数详解
当我们在代码中直接定义浮点数如下所示时,会遇到 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 播放 SPINOR 中的内容,美中不足的是 SPI 容量有限无法播放长视频。这次的作品能够实现读取和发送SD卡中的JPG 图片,从而实现长时间的播放。
实验是基于DFRobot 的ESP32-S3-WROOM-1-N4模组(DFR0896)【参考1】来实现的,需要注意的是:这个模组没有 PSRAM,项目中需要关闭PSRAM。为了读取 SD 卡,需要使用上一次设计的 OV2640 Shield,其中的 SD 卡是4线模式。
插入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, &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(&PicBuffer, 1, filesize, fd);
s_fb.uvc_fb.buf = PicBuffer;
s_fb.uvc_fb.len=filesize;
fclose(fd);
基本思路是:尝试访问 m\NNNN.jpg 这样的文件,如果文件存在,那么取得他的大小,如果该文件不存在,说明最后一帧处理完成需要从第一张再开始。之后将文件内容读取到PicBuffer作为返回值返回给调用者。
目前测试的是 320X240 的内容,速度上完全没有问题。
参考:
这次带来一个好玩的 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 摄像头即可看到播放内容
上面介绍了如何直接使用代码,接下来介绍一下项目基本实现原理。
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, &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(&PicBuffer, 1, filesize, fd);
s_fb.uvc_fb.buf = PicBuffer;
s_fb.uvc_fb.len=filesize;
fclose(fd);
vTaskDelay(pdMS_TO_TICKS(100));
return &s_fb.uvc_fb;
}