【实战指南】STM32F103 USB CDC虚拟串口开发与调试全解析

📅 发布时间:2026/7/11 17:16:53 👁️ 浏览次数:
【实战指南】STM32F103 USB CDC虚拟串口开发与调试全解析
1. 为什么你需要USB虚拟串口如果你玩过STM32肯定用过串口调试。传统的串口调试需要一根USB转TTL线一头接电脑一头接开发板的RX/TX引脚。这种方式有几个痛点首先你得额外买一根线其次开发板上宝贵的UART外设被占用了如果你的项目本身就需要多个串口资源就很紧张最后线多了桌面也乱。USB虚拟串口VCP就是为了解决这些问题而生的。它利用STM32芯片自带的USB接口在电脑上虚拟出一个COM口。你只需要一根普通的USB数据线就是手机充电那种把开发板的USB口和电脑一连电脑上就会多出一个串口设备。这样一来你既省了一根专用线又释放了一个硬件UART资源调试和通信都方便多了。我刚开始用的时候也觉得挺神奇一根线既能供电又能通信调试信息哗哗地往外打效率提升不是一点半点。尤其对于STM32F103这种经典芯片它的USB是全速12Mbps的虽然比不上高速USB但用来做虚拟串口传输调试数据、发送控制命令速度绰绰有余完全能满足大部分开发场景。2. 动手前的硬件与软件准备工欲善其事必先利其器。在开始写代码之前咱们得把“战场”布置好。2.1 硬件连接别在第一步就踩坑硬件连接其实很简单但也是最容易出错的地方。以常见的正点原子战舰V4开发板为例你需要关注两个地方USB接口找到板子上标有USB SLAVE的Micro-USB口用数据线连接到电脑的USB口。这个口是专门给STM32的USB设备功能使用的。跳线帽这是关键开发板上通常有一个排针用于选择USB数据线D和D-连接到STM32的哪个引脚。对于STM32F103USB的D应接PA12D-应接PA11。你需要用跳线帽短接对应的排针。在正点原子板子上通常是连接P10排针的对应位置。如果这个跳线帽没接或者接错了电脑是绝对识别不到设备的我当年就在这懵了半小时。如果你的开发板是自己画的那就检查原理图确保USB的DPPA12通过一个1.5K电阻上拉到3.3VDMPA11直接连接。这个1.5K的上拉电阻是USB设备检测的关键它告诉电脑“嗨有个设备插上了”。2.2 软件环境与驱动安装软件方面主要分三块开发环境、STM32固件库、电脑端驱动。开发环境KEIL MDK我用的V5、IAR或者STM32CubeIDE都可以选你顺手的。本文的代码示例基于标准外设库在KEIL中演示。STM32固件库你需要准备STM32F1的标准外设库或者HAL库。正点原子的例程通常基于标准库我们移植官方的USB库也会用到里面的核心文件。电脑驱动这是让电脑认识你虚拟串口的关键。ST官方提供了STM32 Virtual COM Port Driver。你可以在ST官网搜索这个名称下载或者直接用资料包里提供的比如en.stsw-stm32102.zip。驱动安装有个小坑需要注意最好在连接开发板之前安装驱动。如果你先插上了板子Windows可能会尝试自动安装驱动但失败导致设备管理器里出现一个带黄色感叹号的“未知设备”。这时你可以手动指定驱动文件夹通常位于C:\Program Files (x86)\STMicroelectronics\Software\Virtual comport driver来安装。Win8/Win10/11系统通常能自动安装但备着驱动安装包总没错。3. 从官方例程到自己的工程代码移植详解不建议从零开始写USB协议栈那太复杂了。ST官方提供了完善的USB设备库我们要做的是“移植”和“适配”。这里我以移植STM32Cube_FW_F1_V1.8.0固件包中的CDC_Standalone例程为例带你走一遍。3.1 工程骨架搭建首先找一个简单的已有工程作为基础比如一个LED闪烁工程复制一份重命名为“USB_VCP_Test”。在工程目录下我习惯创建一个Middlewares文件夹专门存放像USB、文件系统这类中间件。复制USB库核心文件 从官方例程里找到以下关键文件夹STM32_USB_Device_Library/Core包含USB设备协议栈的核心文件usbd_core.c,usbd_ioreq.c,usbd_ctlreq.c。STM32_USB_Device_Library/Class包含设备类实现文件我们需要CDC类usbd_cdc.c。 把这两个文件夹整个拷贝到你的Middlewares/USB目录下。复制应用层文件 在官方例程的Applications/USB_Device/CDC_Standalone/Src和Inc里找到这几个文件usbd_cdc_interface.c(.h) -这是重中之重CDC功能与你的应用对接就在这里。usbd_desc.c(.h) - USB设备描述符定义了你的设备是什么厂商ID、产品ID等。usbd_conf.c(.h) - USB底层配置如引脚、中断优先级、内存分配。 把它们拷贝到Middlewares/USB/USB_APP目录下。现在你的Middlewares/USB目录结构应该很清晰了这为后续的维护和再次移植打下了好基础。3.2 在KEIL中组装工程打开你的KEIL工程新建几个分组Group让工程结构一目了然USB_CORE添加Core目录下的.c文件。USB_CLASS添加Class目录下的usbd_cdc.c。USB_APP添加USB_APP目录下的三个.c文件。别忘了把相关路径添加到工程的“Include Paths”里否则编译会找不到头文件。第一次编译肯定会报一堆错。别慌这主要是因为官方例程用了它自己的开发板支持包BSP而我们的工程没有。接下来就是修改适配。3.3 核心适配usbd_cdc_interface.c详解这个文件是USB CDC类与你的应用程序之间的桥梁。你需要修改它告诉USB库数据收到了放哪里数据要发送该怎么发首先关注USBD_CDC_fops这个结构体。它是一组函数指针USB库会调用它们USBD_CDC_ItfTypeDef USBD_CDC_fops { CDC_Itf_Init, CDC_Itf_DeInit, CDC_Itf_Control, CDC_Itf_Receive };CDC_Itf_InitUSB初始化时调用。我们在这里设置接收缓冲区。USBD_CDC_SetRxBuffer(USBD_Device, g_usb_rx_buffer);这句就是把一个数组g_usb_rx_buffer告诉USB库用来存放电脑发来的数据。CDC_Itf_Receive数据接收回调函数。当电脑通过虚拟串口发送数据时USB库收完一包数据就会调用这个函数。这是我们处理接收数据的关键入口通常在这里我们把数据从临时缓冲区拷贝到自己的应用处理缓冲区并设置一个“收到数据”的标志位。static int8_t CDC_Itf_Receive(uint8_t *buf, uint32_t *len) { USBD_CDC_ReceivePacket(USBD_Device); // 准备接收下一包 // 将buf中的数据拷贝到自己的全局缓冲区并记录长度 memcpy(g_app_rx_buffer, buf, *len); g_rx_len *len; g_rx_flag 1; // 设置接收完成标志 return USBD_OK; }CDC_Itf_Control处理控制请求比如电脑上的串口调试助手设置波特率、数据位、停止位时就会触发这个函数。我们可以在这里解析LineCoding结构体获取波特率参数虽然对于虚拟串口这个波特率是“虚拟”的但我们可以记录下来用于显示或日志。其次实现数据发送函数。官方例程可能把发送也放在接口文件里我们可以自己提炼一个更易用的发送函数void USB_VCP_SendData(uint8_t *data, uint16_t len) { // 等待上一次发送完成如果使用轮询方式检查状态 while (g_tx_busy_flag); // 简单示例实际需根据USB库状态判断 USBD_CDC_SetTxBuffer(USBD_Device, data, len); USBD_CDC_TransmitPacket(USBD_Device); g_tx_busy_flag 1; }注意USB传输是基于包的全速USB的最大包长是64字节。如果你要发送的数据超过64字节CDC库内部会自动分包。但应用程序最好也能做好缓冲区管理。3.4 主程序的逻辑主函数main.c里的逻辑就清晰了初始化系统时钟、GPIO、延时等基础外设。初始化USB调用USBD_Init()、USBD_RegisterClass()、USBD_CDC_RegisterInterface()等函数把我们准备好的描述符、类接口注册进去然后USBD_Start()。在主循环中不断检查g_rx_flag。如果为1说明收到了数据就进行处理比如回环发送然后清除标志位。可以加一个定时发送比如每秒通过USB_VCP_SendData发送一段“心跳”数据方便测试。这里有个实用小技巧为了处理方便我们可以模仿printf写一个usb_printf函数它内部调用vsprintf格式化字符串再调用USB_VCP_SendData发送这样调试输出就和用串口一样方便了。4. 调试实战常见问题与解决方案代码写完编译通过烧录进去真正的挑战才刚刚开始。下面是我总结的几个典型问题和解决办法。4.1 电脑识别不到设备这是最常见的问题。请按以下顺序排查硬件连接确认USB线是数据线不是充电线跳线帽是否正确连接PA11/PA12。供电有些开发板需要单独供电或者USB口供电不足。尝试给开发板独立供电或者换一个电脑USB口。驱动问题打开设备管理器插拔USB线看是否有新设备出现或消失。如果是黄色感叹号尝试手动更新驱动。也可以使用USBlyzer或Bus Hound这类USB协议分析工具需购买查看USB枚举过程在哪一步失败了这对定位底层问题非常有帮助。代码问题检查usbd_desc.c中的厂商IDVID、产品IDPID是否合法。可以使用默认的ST的ID。确保USBD_Start被正确调用且没有因为硬件错误进入中断死循环。4.2 能识别但无法通信无法打开串口如果设备管理器里看到了“STMicroelectronics Virtual COM Port (COMx)”但串口助手打不开它端口被占用关闭可能占用该串口的其他软件如之前的串口助手、IDE的串口终端等。波特率设置虚拟串口的波特率在电脑端可以任意设置它只是一个“名义上”的值实际通信速率取决于USB总线。但两边最好设成一样的虽然不影响底层传输但某些串口助手软件可能会有兼容性检查。通常设成115200就行。缓冲区与流控在串口助手中关闭硬件流控RTS/CTS。虚拟串口一般不支持真正的硬件流控。同时适当调大串口助手的接收缓冲区。4.3 数据收发不完整或丢包表现为发送大量数据时接收方数据不全。64字节倍数问题这是经典坑。USB全速设备的批量传输Bulk Transfer包最大64字节。如果你的应用一次发送的数据正好是64的倍数如64、128、256字节CDC库的发送函数可能在等待一个“短包”小于64字节来表示帧结束。解决方法是在发送函数中如果数据长度是64的倍数则在发送完数据后额外发送一个零长度的包USBD_CDC_TransmitPacket发送长度为0的数据。很多官方库的更新版本已经内部处理了这个问题但老版本可能需要自己处理。发送未完成就发下一包USB发送是异步的。调用USBD_CDC_TransmitPacket后数据只是交给了底层需要等待发送完成回调或查询状态后才能发送下一包数据。否则会导致数据覆盖或丢失。确保你的发送流程有正确的等待或状态机。接收处理太慢如果电脑端发送数据过快MCU端的CDC_Itf_Receive回调处理不过来就会丢包。要确保接收回调函数执行时间尽可能短快速拷贝数据并退出。复杂的处理应放到主循环中根据标志位进行。4.4 如何实现高性能双向通信对于需要高速、双向、大数据量传输的场景比如通过虚拟串口传输图像数据、文件有几点优化建议使用双缓冲或多缓冲为发送和接收分别准备两个缓冲区。当其中一个缓冲区正在被USB底层使用发送或接收时应用程序可以操作另一个缓冲区实现“乒乓操作”避免等待。利用DMA虽然USB本身已经很快但将接收到的数据从USB缓冲区搬运到应用内存或者准备发送的数据从内存搬运到USB缓冲区可以用DMA来减轻CPU负担。不过STM32F103的USB模块与DMA的配合需要仔细配置不是所有型号都支持得那么直接。调整USB核心中断优先级确保USB中断USB_LP_CAN1_RX0_IRQn具有合适的优先级既不能被其他中断阻塞太久也不会阻塞关键的系统定时器。5. 进阶玩法不止于虚拟串口当你掌握了基本的USB虚拟串口后STM32F103的USB还能玩出更多花样让你的项目能力大幅提升。5.1 复合设备VCP HID键盘/MSC U盘一个USB接口可以同时实现多个功能这就是复合设备Composite Device。例如你的设备可以同时是一个虚拟串口和一个HID键盘。当某个事件触发时如按下开发板上的一个键MCU可以模拟键盘向电脑发送按键信号比如自动输入一串密码。这在一些自动化测试场景中非常有用。实现的关键在于修改usbd_desc.c中的配置描述符为CDC类和HID类分别定义不同的接口Interface并正确配置端点资源。ST的USB库提供了相应的框架你需要将两个设备类的代码融合并处理好描述符的合并。5.2 自定义USB设备类如果虚拟串口、HID、大容量存储U盘这些标准类都不满足你的需求你可以定义自定义设备类Custom Class。这给了你最大的灵活性可以定义专属的通信协议和命令格式。上位机需要安装特定的驱动程序或者使用WinUSB、libusb等通用驱动框架来与你的设备通信。这对于需要私有、高效、定制化数据交换的产品来说是终极解决方案。当然开发难度和周期也会相应增加。5.3 基于VCP的IAP在应用编程这是一个非常实用的高级功能。你可以利用USB虚拟串口来实现产品的固件在线升级。基本思路是将Flash划分为Bootloader区和Application区。Bootloader程序启动后初始化USB为虚拟串口等待电脑端的上位机发送新的固件文件通常是bin或hex格式。Bootloader通过虚拟串口接收数据校验如CRC然后擦写Application区的Flash。升级完成后跳转到新的Application程序运行。这样用户只需要用USB线连接产品运行一个上位机升级工具就能轻松完成固件更新无需拆机或使用特殊的编程器极大提升了产品的可维护性。在实现时要特别注意Flash的擦写保护、中断向量表的重映射以及升级过程中的断电保护等问题。从一根简单的USB线开始到实现稳定可靠的虚拟串口通信再到探索复合设备、自定义协议乃至IAP升级STM32F103的USB功能就像一座宝库。我最初也遇到过电脑死活不认设备、数据发一半就卡住的窘境但把每个环节的原理搞清楚一步步调试最终看到串口助手稳定地收发数据时那种成就感是实实在在的。希望这份详细的指南能帮你绕过我踩过的那些坑更顺畅地开启STM32的USB开发之旅。