2026年2月更新,Step to UEFI 文章索引:
2026年上海慕尼黑电子展
现在的上海正处于梅雨季,连续的阴雨让气温完全没有夏季的感觉。今年的慕尼黑电子展是7月1日到3日,和往年一样,我前往参观。
本次展览在上海新国际博览中心进行,在上海估计地铁是最方面的到达方式,场馆就在龙阳路地铁站旁边。

场馆外的大型海报
刚到时,雨还没有停,门口有着各式各样的大型海报。

RISC-V 的广告牌,相比 ARM ,RISC-V 有着巨大的成本优势

DigiKey 的巨大广告牌,他们是颇有实力的零件分销商

进场之后的海报,很多人在此驻足拍照留念
首先走马观花了Murata(株式会社村田制作所)展位:

Murata展位
这是他家推出的超声波传感器,我不清楚这种和激光测距有什么优势:

可以通过超声波实现定位跟踪
感觉这家企业正在努力转型,从常见的电容电感产品转向更精密的专用传感器,作为老牌元器件厂商,这家企业的营理念强调“磨砺精湛技术、供应独特产品”。

有可以用于测量血压的气泵以及胰岛素泵
我业余时间一直在玩USB相关的内容,特别关注了一下 WCH (沁恒微电子)。

这家公司的 MCU 主推 RISC-V 产品线,有着覆盖从低端到高端的MCU (特别是USB 3.0 MCU ,几乎是普通人能拿到支持的唯一选择)

可以看到目前主要有USB/以太网/蓝牙三大产品线
除了USB目前WCH还在深耕蓝牙和无线技术:

蓝牙无线
现场看到的比较特别的产品有CH390芯片,体积极小的芯片能够轻松的给你产品插上有线网络的翅膀:

CH390 测试板
此外,还有USB 3.0隔离器。在我们的笔记本电脑上,USB 通常是直通 SoC 的(我只在一款 ThinkPad 的 产品上见过USB2.0的隔离设计)。如果在现场使用USB 进行调试,会有电流倒灌损坏 SoC 的风险。这种隔离器能够帮助你安心的使用USB 进行调试。作为BIOS工程师,我真心希望能有公司在研发设计笔记本时以寿命为目标。

此外,这次还见到了CH9338(USB2.0)的下一代 USB3.0 双机互联方案 CH9339,USB3.0 的速度更快,同时还提供了双机同屏的功能。

这里有更详细的产品介绍:


接下来是作为 DIY 爱好者,我经常使用的品牌。
首先是嘉立创集团,在这次展会上他们有2个展位

嘉立创集团的展位
现在可以在嘉立创 FA 进行铝合金外壳定制

可以直接进行产品外壳的定制
作为 DIY 爱好者,我一直使用立创EDA绘制电路,我同EDA负责人进行了友好的交流。

嘉立创 EDA 展台
嘉立创PCB一直在坚持每月2片免费PCB , 帮助无数电子爱好者成长:

立创 EDA 设计出来的电路,可以直接在立创商城下单,这个避免了封装和元件不匹配的问题,同时因为立创硬件开源平台的存在,也能最大限度参考他人设计。用阿里巴巴的话术就是“以用户心智渗透为核心抓手,打通上下游链路,拉通各端口的需求对齐,把全流程的颗粒度拉平,完成从流量触达到价值闭环的全链路赋能,沉淀出可复用的行业方法论,最终实现生态内的反哺与双向共赢。”

立创商城展位
微碧半导体,他们会在文章中分享一些 MOSFET 的设计,比如,电池防止反装。我在设计上用过他们的芯片。


微盟电子是一家南京的企业,有一些 LDO、DC-DC 的产品,如果你电压转换的需求,除了 TI 的手册还可以翻翻他们的产品手册。

厚声集团,我用过他们的很多电阻:

圣邦微电子,在大部分笔记本电脑上都会使用他们的供电芯片方案

优利德,是老牌的国产测量仪器厂商,很多人第一次接触这个品牌都是万用表,这次展出的都是示波器产品。

它的对面是同样做示波器的鼎阳品牌

鼎阳展位
还有 KeySight,当然,这种品牌对于 DIY 用户已经过于高端了

这次还看到了一家国产 MRAM 存储芯片制造商,这是我第一次听说 MRAM,查询资料得知MRAM(Magnetoresistive Random Access Memory) 是一种非易失性的磁性随机存储器,它利用磁电阻效应来存储数据。与传统的半导体随机存取存储器(RAM)不同,MRAM使用磁性隧道结(MTJ)作为存储单元,通过改变磁化方向来记录二进制数据。就是说它不需要电力维持存储,更无惧随机掉电。据说存储速度也很高。只是暂时我还想不出有什么必须的应用场景。

