GD32F3x0 USB CDC应用
本文有点长,描述了从0开始移植驱动到应用的过程和思路

准备工作:

因项目需求这两天需要做个USB的虚拟COM口发卡器,实现双向通讯,由于功能较为简单我们选择GD32F350来开发。
先跑跑官方例程:

GD32F3x0_Firmware_Library_V2.2.1\Examples\USBFS\USB_Device\cdc_acm

安装GD32 USB驱动:

USB_Virtual_Com_Port_Driver_v2.0.2.2673

我这里采用keil MDK5来开发,keil的安装这里省略。
安装GD32的DFP包:

https://www.gd32mcu.com/cn/download?kw=GD32F3x&lan=cn
GD32F3x0 AddOn 3.0.0

由于我是用的MDK5,例程采用MDK4,这里我们修改工程后缀

\Examples\USBFS\USB_Device\cdc_acm\MDK-ARM\cdc_acm.uvproj
复制 cdc_acm.uvproj,修改为 cdc_acm.uvprojx

打开项目后是无法编译的(原因MDK5是采用CMSIS驱动),按以下方法添加CMSIS

接下来就可以正常编译和下载了
运行起来,能正常打开COM口,发数据能正常接收,验证板子和例程都没问题。

阅读代码:

阅读例程,不难发现CDC用到了USB类文件

\Firmware\GD32F3x0_usbfs_library\device\class\cdc\Source\cdc_acm_core.c

正式开始阅读:找到app.c main()函数

int main(void)
{
    usb_rcu_config();	//初始化时钟
    usb_timer_init();	//初始化定时器器,USB需要用到定时器做精准延时
    usbd_init(&cdc_acm, USB_CORE_ENUM_FS, &cdc_desc, &cdc_class);	//初始化USB
    usb_intr_config();	//初始化中断
    while(1) {    /* main loop */
        if(USBD_CONFIGURED == cdc_acm.dev.cur_status) {	//检查USB是否准备就续
            if(0U == cdc_acm_check_ready(&cdc_acm)) {	//检查数据是否准备好,当为0时说明有数据需要接
                cdc_acm_data_receive(&cdc_acm);			//接收数据,这里不难发现我们不知道接收到的数据在哪里
            } else {
                cdc_acm_data_send(&cdc_acm);			//发送数据,这里也不知道发的数据在哪里,\
                											或者说我们想法自己的数据该 怎么发?
            }}}}	//为了节约点文章篇幅,我们改改格式

接下来我们把收发的三个函数贴上来

uint8_t cdc_acm_check_ready(usb_dev *udev)	//检查数据是否就绪
{
    if (NULL != udev->dev.class_data[CDC_COM_INTERFACE]) {
        usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];
        if ((1U == cdc->packet_receive) && (1U == cdc->packet_sent)) {//这里发现接收和发送都为1才就续--为什么?
            return 0U;
        }
    }
    return 1U;
}
void cdc_acm_data_receive (usb_dev *udev)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];
    cdc->packet_receive = 0U;	//接收数据前已经把这两个标识置为0了
    cdc->packet_sent = 0U;
    
    //不难发现这个是从数据out端点读数据,数据存放在cdc->data中,每个包最大接收64Byte。
    //实际收到多少数据我们知道吗?  -》NO,这里先不管吧,先大致过一下程序
    usbd_ep_recev(udev, CDC_DATA_OUT_EP, (uint8_t*)(cdc->data), USB_CDC_DATA_PACKET_SIZE);//这里才是接收数据
}
void cdc_acm_data_send (usb_dev *udev)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];
    if (0U != cdc->receive_length) {	//这个不是接收的数据长度吗?原来在这里
        cdc->packet_sent = 0U;			//发送数据前这个标识置0了
         //原理在这里把接收到的数据直接发给上位机了,大致看懂了怎么收发的。
        usbd_ep_send (udev, CDC_DATA_IN_EP, (uint8_t*)(cdc->data), cdc->receive_length);	//发数据到上位机
    
        cdc->receive_length = 0U;
    }
}

分析代码:

返回main,看看这个例程是采用轮询的方法收发数据,并且例程并没有考虑实用性,用户收发数据都要去cdc->data里面找,关键是cdc->data在哪里呀?我们看看收发函数,找到下面这行代码:

usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];

如果每次都要这样去引用,是不是太麻烦,能按我往日做UART的习惯改改例程吗?贴上习惯的代码看看

/*
*	我习惯用中断回调方式接收数据,这样初始化完了就可以不管他了;
*	发送我喜欢阻塞方式,直接调用一个发送函数,传入数据就好了
*/
/* 接收回调 */
void uartCallback(uint8_t *pData, uint16_t size)
{
	uint8_t *p;
	if( pData != NULL && size > 0){
		p = (uint8_t *)osMalloc(size);	//申请内存
		if(NULL == p)
			return;		
		memcpy(p, pData, size);		//复制数据	
		if(sendMsgToTask(gProTaskId, MSG_RECE_DATA, p, size) == false)	//发送消息给应用层
			osFree(p);
	}
}
/* 阻塞发送 */
void uartWrite(uint32_t *uart, uint8_t *pBuf, uint16_t size);

