STM32 USB CDC虚拟串口配置

一、STM32内置USB虚拟串口简述

USB虚拟串口,简称VPC,Virtual Port Com 的简写。但更习惯于把虚拟串口叫作: CDC,因为它是利用 USB 的 CDC类 实现的一种通信接口。

1.1 为什么使用USB虚拟串口

在嵌入式开发中,串口(UART)的局限性比较明显:

1、引脚资源紧张 :高端芯片可能有多个UART,但低端型号如STM32F103C8T6通常只有一两个;

2、速率瓶颈 :即使波特率设到921600,实际有效吞吐也很难超过几百kbps;

3、连接繁琐 :需要额外的USB转TTL模块、电平匹配电路;

4、跨平台兼容差 :某些定制设备还得自己打包驱动。

而USB CDC正好解决了这些痛点:

1、插上就能用 :Windows/Linux/macOS 原生支持,无需任何驱动;

2、高速传输 :USB 2.0 Full Speed理论带宽达12 Mbps;

3、引脚复用: 只需D+和D−两根线,不占用额外GPIO;

4、调试&通信一体化: 一条USB线搞定供电、烧录、日志输出、参数配置。

STM32 芯片,绝大部分型号都带内置USB,如常用的 F1、F4、H7、G4 等系列,能够通过USB接口与计算机或其他USB设备进行通信。STM32内置的USB,均可支持USB 2.0标准,可以支持三种传输速率:

高速模式:最高可达480 Mbps (部分型号支持,且需搭配外部芯片,不常用 )

全速模式:最高可达12 Mbps (最常用)

低速模式:最高可达1.5 Mbps

1.2 硬件电路

高速模式,需要搭配外围USB PHY芯片,如USB3300,硬件成本偏高 。

全速模式,电路很简单。从机在PCB布线时,仅需把STM32的引脚PA11、PA12, 连接至USB座的DP、DM,然后,PA12(DP线)用1.5K电阻上拉至3.3V。

插拔检测:设备未插入时,主机端DP、DM为低电平,当发现被置高,即为有设备插入;

区分速率:DM线上拉是低速模式,DP线上拉是全速\高速模式;

上拉电压:3.3V。USB通信电平是3.3V,而不是总线供电的5V。

当STM32作为USB设备接入PC后,会经历以下几个阶段:

物理连接 :D+上的1.5kΩ上拉电阻告诉主机:"我是低速/全速设备,请开始枚举。"

枚举过程 :主机读取一系列描述符(Device Descriptor, Config Descriptor等),确认这是一个CDC类设备。

驱动加载 :操作系统发现这是个通信类设备,自动加载 usbser.sys (Windows)或创建 /dev/ttyACMx 节点(Linux)。

通信建立 :应用程序(比如PuTTY、串口助手)打开对应的COM端口,开始收发数据。

硬件电路具体如下图:

1.3 相关驱动

Win10、Win11 已带虚拟串口驱动;无需安装任何驱动; Win7 要提前手动安装驱动。STM32 USB虚拟串口驱动 V1.5.0

二、使用 CubeMX 新建工程

2.1 CubeMX 无法连接网络

下载地址:CubeMX下载地址

参考文章:SMT32CUBEMX 无法连接网络问题解决

现象示例:

解决方式:

在防火墙允许STM32CUBEMX安装目录下的该应用访问网络即可。

完成芯片数据包在线导入。

2.2 主要流程

参考文章:【STM32 + CubeMX】 USB 虚拟串口通信

步骤1:启用USB_OTG_FS

在 Pinout 视图中找到PA11(D-)、PA12(D+),它们会被自动选中;

在 Middleware 栏选择 USB_DEVICE ;

设置为"Device Only"模式;

添加Class → 选择"CDC"。

步骤2:时钟配置要点

USB模块必须工作在 精确48MHz 下。

对于F1系列:

  • 如果使用外部晶振(HSE=8MHz),可通过PLL倍频得到72MHz系统时钟,并用分频器输出48MHz给USB;

  • 若无HSE,可启用内部HSI并通过软件校准(部分型号支持);

  • 更推荐的方法是:使用STM32F4/F7/G0等自带HSI48的型号,省去外部晶振烦恼。

CubeMX会自动生成正确的RCC配置,确保USB_CLK有效。

步骤3:生成代码 & 编写逻辑

点击 "Generate Code" 后,工程中会出现几个关键文件:

usbd_cdc_if.c :用户接口层,包含发送、接收、控制回调函数;

usb_device.c :设备初始化与调度;

usbd_cdc.c :标准CDC类处理逻辑;

三、常见问题排查修改

