《DMG474开发指南_V1.1》第三十七章 USB虚拟串口(Slave)实验

第三十七章 USB虚拟串口(Slave)实验


       本章,我们将向大家介绍如何利用USB在开发板实现一个USB虚拟串口,通过USB与电脑数据数据交互。

       本章分为如下几个小节:

       37.1 USB虚拟串口简介

       37.2 硬件设计

       37.3 程序设计

       37.4 下载验证


        37 .1 USB虚拟串口简介

       USB虚拟串口,简称VCP,是Virtual COM Port的简写,它是利用USB的CDC类来实现的一种通信接口。

       我们可以利用STM32自带的USB功能,来实现一个USB虚拟串口,从而通过USB,实现电脑与STM32的数据互传。上位机无需编写专门的USB程序,只需要一个串口调试助手即可调试,非常实用。

       同上一章一样,我们直接来移植官方的USB CDC例程,官方例程路径:7,STM32参考资料 1,STM32CubeG4固件包→STM32Cube_FW_G4_V1.5.1 Projects STM32G474E-EVAL Applications USB_Device CDC_Standalone,该例程采用USB CDC类来实现,利用STM32的USB接口,实现一个USB转串口的功能。


        37 .2 硬件设计


       1 . 例程功能

       本实验利用STM32自带的USB功能,连接电脑USB,虚拟出一个USB串口,实现电脑和开发板的数据通信。本例程功能完全和串口通信实验相同,只不过串口变成了STM32的USB虚拟串口。当USB连接电脑(USB线插入USB_SLAVE接口),开发板将通过USB和电脑建立连接,并虚拟出一个串口。注意:虚拟串口需要安装驱动,相关驱动的路径为:A盘→5,软件资料 1,软件 5,其他小工具 STM32 USB虚拟串口驱动 VCP_V1.5.0_Setup_xx.exe这个驱动软件(根据实际的电脑系统选择),虚拟串口驱动我们还可以在论坛上下载,链接是: http://www.openedv.com/thread-284178-1-1.html

       LED0闪烁,提示程序运行。USB和电脑连接成功后,LED1常亮。


       2. 硬件资源


       1)LED灯

              LED0 – PE0

              LED1 – PE1

       2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)

       3)正点原子1.3寸TFTLCD模块(SPI接口)

       4)USB_SLAVE接口(D-/D+连接在PA11/PA12上)


        37 .3 程序设计


       37 .3.1 程序流程图


图37.3.1.1 USB虚拟串口(Slave)实验程序流程图


       37.3.2 程序解析

       这里我们只讲解核心代码,详细的源码本实验,在上一个实验的基础上,把不需要的文件从工程中移除,并对照官方CDC例子,将相关文件拷贝到USB文件夹下。然后,添加USB相关代码到工程中,最终得到如图37.3.2.1所示的工程:


图37.3.2.1 USB虚拟串口工程分组


       注意:因为USB驱动库需要用到内存管理,因此我们保留了相关代码(malloc.c)。


       1. USB驱动代码

       可以看到,USB部分代码,同上一个实验的结构是一模一样的,只是.c文件稍微有些变化。我们移植的时候,需要关注的只有USB_APP分组下的文件:

       usb_device.c文件,和上一个实验一样,不需要修改,可以直接使用上一个实验的代码。

       usbd_conf.c文件,和上一个实验一样,不需要修改,可以直接使用上一个实验的代码。

       usbd_desc.c文件,和上一个实验不一样,本实验变成USB虚拟串口了(CDC),这个文件直接用ST官方CDC例程的就行。

       usbd_cdc_if.c文件,是重点要修改的,首先介绍usbd_cdc_if.h文件的相关宏定义,具体如下:

#define USB_USART_REC_LEN       200    /* USB串口接收缓冲区最大字节数 */
 
/* 轮询周期,最大65ms,最小1ms */
#define CDC_POLLING_INTERVAL    1      /* 轮询周期,最大65ms,最小1ms */

       USB_USART_REC_LEN宏定义是用于定义USB串口接收缓冲区最大字节数,这里设置为200。CDC_POLLING_INTERVAL宏定义是用于定义USB发送数据轮询周期,作为delay_ms函数的参数,最大65ms,最小1ms,这里设置为最小值即可。

       下面重点介绍usbd_cdc_interface.c文件,首先是一些结构体变量、数组和变量的定义,具体如下:

/* USB虚拟串口相关配置参数 */
USBD_CDC_LineCodingTypeDef LineCoding =
{
    115200,  /* 波特率 */
    0x00,  /* 停止位,默认1位 */
    0x00,  /* 校验位,默认无 */
    0x08   /* 数据位,默认8位 */
};
 
 
/* usb_printf发送缓冲区, 用于vsprintf */
uint8_t g_usb_usart_printf_buffer[USB_USART_REC_LEN];
 
/* USB接收的数据缓冲区,最大USART_REC_LEN个字节,用于USBD_CDC_SetRxBuffer函数 */
uint8_t g_usb_rx_buffer[USB_USART_REC_LEN];
 
 
/* 用类似串口1接收数据的方法,来处理USB虚拟串口接收到的数据 */
uint8_t g_usb_usart_rx_buffer[USB_USART_REC_LEN];       
 
/* 接收状态
 * bit15   , 接收完成标志
 * bit14   , 接收到0x0d
 * bit13~0 , 接收到的有效字节数目
 */
uint16_t g_usb_usart_rx_sta = 0;  /* 接收状态标记 */
 
extern USBD_HandleTypeDef hUsbDeviceFS;
static int8_t CDC_Init_FS(void);
static int8_t CDC_DeInit_FS(void);
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length);
static int8_t CDC_Receive_FS(uint8_t* pbuf, uint32_t *Len);
 
USBD_CDC_ItfTypeDef USBD_Interface_fops_FS =
{
  CDC_Init_FS,
  CDC_DeInit_FS,
  CDC_Control_FS,
  CDC_Receive_FS,
};

       首先是定义一个USBD_CDC_LineCodingTypeDef结构体类型的变量LineCoding,并赋值。波特率为115200,停止位和校验位都为0,数据位,默认8位。

       g_usb_usart_printf_buffer是发送缓冲区,大小由USB_USART_REC_LEN宏来定义,数组是uint8_t类型,所以数字大小为200字节。

       g_usb_rx_buffer则是USB接收的数据缓冲区,用于USBD_CDC_SetRxBuffer函数,大小也是200字节。

       g_usb_usart_rx_buffer是用做类似串口1接收数据的方法,来处理USB虚拟串口接收到的数据,在cdc_vcp_data_rx函数中被调用,大小也是200字节。

       g_usb_usart_rx_sta变量用于表示接收状态,位15表示接收完成标志,位14表示接收到0x0d,位13~位0表示接收到的有效字节数目。

       最后定义一个USBD_CDC_ItfTypeDef结构体类型的变量USBD_Interface_fops_FS,供USB内核调用,并把四个函数的首地址赋值给其成员。下面会介绍到这几个函数,以及一些其它的函数。

       首先是初始化CDC初始化函数,其定义如下:

/**
 * @brief       初始化 CDC
 * @param       无
 * @retval      USB状态
 *   @arg       USBD_OK(0)   , 正常;
 *   @arg       USBD_BUSY(1) , 忙;
 *   @arg       USBD_FAIL(2) , 失败;
 */
static int8_t CDC_Init_FS(void)
{
    USBD_CDC_SetRxBuffer(&hUsbDeviceFS, g_usb_rx_buffer);
    return USBD_OK;
}

       CDC_Init_FS用于初始化CDC,在初始化的时候由USB内核调用,这里我们调用函数:USBD_CDC_SetRxBuffer,设置USB接收数据缓冲区。USB虚拟串口收到的数据,会先缓存在这个buf里面。

       下面介绍的是CDC复位函数,其定义如下:

/**
 * @brief       复位 CDC
 * @param       无
 * @retval      USB状态
 *   @arg       USBD_OK(0)   , 正常;
 *   @arg       USBD_BUSY(1) , 忙;
 *   @arg       USBD_FAIL(2) , 失败;
 */
