零基础国产GD32单片机编程入门(二十五)USB口介绍及CDC类虚拟串口通讯详解及源码

文章目录

一.概要

GD32F103C8T6 USB虚拟串口是一种采用GD32F103C8T6单片机,通过USB接口连接电脑,将电脑的USB接口转换成串口接口,实现与电脑的通信的一种转换器。它可以实现与电脑的通信,还可以实现与外部设备的通信,广泛应用于工业控制、智能家居、智能硬件等领域。

本文介绍了GD32单片机USB口的基本概念,内部结构,以及用USB虚拟串口进行数据通讯的例程。

二.USB2.0基本介绍及虚拟串口介绍

USB2.0使用一对差分信号传输数据,并可以为USB设备提供电源。差分信号名称一般标示为"D+"和"D-"。

USB2.0可以支持三种传输速率:低速USB设备传输速率为1.5Mbps,全速USB设备传输速率为12Mbps,高速USB设备传输速率为480Mbps。GD32F103C8T6就属于全速USB设备传输,最快12Mbps。

在硬件电路方面,全速USB设备内部的"D+"信号应该通过1.5K的电阻上拉到3~3.6V。

USB虚拟串口,简称VCP,是Virtual COM Port的简写,它是利用 USB的 CDC类来实现的一种通信接口。我们可以利用GD32自带的USB功能,来实现一个USB虚拟串口,从而通过USB,实现电脑与GD32单片机的数据互传。

三.GD32单片机USB模块框图

GD32单片机通用串行总线全速设备接口(USBD)模块提供了一个实现符合USB 2.0全速协议外设的方案。它内部包含了一个USB物理层而不需要额外的外部物理层芯片。USBD支持USB 2.0协议所定义的四种传输类型(控制、批量、中断和同步传输)

SIE

• 硬件识别同步信号、进行比特填充、产生以及校验CRC、产生以及验证PID、握手 。

• 根据外设事件来产生SOF、复位信号等。

Packet Buffer interface

• 通过一组收、发buffer来管理512字节的local memory。

• 硬件根据来自SIE的请求来选择合适的buffer。

寄存器

• EndPoint相关的寄存器:该EP的传输类型、地址、当前状态 。

• 控制寄存器:控制USB模块事件(比如唤醒和休眠)和反应USB模块当前状态 。

• 中断寄存器:中断掩码,记录事件。

根据USB标准定义,USB全速模块采用了固定的48MHz时钟。要使用USBD,需要打开两个时钟,一个是USB控制器时钟,它的频率必须配到48MHz,另一个是APB1到USB接口时钟,它也是APB1的总线时钟,其频率可以高于也可以低于48MHz。

四.GD32单片机USB设备模式

USB一般有两种模式,主机模式,设备模式。

USB主机模式:‌在主机模式下,‌单片机能够枚举外部USB设备,‌如键盘、‌鼠标、‌闪存盘等,‌并对其进行配置和管理。‌这种模式适用于需要同时连接多个外部设备并进行数据交换的复杂应用场景。

USB设备模式:‌在设备模式下,‌单片机作为USB设备的角色,‌可以与主机进行通信。‌这包括配置USB设备描述符、‌初始化USB控制器、‌编写类处理函数等,‌以实现特定的通信需求。‌GD32支持多种USB类,‌如CDC(‌通信设备类)‌、‌HID(‌人机接口设备类)‌、‌MSC(‌大容量存储类)‌等,‌以满足不同的应用场景。

GD32F103C8T6只支持设备模式,在设备模式下,GD32可以模拟各种USB类设备,如键盘、鼠标、存储设备等,开发者需要配置USB接口并实现特定的USB类。

五.GD32F103C8T6 USB设备CDC类

CDC(Communication Device Class)类是 USB2.0 标准下的一个子类,定义了通信相关设备的抽象集合,我们虚拟串口通信就是CDC类。USB2.0标准下定义了很多子类,有音频类,CDC类,HID,打印,大容量存储类HUB,智能卡等等,这些在urb.org 官网上有具体的定义,这里我们主要讲的是通信类CDC。

