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

原文在:https://community.cadence.com/cadence_blogs_8/b/fv/posts/understanding-embedded-usb2-eusb2-and-its-usage

使用 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.文件头设计

TypeSeqLengthChecksum
2 Byte2 Bytes2 Bytes2 Bytes

2.包设计

a.文件信息包

TypeSeqLengthChecksumProtocol VersionFileName LengthFileSizeFileName
TYPE_FILE_INFO00 00包括文件头的总长度2 Bytes协议版本 1 byte1 Byte4 BytesN Bytes

b.数据包

TypeSeqLengthChecksumPayLoad
TYPE_DATANN MM包括文件头的总长度4 BytesXX Byte

c. 确认包

TypeSeqLengthChecksum
TYPE_ACKNN MM 收到的前面包序号包括文件头的总长度4 Bytes

d.结束包

 SeqLengthChecksum
TYPE_ENDNN MM包括文件头的总长度4 Bytes

为了验证上述协议,编写一个 C 代码,在 VS2019 中编译成功,使用串口通讯。理论上串口是流通讯,并不是基于包的通讯。但是实际上我们使用的USB 转串口模块之类的,存在一个缓冲区的问题,特别是对于速度很高的情况(例如,6Mbps),收下来的数据都是先缓存在设备内部的,如果取走的速度比进入的速度快就会有数据丢失的问题。这种情况下,通讯包使用缓冲区一样大小的尺寸效率最高。

用于测试的C代码:

编译后的 exe

使用方法:

发送:

FileTransferProtocolTest.exe -s com17 FileTransferProtocolTest.pdb

接收:

FileTransferProtocolTest.exe -r com16

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设备描述符,然后编写自己的描述符时,务必数一下最后的描述符长度。