3.1 无法识别USB口常见排查

  1. 中断配置错误
  • NVIC未使能中断
    检查是否在代码中正确启用USB中断,例如:

    复制代码
     HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 0, 0); // 设置优先级
     HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);         // 使能中断
  • 确保总中断开启、中断优先级冲突

    确保USB中断优先级未被其他高优先级中断阻塞。

  1. 时钟配置错误
  • USB时钟未正确分频
    STM32 USB需要精确的48 MHz时钟。检查RCC配置:

    复制代码
     // 例:使用HSE+PLL生成48MHz USB时钟
     RCC_PLLConfig(RCC_PLLSource_HSE, PLL_M, PLL_N, PLL_P, PLL_Q);
     RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5); // 根据PLL输出调整分频
  • USB外设时钟未使能

    确保调用__HAL_RCC_USB_CLK_ENABLE()启用USB时钟。

  1. 硬件/引脚配置问题
  • USB引脚复用错误
    检查USB的DP(PA12)和DM(PA11)是否配置为复用功能:

    复制代码
     GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;        // 复用推挽
     GPIO_InitStruct.Alternate = GPIO_AF10_USB;     // 复用功能号
  • 上拉电阻未启用

    USB设备需在DP引脚启用1.5kΩ上拉电阻(部分型号通过USB_BCDR寄存器的DPPU位控制)。

  1. USB库配置问题
  • 中断服务函数未实现或命名错误
    确保中断服务函数名称与启动文件(如startup_stm32fxxx.s)中的向量表一致:

    复制代码
     void USB_LP_CAN1_RX0_IRQHandler(void) {  // 中断函数名根据型号可能不同
         HAL_PCD_IRQHandler(&hpcd);           // 调用HAL库中断处理
     }
  • 未处理中断标志

    在自定义中断函数中,需手动清除中断标志(HAL库通常会自动处理)。

  1. 初始化顺序错误
  • USB初始化应在中断配置之后
    确保先配置NVIC,再初始化USB外设:

    复制代码
     MX_NVIC_Init();       // 先配置中断,部分型号不需要
     MX_USB_DEVICE_Init(); // 再初始化USB
  1. 供电与VBUS检测
  • VBUS检测未启用或配置错误
    部分型号需要检测VBUS信号。检查USB_OTG_FSUSB外设的初始化代码,确保VBUS检测模式正确:

    复制代码
     hpcd.Init.vbus_sensing_enable = 0; // 如果无需VBUS检测,设为0

3.2 已识别USB但异常

  1. 能识别,但无法打开串口(提示"端口占用"或"访问被拒绝")

这种情况多出现在Windows系统:

  • 关闭杀毒软件或防火墙尝试;

  • 卸载重复驱动(设备管理器 → 查看隐藏设备 → 删除旧的COM口);

  • 使用管理员权限运行串口工具。

2.数据乱码、丢包严重

可能原因:

  • 主循环中长时间执行 HAL_Delay() 导致中断服务延迟;

  • 多次快速调用 CDC_Transmit_FS() 未等待完成;

  • FIFO溢出或缓冲区太小。

解决方案:

  • 避免在中断中做耗时操作;

  • 使用状态机管理发送流程;

  • 增大缓冲区或引入流量控制(如XON/XOFF模拟)。

四、相关函数修改

4.1 相关文件

类别 文件名 核心作用
HAL库核心文件 usbd_core.c USB设备协议栈核心。 处理底层枚举、控制传输、状态机。
usbd_ctlreq.c 标准设备请求处理器。 响应主机获取描述符、设置地址等标准请求。
usbd_cdc.c CDC类协议实现核心。 管理CDC类的特定请求、抽象数据模型。
用户实现文件 usbd_cdc_if.c CDC类与应用层的接口。 包含所有应用回调函数。
usbd_desc.c / .h 设备描述符定义。 定义了设备的身份信息(VID, PID, 字符串等)和报告给主机的所有描述符。
usbd_conf.c / .h USB外设底层配置与定制。 包含内存分配、PCD句柄、弱定义的回调、以及可选的电源管理配置。
其他重要文件 stm32g4xx_it.c 中断服务程序。 包含USB_LP_IRQHandler等中断入口函数。
usb_device.c USB设备层初始化入口。 由CubeMX生成,调用上述各模块的初始化函数。
基础定义与请求 usbd_def``.h 定义核心数据结构(如USBD_HandleTypeDef)、状态码、宏定义。
usbd_ioreq.c / .h 端点输入/输出请求处理器。 负责处理端点级的数据发送(USBD_LL_Transmit)和接收(USBD_LL_PrepareReceive)的底层细节。

4.2 相关函数

函数名 所在文件 作用与调用时机
CDC_Receive_FS usbd_cdc_if.c 数据接收回调 。当主机通过虚拟串口发送数据,且协议栈完成接收后,自动调用此函数。
CDC_Transmit_FS usbd_cdc_if.c 数据发送函数
MX_USB_Device_Init usbd_device.c 协议栈初始化

4.3 USB模拟拔插

电脑端USB口没有插入设备时,DP和DM线,是低电平状态,而设备端的DP线,有1.5K电阻上拉到3.3V,当设备插入到电脑USB口,USB口的DP线就会被置高电平,主机是依靠这个机制判断设备是否插入、拔出,继而触发不同的动作,如枚举、释放端口等。

