你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】USB 传输数据时出现卡顿现象

[复制链接]
STMCU小助手 发布时间:2022-2-15 19:59
1 前言
在进行 USB 开发的过程中,有多个客户反馈,USB 传输数据时出现卡顿现象。本文将针对这一问题进行分析。

2 问题分析
这几个客户问题现象基本差不多,采用 STM32 作为 Device 设备,在与上位机或者 PC 端双向通讯一段时间后,从 Device 端到 Host 端的数据能够正常,而从 Host 端到 Device 端的数据异常,也就是说,STM32 在一段时间后不再能正常接收数据,但是,如果只是单向通信,就一直都是正常的。
这几个客户,有用 STM32F2 的,也有用 STM32F4 的,有用 CDC 类的,也有用作 HID 设备的,但都使用了 Cube 库。
下面就具体问题以其中一个客户使用 STM32F411 的 USB CDC 类的案例来分析问题,现象如下 USB 通讯数据(CDC 类):

RA%Y@]HA1K@X1)`5GR3T`UB.png

展开 Data Out 数据:

Q~PHG_]1`BYIBE0$[(HE9.png

分析上图发现,并不是 Host 端没有向 Device 端发送 Data Out 数据,而是确实发送了,但被 Device端 NAK 了。那么为什么会被 NAK 呢?
通过在调试下查看寄存器,我们发现当出现问题时,Data OUT 对应的端点 1 是处于关闭状态,那么为什么端点 1 会关闭?查看 STM32 端的接收代码:
usbd_cdc_if.c:
  1. static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
  2. {
  3. /* USER CODE BEGIN 6 */
  4. //USBTask_ReceiveMsg(Buf, *Len); //UserRxBufferFS
  5. USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  6. USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  7. return (USBD_OK);
  8. /* USER CODE END 6 */
  9. }
复制代码

如上代码,在 MCU 端接收到赖在 Host 端的数据后不做任何处理就立即接收下一次数据传输,问题是,这里对接收到的数据啥也没有做,居然还会出现 Data Out 端点关闭的问题,那么 OUT 端点到底是怎么关闭的呢?我们接下来看子函数:
CDC_Receive_FS() ->USBD_CDC_ReceivePacket() ->USBD_LL_PrepareReceive() -
> HAL_PCD_EP_Receive() ->USB_EPStartXfer()
> 最后在 USB_EPStartXfer 函数中有发现再次使能 OUT 端点的代码:
  1. //…
  2. else /* OUT endpoint */
  3. {
  4. /* Program the transfer size and packet count as follows:

  5. * pktcnt = N
  6. * xfersize = N * maxpacket
  7.   */
  8.   USBx_OUTEP(ep->num)->DOEPTSIZ &= ~(USB_OTG_DOEPTSIZ_XFRSIZ);
  9.   USBx_OUTEP(ep->num)->DOEPTSIZ &= ~(USB_OTG_DOEPTSIZ_PKTCNT);
  10.   if (ep->xfer_len == 0U)
  11.   {
  12.   USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_XFRSIZ & ep->maxpacket);
  13.   USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_PKTCNT & (1U xfer_len + ep->maxpacket -1U)/ ep->maxpacket;
  14.   USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_PKTCNT & (pktcnt num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_XFRSIZ & (ep->maxpacket *
  15.   pktcnt));
  16.   }
  17.   if (dma == 1U)
  18.   {
  19.   USBx_OUTEP(ep->num)->DOEPDMA = (uint32_t)ep->xfer_buff;
  20.   }

  21. if (ep->type == EP_TYPE_ISOC)
  22. {
  23. if ((USBx_DEVICE->DSTS & ( 1U num)->DOEPCTL |= USB_OTG_DOEPCTL_SODDFRM;
  24. }
  25. else
  26. {
  27. USBx_OUTEP(ep->num)->DOEPCTL |= USB_OTG_DOEPCTL_SD0PID_SEVNFRM;
  28. }
  29. }
  30. /* EP enable */
  31. USBx_OUTEP(ep->num)->DOEPCTL |= (USB_OTG_DOEPCTL_CNAK | USB_OTG_DOEPCTL_EPENA);
  32. }
复制代码

也就是说,在调用这个函数之前这个 OUT 端点原本就是关闭的?真的吗?这怎么跟我们理解的不一样?不应该是 OUT 端点一旦打开就一直开着的吗?带着这些疑问,我们查看 STM32F411 的参考手册,终于在 22.17.6 Operational model 一节中找到这么一幅图(由于 CDC 类数据传输采用的是 BULK 传输):

DJ$@890HN0K]FT5U7JRG[6H.png

如上图,MCU 对 BULK 类型的 OUT 数据处理例程大体如下:
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,以阻止接收更多数据包;
5> 应用程序处理 RXFLVL 中断和从 RXFIFO 读取数据;
6> 当应用读取完所有数据(等于 XFRSIZ)后,USB 核将产生一个 XFRC 中断(OTG_FS_DOEPINTx);
7> 应用处理这个 OTG_FS_DOEPINTx 中断并通过 OTG_FS_DOEPINTx 的中断为 XFRC 来判断传输完成;

从上面步骤中的第 4 步中可以看出,当 USB 核收到来自 Host 端的数据后会自动将 OUT 端点关闭,这也就是为什么在接收函数中在接收下一次数据时要再次使能这个 OUT 端点的原因。因此我们大体可以判断出在 OUT 数据传输的过程中,USB 核会禁止端点->打开端点->禁止端点…如此不断循环中;那么问题到底出现在哪里呢?会不会在 USB 核自动关闭端点后就没有再次成功打开?带着这样的怀疑心态逐句查看代码,最终在接收函数的子函数中发现这么一段代码:
  1. HAL_StatusTypeDef HAL_PCD_EP_Receive(PCD_HandleTypeDef *hpcd, uint8_t ep_addr, uint8_t
  2. *pBuf, uint32_t len)
  3. {
  4. USB_OTG_EPTypeDef *ep;

  5. ep = &hpcd->OUT_ep[ep_addr & 0x7FU];

  6. /*setup and start the Xfer */
  7. ep->xfer_buff = pBuf;
  8. ep->xfer_len = len;
  9. ep->xfer_count = 0U;
  10. ep->is_in = 0U;
  11. ep->num = ep_addr & 0x7FU;

  12. if (hpcd->Init.dma_enable == 1U)
  13. {
  14. ep->dma_addr = (uint32_t)pBuf;
  15. }

  16. __HAL_LOCK(hpcd);

  17. if ((ep_addr & 0x7FU) == 0U)
  18. {
  19. USB_EP0StartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
  20. }
  21. else
  22. {
  23. USB_EPStartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
  24. }
  25. __HAL_UNLOCK(hpcd);
  26. return HAL_OK;
  27. }
复制代码


之所以会怀疑这里,这是客户提供了一个信息,单向通信的时候就不会有问题!这是因为在发送数据时,发送函数的底层函数内也使用到了这个互斥锁:
  1. CDC_Transmit_FS() -> USBD_CDC_TransmitPacket() -> USBD_LL_Transmit() ->
  2. HAL_PCD_EP_Transmit() :
  3. HAL_StatusTypeDef HAL_PCD_EP_Transmit(PCD_HandleTypeDef *hpcd, uint8_t ep_addr, uint8_t
  4. *pBuf, uint32_t len)
  5. {
  6. USB_OTG_EPTypeDef *ep;

  7. ep = &hpcd->IN_ep[ep_addr & 0x7FU];
  8. /*setup and start the Xfer */
  9. ep->xfer_buff = pBuf;
  10. ep->xfer_len = len;
  11. ep->xfer_count = 0U;
  12. ep->is_in = 1U;
  13. ep->num = ep_addr & 0x7FU;

  14. if (hpcd->Init.dma_enable == 1U)
  15. {
  16. ep->dma_addr = (uint32_t)pBuf;
  17. }

  18. __HAL_LOCK(hpcd);

  19. if ((ep_addr & 0x7FU) == 0U)
  20. {
  21. USB_EP0StartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
  22. }
  23. else
  24. {
  25. USB_EPStartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
  26. }

  27. __HAL_UNLOCK(hpcd);
  28. return HAL_OK;
  29. }
复制代码

接收处理数据时,底层是通过接收中断回调上来的,但发送时,我们往往将发送放到 main 等用户函数中。这两个是不一样的,一个在中断内,一个在中断外,优先级别是不一样的,优先级不一样就有可能导致资源冲突;

我们进一步查看__HAL_LOCK()宏定义:
  1. #define __HAL_LOCK(__HANDLE__)
  2. do{
  3. if((__HANDLE__)->Lock == HAL_LOCKED)
  4. {
  5. return HAL_BUSY;
  6. }
  7. else
  8. {
  9. (__HANDLE__)->Lock = HAL_LOCKED;
  10. }
  11. }while (0U)
复制代码

若__HAL_LOCK(hpcd);失败则直接返回 return HAL_BUSY 的。为了验证在接收过程中是否__HAL_LOCK 失败,我们引进全局变量 Lock_Flag,在发送函数中若成功 LOCK 则设置Lock_Flag=1,UNLOCK 后则复位为 0:
  1. uint8_t Lock_Flag =0;
  2. HAL_StatusTypeDef HAL_PCD_EP_Transmit(PCD_HandleTypeDef *hpcd, uint8_t ep_addr, uint8_t
  3. *pBuf, uint32_t len)
  4. {
  5. USB_OTG_EPTypeDef *ep;

  6. ep = &hpcd->IN_ep[ep_addr & 0x7FU];

  7. /*setup and start the Xfer */
  8. ep->xfer_buff = pBuf;
  9. ep->xfer_len = len;
  10. ep->xfer_count = 0U;
  11. ep->is_in = 1U;
  12. ep->num = ep_addr & 0x7FU;

  13. if (hpcd->Init.dma_enable == 1U)
  14. {
  15. ep->dma_addr = (uint32_t)pBuf;
  16. }

  17. __HAL_LOCK(hpcd);
  18. Lock_Flag =1;

  19. if ((ep_addr & 0x7FU) == 0U)
  20. {
  21. USB_EP0StartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
  22. }
  23. else
  24. {
  25. USB_EPStartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
  26. }

  27. __HAL_UNLOCK(hpcd);
  28. Lock_Flag =0;
  29. return HAL_OK;
  30. }
复制代码

接下来在接收函数中对全局变量 Lock_Flag 值进行判断,若为 1 则锁死程序,因为在 Lock_Flag=1 时,则表示发送函数中已经获取了锁没有释放,此时若再去获取则会导致失败从而返回 HAL_BUSY;这里通过锁死代码以便判断这种情况:
  1. HAL_StatusTypeDef HAL_PCD_EP_Receive(PCD_HandleTypeDef *hpcd, uint8_t ep_addr, uint8_t
  2. *pBuf, uint32_t len)
  3. {
  4. USB_OTG_EPTypeDef *ep;

  5. ep = &hpcd->OUT_ep[ep_addr & 0x7FU];

  6. /*setup and start the Xfer */
  7. ep->xfer_buff = pBuf;
  8. ep->xfer_len = len;
  9. ep->xfer_count = 0U;
  10. ep->is_in = 0U;
  11. ep->num = ep_addr & 0x7FU;

  12. if (hpcd->Init.dma_enable == 1U)
  13. {
  14. ep->dma_addr = (uint32_t)pBuf;
  15. }

  16. if(Lock_Flag ==1)
  17. {
  18. while(1);
  19. }
  20. __HAL_LOCK(hpcd);

  21. if ((ep_addr & 0x7FU) == 0U)
  22. {
  23. USB_EP0StartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
  24. }
  25. else
  26. {
  27. USB_EPStartXfer(hpcd->Instance , ep, hpcd->Init.dma_enable);
  28. }
  29. __HAL_UNLOCK(hpcd);

  30. return HAL_OK;
  31. }
复制代码

通过调试,当出现问题时,程序果然被锁死在这个 while(1)了,这也证明了正是这个互斥锁所致。因此,我们大体可以判断出现问题时流程大致如下:
1> 在 mian 函数中发送数据 CDC_Transmit_FS()
2> USBD_CDC_TransmitPacket()
3> USBD_LL_Transmit()
4> HAL_PCD_EP_Transmit()
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 端点没有再次打开就已
经提前结束,导致接收循环无以为继。

3 解决方案
知道了问题原因所在,接下来解决问题就相对来说比较容易的了。由于此问题是发送与接收处于不同优先等级导致资源冲突所致,那么我们可以将发送也放到与 USB 接收中断相同的中断等级中去,例如可以利用 USB 的 EOPF 中断,在开启 EOPF 中断后,在此中断内发送数据,这样发送与接收中断就处于相同等级了,EOPF 每 1ms 触发一次,速度完全可以。当然开启一个相同优先级的定时器来做发送数据也是可以,只不过定时器间隔得控制好。

此外,其实此问题是出现在 Cube 库的低版本中,例如 CubeF4 V1.5.0 和 CubeF2 V1.3.0 中都存在,但是在最新本的 CubeF4 V1.16.0,CubeF2 V1.6.0 版本中此问题得到了解决;此问题虽然后来发现是版本太旧所致,但从多个客户反馈此问题来看,此问题依然不失为一个很好的参考和教训。

收藏 评论0 发布时间:2022-2-15 19:59

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版