修改代码:

先在《cdc_acm_core.c》每个函数下添加打印信息(UART初始化省略)

/*
*	添加打印信息,省略原代码详细部分
*/
uint8_t cdc_acm_check_ready(usb_dev *udev){
	...
	//USB_DUBG("[cdc]check_ready\n");	//频繁打印先屏蔽
}

void cdc_acm_data_send (usb_dev *udev){
	...
	//USB_DUBG("[cdc]data_send:%d\n",cdc->receive_length);//频繁打印先屏蔽
}

void cdc_acm_data_receive (usb_dev *udev){
	...
	//USB_DUBG("[cdc]data_receive\n");//频繁打印先屏蔽
}

uint8_t cdc_acm_req (usb_dev *udev, usb_req *req){
	...
	USB_DUBG("[cdc]acm_req\n");//打开COM口会多次打印
}

static uint8_t cdc_ctlx_out (usb_dev *udev){
	...
	USB_DUBG("[cdc]ctlx_out:%d\n",cdc->line_coding.dwDTERate);	//打开COM口会多次打印
}

static uint8_t cdc_acm_in (usb_dev *udev, uint8_t ep_num){
	...
	USB_DUBG("[cdc]acm_in\n");	//上位机每发一次数据,会打印一次
}

static uint8_t cdc_acm_out (usb_dev *udev, uint8_t ep_num){
	...
	USB_DUBG("[cdc]acm_out\n");	//上位机每发一次数据,会打印一次
}

通过打印信息分析代码:
初始化或者打开COM我们先不管,重点看看收发数据的打印。
代码定位到cdc_acm_in(),cdc_acm_out ();这两个函数,我们把完整代码贴上来。

static uint8_t cdc_acm_in (usb_dev *udev, uint8_t ep_num)
{
    usb_transc *transc = &udev->dev.transc_in[EP_ID(ep_num)];
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];
    if ((0U == transc->xfer_len % transc->max_len) && (0U != transc->xfer_len)) {
        usbd_ep_send (udev, ep_num, NULL, 0U);
    } else {
        cdc->packet_sent = 1U;
    }
    USB_DUBG("[cdc]acm_in\n");	//上位机每发一次数据,会打印一次
    return USBD_OK;
}
static uint8_t cdc_acm_out (usb_dev *udev, uint8_t ep_num)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];
    cdc->packet_receive = 1U;
    //数据长度原来在这里,说明程序到这里数据应该已经就绪了
    cdc->receive_length = ((usb_core_driver *)udev)->dev.transc_out[ep_num].xfer_count;	
    USB_DUBG("[cdc]acm_out\n");	//上位机每发一次数据,会打印一次
    return USBD_OK;
}

我们发现cdc_acm_out ()函数中已经就绪了,我们是不是可以在这里获取数据,并回调给应用层呢?
代码修改如下:

static uint8_t cdc_acm_out (usb_dev *udev, uint8_t ep_num)
{
	uint8_t i;
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];
    cdc->packet_receive = 1U;
    cdc->receive_length = ((usb_core_driver *)udev)->dev.transc_out[ep_num].xfer_count;
	usbd_ep_recev(udev, ep_num, (uint8_t*)(cdc->data), USB_CDC_DATA_PACKET_SIZE);	
	if(gDoutCb)
		gDoutCb(ep_num, cdc->data, cdc->receive_length);
	USB_DUBG("[cdc]acm_out\n");
    return USBD_OK;
}

然后屏蔽main()中接收函数,运行看看结果,发现代码不跑了,甚至只要屏蔽main()任意一行代码,结果都一样。
这时陷入陷阱,这也是GD32例程不够完善和人性化的地方,开始拼命网上找答案,骚扰FAE,都无果。
甚至FAE说只能做到这个样子,我感觉不应该,这样的东西怎么能用,还是静下行来阅读代码吧。

在初始化函数发现以下代码:

static uint8_t cdc_acm_init (usb_dev *udev, uint8_t config_index)
{
	..
    cdc_handler.packet_receive = 1U;
    cdc_handler.packet_sent = 1U;
    ...
}

初始化后上面两个状态都置为真,那个cdc_acm_check_ready()肯定返回了0,执行了一次cdc_acm_data_receive();
是不是说初始化后必须cdc_acm_data_receive()一次呢,而cdc_acm_data_receive()的关键是usbd_ep_recev();
usbd_ep_recev()我的理解是会读一次FIFO,从而清空FIFO,从而有空间接收后面的数据(至于为什么FIFO有数据或者满了,不做研究),既然如此,我是否可以在初始化函数里直接usbd_ep_recev()一次呢?修改代码如下:

static uint8_t cdc_acm_init (usb_dev *udev, uint8_t config_index)
{
	...	//减少文章篇幅省略些内容
    /* initialize CDC handler structure */
    cdc_handler.packet_receive = 1U;
    cdc_handler.packet_sent = 1U;
    cdc_handler.receive_length = 0U;
	...
    udev->dev.class_data[CDC_COM_INTERFACE] = (void *)&cdc_handler;

	//读一次数据,清空FIF0
	usbd_ep_recev(udev, CDC_DATA_OUT_EP, (uint8_t*)(cdc->data), USB_CDC_DATA_PACKET_SIZE);
	USB_DUBG("[cdc]acm_init\n");	
    return USBD_OK;
}

再次运行,“[cdc]acm_out”,能正常打印了;先泡杯白开水,庆祝一下。
接下来就是大刀阔斧的改代码,加入应用程序,将接收的数据打印出来,核对正确性。

/*
*	发送函数,按习惯修改如下:
*	中间碰上点小问题,就是为什么USB一个包只能发64Byte,请大家自行百度 
*/
void cdc_acm_data_send (usb_dev *udev,uint8_t *data, uint32_t size)
{
	uint32_t len = 0;
	
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];
	
	cdc->receive_length = size;
    if (0U != cdc->receive_length) {

		if(cdc->receive_length <= USB_CDC_RX_LEN){
			memcpy(cdc->data, data, size);
			cdc->packet_sent = 0U;	
			usbd_ep_send (udev, CDC_DATA_IN_EP, (uint8_t*)(cdc->data), cdc->receive_length);
			while(!cdc->packet_sent){
				__NOP();
			}
		}else{
			do{
				cdc->packet_sent = 0U;	
				if(cdc->receive_length >= USB_CDC_RX_LEN){
					memcpy(cdc->data , data + (size - cdc->receive_length), USB_CDC_RX_LEN);
					
					usbd_ep_send(udev, CDC_DATA_IN_EP, (uint8_t*)(cdc->data), USB_CDC_RX_LEN);
					cdc->receive_length -= USB_CDC_RX_LEN;					
				}else{
					memcpy(cdc->data , data + (size - cdc->receive_length), cdc->receive_length);
					
					usbd_ep_send(udev, CDC_DATA_IN_EP, (uint8_t*)(cdc->data), cdc->receive_length);
					cdc->receive_length = 0;	
				}
				while(!cdc->packet_sent){//等待发送结束
					__NOP();
				}				
				//USB_DUBG("[cdc]data_send:%d\n",cdc->receive_length);					
			}while(cdc->receive_length);				
		}	
    }else{
		usbd_ep_send(udev, CDC_DATA_IN_EP, (uint8_t*)(cdc->data), 0);
	}	
}

经过多次测试,又发现了一点问题,设备第一次上电,打开COM口,第一包数据长度正确,但数据值不正确
又回去测例程(修改部分代码,把接收的数据打印出来),发现没问题。
到底是什么原因,我也分析不出来,问FAE,回不知道原因。
好在FAE提供了一个方案:能否自己想办法,丢弃第一个包。
按FAE思路,再看看打印信息,打开COM时不是有很多次cdc_acm_req()和cdc_ctlx_out()的打印吗?是否可能在打开COM口时FIFO又产生了些数据,导致异常?是否在cdc_ctlx_out()中读一次数据可以解决呢?只猜是没用的,动手起来:

static uint8_t cdc_ctlx_out (usb_dev *udev)
{
    usb_cdc_handler *cdc = (usb_cdc_handler *)udev->dev.class_data[CDC_COM_INTERFACE];
    if (udev->dev.class_core->alter_set != NO_CMD) {
        ...
        udev->dev.class_core->alter_set = NO_CMD;
		if(gCtrlCb)					//添加控制命令回调函数
			gCtrlCb(CDC_CMD_EP, &cdc->line_coding);
		//初始化后第一次收不到数据,这里收到控制命令的时候,读一次数据可解决。
		usbd_ep_recev(udev, CDC_DATA_OUT_EP, (uint8_t*)(cdc->data), USB_CDC_DATA_PACKET_SIZE);
		//USB_DUBG("[cdc]ctlx_out:%d\n",cdc->line_coding.dwDTERate); //控制命令输出		
    }
    return USBD_OK;
}

测试代码,完美,收发1K以上的数据正常。
致此花费了2天时间终于把GD32F350 USB CDC驱动移植到自己的应用
总结:
造成移植难度那么大,和GD的例程关系很大
自己对USB确实不了解,也不打算深入研究,只要能应用就好。

驱动代码已经上传到CSDN
有需要请自行搜索 GD32F3x0 USB CDC 驱动实例

物联沃分享整理
物联沃-IOTWORD物联网 » GD32F3x0 USB CDC应用案例

发表评论