USB传输数据时出现卡顿现象 . _% ]+ A0 y. f. [# ^7 c4 L; g1 前言 在进行USB开发的过程中,有多个客户反馈,USB传输数据时出现卡顿现象。本文将针对这一问题进行分析。 2 问题分析 这几个客户问题现象基本差不多,采用STM32作为Device设备,在与上位机或者PC端双向通讯一段时间后,从Device端到Host端的数据能够正常,而从Host端到Device端的数据异常,也就是说,STM32在一段时间后不再能正常接收数据,但是,如果只是单向通信,就一直都是正常的。2 q" e( r" p0 y, b* Z% i 这几个客户,有用STM32F2的,也有用STM32F4的,有用CDC类的,也有用作HID设备的,但都使用了Cube库。 下面就具体问题以其中一个客户使用STM32F411的USB CDC类的案例来分析问题,现象如下USB通讯数据(CDC类): Figure 1 通信一段时间后Data Out数据异常 ( Q' B1 w! q8 x8 I T! q+ o 展开Data Out数据: Figure 2 Data Out数据异常: m- E0 n; y/ V9 T ?; N $ B3 E3 X4 u! z7 x$ e( j/ H 分析上图发现,并不是Host端没有向Device端发送Data Out数据,而是确实发送了,但被Device端NAK了。那么为什么会被NAK呢?1 f X0 b8 {5 b9 c2 ?8 W, ]1 i- f 通过在调试下查看寄存器,我们发现当出现问题时,Data OUT对应的端点1是处于关闭状态,那么为什么端点1会关闭?查看STM32端的接收代码: usbd_cdc_if.c: : c& o) t! l6 p4 _% M' e 如上代码,在MCU端接收到赖在Host端的数据后不做任何处理就立即接收下一次数据传输,问题是,这里对接收到的数据啥也没有做,居然还会出现Data Out端点关闭的问题,那么OUT端点到底是怎么关闭的呢?我们接下来看子函数:4 I7 Q$ ~4 @$ P0 Q* o CDC_Receive_FS() ->USBD_CDC_ReceivePacket() ->USBD_LL_PrepareReceive() ->HAL_PCD_EP_Receive() ->USB_EPStartXfer() 最后在USB_EPStartXfer函数中有发现再次使能OUT端点的代码: 也就是说,在调用这个函数之前这个OUT端点原本就是关闭的?真的吗?这怎么跟我们理解的不一样?不应该是OUT端点一旦打开就一直开着的吗?带着这些疑问,我们查看STM32F411的参考手册,终于在22.17.6 Operational model一节中找到这么一幅图(由于CDC类数据传输采用的是BULK传输): Figure 3 MCU对OUT数据传输的处理流程; c) L+ H- ~* U1 L. v2 H0 c 如上图,MCU对BULK类型的OUT数据处理例程大体如下:9 B- g4 H5 y2 C' B d: B# B 1> Host端试图向一个端点发送OUT token; 2> 当Device端的USB外设接收到这么一个OUT token后,如果RXFIFO空间足够,它将数据包存储到RXFIFO中; 3> 在将数据包内容存储到RXFIFO后,USB外设将产生一个RXFLVL中断(OTG_FS_GINTSTS); 4> 在接收到USB数据包的个数后(PKTCNT),USB核将内部自动将这个OUT端点的NAK为置1,以阻止接收更多数据包;" \6 {% u5 O+ y4 Z& M; h 5> 应用程序处理RXFLVL中断和从RXFIFO读取数据;( M& a4 {" M: n8 b 6> 当应用读取完所有数据(等于XFRSIZ)后,USB核将产生一个XFRC中断(OTG_FS_DOEPINTx);$ | f, }& h/ z Q/ h6 m9 G3 i# s 7> 应用处理这个OTG_FS_DOEPINTx中断并通过OTG_FS_DOEPINTx 的中断为XFRC来判断传输完成; 从上面步骤中的第4步中可以看出,当USB核收到来自Host端的数据后会自动将OUT端点关闭,这也就是为什么在接收函数中在接收下一次数据时要再次使能这个OUT端点的原因。因此我们大体可以判断出在OUT数据传输的过程中,USB核会禁止端点->打开端点->禁止端点…如此不断循环中;那么问题到底出现在哪里呢?会不会在USB核自动关闭端点后就没有再次成功打开?带着这样的怀疑心态逐句查看代码,最终在接收函数的子函数中发现这么一段代码: 之所以会怀疑这里,这是客户提供了一个信息,单向通信的时候就不会有问题!这是因为在发送数据时,发送函数的底层函数内也使用到了这个互斥锁: : d, x) s3 Y4 x) a6 X& X 接收处理数据时,底层是通过接收中断回调上来的,但发送时,我们往往将发送放到main等用户函数中。这两个是不一样的,一个在中断内,一个在中断外,优先级别是不一样的,优先级不一样就有可能导致资源冲突;- _- A) o! K/ j5 x6 W9 [0 f& v ' \1 L4 b3 ~. d" u. Y5 n3 H6 ] 我们进一步查看__HAL_LOCK()宏定义:* O# _; h7 y/ \/ F 若__HAL_LOCK(hpcd);失败则直接返回return HAL_BUSY的。为了验证在接收过程中是否__HAL_LOCK失败,我们引进全局变量Lock_Flag,在发送函数中若成功LOCK则设置Lock_Flag=1,UNLOCK后则复位为0: - ^! r% b( o; A4 n1 h, r 接下来在接收函数中对全局变量Lock_Flag值进行判断,若为1则锁死程序,因为在Lock_Flag=1时,则表示发送函数中已经获取了锁没有释放,此时若再去获取则会导致失败从而返回HAL_BUSY;这里通过锁死代码以便判断这种情况: , U9 D# k8 a) d% ^! d, Y 通过调试,当出现问题时,程序果然被锁死在这个while(1)了,这也证明了正是这个互斥锁所致。因此,我们大体可以判断出现问题时流程大致如下: 1> 在mian函数中发送数据CDC_Transmit_FS() 2> USBD_CDC_TransmitPacket() 3> USBD_LL_Transmit()# T0 J ^' S+ X/ |$ A8 u4 } 4> HAL_PCD_EP_Transmit()5 c: J3 V1 Y6 A5 m3 n0 O, M 5> __HAL_LOCK(hpcd); 此时成功获取互斥锁 6> 恰好此时有一个接收中断,由于USB中断具有优先级,跳转到接收中断内执行;同时,USB核会自动关闭OUT端点; 7> HAL_PCD_DataOutStageCallback() 8> USBD_CDC_DataOut() 9> CDC_Receive_FS() 10> USBD_CDC_ReceivePacket() 11> USBD_LL_PrepareReceive() 12> HAL_PCD_EP_Receive() 13> __HAL_LOCK(hpcd); 此时获取互斥锁失败导致返回,接收函数在OUT端点没有再次打开就已经提前结束,导致接收循环无以为继。8 j) k J% Y2 w- y * R, ^# ~) [7 E: M2 I6 W/ I9 X 3 解决方案 知道了问题原因所在,接下来解决问题就相对来说比较容易的了。由于此问题是发送与接收处于不同优先等级导致资源冲突所致,那么我们可以将发送也放到与USB接收中断相同的中断等级中去,例如可以利用USB的EOPF中断,在开启EOPF中断后,在此中断内发送数据,这样发送与接收中断就处于相同等级了,EOPF每1ms触发一次,速度完全可以。当然开启一个相同优先级的定时器来做发送数据也是可以,只不过定时器间隔得控制好。 此外,其实此问题是出现在Cube库的低版本中,例如CubeF4 V1.5.0和CubeF2 V1.3.0中都存在,但是在最新本的CubeF4 V1.16.0,CubeF2 V1.6.0版本中此问题得到了解决;此问题虽然后来发现是版本太旧所致,但从多个客户反馈此问题来看,此问题依然不失为一个很好的参考和教训。$ y0 b$ Y, e6 z$ J& X1 t. f# U' K & O4 J0 o8 s C, S2 G 5 u$ J) y/ N$ A 文档下载1>> 文档下载2>> 更多实战经验>> |
USB Audio设计与实现
【MCU实战经验】+STM32F107的USB使用
圈圈发布USB图书第二版有感,以及分享一些我学习USB过程...
STM32F4-DISC 实现USB主机(U盘)和USB设备(虚拟串口)自动切换
STM32 USB-HID通信移植步骤STM32 USB HID键盘例程
最全USB HID开发资料,悉心整理一个月,亲自测试
【经验分享】在进行 USB CDC 类开发时,无法发送 64整数倍的数据
如何让CDC类USB设备批量接收64字节以上数据
用STM32F4实现的USB摄像头UVC,配合上位机可识别车牌
STM32 USB CDC 虚拟多串口
st的工程师就是这样修bug的??由于设计的锁中断不安全,直接把锁删除了
" y( b0 z% P# ]" `* o
锁删除之后,还能工作吗?' e7 D, m0 v8 m