OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 2323|回复: 1

《STM32F407 探索者开发指南》第六十五章 USB虚拟串口(Slave)实验

[复制链接]

1117

主题

1128

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4666
金钱
4666
注册时间
2019-5-8
在线时间
1224 小时
发表于 2023-9-26 18:13:12 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2023-9-26 18:13 编辑

第六十五章 USB虚拟串口(Slave)实验

1)实验平台:正点原子探索者STM32F407开发板

2) 章节摘自【正点原子】STM32F407开发指南 V1.1


4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)STM32技术交流QQ群:151941872

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

本章,我们将向大家介绍如何利用USB在开发板实现一个USB虚拟串口,通过USB与电脑数据数据交互。
本章分为如下几个小节:
65.1 USB虚拟串口简介
65.2 硬件设计
65.3 程序设计
65.4 下载验证

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

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

同上一章一样,我们直接移植官方的USBVCP例程,官方例程路径:8,STM32参考资料 à 1,STM32CubeF4固件包 à STM32Cube_FW_F4_V1.26.0à Projects à STM324xG_EVALà Applications à USB_Deviceà CDC_Standalone,该例程采用USB CDC类来实现,利用STM32的USB接口,实现一个USB转串口的功能。

65.2 硬件设计
1. 例程功能
本实验利用STM32自带的USB功能,连接电脑USB,虚拟出一个USB串口,实现电脑和开发板的数据通信。本例程功能完全同实验5(串口通信实验),只不过串口变成了STM32的USB虚拟串口。当USB连接电脑(USB线插入USB_SLAVE接口),开发板将通过USB和电脑建立连接,并虚拟出一个串口(注意:需要先安装:光盘\6,软件资料\1,软件\STM32 USB虚拟串口驱动\VCP_V1.4.0_Setup.exe这个驱动软件,虚拟串口驱动我们还可以在论坛上下载,链接是:http://www.openedv.com/thread-284178-1-1.html)。
LED0闪烁,提示程序运行。USB和电脑连接成功后,LED1常亮。

2. 硬件资源
1)LED灯
    LED0 – PF9
    LED1 – PF10
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)USB_SLAVE接口(D-/D+连接在PA11/PA12上)
5)外部SRAM芯片,通过FSMC驱动

65.3 程序设计
65.3.1 程序流程图
                             
QQ截图20230926181156.png
图65.3.1.1 USB虚拟串口(Slave)实验程序流程图

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

1. USB驱动代码
可以看到,USB部分代码,同上一个实验的在结构上是一模一样的,只是.c文件稍微有些变化。同样,我们移植需要修改的代码,就是USB_APP里面的这三个.c文件了。

usbd_conf.c代码,和上一个实验一样,不需要修改,可以直接使用上一个实验的代码。
usbd_desc.c代码,同上一个实验不一样,上一个实验描述符是USB Device设备,本实验变成了USB虚拟串口了(CDC),所以直接用ST官方的就行。
usbd_cdc_interface.c代码,是重点要修改的,首先介绍usbd_cdc_interface.h文件的相关宏定义,具体如下:
  1. #define     USB_USART_REC_LEN        200      /* USB串口接收缓冲区最大字节数 */
  2. /* 轮询周期,最大65ms,最小1ms */
  3. #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文件,首先是一些结构体变量、数组和变量的定义,具体如下:
  1. /* USB虚拟串口相关配置参数 */
  2. USBD_CDC_LineCodingTypeDef LineCoding =
  3. {
  4.     115200,      /* 波特率 */
  5.     0x00,        /* 停止位,默认1位 */
  6.     0x00,        /* 校验位,默认无 */
  7.     0x08          /* 数据位,默认8位 */
  8. };
  9. /* usb_printf发送缓冲区, 用于vsprintf */
  10. uint8_t g_usb_usart_printf_buffer[USB_USART_REC_LEN];
  11. /* USB接收的数据缓冲区,最大USART_REC_LEN个字节,用于USBD_CDC_SetRxBuffer函数 */
  12. uint8_t g_usb_rx_buffer[USB_USART_REC_LEN];
  13. /* 用类似串口1接收数据的方法,来处理USB虚拟串口接收到的数据 */
  14. /* 接收缓冲,最大USART_REC_LEN个字节 */
  15. uint8_t g_usb_usart_rx_buffer[USB_USART_REC_LEN];      
  16. /* 接收状态
  17. *bit15    ,接收完成标志
  18. *bit14    ,接收到0x0d
  19. *bit13~0  , 接收到的有效字节数目
  20. */
  21. uint16_t g_usb_usart_rx_sta=0;  /* 接收状态标记 */
  22. extern USBD_HandleTypeDef USBD_Device;
  23. static int8_t CDC_Itf_Init(void);
  24. static int8_t CDC_Itf_DeInit(void);
  25. static int8_t CDC_Itf_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length);
  26. static int8_t CDC_Itf_Receive(uint8_t *pbuf, uint32_t *Len);
  27. /* 虚拟串口配置函数(供USB内核调用) */
  28. USBD_CDC_ItfTypeDef USBD_CDC_fops =
  29. {
  30.    CDC_Itf_Init,
  31.     CDC_Itf_DeInit,
  32.    CDC_Itf_Control,
  33.    CDC_Itf_Receive
  34. };