USB CDC类的通信部分主要包含三部分:枚举过程、虚拟串口操作和数据通信。其中虚拟串口操作部分并不一定强制需要,因为若跳过这些虚拟串口的操作,实际上USB依然是可以通信的,之所以会有虚拟串口操作,主要是我们通常使用PC作为Host端,在PC端使用一个串口工具来与其进行通信,PC端的对应驱动将其虚拟成一个普通串口,这样一来,可以方便PC端软件通过操作串口的方式来与其进行通信,但实际上,Host端与Device端物理上是通过USB总线来进行通信的,与串口没有关系,这一虚拟化过程,起决定性作用的是对应驱动,包含如何将每一条具体的虚拟串口操作对应到实际上的USB操作。这里需要注意地是,Host端与Device端的USB通信速率并不受所谓的串口波特率影响,它就是标准的USB2.0全速(12Mbps)速度,实际速率取决于总线的实际使用率、驱动访问USB外设有效速率(两边)以及外部环境对通信本身造成的干扰率等等因素组成。

USB CDC类实现了规范的下列方面:

• 设备描述符管理

• 配置描述符管理

• 枚举为具有2个数据端点(IN和OUT)的CDC设备和一个指令端点(IN)

• 请求管理(如规范6.2节所述)

• 抽象控制模型兼容

• 联合体功能收集(使用1个IN端点控制)

• 数据接口类

CDC软件框架简介

当USBD设备初始化且枚举完成后,USB设备首先通过cdc_acm_check_ready()函数check是否准备数据发送,如果不需要发送就调用cdc_acm_data_receive()函数接收上位机发送的数据,如果需要发送就调用cdc_acm_data_send()将接收到的数据发送给主机。

设备描述符如下所示,其中bDevcieClass为0x02,表明当前设备为CDC设备类。

c 复制代码
/* note:it should use the C99 standard when compiling the below codes */
/* USB standard device descriptor */
const usb_descriptor_device_struct device_descriptor =
{
    .Header = 
     {
         .bLength = USB_DEVICE_DESC_SIZE, 
         .bDescriptorType = USB_DESCTYPE_DEVICE
     },
    .bcdUSB = 0x0200,
    .bDeviceClass = 0x02,
    .bDeviceSubClass = 0x00,
    .bDeviceProtocol = 0x00,
    .bMaxPacketSize0 = USBD_EP0_MAX_SIZE,
    .idVendor = USBD_VID,
    .idProduct = USBD_PID,
    .bcdDevice = 0x0100,
    .iManufacturer = USBD_MFC_STR_IDX,
    .iProduct = USBD_PRODUCT_STR_IDX,
    .iSerialNumber = USBD_SERIAL_STR_IDX,
    .bNumberConfigurations = USBD_CFG_MAX_NUM
};

由配置描述符可知,该USB虚拟串口设备包含两个接口:CMD命令接口和data数据接口。CMD命令接口包含一个IN端点,用于传输命令,该端点采用中断传输方式,轮询间隔为10ms,最大包长为8字节。data数据接口包含一个OUT端点和一个IN端点,这两个端点均采用批量传输方式,最大包长为64字节。另外,该配置描述符中包含了一些类特殊接口描述符,具体请读者参阅CDC类标准协议。

