GD32F3x0 USB CDC应用案例

2023-10-27

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 驱动实例

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

GD32F3x0 USB CDC应用案例 的相关文章

  • Chisel入门------Chisel的基本语法4

    概述 本节将具体的介绍集中常见的硬件电路 并探索如何使用Chisel语言进行描述 10 示例设计 10 1 FIFO Bufer 通过在写入端和读取端插入缓冲器可以解耦 常见的FIFO是先进先出buffer 其中empty信号和full信号
  • IE6 firefox2

    所以说 如果我们这样来写 head width 100px important width 70px IE 说 它是70px 因为IE 不认识 important 提升优先权 发现了重复定义width时候就按最后一个来显示Firefox说

随机推荐

  • Spring详解(0 控制反转和依赖注入 AOP简介 )

    目录 依赖注入 控制反转和依赖注入的关系 Spring中的依赖注入 AOP 面向切面编程 Spring 框架本身的四大原则 1 使用pojo进行轻量级和最小侵入式开发 2 通过依赖注入和基于接口编程实现松耦合 3 通过AOP和默认习惯进行声
  • Linux命令常见命令用例

    文章目录 常见命令 awk find Tcp抓包 常见命令 awk 筛选nginx日志时间大于5s cat access log sed s g awk 11 NF gt 5 gt out5s log 筛选nginx状态码等于200 cat
  • ETL工具模块的创建

    01 logging工具模块开发 掌握 日志记录的工具模块 作用 方便后续在 ETL 程序中记录日志 目标 当我们在项目的其他位置使用logging模块进行日志记录时 不需要进行配置或者只需要进行简单的配置即可使用 为了更方便的使用logg
  • 微信小程序本地存储(wx.setStorage)和(wx.setStorageSync)

    在微信小程序中 可以使用本地存储来保存一些数据比如用户状态 姓名 性别等 本地存储主要包括两种方式 缓存和本地数据存储 缓存 缓存是一种快速访问内存的临时存储机制 可以有效地提高应用程序的响应速度 在微信小程序中 可以使用wx setSto
  • 虚拟机与主机串口通信(主机与主机)

    简介 由于某些主机无串口接口 或者需要两台主机交互通信来实现串口收发端的调试 modubus通讯的调试 为方便起见 查阅资料后 特有此文 一 环境 硬件 x86 系统 本机 win7 64 Vmware ubuntu16 04 软件 Vir
  • 微信小程序学习笔记--商城案例(黑马教程)

    目录 起步 运行于小程序 Git 托管到码云 tabbar 创建tabBar 配置tabbar 导航条颜色 首页 网络配置 请求轮播图数据 渲染轮播图 配置小程序分包 点击轮播图 跳转详情页 全局添加弹窗方法 获取分类导航数据 渲染分类导航
  • numpy的文件存储 .npy .npz 文件

    将数组以二进制格式保存到磁盘 转自 https blog csdn net m0 37041325 article details 78006203 np load和np save是读写磁盘数组数据的两个主要函数 默认情况下 数组是以未压缩
  • 最近写了10篇Java技术博客【SQL和画图组件】

    1 Java获取SQL语句中的表名 2 Java SQL 解析器实践 3 Java SQL 格式化实践 4 Java 画图 画图组件jgraphx项目整体介绍 一 画图组件jgraphx项目导出实践 二 画图组件jgraphx项目连接线实践
  • 小程序开发:在登录时弹窗用户使用协议

    本次讲的是如何在用户打开小程序时候弹窗该小程序的使用协议 阅读确认后方可继续使用小程序 这一点的意义在于 目前小程序对于各个开放接口的使用限制更严格 使用开放接口获取用户信息需要添加使用的用途说明 那我们正好可以使用这一个使用协议弹窗来说明
  • K8s:二进制部署高可用K8s集群

    K8s二进制高可用部署 说在前面 本章相关代码及笔记地址 飞机票 Github Java超神之路 Java全生态技术学习笔记 一起超神吧 CSDN Java超神之路 Java全生态技术学习笔记 一起超神吧 前言 本文所有涉及软件包等文件均在
  • 禅道——安装教程

    禅道安装指南 前言 一 禅道的安装和配置 1 1 其他修改 前言 禅道 项目管理工具 管理软件开发的整个流程 一 禅道的安装和配置 安装 点这里 进入官网 点击下载 点进去之后选择自己相要的版本 这里只要不是最新版本都会稳定
  • Multisim实现555计时器模拟简易电子琴

    555计时器模拟简易电子琴 一 元器件介绍 二 原理分析 三 仿真实验 四 仿真错误 一 元器件介绍 这里用到的元器件有 DIgital power VCC 数字电源 频率计数器 XFC 示波器 XSC ground 数字地 Capacit
  • cmake建立自己的package

    通过cmake建立自己的package cmake提供了CMakePackageConfigHelpers来方便实现我们的需求 详细文章参考如下 1 https blog csdn net xiaoxiaozengz article det
  • go协程、管道

    请感受一下协程的强大 使用了管道序列 package main import fmt time 创建一个管道 用于写入数据 func writeChantest writeChan chan int for i 0 i lt 20 i wr
  • ISO 26262:保障驾驶安全的汽车功能安全标准

    来源 中豪认证 随着汽车科技的迅猛发展 越来越多的电子系统和功能被引入汽车中 为驾驶体验和安全性带来了巨大的改进 然而 这些复杂的电子系统也带来了潜在的风险和安全挑战 为了确保现代汽车在各种情况下的安全性 国际标准化组织于2011年发布了I
  • 实现浏览页面时校验用户是否已经完成登录的功能

    一 实现原理 实现步骤 1 创建自定义过滤器LoginWebFilter java 2 在启动类上加入注解 ServletComponentScan 用来扫描web相关的注解 3 完善过滤器的处理逻辑 二 代码实现 首先在main java
  • 关于Xilinx下载器驱动安装及常见问题解决方法

    PC操作系统平台 Win7x64 ISE14 4 ISE14 7 下载器工作状态指示灯说明 如果Xilinx的下载器与电脑连接之后 下载器上面的指示灯不亮 说明PC上安装的下载器驱动有问题或者是下载器坏掉了 如果下载器与电脑连接之后 并与开
  • CTFshow 每日一练

    一 web签到题 打开链接 查看源码 利用base64解码得到flag 二 web2 看到有提示 SQL注入 先试着使用万能密码登陆 发现有回显 直接sql注入 or 1 1 order by 3 发现到4时不回显 开始爆库名 看看哪个位置
  • 2、ubuntu18.04安装cmake

    本文以安装cmake3 18 0为例 1 获取安装包 wget https cmake org files v3 18 cmake 3 18 0 Linux x86 64 tar gz 2 解压压缩包 tar zxvf cmake 3 18
  • GD32F3x0 USB CDC应用案例

    GD32F3x0 USB CDC应用 本文有点长 描述了从0开始移植驱动到应用的过程和思路 准备工作 因项目需求这两天需要做个USB的虚拟COM口发卡器 实现双向通讯 由于功能较为简单我们选择GD32F350来开发 先跑跑官方例程 GD32