1. 项目缘起:为什么USB CDC大数据量传输这么容易翻车
搞STM32F4的人,十个里有八个踩过USB CDC的坑。CDC(Communication Device Class)这东西看起来简单——不就是把USB虚拟成一个串口嘛,PC端打开个COM口就能收发数据,比写自定义USB驱动省事多了。但真正用起来,尤其是数据量一上来,问题就全冒出来了:丢包、卡死、传输速率上不去、主机端收到的数据缺斤少两、设备端莫名其妙进入NAK状态再也出不来。
我在一个工业数据采集项目里就吃过这个亏。设备端用STM32F407,通过USB CDC往PC端连续传输ADC采样数据,采样率设定在200kSPS,16位精度,算下来原始数据率就是3.2Mbps。理论上USB Full Speed的12Mbps带宽绰绰有余,但实测下来稳定传输速率连1Mbps都不到,而且跑个十几秒就断流。当时排查了整整一周,从USB协议栈翻到HAL库源码,最后才把问题一个个定位清楚。
这篇内容就是把我踩过的坑、验证过的方案、以及最终跑通的稳定传输架构完整梳理出来。核心关键词就几个:USB CDC、STM32F4、大数据量传输、双缓冲、NAK。适合正在用STM32F4做USB通信、被大数据量传输折磨、或者想提前避坑的嵌入式开发者。不管你是刚接触USB CDC的新手,还是已经调过几轮但总觉得不够稳的老手,下面这些内容应该都能帮你省下不少调试时间。
2. 整体方案设计:从“能通”到“稳定高速”的思路拆解
2.1 先搞清楚USB CDC在STM32F4上的数据通路
STM32F4系列的USB外设是OTG_FS(On-The-Go Full Speed),最高支持12Mbps。CDC类在协议栈里其实包含两个接口:一个通信接口(用于控制命令,走中断端点)和一个数据接口(用于批量传输,走Bulk端点)。我们关心的“大数据量传输”走的是Bulk端点,因为Bulk传输不保证带宽但保证可靠性,有重传机制。
数据从MCU到PC的路径大致是这样的:用户代码调用CDC_Transmit_FS(),数据被拷贝到USB协议栈的内部缓冲区,然后USB外设的IN端点等待主机发IN令牌,收到令牌后把数据推上去。如果主机没及时发IN令牌,或者上一个包还没被取走,端点就会返回NAK(Not Acknowledge),表示“我还没准备好,你别问了”。
问题就出在这个NAK上。很多人看到NAK就以为是错误,其实NAK是USB协议里的正常流控手段。但如果你不理解NAK的产生条件和恢复机制,就会陷入“数据发不出去→一直NAK→主机以为设备挂了”的死循环。
2.2 为什么单缓冲架构撑不住大数据量
STM32F4的USB OTG外设,每个端点都有专用的FIFO。以CDC的IN端点(通常是EP1_IN)为例,HAL库默认给它的FIFO大小是64字节(Full Speed下Bulk端点最大包长就是64字节)。当你调用CDC_Transmit_FS()发送一大块数据时,协议栈会把它拆成多个64字节的包,逐个往FIFO里塞。
单缓冲的问题在于:FIFO只有一块,CPU往里面写数据的时候,USB外设不能同时往外发;USB外设往外发的时候,CPU不能往里写。如果CPU写数据的速度跟不上USB外设发送的速度,或者反过来,就会出现等待。更致命的是,当主机端因为某种原因(比如驱动缓冲区满、应用程序没及时读)暂停发送IN令牌时,设备端的FIFO满了之后就会一直返回NAK,而CPU此时如果还在傻等发送完成标志,整个系统就卡住了。
我实测过,在单缓冲+阻塞发送的模式下,STM32F407往PC连续发送1MB数据,平均速率只有300~500kbps,而且CPU占用率极高,因为大部分时间都在轮询发送完成标志。
2.3 双缓冲方案的核心逻辑与选型依据
双缓冲(Double Buffering)的思路很朴素:准备两块缓冲区,一块正在被USB外设发送时,CPU往另一块里填数据;等USB外设发完第一块,自动切换到第二块,同时通知CPU第一块空了可以继续填。这样CPU和USB外设就能并行工作,理论上能把带宽利用率拉满。
在STM32F4上实现双缓冲有两条路:
第一条路是用USB OTG外设硬件自带的双缓冲功能。STM32F4的OTG_FS外设确实支持端点双缓冲,但HAL库对它的封装比较薄,需要直接操作寄存器或者修改底层驱动。而且CDC类的中断处理逻辑和双缓冲配合起来容易出bug,调试成本高。
第二条路是在应用层做双缓冲,也就是在USB协议栈之上再包一层。具体做法是:定义两个发送缓冲区,用一个状态机管理它们的状态(空闲/填充中/发送中)。当一块缓冲区发送完成后,在CDC_TransmitCpltCallback回调里切换到下一块。这种方式不依赖硬件双缓冲,兼容性好,代码可控性强。
我最终选的是第二条路。原因很简单:HAL库的CDC发送回调机制已经够用了,应用层双缓冲不需要动底层寄存器,移植和调试都方便。实测下来,在STM32F407主频168MHz、USB时钟48MHz的配置下,稳定传输速率能跑到8~9Mbps,已经接近Full Speed Bulk传输的理论上限。
2.4 关键参数计算:缓冲区大小怎么定
缓冲区大小不是拍脑袋定的。假设你要传输的数据率是R(bps),USB Full Speed Bulk的实际有效带宽大约是9~10Mbps(扣除协议开销和主机调度延迟),那么缓冲区大小B至少要满足:
B ≥ R × T_latency / 8
其中T_latency是主机端两次IN令牌之间的最大间隔。在Windows环境下,这个间隔通常在1~10ms之间波动。以R=8Mbps、T_latency=5ms计算,B ≥ 8×10^6 × 5×10^-3 / 8 = 5000字节。所以缓冲区至少给到4KB~8KB才比较稳妥。
我实际用的是两个8KB的缓冲区,总共占用16KB RAM。STM32F407有192KB RAM,这点开销完全可以接受。如果你用的是STM32F401这种RAM较小的型号,可以适当降到2KB~4KB,但传输速率会受一些影响。
3. 核心细节解析:NAK、FIFO与回调机制的联动关系
3.1 NAK到底意味着什么,什么时候该紧张
NAK在USB协议里是握手包的一种,设备端返回NAK表示“我现在没法处理你的请求,稍后再试”。对于Bulk IN端点来说,NAK通常出现在以下场景:
- USB外设的IN端点FIFO为空,主机发IN令牌时没数据可发
- 上一个数据包还在发送中,新的IN令牌就来了
- 应用层没有及时往FIFO里补充数据
前两种情况是正常的流控,主机端会自动重试,不需要干预。第三种情况才是需要关注的——如果应用层持续不喂数据,主机端会一直收到NAK,虽然协议上没问题,但实际传输速率会掉到零。
我遇到过一个典型故障:设备端在发送完一包数据后,等待应用层准备下一包数据的时间过长(因为应用层在做Flash写入),导致USB端点连续返回NAK超过100ms。Windows端的CDC驱动在这种情况下会认为设备异常,直接关闭COM口。后来我把Flash写入操作改成异步的,并且把缓冲区加大到16KB,这个问题就再没出现过。
注意:NAK本身不是错误,但持续NAK超过主机驱动的容忍阈值就会导致连接断开。Windows CDC驱动的容忍时间通常在100~500ms之间,具体取决于驱动版本和系统负载。
3.2 STM32F4的USB FIFO分配策略
STM32F4的USB OTG外设总共有320个32位字的FIFO RAM(约1.25KB),需要分配给各个端点。HAL库默认的分配方案是:
| 端点 | 方向 | FIFO大小(32位字) | 用途 |
|---|---|---|---|
| EP0 | IN/OUT | 64 | 控制传输 |
| EP1 | IN | 64 | CDC数据发送 |
| EP2 | OUT | 64 | CDC数据接收 |
| EP3 | IN | 64 | CDC命令 |
这个默认分配对于小数据量够用,但做大数据量传输时,EP1_IN的FIFO只有64字(256字节),意味着USB外设一次最多缓存256字节。如果主机端取数据的速度不够快,FIFO很快就满了,CPU再往里写就会阻塞。
我的优化方案是把EP1_IN的FIFO加大到128字(512字节),同时把EP2_OUT也加大到128字。修改方法是在usbd_conf.c里的HAL_PCD_MspInit或者USBD_LL_Init中调整FIFO分配:
HAL_PCDEx_SetRxFiFo(&hpcd_USB_OTG_FS, 0x80); // 128字给RX HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 0, 0x40); // EP0 64字 HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 1, 0x80); // EP1 128字 HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 2, 0x40); // EP2 64字这样调整后,USB外设的缓冲能力翻倍,NAK出现的频率明显降低。
3.3 发送完成回调的正确使用姿势
HAL库的CDC发送完成回调是CDC_TransmitCpltCallback(),它在USB外设成功把数据推上总线后被调用。很多人在这里直接调用下一次CDC_Transmit_FS(),这是不对的——因为回调是在中断上下文里执行的,而CDC_Transmit_FS()内部有信号量等待操作,在中断里调用会导致死锁。
正确的做法是在回调里只做状态标记,把实际的数据填充和发送放到主循环里:
volatile uint8_t tx_buf_index = 0; volatile uint8_t tx_buf_busy[2] = {0, 0}; void CDC_TransmitCpltCallback(uint8_t *Buf, uint32_t *Len, uint8_t epnum) { if (epnum == CDC_IN_EP) { tx_buf_busy[tx_buf_index] = 0; // 标记当前缓冲区空闲 tx_buf_index ^= 1; // 切换到另一块 } } // 主循环中 void main_loop(void) { if (!tx_buf_busy[tx_buf_index]) { fill_buffer(tx_buf[tx_buf_index]); // 填充数据 tx_buf_busy[tx_buf_index] = 1; CDC_Transmit_FS(tx_buf[tx_buf_index], BUF_SIZE); } }这个模式跑起来之后,CPU和USB外设真正实现了并行,实测CPU占用率从原来的70%降到了15%以下。
3.4 主机端接收端的配合优化
设备端做得再好,主机端拖后腿也白搭。Windows的CDC驱动默认的接收缓冲区大小是4KB,如果应用程序读取速度跟不上,驱动缓冲区满了之后就会暂停发送IN令牌,设备端就会开始NAK。
我的做法是在PC端用重叠I/O(Overlapped I/O)方式打开COM口,并且把接收缓冲区设到64KB。具体是在SetCommTimeouts和SetupComm里调整:
SetupComm(hCom, 65536, 65536); // 收发缓冲区各64KB COMMTIMEOUTS timeouts = {0}; timeouts.ReadIntervalTimeout = 1; timeouts.ReadTotalTimeoutMultiplier = 0; timeouts.ReadTotalTimeoutConstant = 10; SetCommTimeouts(hCom, &timeouts);这样调整后,PC端可以一次性吞下大量数据,不会因为应用程序的读取延迟而反压设备端。
4. 完整实操流程:从零搭建稳定传输框架
4.1 硬件与工程环境准备
硬件平台我用的是STM32F407VGT6核心板,外部晶振8MHz,USB接口直接引出D+和D-。注意USB的D+需要接一个1.5kΩ上拉电阻到3.3V,用来告诉主机这是一个Full Speed设备。STM32F4的OTG_FS内部有可配置的上拉电阻,通过HAL_PCDEx_ActivateVBUS或者直接配置OTG_FS_DCTL寄存器的SDIS位来控制,不需要外部电阻。
软件环境是STM32CubeMX 6.x + Keil MDK 5.x + STM32CubeF4 HAL库。在CubeMX里配置USB_OTG_FS为Device模式,Middleware选CDC类,时钟树把USB时钟设为48MHz(来自PLL的Q分频)。
生成代码后,重点检查usbd_conf.c里的FIFO分配和usbd_cdc_if.c里的收发函数。默认生成的代码是单缓冲阻塞模式,需要按下面的步骤改造。
4.2 双缓冲发送状态机的实现
先定义缓冲区和状态变量:
#define CDC_TX_BUF_SIZE 8192 #define CDC_TX_BUF_NUM 2 static uint8_t cdc_tx_buf[CDC_TX_BUF_NUM][CDC_TX_BUF_SIZE]; static volatile uint8_t cdc_tx_busy[CDC_TX_BUF_NUM] = {0}; static volatile uint8_t cdc_tx_idx = 0; static volatile uint32_t cdc_tx_len[CDC_TX_BUF_NUM] = {0};发送完成回调里只做状态切换:
void CDC_TransmitCpltCallback(uint8_t *Buf, uint32_t *Len, uint8_t epnum) { if (epnum == CDC_IN_EP) { cdc_tx_busy[cdc_tx_idx] = 0; cdc_tx_idx ^= 1; } }主循环里的发送逻辑:
void cdc_send_task(void) { if (cdc_tx_busy[cdc_tx_idx] == 0) { uint32_t len = prepare_data(cdc_tx_buf[cdc_tx_idx], CDC_TX_BUF_SIZE); if (len > 0) { cdc_tx_busy[cdc_tx_idx] = 1; cdc_tx_len[cdc_tx_idx] = len; USBD_CDC_SetTxBuffer(&hUsbDeviceFS, cdc_tx_buf[cdc_tx_idx], len); USBD_CDC_TransmitPacket(&hUsbDeviceFS); } } }这里有个细节:不要用CDC_Transmit_FS(),因为它内部会检查上一次传输是否完成,如果没完成就直接返回USBD_BUSY。用USBD_CDC_SetTxBuffer+USBD_CDC_TransmitPacket的组合更底层,配合我们自己的状态机更可控。
4.3 数据源与缓冲区的解耦设计
在实际项目里,数据源(比如ADC、传感器、Flash读取)的速度和USB发送的速度往往不匹配。如果直接在发送任务里读数据源,容易造成阻塞。我的做法是在中间再加一层环形缓冲区(Ring Buffer),数据源往环形缓冲区里写,发送任务从环形缓冲区里读。
#define RING_BUF_SIZE 32768 static uint8_t ring_buf[RING_BUF_SIZE]; static volatile uint32_t ring_wr = 0; static volatile uint32_t ring_rd = 0; uint32_t ring_write(const uint8_t *data, uint32_t len) { uint32_t available = (ring_rd - ring_wr - 1 + RING_BUF_SIZE) % RING_BUF_SIZE; if (len > available) len = available; for (uint32_t i = 0; i < len; i++) { ring_buf[ring_wr] = data[i]; ring_wr = (ring_wr + 1) % RING_BUF_SIZE; } return len; } uint32_t ring_read(uint8_t *data, uint32_t len) { uint32_t available = (ring_wr - ring_rd + RING_BUF_SIZE) % RING_BUF_SIZE; if (len > available) len = available; for (uint32_t i = 0; i < len; i++) { data[i] = ring_buf[ring_rd]; ring_rd = (ring_rd + 1) % RING_BUF_SIZE; } return len; }这样数据源和USB发送完全解耦,ADC中断里只管往环形缓冲区写,USB发送任务只管从环形缓冲区读,两边互不阻塞。
4.4 实测数据与性能对比
我用同一块STM32F407板子,分别测试了三种方案下的传输性能。测试方法是连续发送1MB的随机数据,PC端用Python脚本接收并校验,重复10次取平均值。
| 方案 | 平均速率 | CPU占用率 | 连续运行稳定性 |
|---|---|---|---|
| 单缓冲阻塞发送 | 420 kbps | 72% | 30秒内必断 |
| 单缓冲+非阻塞发送 | 2.1 Mbps | 45% | 5分钟内偶发断流 |
| 双缓冲+环形缓冲区 | 8.6 Mbps | 14% | 连续2小时无断流 |
双缓冲方案的速率已经接近USB Full Speed Bulk传输的实际上限。如果你需要更高的速率,只能换STM32F4的High Speed USB型号(比如STM32F429带ULPI接口的),或者用STM32H7系列。
4.5 中断优先级与系统实时性保障
USB中断的优先级配置很关键。STM32F4的USB OTG_FS中断默认优先级是0,如果系统里还有别的中断(比如ADC、定时器),需要合理分配优先级,避免USB中断被长时间阻塞。
我的配置是:USB中断优先级设为1,ADC中断设为2,定时器中断设为3。这样USB中断能及时响应,同时ADC中断也不会被USB中断完全压制。注意NVIC的优先级数值越小优先级越高,但分组方式要统一设置。
另外,CDC_TransmitCpltCallback是在USB中断里执行的,里面不要做耗时操作。我见过有人在回调里直接做数据校验和拷贝,结果USB中断执行时间超过100us,导致主机端IN令牌响应超时。回调里只做状态标记,耗时操作全部放到主循环。
5. 常见问题与排查技巧实录
5.1 设备管理器里COM口时有时无
这是最常见的问题,通常有三个原因。第一是USB时钟配置不对,STM32F4的USB OTG_FS要求48MHz时钟,如果PLL的Q分频没配对,USB外设根本起不来。检查SystemClock_Config里的RCC_PLLQInit,确保PLLQ的值让USB时钟等于48MHz。第二是D+上拉电阻没使能,CubeMX里要勾选Activate VBUS或者手动配置OTG_FS_GCCFG寄存器的PWRDWN和VBUSBSEN位。第三是供电不足,USB口如果只提供500mA,而板子上有别的耗电外设,可能导致USB外设复位。
5.2 传输过程中突然断流,重新插拔才能恢复
这个问题的根源通常是NAK超时。主机端连续收到NAK超过一定时间后,会认为设备异常并关闭端口。排查方法是:在CDC_TransmitCpltCallback里加一个计数器,统计两次成功发送之间的NAK次数。如果发现NAK次数持续增长,说明应用层喂数据的速度跟不上。
解决办法有两个:一是加大缓冲区,让应用层有更多时间准备数据;二是优化数据源,把耗时操作(比如Flash写入、复杂计算)放到DMA或者低优先级任务里,不要阻塞USB发送任务。
5.3 接收方向丢数据
CDC的OUT端点(主机到设备)同样有FIFO限制。如果主机发送速度过快,设备端来不及读,FIFO满了之后主机会收到NAK,然后重试。但如果重试次数超过主机驱动的限制,数据就会丢。
我的做法是在CDC_Receive_FS回调里尽快把数据拷到环形缓冲区,然后立即重新挂载OUT端点:
static int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len) { ring_write(Buf, *Len); USBD_CDC_SetRxBuffer(&hUsbDeviceFS, Buf); USBD_CDC_ReceivePacket(&hUsbDeviceFS); return USBD_OK; }注意USBD_CDC_ReceivePacket要尽快调用,否则OUT端点会一直NAK。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| COM口识别不到 | USB时钟不对 | 示波器测PA11/PA12 | 检查PLLQ配置 |
| 传输速率低 | 单缓冲阻塞 | 测CPU占用率 | 改双缓冲 |
| 运行中断流 | NAK超时 | 统计NAK次数 | 加大缓冲区 |
| 接收丢数据 | OUT FIFO满 | 查OUT端点NAK | 加快读取速度 |
| 回调死锁 | 中断里调用阻塞函数 | 查调用栈 | 回调只做标记 |
5.5 几个容易被忽略的细节
第一个细节是USB线缆质量。我遇到过用劣质USB线导致传输速率只有2Mbps的情况,换根好线直接跑到8Mbps。USB Full Speed虽然对线缆要求不高,但线太长或者屏蔽太差还是会影响信号完整性。
第二个细节是PC端驱动的缓冲区设置。Windows默认的CDC驱动缓冲区是4KB,对于高速传输来说太小了。用SetupComm把缓冲区设到64KB以上,效果立竿见影。
第三个细节是STM32F4的USB中断和DMA的配合。如果你同时用了DMA做ADC采样,注意DMA中断和USB中断的优先级关系。我建议USB中断优先级高于DMA中断,因为USB对实时性要求更高。
第四个细节是CDC_Transmit_FS的返回值处理。这个函数返回USBD_OK只表示数据被成功放入了协议栈缓冲区,不表示数据已经发到主机。真正的发送完成要靠回调通知。很多人误以为返回OK就发完了,结果在回调里又发一次,导致数据重复。
6. 进阶优化:从8Mbps到接近理论极限
6.1 减少内存拷贝次数
上面的方案里,数据从数据源到USB FIFO至少经过两次拷贝:数据源→环形缓冲区→双缓冲区→USB FIFO。每次拷贝都消耗CPU时间。如果数据源是DMA可以直接写入的(比如ADC的DMA模式),可以让DMA直接写到双缓冲区,省掉环形缓冲区这一层。
具体做法是把双缓冲区的一块配置为DMA的目标地址,DMA传输完成中断里标记该缓冲区就绪,然后USB发送任务直接发送这块缓冲区。这样数据只经过一次拷贝(DMA硬件拷贝),CPU几乎不参与数据搬运。
6.2 利用USB OTG的硬件双缓冲
STM32F4的OTG_FS外设支持端点硬件双缓冲,配置正确的话,USB外设可以在发送一块缓冲的同时,自动切换到另一块,完全不需要CPU干预。但HAL库对硬件双缓冲的支持不完整,需要修改usbd_conf.c里的端点初始化代码,把HAL_PCD_EP_Open的ep_type参数改成EP_TYPE_BULK_DOUBLE_BUF,并且手动管理PCD_EPTypeDef结构体里的doublebuffer标志。
硬件双缓冲的收益是CPU占用率进一步降低,但调试难度大,容易出现端点状态不一致的问题。如果你的项目对CPU占用率极其敏感,可以尝试;否则应用层双缓冲已经够用了。
6.3 主机端用WinUSB替代CDC
CDC类在Windows上走的是usbser.sys驱动,这个驱动的开销不小,而且缓冲区管理不够灵活。如果PC端软件可以自己控制,可以考虑用WinUSB(通过WCID机制让Windows自动加载WinUSB驱动),直接走Bulk端点,省掉CDC的命令接口和驱动层开销。实测WinUSB方案比CDC方案速率能再提升10%~15%。
不过WinUSB的代价是PC端需要写更多的USB通信代码,不能像COM口那样用标准的串口API。如果你的PC端软件是现成的串口工具,那就只能继续用CDC。
6.4 长时间运行的稳定性验证
大数据量传输不仅要看瞬时速率,还要看长时间运行的稳定性。我做过一个72小时连续传输测试,设备端每秒钟发送1MB数据,PC端接收并校验。测试过程中记录了每次断流的时间和原因。
测试结果:前24小时无断流,24~48小时出现3次短暂NAK超时(每次持续约200ms后自动恢复),48~72小时无断流。分析日志发现,NAK超时都发生在PC端系统负载较高的时候(Windows更新、杀毒软件扫描)。这说明主机端的负载会影响USB传输的稳定性,设备端能做的就是加大缓冲区、降低发送速率波动。
提示:如果你的应用场景对连续性要求极高,建议在设备端加一个看门狗,检测到USB断流后自动重新初始化USB外设,而不是等主机端重新枚举。
6.5 结合DMA双缓冲与DDS技术的扩展思路
最近看到有同行在STM32H7上结合DMAMUX双缓冲和DDS(直接数字频率合成)技术做高精度波形生成,思路很有意思。DDS负责生成波形数据,DMAMUX双缓冲负责把数据搬运到USB端点,CPU几乎不参与。这个架构在STM32F4上也可以借鉴,虽然F4的DMAMUX功能不如H7灵活,但基本的DMA双缓冲模式是支持的。
具体做法是用TIM触发DMA,DMA在双缓冲区之间交替搬运DDS生成的波形数据,USB发送任务直接发送DMA刚填满的那块缓冲区。这样整个数据通路从生成到发送全部由硬件完成,CPU只负责状态管理。对于需要连续输出高频波形数据的场景,这个方案能把CPU占用率压到5%以下。
7. 个人实操体会与后续扩展方向
回过头看,USB CDC大数据量传输的核心矛盾就一个:USB外设的FIFO深度有限,而主机端的取数节奏不可控。双缓冲解决的是CPU和USB外设之间的并行问题,环形缓冲区解决的是数据源和USB发送之间的速度匹配问题,NAK处理解决的是主机端流控和设备端状态机的协调问题。这三者缺一不可。
我在实际项目里最终跑通的配置是:STM32F407主频168MHz,USB时钟48MHz,EP1_IN FIFO 128字,双缓冲区各8KB,环形缓冲区32KB,PC端接收缓冲区64KB。连续传输速率稳定在8.5Mbps左右,CPU占用率12%~15%,72小时无断流。
后续如果要做进一步优化,我会从两个方向入手:一是把数据源改成DMA直接写入双缓冲区,省掉环形缓冲区这一层拷贝;二是研究STM32F4的硬件双缓冲端点配置,把CPU占用率再压一压。不过对于大多数工业采集和通信场景来说,当前的方案已经足够稳定了。
最后分享一个小技巧:调试USB CDC的时候,一定要在PC端用带时间戳的接收工具,把每次收到数据的时间和长度都记录下来。这样一旦出现断流或者速率下降,可以对照时间戳快速定位是设备端的问题还是主机端的问题。我用的是一个Python脚本,基于pyserial库,每收到一包数据就打印时间戳和长度,排查效率比盲猜高得多。