2026年2月更新,Step to UEFI 文章索引:
使用 Ch9338 文件传输
WCH 推出Ch9338 双机互联芯片,前面有介绍和测试过。这次介绍通过编程的方式实现双机文件互传。
在 Ch9338 的 EVT Package 中,有一份《通过CH9338 透传自定义数据说明》。本文基于该文档编写。
调用流程就是文档中描述:

我们编写一个 Windows Console 代码。
// Ch9338Test.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <iostream>
#include <Windows.h>
#include <tchar.h>
#include <Dbt.h>
#include "CH375DLL.H"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <string.h>
#pragma warning(disable:4996)
extern "C"
{
#include "setupapi.h"
}
#pragma comment(lib,"setupapi")
#pragma comment(lib, "WCHKMFU")
//记录设备序号,查找设备时记录
ULONG nDevIndex = 0;
//记录设备路径,查找设备时记录
CHAR szDevicePath[MAX_DEVICE_PATH_SIZE] = "";
//设备句柄
HANDLE hDev = INVALID_HANDLE_VALUE;
//设备的ID
#define szDevID_CH9338_U2 "VID_1A86&PID_8026&MI_01"
#define szDevID_CH9339_U2 "VID_1A86&PID_802A&MI_01"
#define szDevID_CH9339_U3 "VID_1A86&PID_802D&MI_01"
// 如果没有stdint.h,自己定义
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
// 包类型定义
#define TYPE_FILE_INFO 1
#define TYPE_DATA 2
#define TYPE_ACK 3
// 包结构
#pragma pack(1)
typedef struct {
uint16_t type;
uint16_t seq;
uint16_t length;
uint16_t checksum;
} PacketHeader;
#pragma pack()
#define MAX_PACKAGE 1024*16
// 最大数据包大小
#define MAX_DATA_SIZE (MAX_PACKAGE-sizeof(PacketHeader))
#define MAX_FILENAME 256
typedef struct {
uint16_t type;
uint16_t seq;
uint16_t length;
uint16_t checksum;
uint8_t data[MAX_DATA_SIZE];
} SimplePacket;
BOOL SearchDevice()
{
PCHAR lpDevName = NULL;
CHAR szDevName[MAX_DEVICE_PATH_SIZE] = "";
//查找具有指定ID的设备
for (size_t i = 0; i < 16; i++)
{
//获取设备路径
lpDevName = (PCHAR)CH375GetDeviceName(i);
if (lpDevName != NULL)
{
strcpy_s(szDevName, MAX_DEVICE_PATH_SIZE, lpDevName);
CharUpperBuffA(szDevName, strlen(szDevName));
//检查是否具有指定的ID,是则返回
if (strstr(szDevName, szDevID_CH9338_U2) != NULL ||
strstr(szDevName, szDevID_CH9339_U2) != NULL ||
strstr(szDevName, szDevID_CH9339_U3) != NULL)
{
nDevIndex = i;
strcpy_s(szDevicePath, MAX_DEVICE_PATH_SIZE, szDevName);
return TRUE;
}
}
}
return FALSE;
}
//打开设备
BOOL OpenDevice()
{
//打开指定设备
hDev = CH375OpenDevice(nDevIndex);
if (hDev != INVALID_HANDLE_VALUE)
{
return TRUE;
}
return FALSE;
}
// 显示使用方法
void ShowUsage(void) {
printf("用法:\n");
printf("发送文件: Ch9338Test.exe -s <串口> <文件路径>\n");
printf("接收文件: Ch9338Test.exe -r <串口>\n");
printf("例如:\n");
printf(" Ch9338Test.exe -s test.txt\n");
printf(" Ch9338Test.exe -r\n");
}
// 计算校验和
unsigned short CalculateChecksum(const uint8_t* data, uint16_t length) {
uint16_t sum = 0;
int i;
for (i = 0; i < length; i++) {
sum += data[i];
}
return sum;
}
// 创建数据包
void CreatePacket(SimplePacket* pkt, uint16_t type, uint16_t seq,
const uint8_t* data, uint16_t length) {
pkt->type = type;
pkt->seq = seq;
pkt->length = length;
if (data && length > 0) {
memcpy(pkt->data, data, length);
}
pkt->checksum = CalculateChecksum(pkt->data, length);
}
// 打包数据包
int PackPacket(const SimplePacket* pkt, uint8_t* buffer) {
PacketHeader header;
header.type = pkt->type;
header.seq = pkt->seq;
header.length = pkt->length;
header.checksum = pkt->checksum;
memcpy(buffer, &header, sizeof(PacketHeader));
if (pkt->length > 0) {
memcpy(buffer + sizeof(PacketHeader), pkt->data, pkt->length);
}
return sizeof(PacketHeader) + pkt->length;
}
// 解包数据包
int UnpackPacket(const unsigned char* buffer, int bufferSize, SimplePacket* pkt) {
if (bufferSize < sizeof(PacketHeader)) {
return 0; // 数据不足
}
PacketHeader header;
memcpy(&header, buffer, sizeof(PacketHeader));
if (bufferSize < sizeof(PacketHeader) + header.length) {
return 0; // 数据不完整
}
pkt->type = header.type;
pkt->seq = header.seq;
pkt->length = header.length;
pkt->checksum = header.checksum;
if (header.length > 0) {
memcpy(pkt->data, buffer + sizeof(PacketHeader), header.length);
}
// 验证校验和
unsigned short calcChecksum = CalculateChecksum(pkt->data, pkt->length);
if (calcChecksum != pkt->checksum) {
return 0; // 校验失败
}
return 1; // 成功
}
// 发送数据
int SendData(const uint8_t* data, uint32_t length) {
DWORD bytesWritten=length;
if (!CH375WriteEndP((ULONG)hDev, 1, (PVOID) data, (PULONG)&bytesWritten)) {
printf("发送数据失败\n");
return 0;
}
return (bytesWritten == length);
}
// 接收完整的数据包
int ReceivePacket(SimplePacket* pkt) {
unsigned char headerBuffer[sizeof(PacketHeader)];
DWORD bytesRead = MAX_PACKAGE;
if (!CH375SetBufUploadEx((ULONG)hDev, 1, 1, MAX_PACKAGE)) {
printf("CH375SetBufUploadEx failed\n");
return 0;
}
CH375ReadEndP((ULONG)hDev, 1, pkt, &bytesRead);
while (bytesRead == 0) {
bytesRead = MAX_PACKAGE;
if (!CH375ReadEndP((ULONG)hDev, 1, pkt, &bytesRead)) {
printf("CH375ReadEndP error\n");
return 0;
}
Sleep(1);
}
if (!CH375SetBufUploadEx((ULONG)hDev, 0, 1, MAX_PACKAGE)) {
printf("CH375SetBufUploadEx failed\n");
return 0;
}
return UnpackPacket((unsigned char *)pkt, sizeof(PacketHeader) + pkt->length, pkt);
}
// 等待确认包
int WaitForAck() {
SimplePacket ackPkt;
if (!ReceivePacket(&ackPkt)) {
return 0;
}
return ackPkt.type == TYPE_ACK;
}
// 发送文件
int SendFile(const char* filepath) {
FILE* file;
SimplePacket pkt;
unsigned char buffer[MAX_DATA_SIZE];
unsigned char packetBuffer[sizeof(PacketHeader) + MAX_DATA_SIZE];
char filename[MAX_FILENAME];
char fileInfo[MAX_FILENAME + 32];
long filesize;
unsigned short seq = 0;
size_t bytesRead;
long totalSent = 0;
int packetSize;
clock_t start = clock();
// 打开文件
file = fopen(filepath, "rb");
if (!file) {
printf("无法打开文件: %s\n", filepath);
return 0;
}
// 获取文件大小
fseek(file, 0, SEEK_END);
filesize = ftell(file);
fseek(file, 0, SEEK_SET);
// 提取文件名
const char* lastSlash = strrchr(filepath, '\\');
if (!lastSlash) lastSlash = strrchr(filepath, '/');
if (lastSlash) {
strcpy(filename, lastSlash + 1);
}
else {
strcpy(filename, filepath);
}
// 1. 发送文件信息
sprintf(fileInfo, "%s|%ld", filename, filesize);
CreatePacket(&pkt, TYPE_FILE_INFO, 0, (unsigned char*)fileInfo, strlen(fileInfo));
packetSize = PackPacket(&pkt, packetBuffer);
printf("发送文件信息: %s\n", fileInfo);
if (!SendData(packetBuffer, packetSize)) {
printf("SendData Failed\n");
fclose(file);
return 0;
}
if (!WaitForAck()) {
printf("文件信息确认失败\n");
fclose(file);
return 0;
}
// 2. 发送文件数据
while (1) {
bytesRead = fread(buffer, 1, MAX_DATA_SIZE, file);
seq++;
CreatePacket(&pkt, TYPE_DATA, seq, buffer, (unsigned short)bytesRead);
packetSize = PackPacket(&pkt, packetBuffer);
printf("发送数据包 %d, 大小: %d 字节\n", seq, (int)bytesRead);
if (!SendData(packetBuffer, packetSize)) {
fclose(file);
return 0;
}
if (!WaitForAck()) {
printf("数据包 %d 确认失败\n", seq);
fclose(file);
return 0;
}
totalSent += bytesRead;
//printf("进度: %ld/%ld\n", totalSent, filesize);
// 如果是空数据包,表示结束
if (bytesRead == 0) {
break;
}
}
fclose(file);
clock_t end = clock();
printf("文件发送完成, 耗时 %.3fms 速度:%.2fKB/S!\n",
((double)(end - start) / CLOCKS_PER_SEC) * 1000.0,
filesize/1024/ ((double)(end - start) / CLOCKS_PER_SEC));
return 1;
}
// 接收文件
int ReceiveFile() {
SimplePacket pkt;
SimplePacket ackPkt;
unsigned char packetBuffer[sizeof(PacketHeader) + MAX_DATA_SIZE];
char filename[MAX_FILENAME];
char fileInfo[MAX_FILENAME + 32];
char* separator;
long filesize;
unsigned short expectedSeq = 1;
long totalReceived = 0;
FILE* outFile;
int packetSize;
// 1. 接收文件信息
printf("等待接收文件信息...\n");
if (!ReceivePacket(&pkt)) {
printf("未收到文件信息包\n");
return 0;
}
if (pkt.type != TYPE_FILE_INFO) {
printf("收到的不是文件信息包\n");
return 0;
}
// 解析文件信息
memcpy(fileInfo, pkt.data, pkt.length);
fileInfo[pkt.length] = '\0';
separator = strchr(fileInfo, '|');
if (!separator) {
printf("文件信息格式错误\n");
return 0;
}
*separator = '\0';
strcpy(filename, fileInfo);
filesize = atol(separator + 1);
printf("准备接收文件: %s, 大小: %ld 字节\n", filename, filesize);
// 发送确认
CreatePacket(&ackPkt, TYPE_ACK, 0, NULL, 0);
packetSize = PackPacket(&ackPkt, packetBuffer);
if (!SendData(packetBuffer, packetSize)) {
return 0;
}
// 2. 接收文件数据
outFile = fopen(filename, "wb");
if (!outFile) {
printf("无法创建输出文件: %s\n", filename);
return 0;
}
while (1) {
printf("等待数据包 %d...\n", expectedSeq);
if (!ReceivePacket(&pkt)) {
printf("接收数据包失败\n");
fclose(outFile);
return 0;
}
if (pkt.type != TYPE_DATA) {
printf("收到非数据包\n");
continue;
}
if (pkt.seq == expectedSeq) {
// 发送确认
CreatePacket(&ackPkt, TYPE_ACK, pkt.seq, NULL, 0);
packetSize = PackPacket(&ackPkt, packetBuffer);
SendData(packetBuffer, packetSize);
// 检查是否为空数据包(结束标志)
if (pkt.length == 0) {
printf("收到空数据包,传输结束\n");
break;
}
// 写入数据
fwrite(pkt.data, 1, pkt.length, outFile);
totalReceived += pkt.length;
expectedSeq++;
printf("收到数据包 %d, 进度: %ld/%ld\n", pkt.seq, totalReceived, filesize);
}
else {
printf("序列号错误,期望: %d, 收到: %d\n", expectedSeq, pkt.seq);
}
}
fclose(outFile);
printf("文件接收完成: %s\n", filename);
return 1;
}
int main(int argc, char* argv[])
{
// 参数不够,提示
if (argc < 2) {
ShowUsage();
return 1;
}
//查找设备
if (SearchDevice())
{
printf("%s\r\n", szDevicePath);
}
else
{
printf("无法找到 Ch9338 设备\r\n");
return 1;
}
//打开设备
if (OpenDevice())
{
//设置独占设备,防止其他进程操作此设备
if (!CH375SetExclusive((ULONG)hDev, 1))
{
printf("CH375SetExclusive Error\r\n");
}
printf("OpenDevice Successful\r\n");
}
else
{
printf("OpenDevice failed\r\n");
return 2;
}
const char* mode = argv[1];
if (strcmp(mode, "-s") == 0 || strcmp(mode, "/s") == 0) {
// 发送模式
if (argc < 3) {
printf("发送模式需要指定文件路径\n");
ShowUsage();
return 1;
}
const char* filepath = argv[2];
printf("%s\n", filepath);
if (SendFile(filepath)) {
printf("文件发送成功!\n");
}
else {
printf("文件发送失败!\n");
return 1;
}
}
else if (strcmp(mode, "-r") == 0 || strcmp(mode, "/r") == 0) {
//接收模式
if (ReceiveFile()) {
printf("文件接收成功!\n");
}
else {
printf("文件接收失败!\n");
return 1;
}
}
else {
printf("未知模式: %s\n", mode);
ShowUsage();
return 1;
}
//关闭设备
if (hDev != INVALID_HANDLE_VALUE)
{
CloseHandle(hDev);
hDev = INVALID_HANDLE_VALUE;
printf("Close device \r\n");
}
return 0;
}
通讯协议使用之前设计的。
特别注意的是:
1.代码依赖官方提供的 WCHKMFU.lib ,我拿到的只有 32位的,因此代码需要使用 x86 编译
2.根据 Ch375DLL.h 的信息,缓冲区可以最高开到 150MB。缓冲越大,传输效率越高,速度越快。不过我的代码堆限制了局部变量的最大值,如果想改的很大需要优化一些结构。
BOOL WINAPI CH375WriteEndP( // 写出数据块
ULONG iIndex, // 指定CH375设备序号
ULONG iEndP, // 端点号,有效值为1到8。
PVOID iBuffer, // 指向一个缓冲区,放置准备写出的数据
PULONG ioLength ); // 指向长度单元,输入时为准备写出的长度,返回后为实际写出的长度
完整的代码和EXE 下载
【翻译】了解嵌入式 USB2 (eUSB2) 及其用途
对更高处理能力和更低功耗的需求正推动处理器和片上系统 (SoC) 向更先进的低工艺节点发展。对于适用于手机、平板电脑和笔记本电脑的 1.2V 供电 SoC 而言,使用 USB 2.0 接口面临挑战,因为难以支持 3.3V 的 I/O 单元。因此,需要一种低电压 USB 2.0 解决方案来弥补这一差距。
嵌入式 USB 2 (eUSB2) 物理层补充标准是 USB 2.0 规范的补充,旨在满足低电压、高能效 USB 2.0 PHY 解决方案的需求。它消除了小型工艺技术中对 3.3V I/O 信号的需求。
eUSB2 支持 USB 高速、全速和低速三种运行模式,并满足 USB 2.0 L1/L2 链路的电源管理要求。此外,eUSB2 无需对现有的 USB 2.0 软件编程模型进行任何更改。eUSB2 也采用与 USB 2.0 D+ 和 D- 相同的双数据线配置 eD+ 和 eD-。eUSB2 不会影响 Vbus 和电源传输。