复制代码
首先是定义一个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_CDC_fops,供USB内核调用,并把四个函数的首地址赋值给其成员。下面会介绍到这几个函数,以及一些其它的函数。

首先是初始化CDC函数,其定义如下:
  1. /**
  2. *@brief        初始化 CDC
  3. *@param        无
  4. *@retval       USB状态
  5. *  @arg        USBD_OK(0)      , 正常;
  6. *  @arg        USBD_BUSY(1)    , 忙;
  7. *  @arg        USBD_FAIL(2)    , 失败;
  8. */
  9. static int8_t CDC_Itf_Init(void)
  10. {
  11.    USBD_CDC_SetRxBuffer(&USBD_Device, g_usb_rx_buffer);
  12.     return USBD_OK;
  13. }
复制代码
CDC_Itf_Init用于初始化VCP,在初始化的时候由USB内核调用,这里我们调用函数:USBD_CDC_SetRxBuffer,设置USB接收数据缓冲区。USB虚拟串口收到的数据,会先缓存在这个buf里面。

下面介绍的是复位CDC函数,其定义如下:
  1. /**
  2. *@brief        复位 CDC
  3. *@param        无
  4. *@retval       USB状态
  5. *  @arg        USBD_OK(0)      , 正常;
  6. *  @arg        USBD_BUSY(1)    , 忙;
  7. *  @arg        USBD_FAIL(2)    , 失败;
  8. */
  9. static int8_t CDC_Itf_DeInit(void)
  10. {
  11.     return USBD_OK;
  12. }
复制代码
CDC_Itf_DeInit用于复位VCP,我们用不到,所以直接返回USBD_OK即可。

