GD32F103移植USBD CDC教程
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_send和cdc_acm_data_receive这三个接口完成USB数据的收发。但这三个函数内仅通过判断usb_cdc_handler *cdc结构体的相关标志位来控制数据的发送与接收,因此忽略掉它们。
经过一番分析和测试,最终需要修改两个函数:cdc_acm_ctlx_out和cdc_acm_data_out
GD接收USB数据最终有效的是usbd_ep_recev函数。在cdc_acm_ctlx_out函数中,当上位机打开此USB设备后默认置位usb_cdc_handler *cdc结构体的packet_receive和pre_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函数对数据进行判断和解析。