firmware version:GD32F10x_Firmware_Library_V2.2.4

模板工程:cdc_acm

        GD32F103自带一个USBD,虚拟成串口设备来与上位机通信会比USART方便不少(主要是懒得接线~),GD官方给出的例子中结构还是很清晰的,本文仅记录一些要点。

        在官方的cdc_acm工程中,程序会一直等待直到USB枚举成功后才会执行下一步骤。同时,在这个demo中,USB数据的收、发都需要在main的死循环中进行,主要逻辑如下所示:

int main(void)
{
    ... ...

    while (USBD_CONFIGURED != usbd_cdc.cur_status) {
        /* wait for standard USB enumeration is finished */
    }

    while (1) {
        if (0U == cdc_acm_check_ready(&usbd_cdc)) {
            cdc_acm_data_receive(&usbd_cdc);
        } else {
            cdc_acm_data_send(&usbd_cdc);
        }
    }

}

        很显然,这样的逻辑关系很难应用于自己的实际项目中,因此第一步,需要将等待枚举的部分注释掉;第二步则需要将在死循环中查询USB消息的逻辑挪到中断中进行。实际工程中我们更希望main中的结构是这个样子:

int main(void)
{
    /* system clocks configuration */
    rcu_config();

    /* GPIO configuration */
    gpio_config();

    /* USB device configuration */
    usbd_init(&usbd_cdc, &cdc_desc, &cdc_class);

    /* NVIC configuration */
    nvic_config();

    /* enabled USB pull-up */
    usbd_connect(&usbd_cdc);

    while (1)
    {
        /* do others */
    }
}

        通过分析cdc_acm中函数的调用关系,可以定位到Firmware\GD32F10x_usbd_library\class\device\cdc\Source\cdc_acm_core.c文件内,官方例子通过暴露的cdc_acm_check_ready、cdc_acm_data_sendcdc_acm_data_receive这三个接口完成USB数据的收发。但这三个函数内仅通过判断usb_cdc_handler *cdc结构体的相关标志位来控制数据的发送与接收,因此忽略掉它们。

        经过一番分析和测试,最终需要修改两个函数:cdc_acm_ctlx_outcdc_acm_data_out

        GD接收USB数据最终有效的是usbd_ep_recev函数。在cdc_acm_ctlx_out函数中,当上位机打开此USB设备后默认置位usb_cdc_handler *cdc结构体的packet_receivepre_packet_send,并以此标志来在cdc_acm_data_receive函数中对usbd_ep_recev进行调用。自己的需求比较简单,所有直接修改cdc_acm_ctlx_out函数,如下所示:

static uint8_t cdc_acm_ctlx_out (usb_dev *udev)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->class_data[CDC_COM_INTERFACE];

    if (NO_CMD != udev->class_core->req_cmd) {
        cdc->packet_receive = 1U;
        cdc->pre_packet_send = 1U;

        udev->class_core->req_cmd = NO_CMD;

        /* 添加此行 */
        usbd_ep_recev(udev, CDC_OUT_EP, (uint8_t*)(cdc->data), USB_CDC_RX_LEN);
    }

    return USBD_OK;
}

        cdc_acm_data_out函数在收到主机发送的数据时会被调用。相比于官方demo中在死循环内处理USB消息,我们更倾向于在中断中处理,毕竟这样时效性更强!cdc_acm_data_out函数修改如下:

static void cdc_acm_data_out (usb_dev *udev, uint8_t ep_num)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->class_data[CDC_COM_INTERFACE];

    cdc->packet_receive = 1U;

    cdc->receive_length = udev->transc_out[ep_num].xfer_count;
    
    /* 添加自己的回调函数 */
    usbd_cdc_data_out_irq_callback(udev, ep_num);
}

        需要注意的是,自定义回调函数usbd_cdc_data_out_irq_callback的内容应尽可能高效,避免产生其他问题。

        最后,像编写串口中断一样编写usbd_cdc_data_out_irq_callback函数中的内容即可,如果有多个端点,需要分开处理:

void  usbd_cdc_data_out_irq_callback(usb_dev *udev, uint8_t ep_num)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->class_data[CDC_COM_INTERFACE];
    
    usbd_ep_recev(udev, CDC_OUT_EP, (uint8_t*)(cdc->data), USB_CDC_RX_LEN);
    if (ep_num == CDC_OUT_EP)
    {
        if ((usbd_cdc_recv.pos + cdc->receive_length) >= usbd_cdc_recv.size)
            usbd_cdc_recv.pos = 0;
        memcpy(&usbd_cdc_recv.buf[usbd_cdc_recv.pos], (uint8_t*)(cdc->data), cdc->receive_length);
        usbd_cdc_recv.pos += cdc->receive_length;

        #ifdef USE_USB_SHELL
        shell_irq(&shell_main, usbd_cdc_recv.buf[usbd_cdc_recv.pos-1]);
        #endif

    }
    
}

        在我的项目中,使用MobaXtern发送shell命令给单片机。所以上述代码将上位机发来的数据直接存入shell缓冲区内,并调用shell_irq函数对数据进行判断和解析。

 

物联沃分享整理
物联沃-IOTWORD物联网 » GD32F103移植USBD CDC教程

发表评论