下面介绍的是控制CDC的设置函数,其定义如下:
  1. /**
  2. *@brief        控制 CDC 的设置
  3. *@param        cmd      : 控制命令
  4. *@param        buf      : 命令数据缓冲区/参数保存缓冲区
  5. *@param        length   : 数据长度
  6. *@retval       USB状态
  7. *  @arg        USBD_OK(0)      , 正常;
  8. *  @arg        USBD_BUSY(1)    , 忙;
  9. *  @arg        USBD_FAIL(2)    , 失败;
  10. */
  11. static int8_t CDC_Itf_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length)
  12. {
  13.     switch (cmd)
  14.     {
  15.        caseCDC_SEND_ENCAPSULATED_COMMAND:
  16.            break;
  17.        caseCDC_GET_ENCAPSULATED_RESPONSE:
  18.            break;
  19.        caseCDC_SET_COMM_FEATURE:
  20.            break;
  21.        caseCDC_GET_COMM_FEATURE:
  22.            break;
  23.        caseCDC_CLEAR_COMM_FEATURE:
  24.            break;
  25.        caseCDC_SET_LINE_CODING:
  26.            LineCoding.bitrate = (uint32_t) (pbuf[0] | (pbuf[1] << 8) |
  27.                                              (pbuf[2] << 16) | (pbuf[3] << 24));
  28.            LineCoding.format = pbuf[4];
  29.            LineCoding.paritytype = pbuf[5];
  30.            LineCoding.datatype = pbuf[6];
  31.            /* 打印配置参数 */
  32.            printf("linecoding.format:%d\r\n", LineCoding.format);
  33.            printf("linecoding.paritytype:%d\r\n", LineCoding.paritytype);
  34.            printf("linecoding.datatype:%d\r\n", LineCoding.datatype);
  35.            printf("linecoding.bitrate:%d\r\n", LineCoding.bitrate);
  36.            break;
  37.        caseCDC_GET_LINE_CODING:
  38.            pbuf[0] = (uint8_t) (LineCoding.bitrate);
  39.            pbuf[1] = (uint8_t) (LineCoding.bitrate >> 8);
  40.            pbuf[2] = (uint8_t) (LineCoding.bitrate >> 16);
  41.            pbuf[3] = (uint8_t) (LineCoding.bitrate >> 24);
  42.            pbuf[4] = LineCoding.format;
  43.            pbuf[5] = LineCoding.paritytype;
  44.            pbuf[6] = LineCoding.datatype;
  45.            break;
  46.        caseCDC_SET_CONTROL_LINE_STATE:
  47.            break;
  48.        caseCDC_SEND_BREAK:
  49.            break;
  50.        default:
  51.            break;
  52.     }
  53.     return USBD_OK;
  54. }
复制代码
CDC_Itf_Control用于控制VCP的相关参数,根据cmd的不同,执行不同的操作,这里主要用到CDC_SET_LINE_CODING命令,该命令用于设置VCP的相关参数,比如波特率、数据类型(位数)、校验类型(奇偶校验)等,保存在linecoding结构体里面,在需要的时候,应用程序可以读取LineCoding结构体里面的参数,以获得当前VCP的相关信息。

下面介绍的是CDC数据接收函数和处理从USB虚拟串口接收到的数据函数,它们的定义如下:
  1. /**
  2. *@brief        CDC 数据接收函数
  3. *@param        buf      : 接收数据缓冲区
  4. *@param        len      : 接收到的数据长度
  5. *@retval       USB状态
  6. *  @arg        USBD_OK(0)      , 正常;
  7. *  @arg        USBD_BUSY(1)    , 忙;
  8. *  @arg        USBD_FAIL(2)    , 失败;
  9. */
  10. static int8_t CDC_Itf_Receive(uint8_t *buf, uint32_t *len)
  11. {
  12.    SCB_CleanDCache_by_Addr((uint32_t *)buf, *len);
  13.    USBD_CDC_ReceivePacket(&USBD_Device);
  14.    cdc_vcp_data_rx(buf, *len);
  15.     return USBD_OK;
  16. }
  17. /**
  18. *@brief        处理从 USB 虚拟串口接收到的数据
  19. *@param        buf      : 接收数据缓冲区
  20. *@param        len      : 接收到的数据长度
  21. *@retval       无
  22. */
  23. voidcdc_vcp_data_rx (uint8_t *buf, uint32_t Len)
  24. {
  25.     uint8_t i;
  26.     uint8_t res;
  27.     for (i = 0; i < Len; i++)
  28.     {
  29.        res = buf;
  30.        if ((g_usb_usart_rx_sta & 0x8000) == 0)      /* 接收未完成 */
  31.        {
  32.            if (g_usb_usart_rx_sta & 0x4000)          /* 接收到了0x0d */
  33.            {
  34.                 if (res != 0x0a)
  35.                 {
  36.                     g_usb_usart_rx_sta = 0;             /* 接收错误,重新开始 */
  37.                 }
  38.                 else
  39.                 {
  40.                     g_usb_usart_rx_sta |= 0x8000;     /* 接收完成了 */
  41.                 }
  42.            }
  43.            else    /* 还没收到0x0D */
  44.            {
  45.                 if (res == 0x0D)
  46.                 {
  47.                     g_usb_usart_rx_sta |= 0x4000;     /* 标记接收到了0x0D */
  48.                 }
  49.                 else
  50.                 {
  51.                     g_usb_usart_rx_buffer[g_usb_usart_rx_sta & 0x3FFF] = res;
  52.                     g_usb_usart_rx_sta++;
  53.                     if (g_usb_usart_rx_sta > (USB_USART_REC_LEN - 1))
  54.                     {
  55.                         g_usb_usart_rx_sta = 0;        /* 接收数据溢出 重新开始接收 */
  56.                     }
  57.                 }
  58.            }
  59.        }
  60.     }
  61. }
