很多STM32开发者在实现CDC类虚拟串口与PC主机通信过程中,有时会遇到点麻烦而不得其解。那就是当主机端单次发送的数据不超过64字节时,接收正常。一旦发送数据量大于64字节时就接收失败,总是出现丢包现象,似乎只能接收64字节以内的数据。网上有人干脆建议主机每次发送不要超过64字节,当然,也有人提及要作分包处理但没具体实现代码可以参考。
作为CDC类的USB设备,到底能不能正确接收来自主机64字节以上的批量数据呢?
其实是可以的,只是当我们一次传输的数据大于当前端点所支持的最大包长时【这里端点使用BULK传输,一般最大包长默认设置为64字节】,USB模块会做分包传输,将一批数据传输分成多个处理[或称事务],即多个transaction来完成,每个Transaction里的数据包传输的最大数据量为64字节。
原理性的东西,这里不多啰嗦了,网上有成堆的介绍资料【当然,或许有点乱】。当我们弄清整个原理后,就可以自行组织接收处理代码了。下面我利用HAL库,基于STM32F429芯片演示实现过程,重点在接收处理代码。我使用STM32F429Discovery开发板,使用HSUSB模块并令其工作在FSMODE,这样我们就可以方便地使用片内USBFS PHY。
我使用STM32CubeMx工具进行配置,生成基于STM32 HAL库的工程。使用ST提供的STM32CubeIDE进行编译调试。有关配置就不截图了。
另外,我还配置了1个按键并开启相应外部中断。每发生按键事件时,F429USB设备向PC主机发送一段打招呼的字符串,并通过串口助手显示出来。如下图所示:
我在main.c文件里额外定义了下面几个变量:
其中,Flag_KeyPressed和Flag_DataReceived分别表示按键操作和收到从主机发过来的数据的情况。Rx_buffer【】数组用来存放接收来自主机的全部数据,这里的定义长度为512字节【你具体使用时按需设置】。下图是Main.c里的主循环代码截图:
主循环里检查按键标志和收到数据的标志,如有按键发生,则向主机发送前面提到的打招呼的字符串;如有收到来自主机的数据,则向主机回送过去。
今天的重点是讨论USB设备如何从主机接收64字节以上的数据。基于现有HAL库,对于USB设备的接收,我们只需关注一个USB中断接收回调函数,那就是CDC_Receive_HS()函数。该函数在usbd_cdc_if.c文件里。我具体编写的函数代码如下面两幅截图所示。
代码很简单。 我在库代码的基础上增加了橙色方框内的代码。基本功能就是,先读取当前收到的数据长度【SinglePackLength】,分整包和非整包两种情况鉴别后再处理。若是接收的整包数据,继续等待接收下一包;若是非整包,视为此次传输结束,并设置收到标志Flag_DataReceived为非0值,然后在主循环里将收到的数据回送给主机。
其中,Max_Pack_Size是当前CDC类BULK传输端点的最大传输包长,这里为64字节。
Num_Rx_Data表示接收到数据个数,Num_Out_Pack表示接收到的数据包个数,Num_Packet跟Num_Out_Pack内容一样,不过,Num_Packet等于0还表示准备开始新一轮传输的接收。这里多定义Num_Out_Pack,一个重要目的是便于调试时查看结果。
基于上面的接收处理代码,我们来验证结果:
借助PC端的串口助手向STM32F429 USB设备发送了5个字符,我们通过STM32CubeIDE调试环境可以清晰地从上图看到设备收到的数据个数为5,数据包个数为1。显然没问题。那个Rx_buffer数组是我用来存放接收数据的,若在调试窗口打开,数据较多列表显示会很长,这里就没打开了。事实上接收的数据内容也是没问题的。
下图是借助PC端的串口助手向STM32F429 USB设备发送了305个字符的接收情况:
显然,对于305个字符,PC主机端要分成5包才能发送完毕,即4整包【每包64字节】再加1个非完整包。所以USB设备接收结果也正好是5包,即上图中Num_Out_pack的数据,接收到的数据量为305,即上图中Num_Rx_Data的数据。同样,结果OK。
下图是PC端刚好发送一个完整包64字节数据的USB设备接收情况,也一切正常。
也就是说,使用我上面编写的接收处理代码,对主机发送的数据的个数不再局限于64字节以内了。当然,具体应用时我们还可以根据主机端单次传输数据的大小情况及提取数据的方式适当调整这里的Rx_buffer[]数组大小。【注:上面测试时我临时关闭了设备端的数据回送功能,是为了避免截图里的数据混乱,让人分不清原数据和回显数据】
有人可能在上面的接收代码里看到了一个变量Wait_Rx_Dly。刚开始,代码里是没有这个变量的。后来我在测试主机发送64字节整包数据时,发现了一个小问题【最终到底算不算问题,或许要视具体应用场景而定,我这里稍作了点处理】。
问题是这样的:
主机每发送1包64字节数据时,设备端接收没有问题,但只要主机端每次发送64字节完整数据包过来,不论相隔时间多久,设备端依然接收,且总在前次结果上累加,除非主机端发一个非64字节数据包过来,设备接收就总是视为同一次传输。比方像下面截图所示:
上图就是PC主机端借助串口助手分四次且每次发送64字节数据时USB设备的接收情况。从图上不难看出,4次数据都汇集在一起了,关键还没完,还在等待后续数据过来。我是希望一次传输做一次处理,于是我让设备每收到一个完整数据包,就重新给定一个延时,比方3~5ms,用变量Wait_Rx_Dly来记录延时值,在某毫秒定时中断里对该变量做减1操作。若延时到了还没有收到数据或延时过程中收到非完整包则视为本次传输结束,然后去处理刚才收到的数据。
我顺便使用了HAL库里自带的systick中断函数来做这个延时管理。当接收处理代码加上这个延时管理后,就不会出现不同传输批次的数据挤在一块了。问题得以解决。
另外,我上面分享的接收处理代码也有很好的通用性,并不局限于Bulk传输。我们知道,不同传输类型的端点的最大包长往往并不一样,如果使用上面的参考代码,我们只需调整那个最大包长参数【Max_Pack_Size】,并适当调整Rx_buffer[]数组的大小就可以使用了。数据个数、传输包个数这些变量都可以保留使用。