c 复制代码
/* USB device configuration descriptor */
usb_descriptor_configuration_set_struct configuration_descriptor = 
{
     .cdc_loopback_cmd_endpoint = 
    {
        .Header = 
         {
            .bLength = sizeof(usb_descriptor_endpoint_struct), 
            .bDescriptorType = USB_DESCTYPE_ENDPOINT
         },
        .bEndpointAddress = CDC_ACM_CMD_EP,
        .bmAttributes = 0x03,
        .wMaxPacketSize = CDC_ACM_CMD_PACKET_SIZE,
        .bInterval = 0x0A
    },

    .cdc_loopback_data_interface = 
    {
        .Header = 
         {
            .bLength = sizeof(usb_descriptor_interface_struct), 
            .bDescriptorType = USB_DESCTYPE_INTERFACE
         },
        .bInterfaceNumber = 0x01,
        .bAlternateSetting = 0x00,
        .bNumEndpoints = 0x02,
        .bInterfaceClass = 0x0A,
        .bInterfaceSubClass = 0x00,
        .bInterfaceProtocol = 0x00,
        .iInterface = 0x00
    },
     .cdc_loopback_out_endpoint = 
    {
        .Header = 
         {
             .bLength = sizeof(usb_descriptor_endpoint_struct), 
             .bDescriptorType = USB_DESCTYPE_ENDPOINT 
         },
        .bEndpointAddress = CDC_ACM_DATA_OUT_EP,
        .bmAttributes = 0x02,
        .wMaxPacketSize = CDC_ACM_DATA_PACKET_SIZE,
        .bInterval = 0x00
    },

    .cdc_loopback_in_endpoint = 
    {
        .Header = 
         {
             .bLength = sizeof(usb_descriptor_endpoint_struct), 
             .bDescriptorType = USB_DESCTYPE_ENDPOINT 
         },
        .bEndpointAddress = CDC_ACM_DATA_IN_EP,
        .bmAttributes = 0x02,
        .wMaxPacketSize = CDC_ACM_DATA_PACKET_SIZE,
        .bInterval = 0x00
    }
};

为了实现CDC设备类,设备需要支持一些设备类专用请求,这些类专用请求的处理在cdc_acm_req_handler()函数中,该函数的定义如下所示,其中SET_LINE_CODING命令用于响应主机向设备发送设备配置,包括波特率、停止位、字符位数等,收到的数据保存在noti_bu内。GET_LINE_CODING命令用于主机请求设备当前的波特率、停止位、奇偶校验位和字符位数,但在本例程中,主机并未请求该命令,所以设备所设置的串口数据并没有作用,主机可以选择任意波特率与设备进行通信。

c 复制代码
/*!
    \brief      handle the CDC ACM class-specific requests
    \param[in]  pudev: pointer to USB device instance
    \param[in]  req: device class-specific request
    \param[out] none
    \retval     USB device operation status
*/
usbd_status_enum cdc_acm_req_handler (void *pudev, usb_device_req_struct *req)
{
    uint16_t len = CDC_ACM_DESC_SIZE;
    uint8_t  *pbuf= (uint8_t*)(&configuration_descriptor) + 9;

    switch (req->bmRequestType & USB_REQ_MASK) {
    case USB_CLASS_REQ:
        switch (req->bRequest) {
        case SEND_ENCAPSULATED_COMMAND:
            break;
        case GET_ENCAPSULATED_RESPONSE:
            break;
        case SET_COMM_FEATURE:
            break;
        case GET_COMM_FEATURE:
            break;
        case CLEAR_COMM_FEATURE:
            break;
        case SET_LINE_CODING:
            /* set the value of the current command to be processed */
            cdc_cmd = req->bRequest;
            /* enable EP0 prepare to receive command data packet */
            usbd_ep_rx (pudev, EP0_OUT, usb_cmd_buffer, req->wLength);
            break;
        case GET_LINE_CODING:
            usb_cmd_buffer[0] = (uint8_t)(linecoding.dwDTERate);
            usb_cmd_buffer[1] = (uint8_t)(linecoding.dwDTERate >> 8);
            usb_cmd_buffer[2] = (uint8_t)(linecoding.dwDTERate >> 16);
            usb_cmd_buffer[3] = (uint8_t)(linecoding.dwDTERate >> 24);
            usb_cmd_buffer[4] = linecoding.bCharFormat;
            usb_cmd_buffer[5] = linecoding.bParityType;
            usb_cmd_buffer[6] = linecoding.bDataBits;
            /* send the request data to the host */
            usbd_ep_tx (pudev, EP0_IN, usb_cmd_buffer, req->wLength);
            break;
        case SET_CONTROL_LINE_STATE:
            break;
        case SEND_BREAK:
            break;
        default:
            break;
        }
        break;
    case USB_STANDARD_REQ:
        /* standard device request */
        switch(req->bRequest) {
        case USBREQ_GET_INTERFACE:
            usbd_ep_tx(pudev, EP0_IN, (uint8_t *)&usbd_cdc_altset, 1);
            break;
        case USBREQ_SET_INTERFACE:
            if ((uint8_t)(req->wValue) < USBD_ITF_MAX_NUM) {
                usbd_cdc_altset = (uint8_t)(req->wValue);
            } else {
                /* call the error management function (command will be nacked */
                usbd_enum_error (pudev, req);
            }
            break;
        case USBREQ_GET_DESCRIPTOR:
            if(CDC_ACM_DESC_TYPE == (req->wValue >> 8)){
                len = MIN(CDC_ACM_DESC_SIZE, req->wLength);
                pbuf = (uint8_t*)(&configuration_descriptor) + 9 + (9 * USBD_ITF_MAX_NUM);
            }

            usbd_ep_tx(pudev, EP0_IN, pbuf, len);
            break;
        default:
            break;
        }
    default:
        break;
    }

    return USBD_OK;
}