static int8_t CDC_DeInit_FS(void)
{
    return USBD_OK;
}

       CDC_DeInit_FS用于复位CDC,我们用不到,所以直接返回USBD_OK即可。

       下面介绍的是控制CDC的设置函数,其定义如下:

/**
 * @brief       控制 CDC 的设置
 * @param       cmd     : 控制命令
 * @param       buf     : 命令数据缓冲区/参数保存缓冲区
 * @param       length  : 数据长度
 * @retval      USB状态
 *   @arg       USBD_OK(0)   , 正常;
 *   @arg       USBD_BUSY(1) , 忙;
 *   @arg       USBD_FAIL(2) , 失败;
 */
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length)
{
  /* USER CODE BEGIN 5 */
  switch (cmd)
  {
      case CDC_SEND_ENCAPSULATED_COMMAND:
        /* Add your code here */
        break;
 
      case CDC_GET_ENCAPSULATED_RESPONSE:
        /* Add your code here */
        break;
 
      case CDC_SET_COMM_FEATURE:
        /* Add your code here */
        break;
 
      case CDC_GET_COMM_FEATURE:
        /* Add your code here */
        break;
 
      case CDC_CLEAR_COMM_FEATURE:
        /* Add your code here */
        break;
 
      case CDC_SET_LINE_CODING:
        LineCoding.bitrate    = (uint32_t)(pbuf[0] | (pbuf[1] << 8) |\
                                (pbuf[2] << 16) | (pbuf[3] << 24));
        LineCoding.format     = pbuf[4];
        LineCoding.paritytype = pbuf[5];
        LineCoding.datatype   = pbuf[6];
 
        /* 打印配置参数 */
        printf("linecoding.format:%d\r\n", LineCoding.format);
        printf("linecoding.paritytype:%d\r\n", LineCoding.paritytype);
        printf("linecoding.datatype:%d\r\n", LineCoding.datatype);
        printf("linecoding.bitrate:%d\r\n", LineCoding.bitrate);
        break;
 
      case CDC_GET_LINE_CODING:
        pbuf[0] = (uint8_t)(LineCoding.bitrate);
        pbuf[1] = (uint8_t)(LineCoding.bitrate >> 8);
        pbuf[2] = (uint8_t)(LineCoding.bitrate >> 16);
        pbuf[3] = (uint8_t)(LineCoding.bitrate >> 24);
        pbuf[4] = LineCoding.format;
        pbuf[5] = LineCoding.paritytype;
        pbuf[6] = LineCoding.datatype;
        break;
 
      case CDC_SET_CONTROL_LINE_STATE:
        /* Add your code here */
        break;
 
      case CDC_SEND_BREAK:
         /* Add your code here */
        break;
 
      default:
        break;
  }
 
  return (USBD_OK);
  /* USER CODE END 5 */
}

       CDC_Control_FS用于控制CDC的相关参数,根据cmd的不同,执行不同的操作,这里主要用到CDC_SET_LINE_CODING命令,该命令用于设置CDC的相关参数,比如波特率、数据类型(位数)、校验类型(奇偶校验)等,保存在LineCoding结构体里面,在需要的时候,应用程序可以读取LineCoding结构体里面的参数,以获得当前CDC的相关信息。

       下面介绍的是CDC数据接收函数和处理从USB虚拟串口接收到的数据函数,它们的定义如下:

/**
 * @brief       CDC 数据接收函数
 * @param       buf     : 接收数据缓冲区
 * @param       len     : 接收到的数据长度
 * @retval      USB状态
 *   @arg       USBD_OK(0)   , 正常;
 *   @arg       USBD_BUSY(1) , 忙;
 *   @arg       USBD_FAIL(2) , 失败;
 */
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
    USBD_CDC_ReceivePacket(&hUsbDeviceFS);
    cdc_vcp_data_rx(Buf, *Len);
    return USBD_OK;
}
 
/**
 * @brief       处理从 USB 虚拟串口接收到的数据
 * @param       buf     : 接收数据缓冲区
 * @param       len     : 接收到的数据长度
 * @retval      无
 */