信维通信的站台上看到了微泵液冷的散热方案,不过可惜现场没有相关资料


最后讲个好玩的,没想到 Pro’s Kit(宝工)有一个很大的展位。在我仔细端详的时候,小哥热情的和我聊天。我表示作为DIY爱好者,手上有很多你们的螺丝刀之类的产品。在小哥露出满意的神情后,他问了一个让我和他都后悔的问题:你觉得我们的产品还有什么需要改进的地方吗?我脱口而出:太容易生锈。说出来之后我也觉得有些尴尬。急忙补充道:在公司用没问题,但是家用会生锈。小哥想了想终于又问出来一句:是手柄生锈吗?我回答:螺丝刀的头生锈……之后我也急忙离去避免持续的尴尬。

工业界相比学界更加务实,在整个展览中并没有太多的“AI”概念,相比往年,稍微多了一些机器人伺服电机关节的内容。
元件国产化已经是非常明显的趋势,伴随着这种趋势国产元件也在努力实践中证明自己。选用国产元件可以保证及时的供应和支持。
这次展览展馆分布如下,有兴趣的朋友还可以前往观看。

Step to UEFI (310)EDK2 中使用 LIB加入编译
LIB (Static Library – 静态库)和 DLL (Dynamic Link Library – 动态链接库) 都是库文件,他们存在一些差别。比如,LIB 是编译期加入到文件中,成为 EFI 或者 EXE 的一部分。而DLL则是在运行期独立存放在内存中进行调用的。
这次的实验是:在 VS2019 中使用C生成一个 LIB, 然后在 EDK2 环境下调用这个LIB。
首先编写 LIB ,源代码非常简单,LibFile.c 源代码如下:
///
/// 8-byte unsigned value
///
typedef unsigned long long int UINT64;
///
/// Unsigned value of native width. (4 bytes on supported 32-bit processor instructions,
/// 8 bytes on supported 64-bit processor instructions)
///
typedef UINT64 UINTN;
#define EFIAPI __cdecl // Force C calling convention for Microsoft C compiler
#define IN
UINTN
EFIAPI
MyLibAdd(
IN UINTN A,
IN UINTN B
)
{
return A + B;
}
项目属性中设置编译目标为 Lib:

Debug 信息格式设置为 /Z7:

Runtime Library 设置为 /MTd

编译之后就得到了 LibFile.lib 文件。
接下来在 EDK2 中编写代码。
代码非常简单,关键点在于 extern 告诉链接器将要从外部调用 MyLibAdd() 函数。
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/ShellCEntryLib.h>
extern UINTN EFIAPI MyLibAdd(IN UINTN A,IN UINTN B);
INTN
EFIAPI
ShellAppMain (
IN UINTN Argc,
IN CHAR16 **Argv
)
{
Print(L"LibTest Result: %d \n",MyLibAdd(1,2));
return(0);
}
接下来编写 INF 文件,关键点在于 MSFT:*_*_*_DLINK_FLAGS 告诉 Lib 所在的目录位置。
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = libtest
FILE_GUID = a912f198-7f0e-2026-0429-b757b806ec83
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 0.1
ENTRY_POINT = ShellCEntryLib
#
# VALID_ARCHITECTURES = IA32 X64
#
[Sources]
LibTest.c
[Binaries]
LIB|LibFile.lib
[Packages]
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
[LibraryClasses]
UefiLib
ShellCEntryLib
[BuildOptions]
MSFT:*_*_*_DLINK_FLAGS = /LIBPATH:$(WORKSPACE)/AppPkg/Applications/LibTest LibFile.lib
特别注意:生成的 Lib 可以有 IA32 也可以有 X64, 同时还有 Release 和Debug 版本的区别,不可以混用。相对的,如果你要提供 LIB 给他人使用,那么很可能需要同时提供 Release 和 Debug 两个版本。本次实验测试的是 X64 Lib, Debug 版本。对应的编译命令是(Default 是 Debug 版本):
build -a X64 -p AppPkg\AppPkg.dsc -t VS2019
编译后的文件在模拟器中测试:

本次实验的VC 工程文件在这里下载:
本次实验的 EDK2 代码在这里下载:
使用 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设备描述符,然后编写自己的描述符时,务必数一下最后的描述符长度。