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

【经验分享】在进行 USB CDC 类开发时,无法发送 64整数倍的数据

[复制链接]
STMCU小助手 发布时间:2022-2-14 22:52
1 前言
在向客户推 STM32F4 芯片的时候,客户反馈使用 CDC 类无法发送 64 个字节,于是通过深入研究问题,发现问题之所在,到解决问题。本文将基于 STM32F4DISCOVERY 板,一步步重现问题,一方面介绍如何使用 USB 的 CDC 类进行开发,另一
方面,对在开发过程中碰到发送 64 整数倍数据时会失败的问题分析及解决方案。
2 硬件介绍
在创建工程之前,我们首先即将使用的硬件进行必要的介绍。

J)W6NU6JC8YHQZ@Q17E3GML.png

如上图所示,USB 电路使用 PA11,PA12,全速 USB OTG,当然,这里只做 device,英雌只需要看上图的下面部分。

93AYUZ10Q[IZ1BME29WZ)YP.png

如上图,本例中将使用到 1 个用户按键,PA0,按下时为 1 电平。
另外,晶振使用的是外部 HSE 8M 晶振。

3 创建 CubeMx 工程
打开 CubeMX(V4.17.0),创建一个以 STM32F407VGTx 的工程,使用 FS-USB,并使用 PA0 外部输入 EXIT,如下图所示:

A{`X~9][I8MU}0@AQW557TO.png

使能外部 HSE,使用外部 8M HSE,其时钟树如下设置:

G@G3TCBVUSWMDBXOIB$~K.png

接下来是配置参数,这里只修改 USB 中断优先级为 1,而 PA0 的外部中断优先级设置为 4,如下:

8UI{~`MPTM)VF@R[W0I3B2E.png

然后再中间件将 USB class 设置为 Communicaiton Device Class,如下:

ZM6_{FZG]Z%)}{VA~`9%1Y9.png

最后将工程的堆设为 0.5K,栈设为 1.5K :




最后生成一个 F407_CDC_Test 的工程。

4 修改工程代码
我们对生成的工程不做任何修改,直接编译后烧进开发板后是可以被 PC 识别为虚拟串口的,如下图所示:

EX1@A8Y[@W%)GHIRI@HSP`L.png


(注意:这里所说的虚拟串口主要是指其可以被当做串口来用,但其速度跟串口所设置的波特率完全没有关系,用户不要被名字所迷惑,虽然使用起来跟串口没有区别,但其本质还是 USB,在初始化设置波特率不会对 USB 的通讯速率产生任何影响,本文档所描述的是全速 USB,因此,其最大速率就固定为 12M/S,这个是由全速 USB 外设标准 48M 输入时钟所决定的)此时是没有任何具体功能的,为了更好的看到通讯的数据,我们将使用串口通讯工具来进行测试,这里我们使用的串口工具是: sscom32.

4.1 验证接收功能
我们将使用 PC 串口工具 SSCOM32 通过 USB 向 MCU 发送数据,为了能在 PC 端能看到 MCU 是否能接收到数据,我们在MCU 端修改代码,让 MCU 一旦接收到来自 PC 端的数据后,立马返回一模一样的数据,因此需要在生成的源码文件usbd_cdc_if.c 文件中找到到函数:CDC_Receive_FS(),添加处理函数,如下:
  1. static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
  2. {
  3. /* USER CODE BEGIN 6 */
  4. HanldeReceiveData(Buf,*Len);
  5. USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  6. USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  7. return (USBD_OK);
  8. /* USER CODE END 6 */
  9. }
  10. /* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */
  11. static void HanldeReceiveData(uint8_t* Buf, uint32_t Len)
  12. {
  13. CDC_Transmit_FS(Buf,Len);
  14. }
  15. /* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */
复制代码

编译后烧进板子进行验证:




如上如所示,串口工具能够收到来自 MCU 的返回数据,与发送数据完全一样,这说明,MCU 已经接收到了 PC 端发送的数据,另外,PC 端也能接收到 MCU 端发送的数据。

4.2 验证发送功能
接下来我们来通过按键响应来主动向 PC 端发送数据,我们在按键回调函数内添加代码如下:
  1. /* USER CODE BEGIN 0 */
  2. static uint8_t SendData[256] ={0};
  3. void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
  4. {
  5. uint16_t i;
  6. for(i =0; i<sizeof(SendData); i++)
  7. {
  8. SendData[i] =i;
  9. }
  10. CDC_Transmit_FS(SendData,63);
  11. }
  12. /* USER CODE END 0 */
复制代码


即用户按下按键,MCU 则向 PC 端发送一次数据,这里发送的是 63 个字节,内容为 0~62,测试后 PC 端的串口工具完全能收到 MCU 端发送的 63 个字节,如下图所示:


SEBIV9]U4$WIG0_M3R~2Z{S.png

但是当我们将代码修改为发送 64 个字节后 :
  1. /* USER CODE BEGIN 0 */
  2. static uint8_t SendData[256] ={0};
  3. void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
  4. {
  5. uint16_t i;
  6. for(i =0; i<sizeof(SendData); i++)
  7. {
  8. SendData[i] =i;
  9. }
  10. CDC_Transmit_FS(SendData,64);
  11. }
  12. /* USER CODE END 0 */
复制代码


修改后进行验证,发现 PC 端串口工具不能接收到数据,,这里代码基本完全没有变化,只是发送长度之前为 63,这里为 64,结果却不相同,很明显,USB CDC 协议栈哪里出了问题。


我们先使用 USB 分析仪对发送 64 个字节时进行 USB 总线监控:

UHYM$P2DBXA(2X[O{3P5UIN.png

如上图,我们发现,当发送 64 个字节时,由于正好是最大包长(64),按 USB 标准来看,应该多发一次空的 transaction,但是这里,仅仅只发了一次 transaction,这也就是为什么串口没有接收到数据的原因(MCU 端的 USB 实际上是已经发送了 64 个字节,但由于缺少一个 transaction,因此 PC 端的驱动会认为数据格式不完整,而放弃所有已经接收到的数据,从而使上层看起来没有接收到任何内容)。

7UTMXXMIA%@9O@(S1N[N2IG.png

因此,接下来的工作就是找到 USB 协议栈中的相应处理环节,然后将缺少的那个空的 transaction 补上即可。



4.3 USB CDC 协议栈修改
4.3.1 USB 数据发送流程分析
在对 USB CDC 协议栈进行修改之前,我们先来梳理下 USB 发送的流程。
发送 USB 数据大概过程如下:
1> 填写 DIEPTSIZ 寄存器的发送包数(pakage count)和传输大小(transfer size)。
2> 使能发送断点的发送空中断(DIEPEMPMSK,利用发送空中断 TXFE 来将发送数据填充到 DFIFO)。
3> 使能断点。
4> 后续就是中断的事了。
后续将会有 3 次中断:
1> USB_OTG_DIEPINT_TXFE 中断:在此中断处理中,程序将发送缓冲的数据分包填充到 DFIFO(不能超过最大包长,只
有最后一包数据才有可能小于最大包长)。
2> USB_OTG_DIEPINT_TXFE 中断: 还是 TXFE 中断,上次 TXFE 填充的发送数据全部发送完了后,最终还是会继续触发
TXFE 中断,也就是这次中断,在这次 FXFE 中断中禁止 FXFE。也就是说,后续不会再有 TXFE 中断,除非再次使能。
3> USB_OTG_DIEPINT_XFRC 中断:传输完成中断,表示到这次中断为止,传输完成。在这个中断中将回调HAL_PCD_DataInStageCallback()函数,就相当于发送中断一样。
这就是 USB 数据发送的流程,这里需要注意地是,对于端点 0 和非端点 0 来说,在具体流程实现上还是稍微有所差异的。究其原因,主要是端点 0 和非端点 0 的 DIEPTSIZ 寄存器的包大小和传输大小位宽是不一样的。如下图:


U]DU%UWNN]5M$S(CI~(RJ~6.png

58TAN7_CCWDLK]2@H7LSE3P.png

对比上图,端点 0 的 DIEPTSIZ 寄存器的 XFRSIZ 位宽为 7,最大值为 127,也就是说最多一次只能传输 127 个字节,按最大包长 64 字节来算,就是是最多两包数据。如果需要发送超过 127 个字节时,又该如何做呢?查看 USB 协议栈内核代码,发现每次端点 0 发送数据时,在发送代码中固定每次最多可以传输 64 字节,然后在传输完成中断处理时,再将剩下的数据接着传输(usb core),当然,每次传输最多也是 64 个字节,就这样,直到发送完所有数据为止。为什么每次传输最大设置为 64?
不是 XFRSIZ 位宽为 7,理论上可以为 127 吗?我的理解是,这样也是可以的,只要包长控制在 64 个字节内就可以了,至于每次传输多少字节,只要 XFRSIZ 位宽够用,你可以设置 127 个字节范围内任何数据均可。代码中设置为 64,主要为了图方便。
但是,对于非端点 0,XFRSIZ 位宽为 19 位,524288 个字节,足够传输所有实际数据了,因此,在发送代码中,并没有限定传输数据的长度,在 TXFE 中断中也能将所有待发送的字节填入 DFIFO。但是,当发送的数据刚好是 64 的整数倍时,按USB 标准,应该继续发送一次空字节,以表示数据全部发送完毕。如下:


4.3.2 代码修改
对比端点 0 的处理,发现端点 0 在传输完成中断(XFRC)中,有对这种情况的判断,一旦检测到这种情况,则会发送一次空传输。如下:
usb_core.c 文件中的 USBD_LL_DataInStage()函数 :
  1. USBD_StatusTypeDef USBD_LL_DataInStage(USBD_HandleTypeDef *pdev ,uint8_t epnum, uint8_t
  2. *pdata)
  3. {
  4. USBD_EndpointTypeDef *pep;

  5. if(epnum == 0)
  6. {
  7. pep = &pdev->ep_in[0];

  8. if ( pdev->ep0_state == USBD_EP0_DATA_IN)
  9. {
  10. if(pep->rem_length > pep->maxpacket)
  11. {
  12. pep->rem_length -= pep->maxpacket;
  13. //继续发送剩余数据
  14. USBD_CtlContinueSendData (pdev,
  15. pdata,
  16. pep->rem_length);

  17. /* Prepare endpoint for premature end of transfer */
  18. USBD_LL_PrepareReceive (pdev,
  19. 0,
  20. NULL,
  21. 0);
  22. }
  23. else
  24. { /* last packet is MPS multiple, so send ZLP packet */
  25. if((pep->total_length % pep->maxpacket == 0) &&
  26. (pep->total_length >= pep->maxpacket) &&
  27. (pep->total_length ep0_data_len ))
  28. {
  29. //再多发送一次空数据
  30. USBD_CtlContinueSendData(pdev , NULL, 0);
  31. pdev->ep0_data_len = 0;

  32. /* Prepare endpoint for premature end of transfer */
  33. USBD_LL_PrepareReceive (pdev,
  34. 0,
  35. NULL,
  36. 0);
  37. }
  38. else
  39. {
  40. if((pdev->pClass->EP0_TxSent != NULL)&&
  41. (pdev->dev_state == USBD_STATE_CONFIGURED))
  42. {
  43. pdev->pClass->EP0_TxSent(pdev);
  44. }
  45. USBD_CtlReceiveStatus(pdev);
  46. }
  47. }
  48. }
  49. if (pdev->dev_test_mode == 1)
  50. {
  51. USBD_RunTestMode(pdev);
  52. pdev->dev_test_mode = 0;
  53. }
  54. }
  55. else if((pdev->pClass->DataIn != NULL)&&
  56. (pdev->dev_state == USBD_STATE_CONFIGURED))
  57. {
  58. pdev->pClass->DataIn(pdev, epnum); //非 0 端点回调 CDC 类的 DataIn()函数处理
  59. }
  60. return USBD_OK;
  61. }
复制代码

从上述代码,我们明显可以可以看出,USB 协议栈在对于端点 0 的数据明确做了一系列处理,以使其可以续发数据以及发送空数据传输,向主机端表示所有数据发送完毕。而对于非端点 0 的数据,则直接向上回调相应 USB 类的 DataIn 处理函数,
把责任完全撇给 USB 类去处理。
接下来查看 CDC 类的 DataIn()函数 :
  1. static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum)
  2. {
  3. USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData;

  4. if(pdev->pClassData != NULL)
  5. {

  6. hcdc->TxState = 0;
  7. return USBD_OK;
  8. }
  9. else
  10. {
  11. return USBD_FAIL;
  12. }
  13. }
复制代码

虽然 USB 类的 DataIn()回调函数是不需要处理做续发数据处理(19 位的 XFRSIZ 位宽已足够表示数据长度),但是对于最大包长的整数倍长度数据的最后一个空包并没有做相应处理,因此,我们需要对其进行改造:
  1. static uint8_t USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum)
  2. {
  3. USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData;
  4. PCD_HandleTypeDef *hpcd =pdev->pData;
  5. USB_OTG_EPTypeDef *ep;
  6. ep = &hpcd->IN_ep[epnum];
  7. if(ep->xfer_len >0 &&ep->xfer_len%ep->maxpacket ==0)
  8. {
  9. USBD_LL_Transmit (pdev,epnum,NULL,0);
  10. return USBD_OK;
  11. }
  12. else
  13. {
  14. if(pdev->pClassData != NULL)
  15. {
  16. hcdc->TxState = 0;
  17. return USBD_OK;
  18. }
  19. else
  20. {
  21. return USBD_FAIL;
  22. }
  23. }
  24. }
复制代码

将修改后的代码进行测试,测试发送 64,256 长度字节内容均可以成功发送 :

{XK[N_){B@{_~NQ[~8W1DFX.png

7WWTM`IJ{GE0A2R`KR_K~LI.png

从上图可以明显看到最后那次空事务(transaction),同时,使用串口工具也能正常接收到 64 个字节数据 :

QLCMG@H(1`PO9E}~33GGJSJ.png

由此证明此修改是生效的。


5 结束语
1 此问题是在使用 CubeMx V4.17.0 发现的问题,不排除后续 CubeMx 更新版本中会解决此问题。
2 此问题同样适用于其他 USB 类,本着不轻易修改 USB 协议栈原则,因此没有将修改转移到 USB 协议栈的内核中。因此,在其他 USB 类中的非 0 端点出现类似问题时,可以参考本文的 DataIn()函数修改。

收藏 评论2 发布时间:2022-2-14 22:52

举报

2个回答
lamblee 回答时间:2023-9-5 18:30:21

19 位的 XFRSIZ 位宽表示的长度只有0.5Mbyte,但是我需要一包发送1.5Mbyte大小的数据,该如何在USBD_CDC_DataIn()续包发送数据。因为上位机没有源码,所以无法通过上位机修改包的大小。

笑不出莱 回答时间:2024-10-17 17:04:48

报:当前使用MX6.12.0版本生成的USB代码已修复该问题

static uint8_t USBD_CDC_DataIn(USBD_HandleTypeDef pdev, uint8_t epnum) { USBD_CDC_HandleTypeDef hcdc = (USBD_CDC_HandleTypeDef )pdev->pClassData; PCD_HandleTypeDef hpcd = pdev->pData;

if (pdev->pClassData != NULL) { if ((pdev->ep_in[epnum].total_length > 0U) && ((pdev->ep_in[epnum].total_length % hpcd->IN_ep[epnum].maxpacket) == 0U)) { / Update the packet total length / pdev->ep_in[epnum].total_length = 0U;

/ Send ZLP / USBD_LL_Transmit(pdev, epnum, NULL, 0U); } else { hcdc->TxState = 0U; } return USBD_OK; } else { return USBD_FAIL; } }

所属标签

相似分享

官网相关资源

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