基于STM32F103的USB CDC在Windows7与Ubuntu上的传输差异问题
从你的描述和代码来看,问题核心在于Windows7自带的USB CDC驱动对高频小数据包的批量传输处理效率远低于Linux内核驱动,再加上你的代码没有考虑USB传输的异步特性,导致大量发送请求被丢弃或阻塞。以下是针对性的解决方案:
1. 修复STM32端的发送逻辑:避免无等待的高频发送
你当前在4000Hz的Systick回调里直接调用CDC_Transmit_FS,但USB CDC的传输是异步的——如果上一次传输还未完成,新的发送请求会返回USBD_BUSY并直接失败,而你的代码没有检查返回值,导致大量数据包根本没发出去。Windows7驱动响应速度慢,这种失败概率更高,而Linux驱动处理更快,所以能跟上频率。
修改代码,加入传输完成的同步机制:
uint8_t tx_in_progress = 0; // 标记当前是否有传输在进行 uint8_t buff[21]; // 你的数据包缓冲区 void HAL_SYSTICK_Callback(void) //4000Hz触发 { if (!tx_in_progress) { tx_in_progress = 1; // 先更新buff里的新数据(如果需要的话) update_tx_data(buff); // 发起传输,传输完成后会触发回调 CDC_Transmit_FS(buff, 21); } } // USB CDC传输完成回调函数 void HAL_CDC_TransmitCpltCallback_FS(uint8_t *Buf, uint32_t *Len) { tx_in_progress = 0; // 标记传输完成,允许下一次发送 }
2. 合并小数据包,减少USB传输次数
STM32F103是全速USB设备,批量端点的最大包长为64字节。你每次发21字节的小包,在Windows7下,驱动每1ms(USB帧周期)只能处理一次批量传输请求,自然只能达到1000包/秒。而Linux可以在一个帧内处理多个小数据包。
可以将多个21字节的小包合并成一个接近64字节的大包再发送:
uint8_t tx_buff[63]; // 3*21=63字节,接近最大包长 uint8_t tx_packet_count = 0; uint8_t single_buff[21]; void HAL_SYSTICK_Callback(void) { // 将当前数据包复制到合并缓冲区 memcpy(&tx_buff[tx_packet_count * 21], single_buff, 21); tx_packet_count++; // 攒够3包就发送 if (tx_packet_count >= 3) { tx_packet_count = 0; // 等待上一次传输完成再发送 while (CDC_Transmit_FS(tx_buff, 63) == USBD_BUSY); } }
这样发送频率降到~1333Hz,Windows7驱动能轻松处理,同时总数据量和之前一致(400021=84000字节/秒,133363≈84000字节/秒)。
3. 替换Windows7的USB CDC驱动
Windows7自带的CDC驱动非常老旧,对批量传输的优化很差。可以安装ST官方提供的USB CDC驱动,或者使用通用USB驱动如libusb-win32/libusbK,这些驱动对小数据包高频传输的支持更好,能大幅提升传输效率。
4. 优化PC端的接收逻辑
如果用Python测试,要确保接收代码足够高效:
- 避免使用过小的缓冲区,或者用
read_all()一次性读取所有缓存数据 - 不要在读取循环里加入不必要的延迟
- 可以用线程异步读取,避免主线程阻塞
示例Python代码:
import serial import time ser = serial.Serial('COM3', 115200, timeout=0, write_timeout=0) start = time.time() packet_count = 0 try: while time.time() - start < 1.0: # 一次性读取所有可用数据,按21字节分包计数 data = ser.read_all() packet_count += len(data) // 21 print(f"Received {packet_count} packets in 1 second") finally: ser.close()
为什么Ubuntu下正常?
Linux内核的USB CDC驱动对批量传输的调度更灵活,允许在单个USB帧(1ms)内处理多个小数据包请求,同时用户态工具如CuteCOM的读取逻辑也更高效,能及时取走缓冲区的数据,不会因为缓冲区满导致丢包。
内容的提问来源于stack exchange,提问作者Danding

