首页 > 代码库 > 在进行USB CDC类开发时,无法发送64整数倍的数据

在进行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

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

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