1. 项目概述
最近在做一个需要STM32与PC高速通信的项目,传统的串口速率已经不能满足需求,于是决定尝试用USB HID协议实现虚拟串口通信。选择STM32F105这款带USB OTG功能的芯片,折腾了一周终于调通,把完整开发过程记录下来分享给大家。
USB HID设备最大的优势就是免驱(Windows系统自带驱动),而且传输速度比普通串口快很多。虽然HID协议本身有64字节的包大小限制,但通过合理分包处理,实测可以达到1Mbps以上的有效传输速率,完全能满足大多数嵌入式应用场景。
2. 硬件准备与原理
2.1 硬件选型要点
这次用的是STM32F105RBT6核心板,选择时要注意几个关键点:
- 必须带USB FS/OTG功能(PA11/PA12引脚)
- 外部晶振建议8MHz(方便配置到72MHz主频)
- 最好有板载USB Type-C或Micro-B接口
我的硬件配置清单:
- STM32F105RBT6开发板(带USB接口)
- USB Type-A转Type-C数据线(注意要选数据线,不是充电线)
- ST-Link V2调试器
- USB转串口模块(用于调试信息输出)
注意:如果板子没有USB接口,需要自行连接PA11(DM)和PA12(DP)到USB插座的对应引脚,记得串联22Ω电阻做阻抗匹配。
2.2 USB HID通信原理
USB HID协议本质上是通过报告描述符(Report Descriptor)定义数据格式。虚拟串口的实现原理是:
- 自定义一个HID报告描述符,声明64字节的输入/输出报告
- PC端通过HID API读写这些报告数据
- MCU端将收到的HID报告数据当作串口数据解析
与标准CDC虚拟串口相比,HID方案的优点是:
- 无需安装额外驱动
- 协议栈更简单,资源占用少
- 兼容性更好(连老旧的WinXP都支持)
缺点是:
- 固定64字节包大小(需要自己处理分包)
- 没有标准的串口控制信号(如DTR/RTS)
3. 开发环境搭建
3.1 软件工具准备
开发环境我选择STM32CubeIDE + STM32CubeMX组合,版本信息如下:
- STM32CubeIDE 1.15.0
- STM32CubeF1固件库 V1.8.5
- Windows驱动:STTub30(用于ST-Link)
安装时要注意:
- 先装Java运行时环境(STM32CubeIDE依赖)
- 安装路径不要有中文和空格
- 安装完成后更新Cube库到最新版本
3.2 工程创建步骤
-
打开STM32CubeIDE,新建工程:
- 选择"Start new STM32 project"
- 在MCU选择器输入"STM32F105R8"(兼容RBT6)
-
配置时钟树:
- HSE选择"Crystal/Ceramic Resonator"
- PLLMUL设为x9
- 确保USB时钟是48MHz(72MHz/1.5)
-
USB外设配置:
- 模式选择"Device Only"
- Class选择"HID"
- 端点大小设为64字节
-
调试串口配置(可选但推荐):
- 启用USART1
- 波特率115200
- 8N1格式
4. 关键代码实现
4.1 HID报告描述符
这是整个项目的核心,定义了数据格式:
c复制__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[] __ALIGN_END = {
0x06, 0x00, 0xFF, // USAGE_PAGE (Vendor Defined)
0x09, 0x01, // USAGE (Vendor Usage 1)
0xA1, 0x01, // COLLECTION (Application)
0x09, 0x02, // USAGE (Vendor Usage 2)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x40, // REPORT_COUNT (64)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x09, 0x03, // USAGE (Vendor Usage 3)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x40, // REPORT_COUNT (64)
0x91, 0x02, // OUTPUT (Data,Var,Abs)
0xC0 // END_COLLECTION
};
这段描述符定义了一个64字节的输入报告和64字节的输出报告,每个字节的取值范围是0-255。
4.2 数据收发实现
发送函数封装:
c复制uint8_t USBD_CUSTOM_HID_SendData_FS(uint8_t *pData, uint16_t Length) {
uint8_t result = USBD_FAIL;
if((pData != NULL) && (Length <= VCOM_DATA_BUFFER_SIZE)) {
memset(HID_SendBuffer, 0, VCOM_DATA_BUFFER_SIZE);
memcpy(HID_SendBuffer, pData, Length);
result = USBD_HID_SendReport(&hUsbDeviceFS, HID_SendBuffer, VCOM_DATA_BUFFER_SIZE);
}
return result;
}
接收回调函数:
c复制void USBD_CUSTOM_HID_ReceiveData_FS(uint8_t *pData, uint16_t Length) {
if((pData != NULL) && (Length > 0)) {
// 打印接收到的数据到调试串口
printf("HID Received: ");
for(int i=0; i<Length; i++) {
if(pData[i] >= 32 && pData[i] <= 126) {
putchar(pData[i]); // 打印可显示字符
} else {
printf("[%02X]", pData[i]); // 非打印字符显示为十六进制
}
}
printf("\r\n");
// 回显数据(可选)
USBD_CUSTOM_HID_SendData_FS(pData, Length);
}
}
5. 调试与优化技巧
5.1 常见问题排查
-
设备无法识别
- 检查USB DP(D+)引脚是否有1.5k上拉电阻
- 确认USB时钟准确配置为48MHz
- 用USB分析仪抓包看枚举过程
-
数据传输不稳定
- 确保每次发送前清空缓冲区
- 添加重试机制(特别是发送失败时)
- 检查电源稳定性(USB供电不足会导致异常)
-
接收数据丢失
- 在OutEvent回调中及时读取数据
- 考虑使用双缓冲机制
- 增加流量控制(如XON/XOFF)
5.2 性能优化方案
- 分包传输优化
c复制#define PKT_HEADER 0x55AA
#define PKT_TAIL 0xAA55
typedef struct {
uint16_t header;
uint16_t seq;
uint16_t len;
uint8_t data[60];
uint16_t crc;
uint16_t tail;
} HID_Packet;
void SendLargeData(uint8_t *data, uint32_t len) {
HID_Packet pkt;
uint16_t seq = 0;
while(len > 0) {
pkt.header = PKT_HEADER;
pkt.seq = seq++;
pkt.len = len > 60 ? 60 : len;
memcpy(pkt.data, data, pkt.len);
pkt.crc = Calculate_CRC(data, pkt.len);
pkt.tail = PKT_TAIL;
USBD_CUSTOM_HID_SendData_FS((uint8_t*)&pkt, sizeof(pkt));
data += pkt.len;
len -= pkt.len;
HAL_Delay(1); // 适当延时防止堵塞
}
}
- 低功耗处理
c复制void HAL_PCD_SuspendCallback(PCD_HandleTypeDef *hpcd) {
// 进入低功耗前关闭外设
HAL_UART_DeInit(&huart1);
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
// 进入STOP模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
}
void HAL_PCD_ResumeCallback(PCD_HandleTypeDef *hpcd) {
// 唤醒后重新初始化
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
}
6. 实际应用建议
- 数据校验机制
建议在应用层添加CRC校验,特别是传输重要数据时:
c复制uint16_t Calculate_CRC(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for(uint16_t i=0; i<len; i++) {
crc ^= data[i];
for(uint8_t j=0; j<8; j++) {
if(crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
- 流量控制方案
当MCU处理不过来时,可以实现简单的XON/XOFF控制:
c复制#define XON 0x11
#define XOFF 0x13
volatile uint8_t flow_control = XON;
void USBD_CUSTOM_HID_ReceiveData_FS(uint8_t *pData, uint16_t Length) {
if(pData[0] == XON) {
flow_control = XON;
} else if(pData[0] == XOFF) {
flow_control = XOFF;
} else if(flow_control == XON) {
// 正常处理数据
Process_Data(pData, Length);
}
}
- 多接口复合设备
如果需要同时实现HID和CDC功能,可以配置为复合设备:
c复制// 在CubeMX中:
// 1. 启用USB_DEVICE
// 2. 添加HID和CDC两个Class
// 3. 修改描述符合并两个接口
// 注意:Windows可能需要自定义驱动才能识别复合设备
7. 开发心得
调试USB HID设备最大的坑就是报告描述符的配置,我总结了几个关键点:
- 报告描述符中的LOGICAL_MAXIMUM必须正确设置,否则Windows会拒绝设备
- 输入和输出报告的大小要匹配端点配置
- 每次发送数据前最好清空缓冲区,避免残留数据干扰
- 调试时先用现成的HID工具测试(如HIDAPI示例程序),再开发自己的PC端软件
实测这个方案在Win10/11和Linux上都能即插即用,传输速度稳定在800KB/s左右,比普通串口快很多。如果需要更高速率,可以考虑改用CDC或者自定义USB类,但就得面对驱动安装的问题了。