1 前言

本文将基于STM32F4DISCOVERY板,介绍如何使用USB的CDC类进行开发,以及在开发过程中碰到发送64整数倍数据时会失败的问题分析及解决方案。

2 硬件介绍

在创建工程之前,我们首先即将使用的硬件进行必要的介绍。


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

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

3 创建CubeMx工程

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


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

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

图5 中断优先级设置
然后再中间件将USB class设置为Communicaiton Device Class,如下:

图6 USB Class设置
最后将工程的堆设为0.5K,栈设为1.5K :

图7 堆栈设置
最后生成一个 F407_CDC_Test的工程。

4 修改工程代码

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


图8 PC识别为虚拟串口
当然,这里的前提是必须在PC机上安装了ST发布的虚拟串口驱动(STSW-STM32102下载地址 : http://www.st.com/content/st_com/en/products/development-tools/software-development-tools/stm32-software-development-tools/stm32-utilities/stsw-stm32102.html )。

(注意:这里所说的虚拟串口主要是指其可以被当做串口来用,但其速度跟串口所设置的波特率完全没有关系,用户不要被名字所迷惑,虽然使用起来跟串口没有区别,但其本质还是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(),添加处理函数,如下:

static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{/* USER CODE BEGIN 6 */HanldeReceiveData(Buf,*Len);USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);USBD_CDC_ReceivePacket(&hUsbDeviceFS);return (USBD_OK);/* USER CODE END 6 */
}
/* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */
static void HanldeReceiveData(uint8_t* Buf, uint32_t Len)
{CDC_Transmit_FS(Buf,Len);
}
/* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */

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


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

4.2 验证发送功能

接下来我们来通过按键响应来主动向PC端发送数据,我们在按键回调函数内添加代码如下:

/* USER CODE BEGIN 0 */
static uint8_t SendData[256] ={0};
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{uint16_t i;for(i =0; i<sizeof(SendData); i++){SendData[i] =i;}CDC_Transmit_FS(SendData,63);
}
/* USER CODE END 0 */

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


图10 PC端收到63个字节
但是当我们将代码修改为发送64个字节后 :

/* USER CODE BEGIN 0 */
static uint8_t SendData[256] ={0};
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{uint16_t i;for(i =0; i<sizeof(SendData); i++){SendData[i] =i;}CDC_Transmit_FS(SendData,64);
}
/* USER CODE END 0 */

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

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


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

图12 USB2.0标准对于最大包长整数倍的要求
因此,接下来的工作就是找到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寄存器的包大小和传输大小位宽是不一样的。如下图:


图13 端点0的DIEPTSIZ寄存器

图14 端点1~3的DIEPTSIZ寄存器
对比上图,端点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()函数 :

USBD_StatusTypeDef USBD_LL_DataInStage(USBD_HandleTypeDef *pdev ,uint8_t epnum, uint8_t *pdata)
{USBD_EndpointTypeDef    *pep;if(epnum == 0) {pep = &pdev->ep_in[0];if ( pdev->ep0_state == USBD_EP0_DATA_IN){if(pep->rem_length > pep->maxpacket){pep->rem_length -=  pep->maxpacket;//继续发送剩余数据USBD_CtlContinueSendData (pdev, pdata, pep->rem_length);/* Prepare endpoint for premature end of transfer */USBD_LL_PrepareReceive (pdev,0,NULL,0);  }else{ /* last packet is MPS multiple, so send ZLP packet */if((pep->total_length % pep->maxpacket == 0) &&(pep->total_length >= pep->maxpacket) &&(pep->total_length < pdev->ep0_data_len )){//再多发送一次空数据USBD_CtlContinueSendData(pdev , NULL, 0);pdev->ep0_data_len = 0;/* Prepare endpoint for premature end of transfer */USBD_LL_PrepareReceive (pdev,0,NULL,0);}else{if((pdev->pClass->EP0_TxSent != NULL)&&(pdev->dev_state == USBD_STATE_CONFIGURED)){pdev->pClass->EP0_TxSent(pdev); }          USBD_CtlReceiveStatus(pdev);}}}if (pdev->dev_test_mode == 1){USBD_RunTestMode(pdev); pdev->dev_test_mode = 0;}}else if((pdev->pClass->DataIn != NULL)&& (pdev->dev_state == USBD_STATE_CONFIGURED)){pdev->pClass->DataIn(pdev, epnum); //非0端点回调CDC类的DataIn()函数处理}  return USBD_OK;
}

从上述代码,我们明显可以可以看出,USB协议栈在对于端点0的数据明确做了一系列处理,以使其可以续发数据以及发送空数据传输,向主机端表示所有数据发送完毕。而对于非端点0的数据,则直接向上回调相应USB类的DataIn处理函数,把责任完全撇给USB类去处理。

接下来查看CDC类的DataIn()函数 :

static uint8_t  USBD_CDC_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum)
{USBD_CDC_HandleTypeDef   *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData;if(pdev->pClassData != NULL){hcdc->TxState = 0;return USBD_OK;}else{return USBD_FAIL;}
}

虽然USB类的DataIn()回调函数是不需要处理做续发数据处理(19位的XFRSIZ位宽已足够表示数据长度),但是对于最大包长的整数倍长度数据的最后一个空包并没有做相应处理,因此,我们需要对其进行改造:

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;USB_OTG_EPTypeDef *ep;ep = &hpcd->IN_ep[epnum];if(ep->xfer_len >0 &&ep->xfer_len%ep->maxpacket ==0){USBD_LL_Transmit (pdev,epnum,NULL,0);return USBD_OK;}else{if(pdev->pClassData != NULL){hcdc->TxState = 0;return USBD_OK;}else{return USBD_FAIL;}}
}

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


图15 USB分析仪捕捉到的64字节长度数据

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

图17 串口接收到的64个字节

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

5 另外一种方法

其实,在应用层使用发送接口时,如果要发送64的整数倍数据,还有一种方式也是可以规避这个问题,如下 :

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){uint8_t ret =USBD_FAIL;USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;ret =CDC_Transmit_FS(SendData,64);if(ret ==USBD_OK){while(hcdc->TxState !=0); //wait for last send completeret =CDC_Transmit_FS(SendData,0); //Send ZLP}}

如上代码所示,在发送完64给字节,接着等待发送完成后,在发送一个零长度的数据,这样也是可以的,只不过在等待发送完成标志时一直阻塞在那里,这也是一种规避的方式。

6 结束语

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

本文所涉及的示例代码下载地址:http://download.csdn.net/detail/flydream0/9686011

在进行USB CDC类开发时,无法发送64整数倍的数据相关推荐

  1. 在进行USB CDC类开发时,无法发送64整数倍的数据(续)

    1 前言 此文延续之前相同文章的话题,是对上篇文章的补充,之所以会有此文,主要是之前发现问题是在STM32F4上,解决方案也是基于CubeF4,但是,当相同问题出现在STM32F0上时,使用之前的代码 ...

  2. 如何让CDC类USB设备批量接收64字节以上数据

    很多STM32开发者在实现CDC类虚拟串口与PC主机通信过程中,有时会遇到点麻烦而不得其解.那就是当主机端单次发送的数据不超过64字节时,接收正常.一旦发送数据量大于64字节时就接收失败,总是出现丢包 ...

  3. STM32 USB使用记录:使用CDC类虚拟串口(VCP)进行通讯

    文章目录 目的 基础说明 使用STM32CubeIDE配置生成代码 用户代码分析 回环测试 串口参数设置 USB HS使用与演示 通讯速率测试 测试代码 USB FS测试 USB HS测试 影响速度的 ...

  4. USB CDC从理论到实践

    本文摘自ST官网的"USB CDC类入门培训".整理的内容是我能够看得懂的,认为比较实用的,记录下来,以便以后查阅,同时也把原文档中的笔误给更正了一下.若要看更详细的可以去ST技术 ...

  5. WinCE平台USB摄像头驱动开发

    (转载)http://tech.e800.com.cn/articles/2009/116/1257487620781_1.html 由于良好的性能.低廉的价格和灵活方便的特性,USB 摄像头正被广泛 ...

  6. STM32 USB CDC 虚拟多串口

    转自: http://www.stmcu.org.cn/module/forum/thread-613510-1-1.html 楼主  发表于 2017-9-28 22:30:04 | 只看该作者 | ...

  7. [原创] STM32 USB CDC 虚拟多串口

        creep 该用户从未签到 61 主题 1995 帖子 26 蝴蝶豆 版主 最后登录 2018-10-19 发消息 电梯直达 楼主  发表于 2017-9-28 22:30:04 | 只看该作 ...

  8. 浅谈WinCE平台USB摄像头驱动开发流程

    转自http://tech.e800.com.cn/articles/2009/116/1257487620781_1.html 由于良好的性能.低廉的价格和灵活方便的特性,USB 摄像头正被广泛的集 ...

  9. ESP32-S2应用开发——USB通信(CDC类)

    ESP32S2应用开发--USB通信(CDC类) 目录 ESP32S2应用开发--USB通信(CDC类) 前言 1 硬件介绍 1.1 硬件连接 2 软件开发 2.1 安装开发板 2.2 安装库 2.3 ...

最新文章

  1. 组策略妙用----通过组策略禁止域用户更改IP地址
  2. 汇编语言(三十四)之输出中文
  3. 鲜为人知的Java 8功能:广义目标类型推断
  4. spark学习-53-Spark下Java版HBase下的根据权重获取最真实数据
  5. 极简主义2020UI设计正流行,欣赏下可临摹的案例模板!
  6. 大数据元数据管理系统功能有哪些
  7. 视觉定位VBL 视觉里程计VO 视觉SLAM 区别与联系
  8. 城建坐标与经纬度转换工具
  9. vss导入git vss导入svn
  10. 细粒度锁的实现之分级锁的设计实现
  11. 前端-深克隆与浅克隆
  12. 加盟店 -- 祖坟刨干记
  13. windows使用scrapy爬取微信评论
  14. 有道云笔记不同步_不怕误同步 有道云笔记历史记录教程
  15. Log4cplus编译
  16. 3Dmax Script 自动减面
  17. 【君思智慧园区】 园区如何摆脱同质化发展的困境
  18. 八、HTML常用标签
  19. Hbase JavaAPi介绍和使用示例(待更新)
  20. 电子商务要学计算机吗 难学吗,电子商务专业对数学要求高吗

热门文章

  1. 福州的宝贝原来都藏在这里
  2. 蚂蚁客服介绍-微服网络
  3. EF使用时报错‘Format of the initialization string does not conform to specification starting at index 0.'
  4. 外设驱动库开发笔记45:MS4515DO压力传感器驱动
  5. 阿里研究院崔瀚文:“单身经济”背后的“新家园”
  6. java ssm 基于springboot的志愿者招募网站
  7. mysql/mariadb 进阶知识之表设计和查询
  8. 极光推送:java后台向APP推送消息(android,ios极光推送消息)
  9. 虚拟机下安装UEFI+GPT+win7x64
  10. ES 查询示例 搜索 分组 去重 分页 排序