[STM32F4] 使用STM32F407ZET6的高速USB虚拟串口DMA发送数据试验记录

[复制链接]
266|0
Xiashiqi 发表于 2025-10-10 14:33 | 显示全部楼层 |阅读模式
芯片型号:

单片机:STM32F407ZET6

外部PHY芯片 USB3300

工具:STM32CubeMX 6.9.2

MDK5

USB种类:

高速USB(HS),单片机作为设备,PC机(WIN 11  64位)作为主机。设备启用DMA通过虚拟串口向主机传输数据。

因手里的项目使用全速USB虚拟串口进行数据传输时,目前能做到的最快速度之达到730KB左右,对于要传输以GB为单位的原始数据,实属杯水车薪,因此前段时间通过外扩PHY芯片(USB3300)实现了高速USB虚拟串口,但是测试下来发现,速度也只有6MB/秒,经过排查发现程序自己的开销也会耗费大量的时间,比如USB 协议规定了端点的最大包长(高速 Bulk 端点通常为 512 字节),如果在没有启动DMA的情况下,主程序从EMMC中读取了16K字节的数据,必须将这16K字节的数据拆分成32个512字节进行传输,即进行32次发送,32次发送的代码执行与状态查询的开销。因此想通过DMA模式将缓冲区的数据发送到USB虚拟串口,这样只需要调用一次发送函数,等中断回调函数触发后置标志,即可开启下一次发送。

以下是CUBMX的配置相关内容记录:

基本的配置这里就不记录了,只说关键的配置。

6016568e8a8c3c7534.png

高速USB  设备模式   使能DMA。这里要我需要注意的是,USB虚拟串口DMA不需要指定数据流以及配置,CUBMX上对于DMA的配置就只有使能DMA这一项。

生成代码,我这里自己建立了一个文件,作为全局变量定义和声明的单独模块,并包含了所用的外设对应文件的头文件,以后只要是包含我这个头文件,便包含了所有的头文件。

这是自定义的.c文件

HAL_StatusTypeDef Ret;
HAL_MMC_CardInfoTypeDef EMMC_Info;
uint8_t Emmc_Dma_Read_OK_** = READ_OK;//DMA读取EMMC数据完成标志 初始化为1使得首次读取能够发起
uint8_t rx_byte = 0;//存放串口接收到的一个字节数据
uint8_t Dma_Read_Emmc_Buf1[BLOCK_SIZE*READ_BLOCK_NUM];//缓冲区1
uint8_t __attribute__((aligned(4))) USB_TX_BUF[512]={0x00};
uint8_t USB_TX_BUF_Big[512*32]={0x00};
uint8_t USB_Send_** = USBD_FAIL;
uint8_t tempData = 0;
uint8_t USB_Send_Data_** = USB_SEND_OK;//USB发送数据结束标志



这里定义了需要用到的全局变量,包括用于发送的缓冲数组,需要注意的是发送的数组必须是全局变量或者静态变量,生命周期要长,否则调用函数传参后,生命周期不够长的话DMA就找不到数据源,会出错。

接下来在自定义的.h文件中对上述变量以extern进行声明,这样其他的模块文件中只需要包含自定义的头文件即可使用。

#define USB_SEND_OK 1
#define USB_SENDING 0

//全局变量
extern HAL_StatusTypeDef Ret;
extern HAL_MMC_CardInfoTypeDef EMMC_Info;
extern uint8_t Emmc_Dma_Read_OK_**;//DMA读取EMMC数据完成标志 初始化为1使得首次读取能够发起
extern uint8_t rx_byte;//存放串口接收到的一个字节数据
extern uint8_t Dma_Read_Emmc_Buf1[BLOCK_SIZE*READ_BLOCK_NUM];//缓冲区1
extern uint8_t USB_TX_BUF[512];
extern uint8_t USB_TX_BUF_Big[512*32];
extern uint8_t USB_Send_**;
extern uint8_t tempData;
extern uint8_t USB_Send_Data_**;



接下来,写一个发送数据的函数,这个函数将main函数中被调用,用于发送数据。

//USB数据接收处理函数
void USB_Send_Data(uint8_t *PBuf)
{
        if(USB_Send_Data_** == USB_SEND_OK)
        {
                USB_Send_Data_** = USB_SENDING;
            USB_Send_** = CDC_Transmit_HS(PBuf,512*32);
        if(USB_Send_** == USBD_OK)
            {
                //send ok
                    tempData = 1;
                    printf("Send_OK\r\n");
            }
            else
        {
                //send Fail
                    if(USB_Send_** == USBD_BUSY)
                    {
                        printf("Send_Fail_USBD_BUSY\r\n");
                    }
                    else if(USB_Send_** == USBD_FAIL)
                    {
                        tempData = 3;
                            printf("Send_Fail_USBD_3\r\n");
                    }
                    else
            {
                        tempData = 4;
                            printf("Send_Fail_USBD_4\r\n");
                    }
            }               
        }
}



这个 USB_Send_Data 函数的主要功能是通过 USB 虚拟串口(CDC)发送数据,并包含发送状态判断和错误处理逻辑,具体功能如下:

1. 发送条件判断
函数首先检查全局标志 USB_Send_Data_** 是否为 USB_SEND_OK(发送就绪状态):若未就绪(如处于发送中状态 USB_SENDING),则不执行任何发送操作,直接退出;
若就绪,则进入数据发送流程。

USB_Send_Data_**这个是发送成功的标志,每次发送16K字节后主动将这个变量设置为0,然后再在中断回调函数中将其置1-USB_SEND_OK,作为是否发起一次16K字节数据传输的条件。

这个标志在由CUBMX创建的工程文件中的usbd_cdc_if.c文件中的CDC_TransmitCplt_HS回调函数中进行置位:

static int8_t CDC_TransmitCplt_HS(uint8_t *Buf, uint32_t *Len, uint8_t epnum)
{
  uint8_t result = USBD_OK;
  /* USER CODE BEGIN 14 */
  UNUSED(Buf);
  UNUSED(Len);
  UNUSED(epnum);

  USB_Send_Data_** = USB_SEND_OK ;

  /* USER CODE END 14 */

  return result;
}




2. 启动数据发送
将 USB_Send_Data_** 置为 USB_SENDING(标记为发送中),防止重复发送;
调用 CDC_Transmit_HS 函数发送数据:
发送缓冲区为传入的 PBuf(外部提供的数据源);
发送长度固定为 512*32 = 16384 字节(16K 字节,需配合 USB 高速端点的分包传输);
发送结果存储在 USB_Send_** 中。


3. 发送结果处理
根据 CDC_Transmit_HS 的返回值(USB_Send_**)处理不同情况:

发送成功(USBD_OK):
设置 tempData = 1(可能用于外部状态判断);
打印日志 Send_OK 提示发送启动成功(注意:此处仅表示 “发送请求被接受”,不代表数据已完全发送到主机)。
发送失败:
若返回 USBD_BUSY(设备忙碌):打印 Send_Fail_USBD_BUSY,表示当前 USB 正在处理其他发送任务,无法接受新请求;
若返回 USBD_FAIL(发送失败):设置 tempData = 3,打印 Send_Fail_USBD_3;
其他错误:设置 tempData = 4,打印 Send_Fail_USBD_4,用于捕获未定义的错误类型。

我需要注意的是:

CDC_Transmit_HS这个函数,如果是在启用DMA的情况下,会将你传入的数据长度(这里是512*32)的数据进行分包,这个分包是底层进行的,上层只管调用并传参即可。

再写一个初始化变量的函数,用于向缓冲数组中写入固定已知的数据,一次来验证数据收发是否一致,这个试验主要针对USB,为了保证篇幅不要太大,数据源就直接用初始化的,不从EMMC中读取了:

//数据初始化函数
void Data_Init(void)
{
   memset(USB_TX_BUF_Big,0xA6,sizeof(USB_TX_BUF_Big));
       
       
}


在主函数中,进行数据初始化和调用,以下是主函数:

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_SDIO_MMC_Init();
  MX_USB_DEVICE_Init();
  /* USER CODE BEGIN 2 */
  //HAL_MMC_GetCardInfo(&hmmc, &EMMC_Info);//获取EMMC芯片信息
  Data_Init();


  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  HAL_Delay(5000);
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
       
       
    //printf("程序运行 打印测试\r\n");          
        if(Emmc_Dma_Read_OK_** == READ_OK)  
        {

                HAL_Delay(100);
                USB_Send_Data(USB_TX_BUF_Big);

        }



这里面我需要注意的是:

注意1:主函数不能一上来就直接调用发送函数,否则会导致出错,经过分析,发现原因是USB接入主机后需要枚举识别,这需要时间。

注意2:目前手头没有称手的串口调试工具,目前使用的是sscom5.13.1,这个串口调试助手好用,但是面对海量数据的时候,会有卡顿。

注意3:使用USB虚拟串口作为设备向主机传输数据的时候,主机也必须接收,比如PC机上的串口调试助手需要识别到这个COM口,并打开,如果串口调试助手不接收,会导致设备端认为数据传输没有完成,进而导致发送完成中断回调函数不触发调用,那么呢,USB_Send_Data_**这个标志就不会恢复,主程序也不会开启新的传输,现象就是好像程序没起作用,数据没发出来。

因此,我在进入while(1)循环前先延时5秒,主要就是等枚举完成,并未打开串口调试助手留足够的时间。

另外为了防止串口调试助手被海量数据卡死,我在while (1)里面加了1秒的延时,到时候可以更改HAL_Delay(1000);里面的这个1000(单位ms)来控制传输启用的间隔。

我是用两个串口调试助手来做试验:

串口1是打印调试口,是1路232,USB口主要用于虚拟串口数据的接收和显示。

整个试验程序的流程就是:


9103668e8a8a6c3df3.png

8381168e8a8a24097d.png


9216668e8a89e4540b.png

一次收16384字节,字数没错,内容全是16进制A6,内容也没错。

1807668e8a89882cf7.png

串口1秒打印一次发送函数返会成功。
————————————————
版权声明:本文为CSDN博主「用歌声补够」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dgw2016/article/details/151799497

您需要登录后才可以回帖 登录 | 注册

本版积分规则

97

主题

282

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部