GD32F3x0 USB CDC应用案例
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应用案例相关推荐
- stm32f407 usb cdc设备无法启动问题
最新要做一个项目,要求基于STM32F407实现USB CDC设备,首先想到的就是直接用STM32CUBEMX工具来生成,OK,话不多说,直接上过程: RCC配置: Sys配置 USB_OTG_FS配 ...
- USB CDC 可变形参
控制台的三种连接方式: 1.IP网络 2.USB 3.UART 一:介绍USB CDC方式: 1.控制台配置如下: 2.USB Product ID 可以是:0x0000/0x5300/0x0238 ...
- 在进行USB CDC类开发时,无法发送64整数倍的数据(续)
1 前言 此文延续之前相同文章的话题,是对上篇文章的补充,之所以会有此文,主要是之前发现问题是在STM32F4上,解决方案也是基于CubeF4,但是,当相同问题出现在STM32F0上时,使用之前的代码 ...
- 在进行USB CDC类开发时,无法发送64整数倍的数据
1 前言 本文将基于STM32F4DISCOVERY板,介绍如何使用USB的CDC类进行开发,以及在开发过程中碰到发送64整数倍数据时会失败的问题分析及解决方案. 2 硬件介绍 在创建工程之前,我们首 ...
- TI CC2540 USB CDC Serial Port驱动安装失败原因及解决方法
TI CC2540 USB CDC Serial Port驱动安装失败原因及解决方法 参考文章: (1)TI CC2540 USB CDC Serial Port驱动安装失败原因及解决方法 (2)ht ...
- USB CDC从理论到实践
本文摘自ST官网的"USB CDC类入门培训".整理的内容是我能够看得懂的,认为比较实用的,记录下来,以便以后查阅,同时也把原文档中的笔误给更正了一下.若要看更详细的可以去ST技术 ...
- USB CDC 4G Module 调试问题总结
USB CDC 4G Module ESP32S2 自定义开发板 SIM7600C1 其他按照github USB CDC 4G Module 使用说明 确保硬件正确SIM卡正常 编译注意做好在4.4 ...
- linux cdc设备驱动,Linux下USB CDC ACM 驱动简析
一.硬件平台:TI AM335X 芯片 二.软件平台:Ubuntu 10.04 三.USB CDC ACM 驱动简介 USB的CDC类是USB通信设备类 (Communication Device C ...
- STM32 USB CDC 虚拟多串口
转自: http://www.stmcu.org.cn/module/forum/thread-613510-1-1.html 楼主 发表于 2017-9-28 22:30:04 | 只看该作者 | ...
最新文章
- 新一轮光伏电站产能过剩隐忧初显
- jmeter 正则获取参数集合和ForEach控制器结合使用(转)
- Android junit单元测试
- 10个人有9个答错,另外1个只对一半:数据库的锁,到底锁的是什么?
- 使用JDBC完成数据的增删改查
- JavaWeb之Servlet入门(一)
- 配置文件中符号报错,无法识别
- unzip命令找不到
- 【讨论】js对数组去重复值
- MSSQL2008如何关闭代码智能提示?
- 2020最新文本检测算法TextFuseNet
- 熊猫烧香病毒-源码学习
- 灵悟礼品网上专卖店——分析类似项目的优缺点
- Python线程安全的单例模式
- 服务器端请求伪造——SSRF
- 物联网商机发展空间无限、远大于互联网,物联网创业和项目也有坑——物联网避坑指南之1
- PDF打开后却不能编辑要怎么办?
- 图论入门(一),拓扑排序生成拓扑序列与Dijkstra求最短路
- 笔记本计算机屏幕亮度暗,笔记本屏幕暗,教您怎么解决
- 序列的运算、操作、函数/方法
热门文章
- Windows内核原理与实现之 NDIS(网络驱动程序接口规范)
- 门窗软件测试自学,AutoCAD 2014室内装潢设计完全自学手册[9787111482352]
- 计算机硬件 平面图,看懂室内平面图画法、平面配置原则,一次了解常见的平面图种类!...
- CE进阶操作--自带小游戏TutorialGame的修改方法
- Allegro PCB Design GXL (legacy) 从dxf文件中导入板框
- git did not exit cleanly (exit code 128)已解决
- TortoiseGit(小乌龟) git did not exit cleanly (exit code 1)
- MNIST导入图片数据集
- Daily Practice 5th:Educational Codeforces Round 120 (Rated for Div. 2)
- Python练习题——coffee