ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F407 USB VCP虚拟串口实战:从CubeMX配置到高速数据收发

STM32F407 USB VCP虚拟串口实战:从CubeMX配置到高速数据收发 1. 为什么要在 STM32F407 上折腾 USB VCPSTM32F407 这颗芯片在工控和嵌入式圈子里算是老熟人了168MHz 主频、1MB Flash、192KB SRAM外设资源丰富到让人挑不出大毛病。但真正让它在调试阶段大放异彩的是那颗内置的 USB OTG FS 控制器。很多项目里我们用 UART 打印调试信息波特率一高就丢包接个 USB 转 TTL 模块还得多占一个串口。而 USB VCPVirtual COM Port虚拟串口的思路是让 MCU 直接通过 USB 接口模拟出一个串口设备PC 端识别为标准 COM 口不需要额外转换芯片插上就能用。这个方案解决的核心问题是调试通道的带宽和便利性。USB FS 全速模式下理论带宽 12Mbps实际 CDC 类通信跑个几百 KB/s 很轻松比传统 UART 的 115200 或 921600 快出一个量级。而且 USB 接口供电和通信一体一根线搞定对于 STM32F407 这种带 OTG 的芯片来说不用白不用。适合谁来参考这篇内容如果你手上有 STM32F407 的开发板正点原子探索者、野火霸天虎、或者自己画的板子都行用过 STM32CubeMX 生成过工程对 HAL 库有基本了解那这篇实战记录可以直接抄作业。如果你之前只用过标准库也没关系我会把 CubeMX 的配置逻辑讲清楚底层寄存器操作 HAL 已经封装好了重点在于理解 USB 设备枚举的流程和 CDC 类的数据通路。我这次用的硬件是 STM32F407ZGT6 核心板外部晶振 8MHzUSB 接口用的是 OTG FS 的 PA11DM和 PA12DPVBUS 检测没接直接靠 USB 供电。软件环境是 STM32CubeMX 6.8.1 Keil MDK 5.38 STM32CubeF4 固件包 1.27.1。下面从工程配置开始一步步把 VCP 跑通。2. 工程配置与 USB VCP 协议栈选型2.1 CubeMX 里的关键配置项打开 CubeMX 新建工程选好 STM32F407ZGTx 之后第一件事是配时钟。外部晶振 HSE 选 8MHzPLL 配置成 168MHz 系统时钟USB 时钟必须是 48MHz这个在时钟树里 CubeMX 会自动帮你算好分频系数。注意 USB 时钟不能有偏差否则枚举会失败这是很多新手踩的第一个坑。接着配 USB OTG FS。在 Connectivity 里选 USB_OTG_FSMode 选 Device_Only因为我们要做从设备。Speed 选 Full SpeedPHY 选 Internal PHY。中断优先级建议设成 5 或者更低数值大优先级低因为 USB 中断比较频繁别让它抢占关键任务。中间件里选 USB_DEVICEClass for FS IP 选 Communication Device Class (Virtual Port Com)。这个选项会自动把 CDC 类的描述符、端点配置、回调函数框架都生成好。CDC 类用了三个端点EP0 控制端点、EP1 IN 中断端点用于发送串口控制信息、EP2 OUT 批量端点和 EP3 IN 批量端点用于数据收发。批量端点最大包长 64 字节这是 USB FS 的硬限制。2.2 为什么选 CDC 类而不是自己写描述符USB 协议栈里CDC 类是专门为通信设备定义的虚拟串口就是它的典型应用。自己从零写描述符当然可以但没必要。CDC 类规范里定义好了接口关联描述符IAD、通信接口和数据结构PC 端的驱动Windows 的 usbser.sys、Linux 的 cdc_acm直接就能识别。用 CubeMX 生成的标准 CDC 描述符VID 和 PID 可以用默认的 STMicroelectronics 的 0x0483 和 0x5740也可以自己改但改 VID 需要花钱注册自己玩就用默认的。有个细节要注意CubeMX 生成的 CDC 描述符里字符串描述符默认是 STM32 Virtual ComPort。如果你想让 PC 端显示自定义名字可以在 usbd_desc.c 里改。但改完要重新枚举而且 Windows 可能会缓存旧设备信息需要到设备管理器里卸载设备再重新插拔。2.3 堆栈和中断向量配置USB 协议栈需要一定的内存。CubeMX 默认给 USB 分配的堆是 0x200 字节对于 CDC 类来说够用但如果你后面要加自定义命令或者多接口建议把 Heap Size 调到 0x400 或 0x800。栈大小保持默认 0x400 就行。中断向量表里USB_OTG_FS_IRQHandler 是必须的CubeMX 会自动生成。另外注意如果你用了 FreeRTOSUSB 中断的优先级要设置成比 configMAX_SYSCALL_INTERRUPT_PRIORITY 数值更低优先级更高否则在中断里调用 FreeRTOS API 会触发断言。这个坑我在后面 FreeRTOS 集成那节会细说。3. 代码实现从枚举到数据收发3.1 生成的代码框架解读CubeMX 生成工程后USB 相关的代码分布在几个文件里。usbd_cdc_if.c是用户接口层里面有CDC_Receive_FS回调函数USB 收到数据后会进这里。usbd_cdc.c是 CDC 类的协议实现一般不用动。usbd_desc.c是描述符改 VID/PID 和字符串就在这里。usbd_conf.c是底层配置包括端点初始化和中断处理。主循环里CubeMX 生成的代码会调用MX_USB_DEVICE_Init()这个函数初始化 USB 设备栈并启动。之后 USB 枚举是自动完成的PC 端会弹出新设备安装驱动后出现一个 COM 口。你可以在设备管理器里看到 STM32 Virtual ComPort 或者你自定义的名字。3.2 发送数据的正确姿势发送数据用CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)这个函数。它内部会检查 USB 是否已连接、上一个发送是否完成然后把数据拷贝到端点缓冲区触发 IN 传输。注意这个函数是非阻塞的调用后立即返回实际发送在中断里完成。所以你不能连续调用它发送大块数据必须等上一次发送完成。我见过很多人在这里翻车在CDC_Transmit_FS后面紧接着又调一次结果第二次返回 USBD_BUSY数据丢了。正确的做法是检查返回值如果是 USBD_OK 就继续如果是 USBD_BUSY 就等一会儿再试。或者用一个发送完成回调CDC_TransmitCplt_FS在里面置标志位主循环检测到标志位再发下一包。uint8_t tx_buf[64]; uint8_t tx_busy 0; void CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum) { tx_busy 0; } void send_data(uint8_t *data, uint16_t len) { while (tx_busy); tx_busy 1; CDC_Transmit_FS(data, len); }上面这段代码是简化版实际用的时候要注意tx_busy的原子性。如果主循环和中断都会访问最好用__disable_irq()保护一下或者用 FreeRTOS 的信号量。3.3 接收数据的处理流程接收数据在CDC_Receive_FS回调里处理。USB OUT 端点收到数据后协议栈会调用这个函数参数 Buf 是数据指针Len 是长度。注意这个回调是在中断上下文里执行的所以里面不能做耗时操作比如浮点运算、大块内存拷贝、调用 printf 等。正确的做法是把数据拷贝到一个环形缓冲区然后置个标志位让主循环去处理。#define RX_BUF_SIZE 512 uint8_t rx_ring[RX_BUF_SIZE]; volatile uint16_t rx_head 0, rx_tail 0; static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { for (uint32_t i 0; i *Len; i) { uint16_t next (rx_head 1) % RX_BUF_SIZE; if (next ! rx_tail) { rx_ring[rx_head] Buf[i]; rx_head next; } } USBD_CDC_SetRxBuffer(hUsbDeviceFS, Buf[0]); USBD_CDC_ReceivePacket(hUsbDeviceFS); return USBD_OK; }这里有个关键点处理完数据后必须调用USBD_CDC_SetRxBuffer重新设置接收缓冲区然后调用USBD_CDC_ReceivePacket开启下一次接收。漏掉任何一步后续数据就收不到了。这是 CDC 类接收的标准流程CubeMX 生成的代码里已经包含了但如果你自己改动了回调千万别把这俩忘了。3.4 端点缓冲区对齐问题STM32F407 的 USB OTG 外设对缓冲区地址有对齐要求。CubeMX 生成的代码里USBD_CDC_Init函数会调用USBD_malloc分配缓冲区这个函数在usbd_conf.c里实现用的是malloc。但malloc返回的地址不保证 4 字节对齐而 USB OTG 要求缓冲区地址必须是 4 字节对齐的。如果你发现枚举成功但数据收发异常先检查这里。解决办法很简单把USBD_malloc改成用静态数组或者用__attribute__((aligned(4)))修饰。我一般直接用静态数组避免动态内存的碎片问题__ALIGN_BEGIN uint8_t usb_buf[512] __ALIGN_END; void *USBD_malloc(uint32_t size) { return usb_buf; } void USBD_free(void *p) { }4. 实测中遇到的坑与排查记录4.1 枚举失败PC 端提示未知 USB 设备这是最常见的问题原因通常有三个时钟不对、VBUS 检测没处理、DP 上拉电阻没使能。STM32F407 的 USB OTG FS 内部有 DP 上拉电阻但需要在初始化时使能。CubeMX 生成的代码里HAL_PCD_MspInit会调用HAL_PCDEx_SetConnectionState或者直接操作USB_OTG_GCCFG寄存器来使能内部上拉。如果你用的是自己写的初始化代码检查一下USB_OTG_FS-GCCFG的PWRDWN和VBUSBSEN位。时钟问题更隐蔽。USB 需要精确的 48MHz 时钟如果 PLL 配置有偏差枚举会时好时坏。用示波器测 PA8MCO输出的时钟或者直接看 CubeMX 时钟树里 USB 时钟是不是 48MHz。我遇到过有人把 HSE 设成 25MHz 但实际焊的是 8MHz 晶振结果 USB 死活枚举不了查了半天才发现是硬件问题。4.2 数据丢包发送太快导致 USBD_BUSY前面提过CDC_Transmit_FS是非阻塞的。如果你在主循环里连续调用第二次大概率返回 USBD_BUSY。我实测过在 168MHz 主频下两次调用之间至少间隔 1ms 才能保证不丢。但 1ms 对于 64 字节的包来说带宽只有 64KB/s远低于 USB FS 的理论值。要跑满带宽必须用发送完成回调。每次发送完成后回调里置标志位主循环检测到标志位立即发下一包。这样可以把间隔缩短到几十微秒实测能跑到 500KB/s 以上。注意回调里不要做耗时操作只置标志位就行。4.3 Windows 驱动安装失败Windows 10 及以上版本自带 CDC 驱动插上就能识别。但 Windows 7 需要手动安装 ST 的 VCP 驱动或者用 inf 文件指定。如果你在 Win7 上遇到黄色感叹号去设备管理器里手动更新驱动指向 STM32CubeF4 固件包里的Middlewares/ST/STM32_USB_Device_Library/Class/CDC目录下的驱动文件。还有一个坑如果你改了 VID/PIDWindows 会认为是新设备需要重新安装驱动。而且 Windows 会缓存设备信息有时候改了描述符但设备管理器里还是显示旧名字。这时候需要到设备和打印机里删除设备或者用usbdeview工具清除缓存。4.4 FreeRTOS 集成时的优先级问题如果你在项目里用了 FreeRTOSUSB 中断的优先级必须设置成比configMAX_SYSCALL_INTERRUPT_PRIORITY数值更低即优先级更高。STM32F407 的 NVIC 优先级是数值越小优先级越高而 FreeRTOS 的configMAX_SYSCALL_INTERRUPT_PRIORITY通常设成 5。所以 USB 中断优先级要设成 4 或更小。如果设反了在 USB 中断里调用xSemaphoreGiveFromISR会触发configASSERT失败程序卡死在断言里。这个坑我踩过两次第一次查了半天才发现是优先级问题。后来养成习惯所有中断优先级都在 CubeMX 里统一规划USB 中断固定设成 4。4.5 常见问题速查表现象可能原因排查方法PC 端无反应时钟不对、上拉未使能测 PA11/PA12 波形检查 GCCFG 寄存器枚举成功但无 COM 口描述符错误、驱动未装用 USB 分析仪抓包检查设备管理器发送丢包未等发送完成检查 CDC_Transmit_FS 返回值用回调接收丢包未重新设置接收缓冲区检查 CDC_Receive_FS 里是否调用 SetRxBuffer数据错位缓冲区未对齐检查 USBD_malloc 返回地址是否 4 字节对齐FreeRTOS 断言中断优先级错误检查 USB 中断优先级是否高于 syscall 阈值5. 性能优化与进阶玩法5.1 提高吞吐量的几个手段默认配置下CDC 的批量端点最大包长 64 字节USB FS 每帧 1ms理论最大带宽 64KB/s。但实际测试中由于协议栈开销和主机调度能跑到 500KB/s 以上就算不错了。想再往上提可以试试这几个方法第一把发送缓冲区改成双缓冲。一个缓冲区在发送时另一个缓冲区准备数据发送完成回调里切换缓冲区。这样能减少等待时间实测能提升 20% 左右。第二减少CDC_Transmit_FS里的内存拷贝。这个函数内部会把你的数据拷贝到端点缓冲区如果数据量大拷贝开销不小。可以直接操作端点缓冲区但这样会破坏协议栈的封装不推荐新手尝试。第三调整 USB 中断优先级让它能及时响应。但注意别影响到其他关键中断比如电机控制里的 PWM 中断。5.2 用 VCP 做固件升级通道USB VCP 除了调试还可以做固件升级OTA。思路是PC 端发送固件数据MCU 收到后写入 Flash最后跳转到新固件。STM32F407 的 Flash 操作 HAL 库已经封装好了注意擦除前要解锁写入时要按扇区对齐。但这里有个坑USB 中断和 Flash 操作不能同时进行。Flash 擦写时 CPU 会暂停如果此时 USB 主机发来数据会丢包。解决办法是在 Flash 操作前先断开 USB 连接或者用双 Bank 交替升级。STM32F407 的 Flash 是单 Bank 的所以只能先断开再操作。5.3 和 FreeRTOS 配合的推荐架构如果项目里用了 FreeRTOS建议把 USB 数据处理放在独立任务里。USB 中断只负责把数据丢进队列任务从队列取数据并处理。发送也是类似任务把数据放进发送队列USB 发送完成回调触发信号量任务继续发下一包。QueueHandle_t usb_rx_queue; SemaphoreHandle_t usb_tx_sem; void USB_Task(void *arg) { uint8_t buf[64]; while (1) { if (xQueueReceive(usb_rx_queue, buf, portMAX_DELAY) pdTRUE) { // 处理数据 } if (xSemaphoreTake(usb_tx_sem, 0) pdTRUE) { // 发送下一包 } } }这种架构下USB 中断里只做xQueueSendFromISR和xSemaphoreGiveFromISR耗时极短不会影响其他中断。任务里处理数据可以放心用浮点运算和 printf。5.4 实测数据与经验值我在 168MHz 主频、USB FS 全速模式下实测单次发送 64 字节间隔 100us连续发送 10000 包丢包率为 0。平均吞吐量约 580KB/s。接收方向PC 端用串口助手以 500KB/s 速率发送MCU 端用环形缓冲区接收丢包率约 0.1%主要发生在缓冲区满的时候。把缓冲区从 512 字节加大到 2048 字节后丢包率降到 0.01% 以下。功耗方面USB 通信时整机电流约 80mA不含外设比用 UART 加 USB 转串口芯片的方案省了大概 20mA。对于电池供电的设备这个差距不算大但少一个芯片就少一份成本和故障率。6. 移植到其他 STM32 型号的注意事项这套代码从 STM32F407 移植到 STM32F103 或者 STM32F411 上大部分逻辑是通用的但有几个地方要改。首先是时钟配置不同型号的 PLL 参数不一样USB 时钟必须是 48MHz。F103 的 USB 和 CAN 共用时钟配置时要注意。其次是端点缓冲区大小F103 的 USB 缓冲区只有 512 字节比 F407 小如果用了多个端点要仔细分配。另外F103 的 USB 只支持全速没有 OTG 的额外功能描述符里的一些 OTG 相关字段要去掉。CubeMX 生成代码时会自动处理这些差异但如果你手动移植要对照参考手册检查。最后如果你用的是 GD32 或者 APM32 这类兼容芯片USB 外设的寄存器地址可能不同但 HAL 库的 API 基本兼容。移植时重点检查时钟树和中断向量表其他部分一般不用大改。我个人在实际操作中的体会是USB VCP 这东西入门不难CubeMX 点几下就能跑起来但要想跑得稳、跑得快得把协议栈的收发流程吃透。尤其是发送完成回调和接收缓冲区的管理这两个地方处理好了基本就不会出大问题。另外调试 USB 问题时一个 USB 分析仪能省很多时间没有的话至少装个 Bus Hound 或者 Wireshark 的 USB 抓包插件能看到枚举过程和数据传输细节比盲猜强得多。
返回列表