void cdc_vcp_data_rx(uint8_t *buf, uint32_t Len)
{
    uint8_t i;
    uint8_t res;
 
    for (i = 0; i < Len; i++)
    {
        res = buf[i];
 
        if ((g_usb_usart_rx_sta & 0x8000) == 0)  /* 接收未完成 */
        {
            if (g_usb_usart_rx_sta & 0x4000)   /* 接收到了0x0d */
            {
                if (res != 0x0a)
                {
                    g_usb_usart_rx_sta = 0;   /* 接收错误,重新开始 */
                }
                else
                {
                    g_usb_usart_rx_sta |= 0x8000;  /* 接收完成了 */
                }
            }
            else    /* 还没收到0x0D */
            {
                if (res == 0x0D)
                {
                    g_usb_usart_rx_sta |= 0x4000;  /* 标记接收到了0x0D */
                }
                else
                {
                    g_usb_usart_rx_buffer[g_usb_usart_rx_sta & 0x3FFF] = res;
                    g_usb_usart_rx_sta++;
 
                    if (g_usb_usart_rx_sta > (USB_USART_REC_LEN - 1))
                    {
                        g_usb_usart_rx_sta = 0;  /* 接收数据溢出 重新开始接收 */
                    }
                }
            }
        }
    }
}

       CDC_Receive_FS和cdc_vcp_data_rx,这两个函数一起,用于CDC数据接收,当STM32的USB接收到电脑端串口发送过来的数据时,由USB内核程序调用CDC_Receive_FS,然后在该函数里面再调用cdc_vcp_data_rx函数,实现CDC的数据接收,只需要在该函数里面,将接收到的数据,保存起来即可,接收的原理和串口通信实验完全一样。

       下面介绍的是USB发送数据函数,其定义如下:

/**
 * @brief       通过 USB 发送数据
 * @param       buf     : 要发送的数据缓冲区
 * @param       len     : 数据长度
 * @retval      无
 */
void cdc_vcp_data_tx(uint8_t *data, uint32_t Len)
{
    USBD_CDC_SetTxBuffer(&hUsbDeviceFS, data, Len);
    USBD_CDC_TransmitPacket(&hUsbDeviceFS);
    delay_ms(CDC_POLLING_INTERVAL);
}

       cdc_vcp_data_tx用于发送Len个字节的数据给CDC,由CDC通过USB传输给电脑,实现CDC的数据发送。

       下面介绍的是通过USB格式化输出函数,其定义如下:

/**
 * @brief  通过 USB 格式化输出函数
 * @note  通过USB VCP实现printf输出
 *    确保一次发送数据长度不超USB_USART_REC_LEN字节
 * @param  格式化输出
 * @retval  无
 */
void usb_printf(char *fmt, ...)
{
    uint16_t i;
    va_list ap;
    va_start(ap, fmt);
    vsprintf((char *)g_usb_usart_printf_buffer, fmt, ap);
    va_end(ap);
    i = strlen((const char *)g_usb_usart_printf_buffer); /* 此次发送数据的长度 */
    cdc_vcp_data_tx(g_usb_usart_printf_buffer, i);   /* 发送数据 */
}

       usb_printf用于实现和普通串口一样的printf操作,该函数将数据格式化输出到USB CDC,功能完全同printf,方便大家使用。

       USB CDC相关代码,就给大家介绍到这里,详细的介绍,请大家参考:UM1734(STM32Cube USB device library).pdf这个文档。


       2. main.c代码

       下面是main.c的程序,具体如下:

extern volatile uint8_t g_device_state;     /* USB连接情况 */
 
