STM32 USB虚拟串口实现与优化指南

张开发
2026/4/9 2:02:12 15 分钟阅读

分享文章

STM32 USB虚拟串口实现与优化指南
1. STM32 USB虚拟串口实现概述在嵌入式开发中USB虚拟串口(Virtual COM Port, VCP)是一个非常实用的功能。它允许开发者通过USB接口模拟传统的串口通信既保留了串口简单易用的特性又具备了USB的高速传输优势。我最近在一个工业控制器项目中使用STM32F103实现了这个功能下面将详细分享整个实现过程。使用USB虚拟串口主要有以下优势省去了额外的USB转串口芯片降低成本传输速度比传统串口快得多(全速USB可达12Mbps)只需一根USB线即可完成供电和通信在PC端可以像操作普通串口一样使用这个实现基于STM32F103C8T6(蓝桥杯开发板常用型号)使用HAL库和CubeMX配置工具。开发环境为VSCodeGCC但提供的工程也支持Keil MDK。2. 硬件准备与CubeMX配置2.1 硬件连接实现USB虚拟串口只需要连接USB的DM(PA11)和DP(PA12)两根数据线。在我的项目中硬件连接如下USART1(TX:PA9, RX:PA10)用于输出调试信息USART2(TX:PA2, RX:PA3)作为实际串口与外部设备通信LED(PC13)系统状态指示灯USB(PA11, PA12)USB数据线注意STM32的USB接口需要5V供电如果使用USB供电请确保开发板的5V引脚已正确连接。2.2 CubeMX基础配置在Pinout Configuration界面设置系统时钟为72MHz配置USART1为异步模式波特率921600(高波特率减少中断占用)配置USART2为异步模式波特率115200开启全局中断在Middleware选项卡启用USB_DEVICE选择Communication Device Class (Virtual Port Com)保持默认配置时钟树配置确保USB时钟为48MHz(来自PLL)系统时钟72MHz生成代码前设置工程名为USB_VCP选择Toolchain为Makefile(或MDK-ARM)勾选Generate peripheral initialization as a pair of .c/.h files生成代码后基础工程框架就准备好了。接下来我们需要添加具体的功能实现。3. USB虚拟串口核心功能实现3.1 发送功能实现USB虚拟串口的发送功能主要在usbd_cdc_if.c文件中实现。关键函数是CDC_Transmit_FSuint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) { USBD_CDC_HandleTypeDef *hcdc (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; if(hcdc-TxState ! 0) return USBD_BUSY; USBD_CDC_SetTxBuffer(hUsbDeviceFS, Buf, Len); return USBD_CDC_TransmitPacket(hUsbDeviceFS); }测试发送功能的简单方法是在main循环中添加以下代码while(1) { char msg[] Hello from STM32!\r\n; CDC_Transmit_FS((uint8_t*)msg, strlen(msg)); HAL_Delay(1000); }下载程序后连接USB到电脑在设备管理器中应该能看到新增的串口设备。使用串口调试助手打开该串口应该每秒收到一次Hello from STM32!消息。提示首次使用时需要安装STM32的USB虚拟串口驱动可以在ST官网下载STSW-STM32102驱动包。3.2 解决USB重新枚举问题在开发过程中我发现一个问题每次下载程序后都需要重新插拔USB才能识别。这是因为芯片在复位后没有主动重新枚举USB设备。解决方法是在USB初始化前将DP(PA12)引脚拉低一段时间void USB_Reset(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_SET); }在main函数中调用这个函数SystemClock_Config(); USB_Reset(); // 放在USB初始化前 MX_GPIO_Init(); MX_USART1_UART_Init(); MX_USB_DEVICE_Init(); MX_USART2_UART_Init();这样处理后每次程序启动都会自动重新枚举USB设备无需手动插拔。3.3 接收功能实现USB数据的接收是通过回调函数CDC_Receive_FS实现的。当USB接收到数据时会自动调用这个函数。我们可以在这里将USB数据转发到USART2static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { extern UART_HandleTypeDef huart2; // 将USB接收到的数据转发到USART2 HAL_UART_Transmit_IT(huart2, Buf, *Len); USBD_CDC_SetRxBuffer(hUsbDeviceFS, Buf[0]); USBD_CDC_ReceivePacket(hUsbDeviceFS); return (USBD_OK); }同时我们还需要实现USART2的中断接收将串口数据转发回USB// 在usart.c中添加接收缓冲区和相关变量 #define RX_BUF_SIZE 256 uint8_t uart_rx_buf[RX_BUF_SIZE]; uint16_t uart_rx_pos 0; // USART2中断回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART2) { if(uart_rx_pos RX_BUF_SIZE-1) { uart_rx_buf[uart_rx_pos] uart2_rx_byte; } HAL_UART_Receive_IT(huart, uart2_rx_byte, 1); } }然后在主循环中定时检查并发送缓冲区数据while(1) { if(uart_rx_pos 0) { CDC_Transmit_FS(uart_rx_buf, uart_rx_pos); uart_rx_pos 0; } HAL_Delay(1); }4. 高级功能实现与优化4.1 动态波特率设置标准的USB虚拟串口应该支持动态修改波特率。这个功能在CDC_Control_FS函数中实现static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) { switch(cmd) { case CDC_SET_LINE_CODING: { extern UART_HandleTypeDef huart2; // 解析波特率(小端模式) uint32_t baudrate *(uint32_t*)pbuf; huart2.Init.BaudRate baudrate; // 解析停止位 switch(pbuf[4]) { case 2: huart2.Init.StopBits UART_STOPBITS_2; break; default: huart2.Init.StopBits UART_STOPBITS_1; break; } // 解析校验位 switch(pbuf[5]) { case 1: huart2.Init.Parity UART_PARITY_ODD; break; case 2: huart2.Init.Parity UART_PARITY_EVEN; break; default: huart2.Init.Parity UART_PARITY_NONE; break; } HAL_UART_Init(huart2); } break; // 其他命令处理... } return USBD_OK; }这样当在PC端串口工具中修改波特率时USART2的波特率会自动同步更新。实测最高可支持1.5Mbps的波特率。4.2 数据缓冲与流量控制在实际使用中发现如果直接连续调用CDC_Transmit_FS当间隔小于100μs时会出现数据丢失。这是因为USB协议不是实时传输的需要适当的缓冲和流量控制。我实现了一个简单的环形缓冲区方案#define BUF_SIZE 512 typedef struct { uint8_t data[BUF_SIZE]; uint16_t head; uint16_t tail; } RingBuffer; RingBuffer usb_tx_buf {0}; // 添加数据到缓冲区 int Buffer_Write(RingBuffer *buf, uint8_t *data, uint16_t len) { uint16_t i; for(i0; ilen; i) { if(((buf-head 1) % BUF_SIZE) buf-tail) return 0; // 缓冲区满 buf-data[buf-head] data[i]; buf-head (buf-head 1) % BUF_SIZE; } return 1; } // 从缓冲区读取数据 int Buffer_Read(RingBuffer *buf, uint8_t *data, uint16_t max_len, uint16_t *read_len) { uint16_t i 0; while(buf-tail ! buf-head i max_len) { data[i] buf-data[buf-tail]; buf-tail (buf-tail 1) % BUF_SIZE; } *read_len i; return (i 0); }然后在主循环中定时发送缓冲区数据// 每500us检查并发送一次数据 if(HAL_GetTick() - last_send_time 1) // 1ms { uint8_t tmp_buf[64]; uint16_t len; if(Buffer_Read(usb_tx_buf, tmp_buf, sizeof(tmp_buf), len)) { CDC_Transmit_FS(tmp_buf, len); } last_send_time HAL_GetTick(); }接收端也做类似处理这样就能有效避免数据丢失问题。5. 常见问题与解决方案5.1 USB设备无法识别可能原因及解决方法未正确安装驱动 - 下载安装ST官方USB虚拟串口驱动DP引脚未正确上拉 - 检查PA12是否通过1.5k电阻上拉到3.3V电源问题 - 确保USB提供足够的5V电源未正确枚举 - 添加USB重新枚举代码5.2 数据传输不稳定优化建议降低单次传输数据量(建议不超过64字节)增加发送间隔(建议不小于1ms)使用环形缓冲区减少数据丢失检查USB线缆质量尽量使用屏蔽线5.3 高波特率下数据错误调试技巧确保系统时钟和USB时钟配置正确检查USART的时钟源和分频系数降低波特率测试是否是硬件限制检查PCB布线确保信号完整性在实际项目中我发现使用DMA传输可以进一步提高性能。通过将USART和USB都配置为DMA模式可以显著降低CPU负载并提高传输稳定性。

更多文章