数据接收通过cdc_acm_data_receive()函数实现,该函数的程序如下所示。在该函数中,首先将packet_receive标志位设置为0,表明接下来将进行接收数据,当接收完成时,在cdc_acm_data_handler ()函数中,将packet_receive标志位置1,表明数据接收完成。usbd_ep_rx()用于配置接收操作,利用CDC_OUT_EP端点,将接收到的数据放置在用户缓冲区中。

c 复制代码
/*!
    \brief      receive CDC ACM data
    \param[in]  pudev: pointer to USB device instance
    \param[out] none
    \retval     USB device operation status
*/
void cdc_acm_data_receive(void *pudev)
{
    packet_receive = 0;

    usbd_ep_rx(pudev, CDC_ACM_DATA_OUT_EP, (uint8_t*)(usb_data_buffer), CDC_ACM_DATA_PACKET_SIZE);
}

/*!
    \brief      endpoint prepare to receive data
    \param[in]  pudev: pointer to usb core instance
    \param[in]  ep_addr: endpoint address
                  in this parameter:
                    bit0..bit6: endpoint number (0..7)
                    bit7: endpoint direction which can be IN(1) or OUT(0)
    \param[in]  pbuf: user buffer address pointer
    \param[in]  buf_len: buffer length
    \param[out] none
    \retval     none
*/
void  usbd_ep_rx (usbd_core_handle_struct *pudev, uint8_t ep_addr, uint8_t *pbuf, uint16_t buf_len)
{
    usb_ep_struct *ep;
    uint8_t ep_num = ep_addr & 0x7FU;

    ep = &pudev->out_ep[ep_num];

    /* configure the transaction level parameters */
    ep->trs_buf = pbuf;
    ep->trs_len = buf_len;

    /* enable endpoint to receive */
    USBD_ENDP_RX_STATUS_SET(ep_num, EPRX_VALID);
}
/*!
    \brief      handle CDC ACM data
    \param[in]  pudev: pointer to USB device instance
    \param[in]  rx_tx: data transfer direction:
      \arg        USBD_TX
      \arg        USBD_RX
    \param[in]  ep_id: endpoint identifier
    \param[out] none
    \retval     USB device operation status
*/
usbd_status_enum  cdc_acm_data_handler (void *pudev, usbd_dir_enum rx_tx, uint8_t ep_id)
{
    if ((USBD_TX == rx_tx) && ((CDC_ACM_DATA_IN_EP & 0x7F) == ep_id)) {
        packet_sent = 1;
        return USBD_OK;
    } else if ((USBD_RX == rx_tx) && ((EP0_OUT & 0x7F) == ep_id)) {
        cdc_acm_EP0_RxReady (pudev);
    } else if ((USBD_RX == rx_tx) && ((CDC_ACM_DATA_OUT_EP & 0x7F) == ep_id)) {
        packet_receive = 1;
        receive_length = usbd_rx_count_get(pudev, CDC_ACM_DATA_OUT_EP);
        return USBD_OK;
    }
    return USBD_FAIL;
}