int main(void)
{
    uint16_t len;
    uint16_t times = 0;
    uint8_t usbstatus = 0;
    
    HAL_Init();        /* 初始化HAL库 */
    sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
    delay_init(170);       /* 延时初始化 */
    usart_init(115200);      /* 串口初始化为115200 */
    led_init();        /* 初始化LED */
    lcd_init();        /* 初始化LCD */
    
    my_mem_init(SRAMIN);     /* 初始化内部SRAM内存池 */
    my_mem_init(SRAMCCM);     /* 初始化内部SRAMCCM内存池 */
    /* 显示提示信息 */
    lcd_show_string(10, 10, 140, 32, 32, "STM32", RED);
    lcd_show_string(10, 42, 140, 24, 24, "USB Virtual USART TEST", RED);
lcd_show_string(10, 66, 140, 24, 24, "ATOM@ALIENTEK", RED);
/* 提示正在建立连接 */
    lcd_show_string(10, 100, 200, 16, 16, "USB Connecting...", RED); 
    
    MX_USB_Device_Init();     /*  USB初始化 */
    
    while (1)
    {
        delay_ms(1);
        if (usbstatus != g_device_state)  /* USB连接状态发生了改变 */
        {
            usbstatus = g_device_state;  /* 记录新的状态 */
 
            if (usbstatus == 1)
            {
/* 提示USB连接成功 */
                lcd_show_string(10, 100, 200, 16, 16, "USB Connected    ", RED);
                LED1(0);      /* 绿灯亮 */
            }
            else
            {
/* 提示USB断开 */
                lcd_show_string(10, 100, 200, 16, 16, "USB disConnected ", RED);
                LED1(1);   /* 绿灯灭 */
            }
        }
 
        if (g_usb_usart_rx_sta & 0x8000)
        {
            len = g_usb_usart_rx_sta & 0x3FFF; /* 得到此次接收到的数据长度 */
            usb_printf("\r\n您发送的消息长度为:%d\r\n\r\n", len);
            cdc_vcp_data_tx(g_usb_usart_rx_buffer, len);
            usb_printf("\r\n\r\n");    /* 插入换行 */
            g_usb_usart_rx_sta = 0;
        }
        else
        {
            times++;
 
            if (times % 5000 == 0)
            {
                usb_printf("\r\nSTM32开发板USB虚拟串口实验\r\n");
                usb_printf("正点原子@ALIENTEK\r\n\r\n");
            }
 
            if (times % 200 == 0)usb_printf("请输入数据,以回车键结束\r\n");
 
            if (times % 30 == 0)
            {
                LED0_TOGGLE();     /* 闪烁LED,提示系统正在运行 */
            }
 
            delay_ms(10);
        }
    }
}

       此部分代码比较简单,首先进行一系列的初始化和显示提示信息,然后初始化USB设备,最后在循环里面轮询USB状态并检查是否接收到数据,如果接收到了数据,则通过cdc_vcp_data_tx函数,将数据原原本本的返回给电脑端串口调试助手。


        37 .4 下载验证

       本例程的测试,需要在电脑上先安装ST提供的USB虚拟串口驱动软件,该软件(V1.5.0版)下载地址: http://www.openedv.com/thread-284178-1-1.html ,下载完以后,根据自己电脑的系统,选择合适的驱动安装即可。

       将程序下载到开发板后(注意:USB数据线,要插在USB_SLAVE口!而不是USB_TTL端口!),我们打开设备管理器(这里以WIN10为例),在端口(COM和LPT)里面可以发现多出了一个COM4的设备,这就是USB虚拟的串口设备端口,如图37.4.1所示:


图37.4.1 通过设备管理器查看USB虚拟的串口设备端口


       如图37.4.1,STM32通过USB虚拟的串口,被电脑识别了,端口号为:COM4(可变),字符串名字为:STMicroelectronics Virtual COM Port(COM4)。此时,开发板的LDE1常亮,同时,LED0在闪烁,提示程序运行。开发板的LCD显示USB Connected,如图37.4.2所示:


图37.4.2 USB虚拟串口连接成功


       然后我们打开XCOM,选择COM4(需根据自己的电脑识别到的串口号选择),并打开串口(注意:波特率可以随意设置),就可以进行测试了,如图37.4.3所示:


图37.4.3 STM32虚拟串口通信测试


       可以看到,我们的串口调试助手,收到了来自STM32开发板的数据,同时,按发送按钮(串口助手必须勾选:发送新行),也可以收到电脑发送给STM32的数据(原样返回),说明我们的实验是成功的。

       至此,USB虚拟串口实验就完成了,通过本实验,我们就可以利用STM32的USB,直接和电脑进行数据互传了。最后祝大家身体健康、学习进步!


请使用浏览器的分享功能分享到微信等