当虚拟串口所用的USB线一直插在USB口上,在STM32烧录程序重新运行后,程序里的USB代码等待着主机方发起枚举过程;而这个期间虚拟串口的USB线没有断开,主机方认为设备方一直在线,早已枚举成功,一直对其轮询数据收发。

复制代码
    __HAL_RCC_GPIOA_CLK_ENABLE();                   // 使能GPIOA端口
    GPIO_InitTypeDef GPIO_InitStruct = {0};         // 声明结构体; 如果与文中位置相同,这行可不写
    GPIO_InitStruct.Pin = GPIO_PIN_12;              // 引脚PA12, 即D+
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;     // 引脚工作模式
    GPIO_InitStruct.Pull = GPIO_PULLDOWN;           // 下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;    // 引脚反转速度
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);         // 初始化
    HAL_Delay(5);                                   // 持续片刻

4.4 连续发送优化

USB虚拟串口通信的几个重点 (特指:USB2.0、全速模式、中断传输):

USB是轮询机制,主机对设备不断轮询,间隔最小1ms;不是固定的1ms, 是最小间隔时间;

USB的数据,是按包传输的;

每个设备,每1ms,最多传输1包数据;

每包最多64字节(有效负载);

CDC_Transmit_FS ( ) 函数:

它的第2个参数,"字节数",范围:0~2048; 这个2048可以在CubeMX里进行设置大小;

字节数 <= 64,算1包。如:发3个字节,也算1包。

字节数 == 0,算1包。俗称空包; 如果上一帧刚好发送64字节,再发一个空包作为结束包;

字节数 > 64, CDC_Transmit_FS ( ) 背后有缓存自动分包1ms左右发1包,直至发完;
++如果上一包还没发完,再次调用CDC_Transmit_FS ( ) ,将放弃本次调用。++

复制代码
uint8_t CDC_Transmit_FS(uint8_t *Buf, uint16_t Len)
{
    uint8_t result = USBD_OK;
    /* USER CODE BEGIN 7 */
    
    // 获得设备的状态信息结构体
    USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)hUsbDeviceFS.pClassData; 
    // if (hcdc->TxState != 0){
    // return USBD_BUSY;
    // }

    const static uint8_t USB_TX_MAX_LEN = 250;
    uint16_t txLen = Len;
    uint16_t temp = 0;

    while(txLen > 0)
    {
        USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, temp = (txLen > USB_TX_MAX_LEN ? USB_TX_MAX_LEN : txLen));
        result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
        txLen -= temp;

        uint32_t timeStart = HAL_GetTick();
        while (hcdc->TxState)
        {
            if (HAL_GetTick() - timeStart > 20)
            return USBD_BUSY;
        }
    }
    
    /* USER CODE END 7 */
    return result;
}

4.5 接收机制注意

CDC_Receive_FS

1、每当接收到一包数据,硬件自动触发中断函数, 继而调用此接收回调函数,无需人工调用。

2、与发送机制相似,每间隔1ms,最多接收1包数据,每包最大64字节。

如果需要接收超过64字节的数据帧,注意,指上位机发送的1个完整数据帧,而非USB的单包数据,如上位机发来一张图片数据,8350个字节,则需要在此回调函数中添加额外的代码来判断帧数据传输完整结束 、手动将多个数据包拼接成完整的数据帧。

3、接收到数据时,缓存不会提前自动清零,新数据从Buf的起始位置开始,覆盖存放。

由于该回调函数是被中断函数调用的,因此建议函数内部的处理尽可能地简短,以避免影响系统的实时性(中断函数运行期间,会令程序持续挂起)。

参考文章:

【亲测免费】 STM32 USB虚拟串口驱动 V1.5.0

SMT32CUBEMX 无法连接网络问题解决

【STM32 + CubeMX】 USB 虚拟串口通信

stm32usb无法进入中断

相关推荐
FakeOccupational2 小时前
【电路笔记 STM32】STM32下载器完整配置流程:驱动安装+硬件连接+芯片包安装+软件测试
笔记·stm32·嵌入式硬件
fanged2 小时前
STM32(6)--HAL2(PWM/I2C/ADC)(TODO)
stm32·单片机·嵌入式硬件
极客 - L U2 小时前
常规细节积累
单片机·嵌入式硬件
代码游侠13 小时前
学习笔记——时钟系统与定时器
arm开发·笔记·单片机·嵌入式硬件·学习·架构
CQ_YM13 小时前
ARM--SDK、led、beep与链接脚本
c语言·arm开发·嵌入式硬件·嵌入式
xiaobobo333013 小时前
EIDE的最新版本已经默认只支持debug调试STM32单片机了
stm32·单片机·debug·eide
small_planet13 小时前
通过mqtt使用webhook转发消息实现远程查看单片机日志
单片机·运维开发
Porco.w15 小时前
STM32之ESP8266
stm32·单片机·嵌入式硬件
梁洪飞16 小时前
noc 片上网络
linux·arm开发·嵌入式硬件·arm