eUSB2 PHY 的主要特性:
- 支持高速、全速和低速运行
- 支持单端数字低压信号
- 支持在原生模式下选择单速配置(USB设备通常支持多个速度等级(如Low Speed、Full Speed、High Speed等),连接时会进行速度协商,从最高速度开始尝试,逐步降级,设备需要实现多套PHY和协议栈。这里提到的eUSB的单速度配置,指的是eUSB设备在设计时就预先确定一个特定的速度等级,不进行动态速度协商,只实现该特定速度所需的硬件和软件。这样,设备可以只实现一套Phy 电路,节省成本。)
- 支持基于中继器架构的 USB 2.0 操作
- 支持链路电源管理 LPM-L1 (L1) 和挂起 (L2)
- 支持 eUSB2 设备或中继器配置的寄存器访问协议 (RAP)
- 完全符合 USB 2.0 协议层基本规范
- USB2.0软件编程模型没有变化
- 与 USB 2.0 定义的物理层不兼容
- 与 USB2.0 及其衍生标准定义的 USB2.0 连接器不兼容
eUSB2 有两种主要工作模式:原生模式和中继器模式。
原生模式
eUSB2接口可用于连接同一电路板上的两个设备,如下图所示,其中主机SoC连接到设备SoC。这称为原生模式。原生模式是USB主机和设备之间专用的内部连接。

