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);   //初始化USBusb_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;//读一次数据,清空FIF0usbd_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 驱动实例

GD32F3x0 USB CDC应用案例相关推荐

  1. stm32f407 usb cdc设备无法启动问题

    最新要做一个项目,要求基于STM32F407实现USB CDC设备,首先想到的就是直接用STM32CUBEMX工具来生成,OK,话不多说,直接上过程: RCC配置: Sys配置 USB_OTG_FS配 ...

  2. USB CDC 可变形参

    控制台的三种连接方式: 1.IP网络 2.USB 3.UART 一:介绍USB CDC方式: 1.控制台配置如下: 2.USB Product ID 可以是:0x0000/0x5300/0x0238 ...

  3. 在进行USB CDC类开发时,无法发送64整数倍的数据(续)

    1 前言 此文延续之前相同文章的话题,是对上篇文章的补充,之所以会有此文,主要是之前发现问题是在STM32F4上,解决方案也是基于CubeF4,但是,当相同问题出现在STM32F0上时,使用之前的代码 ...

  4. 在进行USB CDC类开发时,无法发送64整数倍的数据

    1 前言 本文将基于STM32F4DISCOVERY板,介绍如何使用USB的CDC类进行开发,以及在开发过程中碰到发送64整数倍数据时会失败的问题分析及解决方案. 2 硬件介绍 在创建工程之前,我们首 ...

  5. TI CC2540 USB CDC Serial Port驱动安装失败原因及解决方法

    TI CC2540 USB CDC Serial Port驱动安装失败原因及解决方法 参考文章: (1)TI CC2540 USB CDC Serial Port驱动安装失败原因及解决方法 (2)ht ...

  6. USB CDC从理论到实践

    本文摘自ST官网的"USB CDC类入门培训".整理的内容是我能够看得懂的,认为比较实用的,记录下来,以便以后查阅,同时也把原文档中的笔误给更正了一下.若要看更详细的可以去ST技术 ...

  7. USB CDC 4G Module 调试问题总结

    USB CDC 4G Module ESP32S2 自定义开发板 SIM7600C1 其他按照github USB CDC 4G Module 使用说明 确保硬件正确SIM卡正常 编译注意做好在4.4 ...

  8. linux cdc设备驱动,Linux下USB CDC ACM 驱动简析

    一.硬件平台:TI AM335X 芯片 二.软件平台:Ubuntu 10.04 三.USB CDC ACM 驱动简介 USB的CDC类是USB通信设备类 (Communication Device C ...

  9. STM32 USB CDC 虚拟多串口

    转自: http://www.stmcu.org.cn/module/forum/thread-613510-1-1.html 楼主  发表于 2017-9-28 22:30:04 | 只看该作者 | ...

最新文章

  1. 新一轮光伏电站产能过剩隐忧初显
  2. jmeter 正则获取参数集合和ForEach控制器结合使用(转)
  3. Android junit单元测试
  4. 10个人有9个答错,另外1个只对一半:数据库的锁,到底锁的是什么?
  5. 使用JDBC完成数据的增删改查
  6. JavaWeb之Servlet入门(一)
  7. 配置文件中符号报错,无法识别
  8. unzip命令找不到
  9. 【讨论】js对数组去重复值
  10. MSSQL2008如何关闭代码智能提示?
  11. 2020最新文本检测算法TextFuseNet
  12. 熊猫烧香病毒-源码学习
  13. 灵悟礼品网上专卖店——分析类似项目的优缺点
  14. Python线程安全的单例模式
  15. 服务器端请求伪造——SSRF
  16. 物联网商机发展空间无限、远大于互联网,物联网创业和项目也有坑——物联网避坑指南之1
  17. PDF打开后却不能编辑要怎么办?
  18. 图论入门(一),拓扑排序生成拓扑序列与Dijkstra求最短路
  19. 笔记本计算机屏幕亮度暗,笔记本屏幕暗,教您怎么解决
  20. 序列的运算、操作、函数/方法

热门文章

  1. Windows内核原理与实现之 NDIS(网络驱动程序接口规范)
  2. 门窗软件测试自学,AutoCAD 2014室内装潢设计完全自学手册[9787111482352]
  3. 计算机硬件 平面图,看懂室内平面图画法、平面配置原则,一次了解常见的平面图种类!...
  4. CE进阶操作--自带小游戏TutorialGame的修改方法
  5. Allegro PCB Design GXL (legacy) 从dxf文件中导入板框
  6. git did not exit cleanly (exit code 128)已解决
  7. TortoiseGit(小乌龟) git did not exit cleanly (exit code 1)
  8. MNIST导入图片数据集
  9. Daily Practice 5th:Educational Codeforces Round 120 (Rated for Div. 2)
  10. Python练习题——coffee