先快速浏览一下整体方案:借助 STM32CubeMX 配置工具和 HAL 库,将 USB Device 配置为 CDC 类,从而实现虚拟串口功能。整个流程并不复杂,关键在于几个核心配置与代码细节,按步骤操作基本不会出错。

一、硬件准备清单
| 项目 | 说明 |
|---|---|
| MCU | STM32F103C8T6 |
| USB 引脚 | PA11(DM)、PA12(DP) |
| 上拉电阻 | DP 需 1.5kΩ 上拉到 3.3V(多数开发板已集成) |
| 晶振 | 8 MHz 外部晶振(必须使用) |
| Boot0 | 接地(0),从 Flash 启动 |
二、CubeMX 关键配置步骤
1、SYS 设置
Debug:Serial Wire Timebase Source:SysTick
2、RCC 配置
HSE:Crystal/Ceramic Resonator LSE:Disable
3、USB 外设
USB Device(FS) Class:Communication Device Class (CDC)
4、USB_DEVICE 中间件
Class:CDC USBD_CDC_APP:Enabled
5、时钟树配置
HSE = 8 MHz
PLL Source = HSE
PLL Mul = x9
SYSCLK = 72 MHz
USB Prescaler = PLLCLK / 1.5 = 48 MHz
特别注意:USB 时钟必须精确锁定 48 MHz,毫厘之差都会导致设备枚举失败——时钟不对,电脑完全无法识别。
三、生成代码后必须手动修改的地方
1、开启 USB 中断
CubeMX 有时会遗漏这一步,需要手动补充:
/* USER CODE BEGIN 2 */
HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
/* USER CODE END 2 */
2、CDC 接收回调函数(核心环节)
在 usbd_cdc_if.c 中找到以下回调函数,并在其中将接收到的数据转发出去:
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
extern void USB_CDC_RxHandler(uint8_t *buf, uint32_t len);
USB_CDC_RxHandler(Buf, *Len);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
/* USER CODE END 6 */
}
3、自行封装接收处理函数
单独建立一个文件或在主循环中编写,用于处理接收到的数据:
/* usb_cdc.c */
#include "usbd_cdc_if.h"
void USB_CDC_RxHandler(uint8_t *buf, uint32_t len)
{
if (len == 0) return;
// 示例功能:原封不动返回(实现回显)
CDC_Transmit_FS(buf, len);
}
4、发送函数封装
同样将发送接口封装一下,方便后续调用:
#include "usbd_cdc_if.h"
void USB_CDC_SendString(char *str)
{
CDC_Transmit_FS((uint8_t*)str, strlen(str));
}
四、main.c 初始化流程
在主程序中调用 USB 初始化,之后就可以像操作普通串口一样收发数据:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USB_DEVICE_Init(); // 初始化 USB CDC
USB_CDC_SendString("STM32F103 USB CDC Ready!\r\n");
while (1)
{
USB_CDC_SendString("Hello USB CDC\r\n");
HAL_Delay(1000);
}
}
五、PC 端现象验证
| 项目 | 结果 |
|---|---|
| 设备管理器 | 出现 COMx 端口 |
| 波特率 | 任意设置均可(实际被忽略) |
| 串口工具 | 可正常收发数据 |
| 供电 | 直接由 USB 供电即可 |
完全无需额外购买 USB 转 TTL 模块,一根 USB 线即可同时完成供电与通信。
六、常见问题与排查方法
1. 电脑无法识别设备
请检查以下几个关键点:
- 外部晶振是否为 8 MHz?
- USB 时钟是否精确为 48 MHz?
- PA11 / PA12 引脚是否被其他功能复用了?
2. 枚举失败或显示为未知设备
解决方案:
- 更换一根质量合格的 USB 数据线(很多线仅支持充电,无法传输数据)
- 在 PA11/PA12 上串联 22Ω 电阻
- 尽量缩短 USB 线缆长度
3. 发送数据时程序卡死
根本原因在于上一次发送尚未完成,又立即调用了 CDC_Transmit_FS。简单的处理方式如下:
while (CDC_Transmit_FS(buf, len) != USBD_OK) {
HAL_Delay(1);
}
4. 接收数据不完整
USB 单次传输最多只能携带 64 字节的数据包。如果需要一次性接收更长数据,必须在代码中自行实现拼包逻辑——使用环形缓冲区是最稳妥的方案。
七、进阶扩展
USB + printf 重定向
int _write(int fd, char *ptr, int len)
{
CDC_Transmit_FS((uint8_t*)ptr, len);
return len;
}
USB 与串口共存
用 USB 虚拟串口输出调试日志,USART1 则留给外设模块通信,分工明确。
USB + 上位机协议
设计自定义帧头与 CRC 校验,可进一步扩展实现 Bootloader 或参数配置功能。
八、工程级建议汇总
| 项目 | 建议 |
|---|---|
| 开发环境 | CubeMX + HAL 库 |
| 调试顺序 | 先确保 USB 枚举成功 |
| 通信协议 | 包含长度字段与校验机制 |
| 实测速度 | 可达 800 Kbps |