数据发送通过cdc_acm_data_send()函数实现,该函数的程序如下所示。在该函数中,首先将packet_sent标志位设置为0,表明接下来将进行发送数据,当数据发送完成时,在cdc_acm_data_handler ()函数中,将packet_sent标志位设置为1,表明数据发送完成。usbd_ep_tx()用于配置发送操作,利用CDC_IN_EP端点,将以 ep->trs_buf地址为起始trs_len 长度的数据发送给主机。

c 复制代码
void cdc_acm_data_send (void *pudev, uint32_t data_len)
{
    /* limit the transfer data length */
    if (data_len <= CDC_ACM_DATA_PACKET_SIZE) {
        packet_sent = 0;
        usbd_ep_tx(pudev, CDC_ACM_DATA_IN_EP, (uint8_t*)(usb_data_buffer), data_len);
    }
}

usbd_status_enum  cdc_acm_data_handler (void *pudev, usbd_dir_enum rx_tx, uint8_t ep_id)
{
    if ((USBD_TX == rx_tx) && ((CDC_ACM_DATA_IN_EP & 0x7F) == ep_id)) {
        packet_sent = 1;
        return USBD_OK;
    } else if ((USBD_RX == rx_tx) && ((EP0_OUT & 0x7F) == ep_id)) {
        cdc_acm_EP0_RxReady (pudev);
    } else if ((USBD_RX == rx_tx) && ((CDC_ACM_DATA_OUT_EP & 0x7F) == ep_id)) {
        packet_receive = 1;
        receive_length = usbd_rx_count_get(pudev, CDC_ACM_DATA_OUT_EP);
        return USBD_OK;
    }
    return USBD_FAIL;
}
void  usbd_ep_tx (usbd_core_handle_struct *pudev, uint8_t ep_addr, uint8_t *pbuf, uint16_t buf_len)
{
    __IO uint32_t len = 0U;
    uint8_t ep_num = ep_addr & 0x7FU;
    usb_ep_struct *ep = &pudev->in_ep[ep_num];

    /* configure the transaction level parameters */
    ep->trs_buf = pbuf;
    ep->trs_len = buf_len;
    ep->trs_count = 0U;

    /* transmit length is more than one packet */
    if (ep->trs_len > ep->maxpacket) {
        len = ep->maxpacket;
    } else {
        len = ep->trs_len;
    }

    usbd_ep_data_write(ep->trs_buf, (pbuf_reg + ep_num)->tx_addr, (uint16_t)len);
    (pbuf_reg + ep_num)->tx_count = (uint16_t)len;

    /* enable endpoint to transmit */
    USBD_ENDP_TX_STATUS_SET(ep_num, EPTX_VALID);
}

六.配置一个USB虚拟串口收发例程

STLINK接GD32F103C8T6开发板,STLINK接电脑USB口。

主要代码

c 复制代码
#include "gd32f10x.h"
#include "gd32f10x_libopt.h"
#include "systick.h"
#include "cdc_acm_core.h"

extern uint8_t packet_sent, packet_receive;
extern uint32_t receive_length;
extern uint8_t usb_data_buffer[CDC_ACM_DATA_PACKET_SIZE];

//定义 CDC-ACM 类的 USB 设备
usbd_core_handle_struct  usb_device_dev = 
{
    .dev_desc = (uint8_t *)&device_descriptor,
    .config_desc = (uint8_t *)&configuration_descriptor,
    .strings = usbd_strings,
    .class_init = cdc_acm_init,
    .class_deinit = cdc_acm_deinit,
    .class_req_handler = cdc_acm_req_handler,
    .class_data_handler = cdc_acm_data_handler
};

