尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32 USB CDC 虚拟串口实战:UART 占完了?插根线变出一个 COM 口

STM32 USB CDC 虚拟串口实战:UART 占完了?插根线变出一个 COM 口 温控器做到后期要加 PC 联机功能上位机下发目标温度和 PID 参数板子回传实时温度曲线。问题来了F103 只有三个 USARTUSART1 给了调试打印printf 重定向USART2 给了 Modbus 从机通信剩一个 USART3 还要留给未来的蓝牙模块。当时想过软件模拟串口用 GPIO 去做时序抖得要死9600 波特率都勉强。后来发现 STM32 自带 USB 外设配成 CDCCommunications Device Class设备后Windows 和 Linux 都自带驱动插上就多一个 COM 口波特率随便设反正走的是 USB 批量传输波特率只是给上位机串口助手看的。一根 USB 线同时解决供电 通信省掉一根单独的调试线。原理人话版USB 不是另一根串口它是一套完整的总线协议。你的 STM32 在这条总线上是个Device从设备PC 是 Host主机。Host 掌控一切发复位、分配地址、问你要描述符、配置端点。Device 老老实实回答。CDC 类把这套复杂的 USB 协议包装成了一个看起来像串口的东西。它实际占用两个接口Control Interface通过默认端点 EP0 收发 CDC 类专用请求比如上位机设波特率、设停止位虽然你大概率不需要真的去处理这些。Data Interface两个批量端点Bulk IN 和 Bulk OUTIN 是你发给 PC 的数据OUT 是 PC 发给你的数据。这就是虚拟串口的收和发。对开发者来说你只需要关心三件事时钟USB 要求精确的 48MHz 时钟误差不能超过 ±0.25%。通常从外部晶振HSE经 PLL 倍频得到。描述符告诉 Host “我是谁、我能干什么”。CubeMX 生成的中间件帮你填好了大部分内容。收发 APIUSBD_CDC_ReceivePacket()/USBD_CDC_TransmitPacket()这类函数封装了底层的端点操作。CubeMX 怎么配第一步USB 外设在 Connectivity 里找到USB注意不是 USB_OTGF103 这类低端芯片用的是基本 USB 2.0 全速设备外设模式选Device OnlyClock Source选HSE (External Crystal)。内部 HSI 精度不够跑 USB必须用外部晶振通常是 8MHz 或 12MHzPLL 倍到 48MHz。VBUS有些封装需要你手动指定一个 GPIO 做 VBUS 检测检测 USB 线是否插上了。CubeMX 里如果出现 VBUS 选项选一个空闲 GPIO。第二步USB_DEVICE 中间件Middleware →USB_DEVICEClass For FS IP选Communication Device Class (Virtual Port Com)。这就是 CDC。Vendor ID / Product ID可以保持默认0x0483 / 0x5740ST 官方 VID/PID也可以改成自己的需要付费申请 VID。第三步确认时钟树这一步最关键。打开 Clock Configuration 标签页确认USB那一栏显示的是48 MHz。如果不是回去调 PLL 倍频系数。48MHz 是 USB 全速设备的唯一合法时钟频率差一点都不行。代码初始化、发数据、收数据CubeMX 生成的MX_USB_DEVICE_Init()会完成外设初始化、中间件注册、描述符填充全部工作。你在 main 里直接启动intmain(void){HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USB_DEVICE_Init();// 启动 USB Device 栈printf(USB CDC starting...\r\n);// 这句走 USART1 调试口uint8_trx_buf[64];while(1){// ---- 发送周期上报温度 ----staticuint32_tlast_tx0;if(HAL_GetTick()-last_tx1000){// 每秒一次floattread_temperature();charmsg[64];intlensnprintf(msg,sizeof(msg),TEMP:%.2f\r\n,t);// CDC 发送: 先检查 TX 状态是否空闲if(USBD_CDC_GetTxState(hUsbDeviceFS)0){USBD_CDC_SetTxBuffer(hUsbDeviceFS,(uint8_t*)msg,len);USBD_CDC_TransmitPacket(hUsbDeviceFS);}last_txHAL_GetTick();}// ---- 接收检查是否有来自 PC 的数据 ----if(user_rx_len0){// user_rx_len 在 CDC_ReceiveCallback 里被设置memcpy(rx_buf,UserRxBuffer,user_rx_len);process_pc_command((char*)rx_buf,user_rx_len);// 解析命令user_rx_len0;// 清零, 准备下一次接收// 重新启动接收USBD_CDC_ReceivePacket(hUsbDeviceFS);}HAL_Delay(10);}}接收回调是关键。USB 中间件收到 OUT 端点的数据后会调用这个函数你必须在这里把数据取走并重新申请接收缓冲区// 全局接收缓冲区和长度标记uint8_tUserRxBuffer[APP_RX_DATA_SIZE];// 通常 64 或 128 字节intuser_rx_len0;// CDC 接收回调: USB 中间件自动调用staticint8_tCDC_Receive_Callback(uint8_t*Buf,uint32_t*Len){// 把数据拷出来, 不要直接用 Buf 指针(它的生命周期由中间件管理)memcpy(UserRxBuffer,Buf,*Len);user_rx_len(int)*Len;returnUSBD_OK;}// 上电时先启动第一次接收, 让中间件准备好 OUT 端点voidusb_cdc_start_rx(void){USBD_CDC_ReceivePacket(hUsbDeviceFS);}★ 两个常见错误模式一是在CDC_Receive_Callback里干重活解析 JSON、写 Flash阻塞了 USB 中断导致后续包丢失二是忘了重新调用USBD_CDC_ReceivePacket()接收了一次之后 OUT 端点不再接受新数据PC 再发什么你都收不到。10 个真实翻车点1. USB 时钟不是精确 48MHzPC 不认或乱码。最常见的翻车原因。HSI 内部 RC 振荡器精度只有 ±1%USB 要求 ±0.25%。必须用外部晶振HSE经过 PLL 精确倍频到 48MHz。CubeMX 时钟配置页面里 USB 那栏必须是绿色的 48MHz红色就是没配对。2. 用了充电-only 的 USB 线D/D- 没接通。很多手机充电线只接了 VBUS 和 GND 两根电源线数据线 D 和 D- 是断的。STM32 检测不到 USB 信号自然不会响应枚举。换一根全功能数据线四芯都通的就能解决。这个坑特别容易让人怀疑代码。3. 发送前不查 TX 状态数据被丢弃或阻塞。USBD_CDC_TransmitPacket()在前一次发送还没完成的时候再次调用要么返回忙你忽略了就没发出去要么阻塞等待看实现方式。规矩每次发送前USBD_CDC_GetTxState() 0确认空闲再发或者自己维护一个环形缓冲区在中断里持续搬运。4. 接收回调里干重活USB 栈卡死。CDC_Receive_Callback运行在 USB 中断上下文里。你在里面sscanf、json_parse、甚至f_write到 SD 卡中断占用时间过长USB 总线的 SOF帧起始包处理超时Host 判定设备无响应直接 Reset 你的设备。回调里只做拷贝 设标志位解析放到主循环。5. 忘了启动第一次 ReceivePacket永远收不到数据。USB 设备端默认不会主动监听 OUT 端点。你必须在初始化完成后手动调一次USBD_CDC_ReceivePacket(hUsbDeviceFS)告诉中间件我有空位了Host 可以往这里发。每次收到数据处理后也要再调一次形成循环。6. D 上拉电阻没使能PC 枚举不到设备。USB 2.0 全速设备要求 D 线上有一个 1.5K 上拉电阻接到 3.3V用来告诉 Host “这里有全速设备”。STM32 的 USB 外设内置了这个上拉但需要在代码里使能CubeMX 模板一般自动处理了。如果 PC 的设备管理器里完全看不到你的设备先怀疑这个。7. 大数据吞吐时没考虑端点缓冲区大小。USB 全速设备的 Bulk 端点最大包长是 64 字节。你一次想发 256 字节的温度波形数据底层会拆成 4 个 64 字节的微包依次发出。如果 Host 端读取速度慢IN 端点缓冲区满了就会停顿。高频大数据场景建议加大 APP_TX_DATA_SIZE发送环形缓冲区大小或者改用 ISO同步端点但 CDC 类规定用 Bulk。8. USB 线同时供电电流不足导致复位。板子本身耗电 80mA你又点亮了一块 TFT 屏背光60mA加上几个传感器总共可能超过 150mA。USB 2.0 规范最低保证 100mA很多 hub 和前端端口只供 500mA。板子在 USB 枚举过程中电流尖峰触发过流保护整个设备被 Reset反复枚举失败。做法枚举期间关掉非必要的外设和大功率负载枚举完成后再开。9. 描述符里的 MaxPacketSize 写错。Bulk IN 和 Bulk OUT 端点的最大包长必须是 64全速设备。你手滑填成了 512那是高速设备的值Host 按这个大小给你安排缓冲区实际传输时数据错位。CubeMX 一般自动填对但如果你手动改过描述符文件回头核对一下。10. PC 端串口助手波特率和 CDC 实际速率无关但某些参数会影响行为。CDC 的 SetLineCoding 请求里包含波特率、停止位、校验位这些字段。STM32 的 CDC 中间件通常只是记录下来而不真正按这个参数去调整因为 USB Bulk 传输没有波特率概念。但有些 PC 端串口助手会在打开端口时发一次 SetLineCoding如果你的回调返回错误某些驱动会拒绝打开端口。最稳的做法收到 SetLineCoding 直接返回 OK参数存起来但不使用。完整例程骨架intmain(void){HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USB_DEVICE_Init();usb_cdc_start_rx();// 启动首次接收led_blink_fast();// 快闪表示正在等 USB 枚举while(1){if(dev_is_configured){// 枚举成功后 USBD_SetConfig 回调里置位led_blink_slow();// 慢闪表示正常工作periodic_report();// 定期发温度handle_received_data();// 处理 PC 下发的命令}HAL_Delay(10);}}硬件清单USB Type-C 或 Mini/Micro USB 连接器一个建议带外壳防反插、22R 串联电阻在 D 和 D- 线上ESD 保护兼阻抗匹配、1.5uF 到 4.7uF 去耦电容靠近 USB 引脚。D 上拉不用你自己接STM32 内部有。适合需要跟 PC 通信但没有多余 UART 的产品调试口、固件升级通道、数据上传下载也适合需要一根线搞定供电 通信的便携设备。不适合要求极低延迟或确定性时延的场景USB 有调度开销不如硬 UART 直连也不适合电气隔离场景USB 差分信号不好做光隔那种还是用隔离 RS485 更靠谱。铁律四条USB 时钟必须精确 48MHz 且来自外部晶振、发送前必查 TX 状态、接收回调只拷贝不干活、上电后必须手动启动首次 ReceivePacket。这四条确认过CDC 虚拟串口就是一个免驱的、稳定的 COM 口。USB CDC 最爽的地方不是技术多牛是你把它插到任何一台 Windows 10 / Linux / Mac 上系统弹个提示已识别串口设备打开串口助手就能收发。不用装驱动不用解释为什么电脑找不到设备。这种即插即用的用户体验是硬 UART 做不到的。
返回列表