文章目录
-
- 前言
- [一、为什么用 USB CDC 而不是外接 CH340](#一、为什么用 USB CDC 而不是外接 CH340)
- 二、核心原理
- [三、CubeMX 配置](#三、CubeMX 配置)
- [四、核心代码实现(HAL 库)](#四、核心代码实现(HAL 库))
-
- [4.1 printf 整包重定向(关键)](#4.1 printf 整包重定向(关键))
- [4.2 接收回调(必须重新使能接收)](#4.2 接收回调(必须重新使能接收))
- [4.3 主循环发送测试](#4.3 主循环发送测试)
- 五、测试验证(量化数据)
-
- [5.1 枚举与基本通信](#5.1 枚举与基本通信)
- [5.2 吞吐对比(实测,F103 @72MHz)](#5.2 吞吐对比(实测,F103 @72MHz))
- 六、故障排查(六类)
- 七、总结
- 参考资料
摘要:调试 STM32 时,外接 CH340 要装驱动、电平转换,还被 115200 波特率天花板卡着,接线乱。本文基于 STM32F103C8T6 自带 USB,用 CubeMX 配 CDC,变免驱虚拟串口:一根 USB 线直连 PC,Win10+ 自动识别。实测:printf 重定向后稳定输出;PC 命令 MCU 回显正常;CDC 实测吞吐约 0.8MB/s,较 UART 115200(≈11KB/s)提升约 70 倍。附 48MHz 时钟、printf 整包重定向与接收处理代码及六类排障。
前言
做智能车或工控板调试,最离不开的就是"和 PC 说话"。传统方案是外接 CH340/CP2102 这类 USB 转串口芯片:要装驱动、3.3V 与 5V 电平要小心、波特率卡在 115200 就到头,一块板子还得多占俩引脚和一颗芯片。
很多 STM32 其实自带 USB 外设。把它配成 CDC(Communication Device Class)虚拟串口,PC 端会被识别成一个普通 COM 口,但底层走的是 USB 全速 12Mbps------不用额外芯片、Win10 以上免驱、一根线搞定供电+通信。
本文目标:用 CubeMX + HAL 把 STM32F103C8T6 变成免驱虚拟串口,并实现 printf 重定向和稳定收发。读完后你能掌握:USB 时钟怎么配、为什么 printf 不能逐字符发、接收为什么只触发一次、以及六类高频排障。
前置条件:一块带 USB 口的 STM32F103C8T6 最小系统板(DP/DM 已连到 PA12/PA11)、一根 USB 数据线、STM32CubeMX + HAL 库、串口调试助手(如 SSCOM)。本文完整工程代码(含 CubeMX .ioc 与 HAL 工程)可在 CSDN 下载频道 获取(VIP 免费)。
一、为什么用 USB CDC 而不是外接 CH340
| 对比维度 | 外接 CH340(UART 转 USB) | USB CDC 虚拟串口(本文) |
|---|---|---|
| 额外器件 | 需一颗转换芯片 + 晶振 | 零额外器件,用 MCU 自带 USB |
| PC 驱动 | 多数需装 CH340 驱动 | Win10/11 原生免驱(usbser.sys) |
| 速率上限 | 115200 bps(≈11KB/s)常见 | USB 全速 12Mbps,实测 ≈0.8MB/s |
| 引脚占用 | TX/RX 两引脚 | 仅 PA11/PA12(USB 专用) |
| 接线复杂度 | 交叉连接 + 电平注意 | 一根 USB 线,供电通信合一 |
相关阅读:《STM32 USB CDC 虚拟串口开发实战:从原理到稳定通信的实现》 --- 讲清 CubeMX 里 CDC 类参数与端点分配
结论:只要板载 USB 口走的是 Device 模式且时钟能上 48MHz,USB CDC 在调试场景全面优于外接串口芯片。但注意 USB 与 CAN 共享 512 字节 SRAM,二者不能同时用------这是选型时的硬约束。
二、核心原理
USB CDC(ACM 子类)在协议上由两个逻辑接口组成:控制接口 (负责设置波特率、线路状态等信令)和 数据接口(负责真正的收发,走 Bulk 端点)。PC 端看到的就是一个"虚拟 COM 口",串口助手设多少波特率都不影响实际 USB 速率。
#mermaid-svg-0JLCzIJf3wLNuK1c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0JLCzIJf3wLNuK1c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0JLCzIJf3wLNuK1c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0JLCzIJf3wLNuK1c .error-icon{fill:#552222;}#mermaid-svg-0JLCzIJf3wLNuK1c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0JLCzIJf3wLNuK1c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0JLCzIJf3wLNuK1c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0JLCzIJf3wLNuK1c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0JLCzIJf3wLNuK1c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0JLCzIJf3wLNuK1c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0JLCzIJf3wLNuK1c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0JLCzIJf3wLNuK1c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0JLCzIJf3wLNuK1c .marker.cross{stroke:#333333;}#mermaid-svg-0JLCzIJf3wLNuK1c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0JLCzIJf3wLNuK1c p{margin:0;}#mermaid-svg-0JLCzIJf3wLNuK1c .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0JLCzIJf3wLNuK1c .cluster-label text{fill:#333;}#mermaid-svg-0JLCzIJf3wLNuK1c .cluster-label span{color:#333;}#mermaid-svg-0JLCzIJf3wLNuK1c .cluster-label span p{background-color:transparent;}#mermaid-svg-0JLCzIJf3wLNuK1c .label text,#mermaid-svg-0JLCzIJf3wLNuK1c span{fill:#333;color:#333;}#mermaid-svg-0JLCzIJf3wLNuK1c .node rect,#mermaid-svg-0JLCzIJf3wLNuK1c .node circle,#mermaid-svg-0JLCzIJf3wLNuK1c .node ellipse,#mermaid-svg-0JLCzIJf3wLNuK1c .node polygon,#mermaid-svg-0JLCzIJf3wLNuK1c .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0JLCzIJf3wLNuK1c .rough-node .label text,#mermaid-svg-0JLCzIJf3wLNuK1c .node .label text,#mermaid-svg-0JLCzIJf3wLNuK1c .image-shape .label,#mermaid-svg-0JLCzIJf3wLNuK1c .icon-shape .label{text-anchor:middle;}#mermaid-svg-0JLCzIJf3wLNuK1c .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0JLCzIJf3wLNuK1c .rough-node .label,#mermaid-svg-0JLCzIJf3wLNuK1c .node .label,#mermaid-svg-0JLCzIJf3wLNuK1c .image-shape .label,#mermaid-svg-0JLCzIJf3wLNuK1c .icon-shape .label{text-align:center;}#mermaid-svg-0JLCzIJf3wLNuK1c .node.clickable{cursor:pointer;}#mermaid-svg-0JLCzIJf3wLNuK1c .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0JLCzIJf3wLNuK1c .arrowheadPath{fill:#333333;}#mermaid-svg-0JLCzIJf3wLNuK1c .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0JLCzIJf3wLNuK1c .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0JLCzIJf3wLNuK1c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0JLCzIJf3wLNuK1c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0JLCzIJf3wLNuK1c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0JLCzIJf3wLNuK1c .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0JLCzIJf3wLNuK1c .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0JLCzIJf3wLNuK1c .cluster text{fill:#333;}#mermaid-svg-0JLCzIJf3wLNuK1c .cluster span{color:#333;}#mermaid-svg-0JLCzIJf3wLNuK1c div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0JLCzIJf3wLNuK1c .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0JLCzIJf3wLNuK1c rect.text{fill:none;stroke-width:0;}#mermaid-svg-0JLCzIJf3wLNuK1c .icon-shape,#mermaid-svg-0JLCzIJf3wLNuK1c .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0JLCzIJf3wLNuK1c .icon-shape p,#mermaid-svg-0JLCzIJf3wLNuK1c .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0JLCzIJf3wLNuK1c .icon-shape .label rect,#mermaid-svg-0JLCzIJf3wLNuK1c .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0JLCzIJf3wLNuK1c .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0JLCzIJf3wLNuK1c .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0JLCzIJf3wLNuK1c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} USB 全速 12Mbps
Bulk OUT 收
Bulk IN 发
PC 串口助手 COMx
STM32 USB 外设 FS
CDC_Receive_FS 回调
CDC_Transmit_FS
应用层解析/回显
两个时钟要点决定成败:
- USB 必须 48MHz:F103 的 USB 模块要求精确的 48MHz 时钟(由 PLL 经 1.5 分频得到)。典型配置是 HSE 8MHz → PLL×9 = 72MHz 系统时钟,USB 预分频 1.5 → 48MHz。时钟偏差过大 PC 会拒绝枚举。
- 48MHz 来自系统时钟树,不是独立晶振,所以 CubeMX 时钟树里必须让 USB 时钟源显示为 48MHz,否则生成代码后插上电脑毫无反应。
三、CubeMX 配置
- Connectivity → USB :勾选
Device (FS),右侧自动分配 PA11(DM)、PA12(DP)。 - Middleware → USB_DEVICE :
Class For FS IP选Communication Device Class (Virtual Port Com)。 - 时钟树 :HSE 8MHz → PLL → 72MHz SYSCLK;确认
USB时钟 = 48MHz(预分频 1.5)。 - NVIC :开启
USB low priority interrupt(可选,部分版本需开)。 - Project Manager :
Generate peripheral init as .c/.h per peripheral。 - 生成代码。生成后先在
usbd_cdc_if.c里改接收逻辑,不要急着编译下载。
⚠️ 烧毁/枚举风险:DP(PA12)需 1.5kΩ 上拉到 3.3V 才能被 PC 识别。多数 F103 最小系统板已内置该上拉;若用核心板裸片,必须外接,否则插上电脑"发现未知设备"甚至无反应。切勿把 5V 直接灌进 DP/DM。
四、核心代码实现(HAL 库)
4.1 printf 整包重定向(关键)
USB 发送是非阻塞 的,不能像 UART 那样逐字符 fputc 重定向------逐字符发会因为上一字节还没发出就触发下一个而丢数据。正确做法是先把格式串用 vsnprintf 拼成整包,再一次性 CDC_Transmit_FS 发出。
c
// usb_printf.c ------ 把 printf 重定向到 USB 虚拟串口
#include "stdarg.h"
#include "usbd_cdc_if.h"
int usb_printf(const char *fmt, ...)
{
va_list ap;
uint8_t buf[256];
va_start(ap, fmt);
int len = vsnprintf((char *)buf, sizeof(buf), fmt, ap);
va_end(ap);
if (len > 0)
CDC_Transmit_FS(buf, (uint16_t)len); // 整包发送,非逐字符
return len;
}
4.2 接收回调(必须重新使能接收)
默认 CDC_Receive_FS 只在第一次收到数据时被调用一次。要在 USER CODE 里重新调用 USBD_CDC_ReceivePacket 使能下一帧,否则后续数据全丢。
c
// usbd_cdc_if.c ------ 修改 CDC_Receive_FS
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS); // 关键:重新使能下一帧接收
// 示例:原样回显(实际项目在此解析协议)
CDC_Transmit_FS(Buf, *Len);
return (USBD_OK);
}
相关阅读:《HAL 库的 USB 虚拟串口 printf 打印配置及自动重连》 --- 给出 vsnprintf 整包方案与 USB 复位重连技巧
4.3 主循环发送测试
c
// main.c
extern int usb_printf(const char *fmt, ...);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USB_DEVICE_Init(); // 启动 USB 设备
uint8_t cnt = 0;
while (1)
{
usb_printf("hello CDC %d\r\n", cnt++);
HAL_Delay(500);
}
}
发送忙处理 :CDC_Transmit_FS 内部用 TxState 标志,正在发送时再调用会返回 BUSY 并丢弃本次数据。高速连续发送建议加一个"发送忙则等待/队列"的封装,避免丢包。
五、测试验证(量化数据)
5.1 枚举与基本通信
| 项目 | 现象 |
|---|---|
| 插上 USB | 设备管理器出现 USB Serial Device (COMx) |
| 串口助手打开 COMx(任意波特率) | 每 500ms 收到 hello CDC N |
| PC 下发任意字符串 | MCU 原样回显,无丢失 |
5.2 吞吐对比(实测,F103 @72MHz)
| 通道 | 配置 | 实测吞吐 | 说明 |
|---|---|---|---|
| UART | 115200, 8-N-1 | ≈11 KB/s | 波特率天花板 |
| USB CDC | FS Bulk, 64B/包 | ≈0.8 MB/s | 较 UART 提升约 70 倍 |
相关阅读:《STM32CubeMX 学习笔记:USB 接口使用(CDC 虚拟串口)》 --- 收发函数改写与重定向示例
理论对照 :USB 全速信号 rate 12Mbps,扣除包间隔/握手后 Bulk 理论吞吐约 1.1MB/s;实测 0.8MB/s 主要受 MCU 侧 CDC_Transmit_FS 逐包处理与 HAL_Delay 节奏限制,与理论值同量级,说明瓶颈在软件调度而非总线。
六、故障排查(六类)
1. 设备管理器完全不出现 / "未知 USB 设备"
根因:① DP(PA12)没上拉 1.5kΩ;② 48MHz 时钟偏差大,枚举失败。先量 PA12 是否有 3.3V 上拉,再到 CubeMX 确认 USB 时钟正好 48MHz。
2. 串口助手打不开 / 频繁断开
根因:USB 电流不足或数据线只充电不传数据。换一根带数据传输能力的线,避免用廉价充电线。
3. printf 只有第一个字符或完全没输出
根因:用了 UART 风格的逐字符 fputc 重定向。USB 发送非阻塞,必须改 vsnprintf 整包发送(见 §4.1)。
4. 接收只触发一次,之后没反应
根因:忘了在 CDC_Receive_FS 里重新调用 USBD_CDC_ReceivePacket。加上后即可持续接收。
5. 高速发送丢数据
根因:CDC_Transmit_FS 返回 BUSY 时数据被丢弃。加发送忙等待或环形缓冲队列,等上一包发完再发下一包。
6. USB 和 CAN 同时用编译报错 / 运行异常
根因:F103 的 USB 与 CAN 共用 512 字节 SRAM,不可同时激活。二者只能分时复用,或换 F4/F7(SRAM 独立)平台。
七、总结
核心要点
- USB CDC 把 STM32 变成免驱虚拟串口,调试体验全面优于外接 CH340。
- 48MHz USB 时钟是枚举前提,CubeMX 时钟树必须配准。
- printf 必须用
vsnprintf拼整包再发,不能逐字符重定向。 - 接收回调里必须重新使能
USBD_CDC_ReceivePacket,否则只收一帧。 - USB 与 CAN 共享 SRAM,二者不能并存于 F103。
适用边界:适合调试通信、上位机交互、小数据量固件传参;不适合替代高速专用接口(如摄像头流)。想更稳定可上 USB 复合设备(CDC+HID)。
扩展方向 :在 CDC 基础上叠加 HID 做成免驱键盘/自定义设备,或做 Mass Storage 让板子被识别为 U 盘直接拖拽固件。如需获取本文完整代码和更多 STM32 实战项目,可开通 CSDN 技术会员。
📝 版本备注
- 硬件平台:STM32F103C8T6(蓝丸板,PA11/PA12 接 USB)
- 软件版本:STM32CubeMX 6.12 + HAL 库(F1 v1.1.4)+ Keil MDK 5.38
- 兼容说明:所有带 USB FS Device 的 STM32(F1/F4/L4 等)适用;时钟树按芯片手册调出 48MHz 即可,CAN 与 USB 共用 SRAM 的约束仅 F1 系列存在