复制代码
CDC_Itf_Receive和cdc_vcp_data_rx,这两个函数一起,用于VCP数据接收,当STM32的USB接收到电脑端串口发送过来的数据时,由USB内核程序调用CDC_Itf_Receive,然后在该函数里面再调用cdc_vcp_data_rx函数,实现VCP的数据接收,只需要在该函数里面,将接收到的数据,保存起来即可,接收的原理和实验5串口通信实验完全一样。

下面介绍的是通过USB发送数据函数,其定义如下:
  1. /**
  2. *@brief        通过 USB 发送数据
  3. *@param        buf      : 要发送的数据缓冲区
  4. *@param        len      : 数据长度
  5. *@retval       无
  6. */
  7. voidcdc_vcp_data_tx(uint8_t *data, uint32_t Len)
  8. {
  9.    USBD_CDC_SetTxBuffer(&USBD_Device, data, Len);
  10.    USBD_CDC_TransmitPacket(&USBD_Device);
  11.    delay_ms(CDC_POLLING_INTERVAL);
  12. }
复制代码
cdc_vcp_data_rx用于发送Len个字节的数据给VCP,由VCP通过USB传输给电脑,实现VCP的数据发送。

下面介绍的是通过USB格式化输出函数,其定义如下:
  1. /**
  2. *@brief        通过 USB 格式化输出函数
  3. *  @note       通过USB VCP实现printf输出
  4. *               确保一次发送数据长度不超USB_USART_REC_LEN字节
  5. *@param        格式化输出
  6. *@retval       无
  7. */
  8. void usb_printf(char *fmt, ...)
  9. {
  10.     uint16_t i;
  11.    va_list ap;
  12.    va_start(ap, fmt);
  13.    vsprintf((char *)g_usb_usart_printf_buffer, fmt, ap);
  14.    va_end(ap);
  15.     i = strlen((const char *)g_usb_usart_printf_buffer);   /* 此次发送数据的长度 */
  16.    cdc_vcp_data_tx(g_usb_usart_printf_buffer, i);           /* 发送数据 */
  17.    SCB_CleanDCache_by_Addr((uint32_t *)g_usb_usart_printf_buffer, i);
  18. }
复制代码
usb_printf用于实现和普通串口一样的printf操作,该函数将数据格式化输出到USB VCP,功能完全同printf,方便大家使用。

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