中继模式
虽然原生模式解决了低电压和低功耗连接的难题,但 eUSB2 信号与 USB2 信号不兼容,因此无法与外部 USB 端口兼容。这就需要 eUSB2 中继器模式。任何支持 eUSB2 的 SoC 都可以与 eUSB2 中继器配合使用,以保持与 USB 生态系统(包括主机、集线器和设备)的互操作性和向下兼容性。

从上面的拓扑图中可以看出,eUSB2 中继器是一个用于在 eUSB2 信号和 USB2 信号之间进行转换的组件。eUSB2 中继器还可以分为两种类型:eUSB2 主机中继器和 eUSB2 外设中继器,如图所示,以方便连接各种类型的设备。
如果用高速公路系统做一个类比的话:
主机中继器 = 高速公路收费站和调度中心(管理交通流量,决定路线)
外设中继器 = 各个出入口匝道(只负责车辆进出,不做路线规划)
可以看到,通常情况下,我们需要的只是外设中继。
Cadence 拥有成熟的验证 IP 解决方案,可用于验证 eUSB2 设计中原生模式和中继模式的各种方面和拓扑结构。更多详情,请参阅Cadence eUSB2 VIP页面或发送电子邮件至support@cadence.com。
使用 Ch32V307 实现一个 USB RGB摄像头
上一次实现了 YUV 摄像头,这次带来的是实现 RGB 摄像头,理论上更容易实现在摄像头上绘制期望的内容。
代码是基于上次 YUV摄像头实现的,修改如下:
1.描述符的修改:需要改为 RGB24 的GUID, 同时需要修改一个点占用多少个Bits(下图中 24Bits==3Bytes)
0x01, // Index of this format descriptor
0x01, // Number of frame descriptors following that correspond to this format
//0x14,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x80,0x00,0x00,0xAA,0x00,0x38,0x9B,0x71, // Globally Unique Identifier used to identify stream-encoding format 59563132-1000-800000AA-389B71
0x7d,0xeb,0x36,0xe4,0x4f,0x52,0xce,0x11,0x9f,0x53,0x00,0x20,0xaf,0x0b,0xa7,0x70,
0x18, // Number of bits per pixel used to specify color in the decoded video frame
0x01,
2。下面描述符中需要根据分辨率计算进行填充
/*CS_INTERFACE Descriptor (46 bytes) */
0x2E, // Descriptor size is 46 bytes
0x24, // CS_INTERFACE Descriptor Type
0x05, // VS_FRAME_UNCOMPRESSED descriptor subtype
0x01, // Index of this frame descriptor
0x01, // D0: Still image supported 1
0xA0,0x00, // Width of decoded bitmap frame in pixels
0x78,0x00, // Height of decoded bitmap frame in pixels
0x00,0x08,0x07,0x00, // 最小的bps,注意计算方法是:图像长度x宽度x3x8x最慢的fps. 最后的单位是 bps,这里是 160*120*3*8*1=0x70800
0x00,0x18,0x15,0x00, // 最大的bps,注意计算方法是:图像长度x宽度x3x8x最快的fps. 最后的单位是 bps,这里是 160*120*3*8*3=0x151800
0x00,0xE1,0x00,0x00, // 一张图片的尺寸:图像长度x宽度x3
0xD5,0xDC,0x32,0x00, // 指定下面的哪个作为默认的FPS
0x05, // 这里给出下面有多少种 FPS
0xD5,0xDC,0x32,0x00, // FPS 这里是 3,333,333 ,单位是 100ns, 因此实际表示 333,333,300ns 就是 333ms,算下来是3FPS, 最快3FPS
0x00,0x09,0x3D,0x00, // Shortest frame interval supported (at highest frame rate), in 100 ns units
0x40,0x4B,0x4C,0x00, // Shortest frame interval supported (at highest frame rate), in 100 ns units
0xE0,0x70,0x72,0x00, // Shortest frame interval supported (at highest frame rate), in 100 ns units
0x80,0x96,0x98,0x00, // FPS 这里是10,000,000 ,单位是 100ns, 因此实际表示 1,000,000,000ns 就是 1s,算下来是1FPS,最慢1FPS
3.此外,修改下面几个请求的返回值
/* GET_CUR Video_Streaming */
const uint8_t GET_CUR_VideoStreaming[ ] =
{
0x00,0x00,0x01,0x01,0x15,0x16,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0xE1,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x24,0xF4,0x00,0x00,0x00,
0x00,0x00
};
上述数据中0x00 0xE1 是一帧的大小,0xFE 是一个USB包能放置的数据大小(256-2)。
GET_CUR Video_Streaming 用于获取当前 Video Streaming(视频流)接口的配置参数。
接下来的2个和上面的含义类似:
const uint8_t GET_MAX_VideoStreaming[ ]=
{
0x00,0x00,0x01,0x01,0x55,0x58,0x14,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0xE1,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x24,0xF4,0x00,0x00,0x00,
0x00,0x00
};
const uint8_t GET_MIN_VideoStreaming[ ]=
{
0x00,0x00,0x01,0x01,0x15,0x16,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0xE1,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x24,0xF4,0x00,0x00,0x00,
0x00,0x00
};
4.写一个随机数生成器,每次使用随机数填充作为显示内容。
uint32_t seed=1;
uint8_t random() {
seed=seed*1103515245+12345;
return (uint8_t)((seed>>16)&0xFF);
}
uint8_t color=80;
const uint16_t TOTAL=160*120*3;
uint16_t leftLength=TOTAL;
void USBFS_Endp_ZSend (void) {
if (USBFS_Endp_Busy[3] == 0) { // 可以发送
for (int i=2;i<256;i++) {
USBFS_EP3_Buf[i]=random();
}
if (leftLength==TOTAL) {
if (USBFS_EP3_Buf[1] == 0x80) {
USBFS_EP3_Buf[1] = 0x81;
} else {
USBFS_EP3_Buf[1] = 0x80;
}
}
if (leftLength>254) {
USBFSD_UEP_TLEN (3)=256;
leftLength=leftLength-254;
} else {
USBFSD_UEP_TLEN (3)=leftLength+2;
leftLength=0;
}
//printf ("%d %x %x %x\r\n", leftLength, USBFSD_UEP_TLEN (3), USBFS_EP3_Buf[1], USBFS_EP3_Buf[2]);
USBFSD_UEP_TX_CTRL (3) = (USBFSD_UEP_TX_CTRL (3) & ~USBFS_UEP_T_RES_MASK) | USBFS_UEP_T_RES_NONE;
USBFS_Endp_Busy[3] = 1;
if (leftLength == 0) {
leftLength=TOTAL;
}
}
}
工作的测试视频
完整的项目代码
Step to UEFI (309)UEFI 下BMP转JPG 的程序
这次是一个比较完美的程序,可以在UEFI Shell 下将BMP图片转为 JPEG图片。项目来自 https://github.com/MikeWang000000/wsjpeg 。同样是一个单文件项目。
编译的时候加入了一些关闭 Warning 的动作,完整的 INF如下:
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = BMP2JPG
FILE_GUID = 4ea97c46-2026-0429-b445-747010f3ce5f
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 0.1
ENTRY_POINT = ShellCEntryLib
#
# VALID_ARCHITECTURES = IA32 X64
#
[Sources]
BMP2JPG.c
[Packages]
StdLib/StdLib.dec
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
LibC
LibStdio
DevShell
LibMath
[BuildOptions]
MSFT:*_*_*_CC_FLAGS = /wd4114 /wd4244 /wd4305
在模拟器中测试,使用方法是 bmp2jpg.efi [输入文件名] [质量0-100,从差到好]
100%质量压缩,源文件和压缩后的 JPG 基本上相同

