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

1 前言

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

2 硬件介绍

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

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

如上图,本例中将使用到1个用户按键,PA0,按下时为1电平。

另外,晶振使用的是外部HSE 8M晶振。

3 创建CubeMx工程

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

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

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

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

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

最后生成一个 F407_CDC_Test的工程。

4 修改工程代码

我们对生成的工程不做任何修改,直接编译后烧进开发板后是可以被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 */

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

如上如所示,串口工具能够收到来自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个字节,如下图所示:

但是当我们将代码修改为发送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总线监控:

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

因此,接下来的工作就是找到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寄存器的包大小和传输大小位宽是不一样的。如下图:

端点0的DIEPTSIZ寄存器

端点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长度字节内容均可以成功发送 :

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

末尾的空transaction

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

串口接收

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

5 结束语

1 此问题是在使用CubeMx V4.17.0发现的问题,不排除后续CubeMx更新版本中会解决此问题。

2 此问题同样适用于其他USB类,本着不轻易修改USB协议栈原则,因此没有将修改转移到USB协议栈的内核中。因此,在其他USB类中的非0端点出现类似问题时,可以参考本文的DataIn()函数修改。

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

时间: 2024-08-04 03:35:50

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

记录我在百度地图开发和ArcGIS for Android开发时出现的一些错误及解决方案(后续更新)

[1]The import com.baidu.mapapi.map.Geometry conflicts with a type defined in the same file 解决:百度api包下的Geometry和某个类名相冲突,将类名换成另外的名字,不要和百度相关类里面的类名相同 [2]java.lang.ClassCastException: 解决:类型转换错误.查看Test_Geometry项目的Mainfest.xml清单文件,在<applicaiton>标签里面少了对Myap

Android开发时,那些相见恨晚的工具或网站!

本文来我在知乎话题Android开发时你遇到过什么相见恨晚的工具或网站?下的回答! 在实际Android开发过程确实会有很多相见恨晚的工具或网站出现,下面是我自己的一些分享. 1.源码网站 https://github.com/googlesamples Android系统每次推出一些新特性,Google都会写一些Demo放在Github上,对于想要了解新特性怎么玩的同学,肯定不能错过它. https://www.codota.com/ 如果你不知道一个Android的类怎么用,可以在Codot

在做weex开发时使用leancloud文件上传

不同于移动端原生开发,当开发者使用weex移动开发时,使用第三方SDK比较棘手.因为第三方的JS SDK是无法直接拿来使用的,环境不同.必须使用原生SDK,下面我介绍一下自己是如何在weex开发时(安卓)使用leancloud的服务的. 首先去leancloud的安卓SDK下载页面,下载SDK,这里我只使用了最基础的SDK包 <img src="/uploads/default/original/2X/5/582ffee695e0192ae085f0bc0e033543b316f673.p

SSH整合开发时Scope为默认时现象与原理

1.前提知识 1)scope默认值 进行SSH整合开发时,Struts2的action需要用spring容器进行管理,只要涉及到类以bean的形式入到spring容器中,不管是xml配置还是使用注解方式进行配置,都会涉及到spring管理bean的scope,其一共有五种取值,而其默认值为singleton,也就是单例模型,所有对此bean引用为同一个对象. 2)action应为多例 struts2作为MVC中视图(View)层框架,其最主要任务就是接收用户请求,然后调用业务逻辑层进行处理,这种

PIC32MZ 通过USB在线升级 -- USB CDC bootloader

了解更多关于bootloader 的C语言实现,请加我QQ: 1273623966 (验证信息请填 bootloader),欢迎咨询或定制bootloader(在线升级程序). 最近给我的开发板PIC32MZ EC starter kit写了个USB 在线升级程序--USB CDC bootloader.有了它,我可以很方便的升级我的应用程序.我大概是一个星期前开始决定写这个USB在线升级程序的,USB 有很两种类型,USB host和USB device. 由于USB host接触不多,所以我

应用程序框架实战十二:公共操作类开发技巧(初学者必读)

本文专门为初学者而写,因为很多初学者可能还不了解公共操作类的作用和封装技巧,大部分有经验的程序员都会把自己所碰到的技术问题整理封装成类,这就是公共操作类.公共操作类往往具有一些通用性,也可能专门解决某些棘手问题.公共操作类是应用程序框架的核心,主要目标是解决大部分技术问题.我将在本文介绍封装公共操作类的要点,供初学者参考. 开发公共操作类的原因 很多初学者会奇怪,.Net Framework提供的API相当易用,为何还要多此一举,进行一层封装呢.下面列举封装公共操作类的一些动机. .Net Fr

[软件]_[Windows]_[产品开发时常用的文件操作方法]

场景: 1. 开发Windows产品时,很多东西都需要自己封装,因为它不像Cocoa那样有很好的对象模型,通过类就可以访问文件相关方法. 比如复制文件夹? 要知道Win32是否提供复制文件夹这个函数还真的通过baidu. MSDN真的很差. 2. 界面开发时打开选择文件夹窗口等. 3. 设置文件创建时间和修改时间等. 4. 也是可以在产品中移植. bas_utility_file.h: #ifndef __BAS_UTILITY_FILE_H #define __BAS_UTILITY_FILE

Oracle数据库中调用Java类开发存储过程、函数的方法

Oracle数据库中调用Java类开发存储过程.函数的方法 时间:2014年12月24日  浏览:5538次 oracle数据库的开发非常灵活,不仅支持最基本的SQL,而且还提供了独有的PL/SQL,除此之外,还可以用时下最流行的编程语言Java来做开发.随着对oracle的了解越来越多,越来越禁不住oracle的诱惑,oracle技术真的是一门很有趣的学问.之前,我在博客中总结了挺多有关SQL.PL/SQL的,但是对于oracle数据库中Java类的调用却没有总结,也是因为之前不太会,这会儿总

J2EE开发时的包命名规则

http://www.blogjava.net/paulwong/archive/2012/04/15/374675.html 转一个J2EE开发时的包命名规则,养成良好的开发习惯 代码编写规范目的:能够在编码过程中实现规范化,为以后的程序开发中养成良好的行为习惯.代码编写规范使用范围:J2EE项目开发.包命名规范:目的:包的命名规范应当体现出项目资源良好的划分 servlet类所在包命名规范:公司名称.开发组名称.项目名称.web.servlet例如:net.linkcn.web.servle