//中断使能
void nvic_config(void)
{
    /* 1 bit for pre-emption priority, 3 bits for subpriority */
    nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);

    /* enable the USB low priority interrupt */
    nvic_irq_enable(USBD_LP_CAN0_RX0_IRQn, 1, 0);
}
int main(void)
{
	rcu_apb2_clock_config(RCU_APB2_CKAHB_DIV1);//设置主频72M(#define __SYSTEM_CLOCK_72M_PLL_HXTAL         (uint32_t)(72000000)),8M外部晶振  (#define HXTAL_VALUE    ((uint32_t)8000000))
  systick_config();//配置1ms SysTick
	rcu_periph_clock_enable(RCU_GPIOA);
	rcu_periph_clock_enable(RCU_AF);//AF时钟使能 


	/* configure USB model clock from PLL clock */
	rcu_usb_clock_config(RCU_CKUSB_CKPLL_DIV1_5);//72M主频,1.5分频,就是48M

	/* enable USB APB1 clock */
	rcu_periph_clock_enable(RCU_USBD);

	/* USB device configuration */
	usbd_core_init(&usb_device_dev);

	/* NVIC configuration */
	nvic_config();

	/* now the usb device is connected */
	usb_device_dev.status = USBD_CONNECTED;

    while (1)
    {
        if (USBD_CONFIGURED == usb_device_dev.status) {
            if (1 == packet_receive && 1 == packet_sent) {//有数据接收到
                packet_sent = 0;
                /* receive datas from the host when the last packet datas have sent to the host */
                cdc_acm_data_receive(&usb_device_dev);//接收数据
            } else {
                if (0 != receive_length) {
                    /* send receive datas */
                    cdc_acm_data_send(&usb_device_dev, receive_length);//发送接收到的数据
                    receive_length = 0;
                }
            }
        }
    }
 
}

实验效果

下载完程序,用 USB 线接板子USB 口,再接电脑,打开电脑上串口调试器,9600 波特率,8 位数据,无校验,发送 HELLOWORLD,板子就会返回 HELLOWORLD。

七.工程源代码下载

通过网盘分享的文件:27.USB虚拟串口通信实验.zip

链接: https://pan.baidu.com/s/1MDB8vX7EtFcuV_LcKmBh_Q 提取码: cp9x

如果链接失效,可以联系博主给最新链接

程序下载下来之后解压就行

八.小结

USB虚拟串口可以实现与电脑的通信,还可以实现与外部设备的通信,广泛应用于工业控制、智能家居、智能硬件等领域。

相关推荐
松涛和鸣1 小时前
DAY63 IMX6ULL ADC Driver Development
linux·运维·arm开发·单片机·嵌入式硬件·ubuntu
想放学的刺客4 小时前
单片机嵌入式试题(第23期)嵌入式系统电源管理策略设计、嵌入式系统通信协议栈实现要点两个全新主题。
c语言·stm32·单片机·嵌入式硬件·物联网
猫猫的小茶馆4 小时前
【Linux 驱动开发】五. 设备树
linux·arm开发·驱动开发·stm32·嵌入式硬件·mcu·硬件工程
jghhh016 小时前
基于上海钜泉科技HT7017单相计量芯片的参考例程实现
科技·单片机·嵌入式硬件
恶魔泡泡糖6 小时前
51单片机外部中断
c语言·单片机·嵌入式硬件·51单片机
意法半导体STM326 小时前
【官方原创】如何基于DevelopPackage开启安全启动(MP15x) LAT6036
javascript·stm32·单片机·嵌入式硬件·mcu·安全·stm32开发
v_for_van6 小时前
STM32低频函数信号发生器(四通道纯软件生成)
驱动开发·vscode·stm32·单片机·嵌入式硬件·mcu·硬件工程
电化学仪器白超7 小时前
③YT讨论
开发语言·python·单片机·嵌入式硬件
乡野码圣7 小时前
【RK3588 Android12】硬件中断IRQ
单片机·嵌入式硬件
happygrilclh7 小时前
数码管驱动(一):ET6226M -数据手册主要点分析
单片机·嵌入式硬件