选择 1%压缩后的结果明显变差:

完整的代码和测试数据在这里可以下载:
设计一个简单的文件传输协议
1.文件头设计
| Type | Seq | Length | Checksum |
| 2 Byte | 2 Bytes | 2 Bytes | 2 Bytes |
2.包设计
a.文件信息包
| Type | Seq | Length | Checksum | Protocol Version | FileName Length | FileSize | FileName |
| TYPE_FILE_INFO | 00 00 | 包括文件头的总长度 | 2 Bytes | 协议版本 1 byte | 1 Byte | 4 Bytes | N Bytes |
b.数据包
| Type | Seq | Length | Checksum | PayLoad |
| TYPE_DATA | NN MM | 包括文件头的总长度 | 4 Bytes | XX Byte |
c. 确认包
| Type | Seq | Length | Checksum |
| TYPE_ACK | NN MM 收到的前面包序号 | 包括文件头的总长度 | 4 Bytes |
d.结束包
| Seq | Length | Checksum | |
| TYPE_END | NN MM | 包括文件头的总长度 | 4 Bytes |
为了验证上述协议,编写一个 C 代码,在 VS2019 中编译成功,使用串口通讯。理论上串口是流通讯,并不是基于包的通讯。但是实际上我们使用的USB 转串口模块之类的,存在一个缓冲区的问题,特别是对于速度很高的情况(例如,6Mbps),收下来的数据都是先缓存在设备内部的,如果取走的速度比进入的速度快就会有数据丢失的问题。这种情况下,通讯包使用缓冲区一样大小的尺寸效率最高。
用于测试的C代码:
编译后的 exe
使用方法:
发送:
FileTransferProtocolTest.exe -s com17 FileTransferProtocolTest.pdb
接收:
FileTransferProtocolTest.exe -r com16
VS2019 编译 7-Zip的方法
1.首先在官方网站下载 Source Code:https://7-zip.org/download.html
2.打开 VS2019 X64 (注意必须 native x64 command 窗口)
3.进入 cd CPP\7zip 目录下, 运行 NMake 即可