2. main.c代码
下面是main.c的程序,具体如下:
  1. USBD_HandleTypeDef USBD_Device;              /* USB Device处理结构体 */
  2. extern volatile uint8_t g_device_state;    /* USB连接 情况 */
  3. int main(void)
  4. {
  5.     uint16_t len;
  6.     uint16_t times = 0;
  7.     uint8_t usbstatus = 0;
  8.    
  9.    HAL_Init();                             /* 初始化HAL库 */
  10.    sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
  11.    delay_init(168);                       /* 延时初始化 */
  12.    usart_init(115200);                   /* 串口初始化为115200 */
  13.    led_init();                             /* 初始化LED */
  14.    lcd_init();                             /* 初始化LCD */
  15.    key_init();                             /* 初始化按键 */
  16.    my_mem_init(SRAMIN);                  /* 初始化内部SRAM内存池 */
  17.    my_mem_init(SRAMIN);                  /* 初始化外部SRAM内存池 */
  18.    my_mem_init(SRAMCCM);                 /* 初始化内部SRAMCCM内存池 */
  19.    
  20.    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
  21.    lcd_show_string(30, 70, 200, 16, 16, "USB VirtualUSART TEST", RED);
  22.    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
  23.     /* 提示USB开始连接 */
  24.    lcd_show_string(30, 110, 200, 16, 16, "USBConnecting...", RED);
  25.    USBD_Init(&USBD_Device, &VCP_Desc, DEVICE_FS);           /* 初始化USB */
  26.    USBD_RegisterClass(&USBD_Device, USBD_CDC_CLASS);       /* 添加类 */
  27.     /* 为MSC类添加回调函数 */
  28.    USBD_CDC_RegisterInterface(&USBD_Device, &USBD_CDC_fops);
  29.    USBD_Start(&USBD_Device);    /* 开启USB */
  30.    delay_ms(1800);
  31.    
  32.     while (1)
  33.     {
  34.        delay_ms(1);
  35.        if (usbstatus != g_device_state)    /* USB连接状态发生了改变 */
  36.        {
  37.            usbstatus = g_device_state;      /* 记录新的状态 */
  38.            if (usbstatus == 1)
  39.            {
  40.                 /* 提示USB连接成功 */
  41.                 lcd_show_string(30, 110, 200, 16, 16, "USB Connected   ", RED);
  42.                 LED1(0);    /* 绿灯亮 */
  43.            }
  44.            else
  45.            {
  46.                 /* 提示USB断开 */
  47.                 lcd_show_string(30, 110, 200, 16, 16, "USB disConnected ", RED);
  48.                 LED1(1);    /* 绿灯灭 */
  49.            }
  50.        }
  51.        if (g_usb_usart_rx_sta & 0x8000)
  52.        {
  53.            len =g_usb_usart_rx_sta & 0x3FFF;   /* 得到此次接收到的数据长度 */
  54.            usb_printf("\r\n您发送的消息长度为:%d\r\n\r\n", len);
  55.            cdc_vcp_data_tx(g_usb_usart_rx_buffer, len);;
  56.            usb_printf("\r\n\r\n");                /* 插入换行 */
  57.            g_usb_usart_rx_sta = 0;
  58.        }
  59.        else
  60.        {
  61.            times++;
  62.            if (times % 5000 == 0)
  63.            {
  64.                 usb_printf("\r\nSTM32开发板USB虚拟串口实验\r\n");
  65.                 usb_printf("正点原子@ALIENTEK\r\n\r\n");
  66.            }
  67.            if (times % 200 == 0)
  68.            {
  69.                 usb_printf("请输入数据,以回车键结束\r\n");
  70.            }
  71.            if (times % 30 == 0)
  72.            {
  73.                 LED0_TOGGLE();  /* 闪烁LED,提示系统正在运行 */
  74.            }
  75.            delay_ms(10);
  76.        }
  77.     }
  78. }
复制代码
此部分代码比较简单,首先定义了USBD_Device结构体,然后通过USBD_Init等函数初始化USB,不过本章实现的是USB虚拟串口的功能。然后在死循环里面轮询USB状态并检查是否接收到数据,如果接收到了数据,则通过VCP_DataTx将数据通过VCP原原本本的返回给电脑端串口调试助手。

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

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

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

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

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

至此,USB虚拟串口实验就完成了,通过本实验,我们就可以利用STM32的USB,直接和电脑进行数据互传了,具有广泛的应用前景。
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

14

主题

77

帖子

0

精华

初级会员

Rank: 2

积分
190
金钱
190
注册时间
2015-7-22
在线时间
122 小时
发表于 2024-8-30 10:53:07 | 显示全部楼层
407的虚拟串口会和CAN冲突吗?
work is work!!!
回复 支持 反对

使用道具 举报

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

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2024-11-22 08:57

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表