Step to UEFI (308)UEFI Shell 下的 PPM转JPEG 程序
经过大量的测试和研究,终于找到一个可以工作的代码,能够将PPM转为JPEG 格式(通过前面的文章,我们也知道PPM和BMP 区别不大)。
项目来自 https://github.com/schiermike/jpeg-encoder/tree/master ,只有一个文件,能将PPM 转为 JPG 格式。
代码很长,这里就不列出了,有兴趣的可以直接下载研究。
需要注意的是:
1.只能除了宽度是16整数倍的图片(实际使用中,通常的处理方法是先填充到16的整数倍,处理完成之后再裁剪)

2.代码颜色上应该还有一些问题,估计是颜色排列错误

处理后的结果是

3.测试 Lenna图片

处理之后的结果

4.代码对于 PPM 格式有一些要求,文件头的参数需要用 0x0A 来进行分割,如果换成 0x0D 之类的会报错。

完整的代码和测试数据下载:
力科的 USB 抓包工具的小Bug
最近在Ch32V307上实现 USB Camera 的功能,使用之前的一个设计作为参考。结果在照抄的描述符的时候时偶然发现力科的USB T3 存在一个小Bug。
问题的描述为:描述符解析时,部分值不会反映的 Hex Value中:
最典型的是下面这个 GUID, 解析之后只有部分值,其余部分被丢掉了,如果你认为这个Field 的值为 0x32315659 只有4字节,会导致后续的描述符完全错位:

上面这种相对明显,因为 明确知道GUID应该是 16Bytes,但是下面这个就比较隐蔽,如果只将 Hex Value拷贝出来,会导致对应的结构体会不够 12 Bytes。

因此,使用工具 Dump USB设备描述符,然后编写自己的描述符时,务必数一下最后的描述符长度。
ESP32S3+MSM261S4030H0+HT513 扬声器
去年使用ESP32S3+MSM261S4030H0+HT513 做了一个扬声器的项目。具体硬件设计可以在 https://oshwhub.com/arduinoai-hao-zhe/portable-speake 看到。
在使用的时候,遇到的一个问题就是MSM261S4030H0输出是32Bits(实际只有24Bits)。最近忽然想起来查了一下是否有机会按照 16Bits输出,搜索出来的资料说的是:只要设置 I2S Master按照16Bits 取即可。因为这个输出有效数据只有 24Bits,直接截断取前面的16Bit即可。
根据这个资料编写代码如下,工作正常。
#include <Arduino.h>
#include <Wire.h>
#include <driver/i2s.h>
#define I2S_PORT1 I2S_NUM_0
#define I2S_PORT2 I2S_NUM_1
#define SAMPLE_RATE 16000
#define CHANNEL_FORMAT I2S_CHANNEL_FMT_ONLY_LEFT
//#define BITS_PER_SAMPLE I2S_BITS_PER_SAMPLE_32BIT
#define BITS_PER_SAMPLE I2S_BITS_PER_SAMPLE_16BIT
#define MIC_BCK_PIN 37
#define MIC_WS_PIN 38
#define MIC_DATA_PIN 36
#define SPK_BCK_PIN 15
#define SPK_WS_PIN 6
#define SPK_DATA_PIN 7
#define SPK_MCK_PIN 16
// HT513 音量
uint16_t Volume;
#define TOLENCE 16
#define HT513_ADDR_L 0x6c
/**
@brief ht513写寄存器
@param addr 寄存器地址
@param val 要写的值
@retval None
*/
void HT513_WriteOneByte(uint8_t addr, uint8_t val)
{
Wire.beginTransmission(HT513_ADDR_L);
Wire.write(addr);
Wire.write(val);
int ack = Wire.endTransmission(true);
Serial.print("Ack ");
Serial.println(ack, HEX);
}
/**
@brief ht513读寄存器
@param addr 寄存器地址
@retval 读取到的寄存器值
*/
uint8_t HT513_ReadOneByte(uint8_t addr)
{
uint8_t temp = 0;
Wire.beginTransmission(HT513_ADDR_L);
Wire.write(addr);
Wire.endTransmission(false);
uint8_t bytesReceived = 0;
bytesReceived = Wire.requestFrom(HT513_ADDR_L, (uint8_t)1, true);
if (bytesReceived == 1) {
temp = Wire.read();
}
else {
Serial.println("Read Error ");
}
return temp;
}
void setup() {
Serial.begin(115200);
delay(2000);
i2s_config_t MIC_i2sConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channel_format = CHANNEL_FORMAT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t MIC_pinConfig = {
.bck_io_num = MIC_BCK_PIN,
.ws_io_num = MIC_WS_PIN,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = MIC_DATA_PIN
};
i2s_driver_install(I2S_PORT1, &MIC_i2sConfig, 0, NULL);
i2s_set_pin(I2S_PORT1, &MIC_pinConfig);
i2s_config_t SPK_i2sConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channel_format = CHANNEL_FORMAT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t SPK_pinConfig = {
.mck_io_num = SPK_MCK_PIN,
.bck_io_num = SPK_BCK_PIN,
.ws_io_num = SPK_WS_PIN,
.data_out_num = SPK_DATA_PIN,
.data_in_num = I2S_PIN_NO_CHANGE
};
i2s_driver_install(I2S_PORT2, &SPK_i2sConfig, 0, NULL);
i2s_set_pin(I2S_PORT2, &SPK_pinConfig);
analogReadResolution(9);
Wire.begin(18, 17);
// 设置 SD 为LOW
HT513_WriteOneByte(0x12, 0b11110000);
// 设置数据格式为 I2S, 16Bits
HT513_WriteOneByte(0x13, 0b00110000);
// 读取音量设置
Volume = analogRead(3);
uint8_t Vol = (Volume, 0, 511, 0x07, 0xff);
HT513_WriteOneByte(0x16, Vol);
HT513_WriteOneByte(0x15, Vol);
Serial.println(Volume, HEX);
// 调整声道
HT513_WriteOneByte(0x17, 0b10110000);
Serial.println("++++++++++++++++");
// 设置 SD 为HIGH
HT513_WriteOneByte(0x12, 0b11110100);
uint8_t Value = HT513_ReadOneByte(0x12);
Serial.println(Value, HEX);
Value = HT513_ReadOneByte(0x13);
Serial.println(Value, HEX);
Value = HT513_ReadOneByte(0x16);
Serial.println(Value, HEX);
Value = HT513_ReadOneByte(0x17);
Serial.println(Value, HEX);
Serial.print(" starting");
// dsp.beginAEC(256, 1024, 16000); // Initialize AEC with frame size, filter length, and sample rate
// dsp.enableAEC(true); // Enable AEC
}
int8_t mic[512], speaker[521], out[512];
void loop() {
size_t bytesRead = 0;
i2s_read(I2S_PORT1, &mic, sizeof(mic), &bytesRead, portMAX_DELAY);
i2s_write(I2S_PORT2, &mic, sizeof(mic), &bytesRead, portMAX_DELAY);
if (abs(analogRead(3) - Volume) > TOLENCE) {
// 读取音量设置
Volume = analogRead(3);
// 设置 SD 为LOW
HT513_WriteOneByte(0x12, 0b11110000);
uint8_t Vol = map(Volume, 0, 511, 0x07, 0xff);
HT513_WriteOneByte(0x16, Vol);
// 设置 SD 为HIGH
HT513_WriteOneByte(0x12, 0b11110100);
Serial.print(analogRead(3), HEX);
Serial.print(" ");
Serial.print(Volume, HEX);
Serial.print(" ");
Serial.println(Vol, HEX);
}
}
Step to UEFI (307)UEFI 下使用PRIu8 以及类似宏
上次的代码中碰到了C语言中的 PRIu8 ,这次专门研究了一下如何在代码中直接使用。
<inttypes.h> 是 C 标准库提供的头文件,其中有定义(例如EDK2 中的StdLib\Include\X64\machine\int_fmtio.h文件):
#define PRIu8 "u" /* uint8_t */
#define PRIu16 "u" /* uint16_t */
#define PRIu32 "u" /* uint32_t */
它们C99 标准引入的格式说明符宏。可以通过下面的这种方法进行使用:
printf("Value: %" PRIu16 "\n", value);
在 C 语言中,当你编写多个字符串字面量(例如使用双引号括起来的字符串)在代码中相邻放置时,编译器会自动将它们连接起来形成一个单独的字符串,这被称作C 语言的字符串字面量的自动连接,所以上述代码展开后就是:
printf("Value: %u \n", value);
在EDK2 中编译一个简单的代码进行验证:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <Uefi.h>
#include <Library/UefiLib.h>
#define LABZTest "LABZ"
int main(int argc, char *argv[]) {
printf("Value: " LABZTest "\n");
return 0;
}
模拟器中运行结果如下:

上述试验说明 VC2019 完全支持字符串字面量的自动连接。
所以理论上只要引用了inttypes.h 就可以使用PRIu8 这种宏定义,但是还是会遇到编译期的错误。经过研究发现 \StdLib\Include\inttypes.h 中相关引用是被注释掉的。下图中左侧是原始的代码,右侧是修改后的。完全无法理解原始代码要去掉这个引用,经过这样的修改即可正常编译和执行:

完整的测试代码:
#include <stdio.h>
#include <inttypes.h>
#include <Uefi.h>
#include <Library/UefiLib.h>
int main(int argc, char *argv[]) {
printf("Value: %" PRIu8 "\n",1234);
return 0;
}
模拟器中测试,得到了期望的结果:

完整的代码下载: