当前位置: 首页 > news >正文

CH585 高速 USB模拟 CDC串口应用示例

CH585 高速USB 模拟CDC串口应用   ...... 矜辰所致

前言

上一篇文章我们介绍了 CH585 串口的使用,里面提供了一个一般应用的示例,正好在那个示例基础上有了一个新的需求,需要使用 高速 USB 模拟 CDC 串口实现串口数据交互功能。

所以本文我们就来讲一讲如何使用 CH585 的高速 USB 模拟 CDC串口进行数据交互。

相关文章:
CH58x/CH59x 蓝牙芯片 UART 使用
.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!

目录

  • 前言
  • 一、 基础知识
    • 1.1 什么是 CDC 串口
    • 1.2 官方示例
  • 二、 CDC串口应用
    • 2.1 CDC 串口数据收发实现
      • 2.1.1 端点号的确定
      • 2.1.2 数据交互的建立
      • 2.1.3 收发中断处理
    • 2.2 一般应用
      • 2.2.1 基本框架
      • 2.2.2 接收处理
      • 2.2.3 没有标志结尾数据接收
      • 2.2.4 发送
      • 2.2.5 效果图
  • 结语

一、 基础知识

我的博客还从来没有写过 USB 相关内容,详细的 USB 说明我应该会在后面单独写文章去说明,我们本文只做基本的简单说明。

1.1 什么是 CDC 串口

首先我们需要知道什么是 CDC:

CDC — USB Communications Device Class , “通信设备类”,是 USB 官方定义的一套 “让 USB 设备表现得像串口/网卡/调制解调器” 的 标准协议模板。

说直白点,CDC 就是 USB 里的 “串口替身”——
按规范实现 ➜ 电脑免驱 ➜ 打开就是 COM 口。

USB 组织把常见功能做成 统一模板,称为不同的类:

  • HID 类 → 键盘、鼠标
  • MSC 类 → U 盘
  • CDC 类 → 虚拟串口、虚拟网卡、调制解调器

所以 CDC串口就是 :用 USB 协议模拟的串口 。

CDC串口没有波特率误差,没有电平转换,一根 USB 线 就能同时完成 数据 + 调试 + 供电。

1.2 官方示例

在官方的 EVT 包中,提供了USB 模拟 CDC 串口的示例 SimulateCDC, 如下:

在这里插入图片描述

示例应用层代码框架如下:

在这里插入图片描述

示例实现了一个透传的效果,从 CDC 串口接收数据发送到 UART2 输出,从 UART2 接收数据发送到 CDC 串口输出,测试效果如下图:

在这里插入图片描述

CDC 串口是可以设置为波特率的,但是要保证和 UART2 设置的波特率一样才能正常收发,只要 UART2 的波特率在合理的范围内(在文章 CH58x/CH59x 蓝牙芯片 UART 使用 中有提到过波特率过高的问题)。

提到了可以任意波特率,那就顺带提一下如何实现 UART2 也可以任意设置,我们知道 在 UART2 初始化的时候波特率是固定的,但是我们测试下来是可以修改的,这里实现的部分就在 ch585_usbhs_device.c 文件中,当 if( USBHS_SetupReqCode == CDC_SET_LINE_CODING ) 的时候,会对 UART2 的串口重新初始化:

在这里插入图片描述

当 PC 端 打开串口助手、修改波特率/数据位/停止位 时,主机会下发一条 SET_LINE_CODING 请求,7 字节数据里带新的串口参数,在这里把这 7 字节保存,并按新参数重新初始化物理串口 。

这是为了实现示例透传到 串口 2 需要实现的步骤,如果我们直接使用 CDC 串口做数据收发,这里面可以不需要。

整个示例的实现,其实就是在主循环中一直查询收发:

while(1){UART2_DataRx_Deal( );UART2_DataTx_Deal( );}

本文重点不在于分析例程整体是如何实现的,只会在本文应用需要用到的知识点部分做必要的探讨,比如下面会讨论示例中如何建立数据交互这一条通路的。

二、 CDC串口应用

介绍完基本示例,回到我们想要实现的应用上:

把 CDC串口当成普通串口使用,串口接收数据指令,然后程序中处理不同的数据。最好的话能够通过 CDC 串口再打印结果(同时当成 DEBUG 串口)。

要实现上面的应用,我们必须要知道,CDC 串口的接收数据和发送数据是在哪里实现的。

2.1 CDC 串口数据收发实现

在实例中的 ch585_usbhs_device.c 里,CDC 的数据收发全部在 USB2_DEVICE_IRQHandler() 的 端点2(EP2)分支里实现的。

2.1.1 端点号的确定

先说说示例中为什么是 EP2 ?

决定使用 EP2 做为 CDC 串口数据收发的端点是再在USB 描述符里面确定的。

usb_desc.c 文件中,高速 USB 的设备描述符MyCfgDescr_HS 定义如下:

在这里插入图片描述

上图圈出的部分就确定了USB 的 EP2 为数据的收发端点,其中描述符详细的解释如下:

在这里插入图片描述

额外加一个知识点,上面的描述符第二个字节为bDescriptorType ,它有的类型如下:

宏定义名(可选)含义
0x01USB_DESCR_TYP_DEVICE设备描述符
0x02USB_DESCR_TYP_CONFIG配置描述符
0x03USB_DESCR_TYP_STRING字符串描述符
0x04USB_DESCR_TYP_INTERFACE接口描述符
0x05USB_DESCR_TYP_ENDPOINT端点描述符
0x06USB_DESCR_TYP_DEVICE_QUALIFIER设备限定描述符(高速用)
0x07USB_DESCR_TYP_OTHER_SPEED_CONFIG其他速率配置
0x08USB_DESCR_TYP_INTERFACE_POWER接口功耗(已废弃)
0x0FUSB_DESCR_TYP_BOSBOS(USB 3.0 才用)

2.1.2 数据交互的建立

上面的描述符是让主机知道 USB 设备的 EP2 是 CDC 数据口。

那我们还需要让 USB 控制器知道收到数据放在哪里,发送数据读哪里。

这部分的确定是在工程中ch585_usbhs_device.c 文件里面的函数USBHS_Device_Endp_Init 里实现的:


__attribute__ ((aligned(4))) uint8_t  UART2_Tx_Buf[ UART_REV_BUFFLEN ];  /* Serial port 2 transmit data buffer */
__attribute__ ((aligned(4))) uint8_t  UART2_Rx_Buf[ UART_REV_BUFFLEN ];  /* Serial port 2 receive data buffer *//..../R32_U2EP2_RX_DMA = (uint32_t)(uint8_t *)&UART2_Tx_Buf[ 0 ]; //PC 下发 → 进串口 TX 缓存R32_U2EP2_TX_DMA = (uint32_t)(uint8_t *)USBHS_EP2_Tx_Buf;   // 串口 RX 缓存 → 上传给 PCR32_U2EP3_TX_DMA = (uint32_t)(uint8_t *)USBHS_EP3_Tx_Buf;  // EP3 状态口 .../..../

上面的代码实现了端点 DMA 绑定,告诉硬件 buffer 地址。

示例工程在收发数据的时候,使用了 DMA ,所以会自动进行,每次都发完成以后都会触发中断,我们还需要在中断中处理好标志位。

2.1.3 收发中断处理

上面已经把整条通路建立好了,最后只需要处理接收到的数据,示例中虽然是 DMA 自动处理的,也需要在中断中处理一下标志位,如果是我们自己处理数据,就和标志为一并在中断中处理了。

这部分是在工程中ch585_usbhs_device.c 文件里面的函数USB2_DEVICE_IRQHandler 里实现的,在这段代码中:

if( !(intst & USBHS_UDIS_EP_DIR) )   // =0 → OUT/SETUP 令牌/* 主机下发数据 或 控制请求 */case   DEF_UEP2://收到数据处理...
else                                  // =1 → IN 令牌/* 主机请求上传数据 */case   DEF_UEP2://发送数据完成的中断处理,告诉USB可以发送下一包

上面代码中 USBHS_UDIS_EP_DIR 是 USB 中断状态寄存器 里的 方向标志:

0 = 主机发起 OUT(包括 SETUP 和批量 OUT)
1 = 主机发起 IN(批量/中断/控制 IN)

对于 IN 和 OUT,是在主机的角度说明的:

OUT 令牌 = 主机 向外发 → 设备收到,USB 收到数据
IN 令牌 = 主机 向内收 ← 设备发出

在这里插入图片描述

上图中下面的 case DEF_UEP2: 为发送完成中断,是在 USB 发送完成了一包数据以后会产生的中断。

我们发送数据需要使用代码中写的uint8_t USBHS_Endp_DataUp( uint8_t endp, uint8_t *pbuf, uint16_t len, uint8_t mod ) 函数,简单的示例如下:

数据发送

/* 仅等上一次发完(不会永久阻塞) */if (USBHS_Endp_Busy[DEF_UEP2])return;USBHS_Endp_DataUp(DEF_UEP2, buf, len, DEF_UEP_CPY_LOAD);

好了,到这里,我们应该完全知道了 CDC 串口数据收发在示例中是如何实现的。

2.2 一般应用

接下来我们就可以实现我们的一般应用了。

在文章 CH58x/CH59x 蓝牙芯片 UART 使用 中《 三、 一般应用(接收不定长度数据) 》中有一个基本框架,实际上我们完全可以全部搬运过来,用一样的缓存区,然后主函数循环中数据处理也完全一样。

2.2.1 基本框架

串口缓冲区:

#define  cmd_max_len   100typedef struct
{uint8_t rx_buff[cmd_max_len];uint8_t rx_count;uint8_t rx_state;uint8_t rx_back;
}uart_rx_buff;

数据处理:

void app_uart_process(void)
{UINT32 irq_status;if(cmd_uart.rx_state){SYS_DisableAllIrq(&irq_status);// 数据处理,这里通过开启任务处理//UART0_SendString( cmd_uart.rx_buff, cmd_uart.rx_count);tmos_start_task(rfTaskID, CMD_PROCESS_EVENT, 2);cmd_uart.rx_count = 0;cmd_uart.rx_state = FALSE;SYS_RecoverIrq(irq_status);    }
}

主循环调用:

void Main_Circulation()
{while(1){TMOS_SystemProcess();app_uart_process();}
}

清除缓存不需要用到。

移植的时候,我们直接把下面 4个文件拷贝到自己的工程 里面:

在这里插入图片描述

初始化,直接按照例程调用:

    CH58x_BLEInit();HAL_Init();RFRole_Init();cmd_uart_init();
#ifdef USE_CDC_UARTUSBHS_Device_Init(ENABLE);PFIC_EnableIRQ( USB2_DEVICE_IRQn );
#endifMain_Circulation();

接下来还需要进行数据的收发处理。

2.2.2 接收处理

我们还需要处理下接收函数ch585_usbhs_device.c 文件,首先把示例中关于 UART2 的相关代码去掉,换成自己的代码。

我们先定义一个自己的收发缓存:

__attribute__((aligned(4))) static uint8_t  EP2_OUT_Buf[DEF_USB_EP2_HS_SIZE];   // 独立 OUT 缓冲
__attribute__((aligned(4))) static uint8_t  EP2_IN_Buf [DEF_USB_EP2_HS_SIZE];   // 独立 IN  缓冲(回传用)

然后在USBHS_Device_Endp_Init 函数中,把缓存用我们自己定义的缓存:

/* OUT 用独立缓冲,不再映射 UART2_Tx_Buf */R32_U2EP2_RX_DMA = (uint32_t)EP2_OUT_Buf;R32_U2EP2_TX_DMA = (uint32_t)EP2_IN_Buf;

然后修改if( USBHS_SetupReqCode == CDC_SET_LINE_CODING ) 里面的内容( 这里是控制波特率修改后 CDC 串口能否再次使用的地方,如果把里面的代码全部去掉,在 115200 是没问题,但是无法切换其他波特率 ):

/* Non-standard request end-point 0 Data download */
if( USBHS_SetupReqCode == CDC_SET_LINE_CODING ){/* save bauds */baudrate = USBHS_EP0_Buf[ 0 ];baudrate += ((uint32_t)USBHS_EP0_Buf[ 1 ] << 8 );baudrate += ((uint32_t)USBHS_EP0_Buf[ 2 ] << 16 );baudrate += ((uint32_t)USBHS_EP0_Buf[ 3 ] << 24 );// R32_U2EP2_RX_DMA = (uint32_t)(uint8_t *)&UART2_Tx_Buf[ 0 ];R8_U2EP2_RX_CTRL &= ~USBHS_UEP_R_RES_MASK;R8_U2EP2_RX_CTRL |= USBHS_UEP_R_RES_ACK;}

关键的数据处理,我们在DEF_UEP2的接收中断中处理。

其实和 UART 中断处理一样的原理:

case   DEF_UEP2:/* Endp download */uint16_t len = R16_U2EP2_RX_LEN;uint8_t *p   = EP2_OUT_Buf;          // 现在用独立缓冲for(uint16_t i = 0; i < len; i++){uint8_t byte = p[i];if(cmd_uart.rx_count >= cmd_max_len - 1) continue; // 防溢出cmd_uart.rx_buff[cmd_uart.rx_count++] = byte;if(byte == '\n'){/* 去掉末尾 \r 如果有 */if(cmd_uart.rx_count>1 && cmd_uart.rx_buff[cmd_uart.rx_count-2]=='\r')cmd_uart.rx_count--;cmd_uart.rx_buff[cmd_uart.rx_count] = 0;cmd_uart.rx_state = TRUE;          // 通知帧到}}/* 重新使能下一包 OUT */R8_U2EP2_RX_CTRL ^= USBHS_UEP_R_TOG_DATA1;R8_U2EP2_RX_CTRL = (R8_U2EP2_RX_CTRL & ~USBHS_UEP_R_RES_MASK) | USBHS_UEP_R_RES_ACK;R8_U2EP2_RX_CTRL &= ~USBHS_UEP_R_DONE;break;

要编译通过,还要去掉一下 DEF_UEP2的发送完成中断中的一个标志位:

在这里插入图片描述

到这里,我们已经实现了类似我们上文串口 3 一样的功能,而且 App 程序完全不用改动 !

2.2.3 没有标志结尾数据接收

我们上面的数据处理是一定要带回车换行的,如果我们接收的数据没有明确的标志位呢怎么处理呢?

在使用物理串口的时候,我们自带数据超时中断,但是 USB 模拟CDC 串口可没有超时中断。我们需要自己实现超时处理。

这里提供一个思路,我们可以收到一个字节数据就开启一个在超时时间以后触发的 TMOS 事件,如果事件触发了,就表示收到了一帧数据,示意代码如下:

#define CDC_RX_TOUT_MS   10          // 10 ms 没新字节就视为帧结束// EP2 OUT 中断里
case DEF_UEP2:// 数据处理...数据放到 cmd_uart...// 重启定时器tmos_stop_task(rfTaskID, CDC_RX_TOUT_EVENT);tmos_start_task(rfTaskID, CDC_RX_TOUT_EVENT, MS1_TO_SYSTEM_TIME(CDC_RX_TOUT_MS));break;// 超时任务
if (events & CDC_RX_TOUT_EVENT)
{if (cmd_uart.rx_count)           // 缓冲区有数据就当成一帧{cmd_uart.rx_buff[cmd_uart.rx_count] = 0;cmd_uart.rx_state = TRUE; //置位收到一帧数据标志位,然后自行处理。//tmos_set_event(rfTaskID, CMD_PROCESS_EVENT);}return events ^ CDC_RX_TOUT_EVENT;
}

2.2.4 发送

本来实现了接收,想想干脆做成收发一体,就是把 CDC 串口做成带 DEBUG 输出的,我们在上面已经讲过 CDC 发送使用示例中的 USBHS_Endp_DataUp 即可。

这里我们直接自己实现了一个 print 函数:

void cdc_print(const char *fmt, ...)
{static uint8_t buf[128];          // 临时缓冲va_list ap;va_start(ap, fmt);int len = vsnprintf((char *)buf, sizeof(buf), fmt, ap);va_end(ap);if (len <= 0 || len > 64)      // 超限立刻丢弃,自己控制,可以使用DEF_USB_EP2_HS_SIZEreturn;/* 仅等上一次发完(不会永久阻塞) */if (USBHS_Endp_Busy[DEF_UEP2])return;USBHS_Endp_DataUp(DEF_UEP2, buf, len, DEF_UEP_CPY_LOAD);
}

想要输出直接调用即可,如下图:

在这里插入图片描述

2.2.5 效果图

我们来看一下最后整体的效果:

在这里插入图片描述

目前中途是不能修改波特率的,只在插上去上电第一次,点击选择波特率的时候确定本次使用的波特率,如果想改,需要复位。

OK! 完成,下载,指令下发,DEBUG 打印,只需要一根 USB 线搞定 !

结语

本文我们演示了使用 USB 模拟的 CDC 串口实现了串口数据接收处理的应用方法。

对于 USB 协议相关的内容我们并没有深入详细的介绍,只了解一下我们需要搞清的 描述符,但并不影响我们的应用,本文应用的关键点在于我们得知道 USB CDC串口的数据接收和发送的整个流程通路,相信大家通过本文都能很好的使用 CH585 USB 模拟的 CDC 串口。

好了,本文就到这里,谢谢大家!

http://www.dtcms.com/a/564635.html

相关文章:

  • 2024/07 JLPT听力原文 问题四
  • 【AAOS】【源码分析】Car Location服务(二)- NMEA 数据
  • 如何建立国外网站搜索引擎优化岗位
  • 怎么建立网站网址在线做网站需要什么
  • https 可以访问 8866端口吗
  • python excel转为jsonl 格式 和 jsonl格式转为excel
  • docker中使用SSL证书实现前后端Https
  • IDE/编码代理架构与 Cursor 相关研究(汇总)
  • Multi-Stride Predictive RNG:革命性的可控随机数生成算法
  • Let’s Encrypt 证书申请与多服务器 HTTPS 配置指南
  • 艺术名画网站怎么建设多姿wordpress
  • R 绘图 - 散点图
  • 使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
  • 末备案网站如何做cdnwordpress填写
  • 有做网站维护的做垂直行业网站利润分析
  • BSC 链代币加池全教程:从发币到流动性捆绑买入
  • AOI在钢铁行业检测领域中的应用
  • 【Solidity 从入门到精通】第1章 区块链与智能合约的基本原理
  • 股指期货持仓量增加说明什么?
  • 对商家而言网站建设的好处泰州市做网站
  • 深入探讨HarmonyOS中ListItem的滑动操作实现与优化
  • Tomcat SSL连接问题解决方案
  • ProtoBuf语法揭秘:探秘编译魔法与性能优化策略,解锁多层级选项配置的底层奥秘
  • StarRocks数据仓库
  • 玩转Rust高级应用 结合使用 future、任务和线程,如何进行任务内并发(intratask concurrency)支持
  • 移动端商城网站开发网站建设+荆州
  • 2G2核服务器安装ES 7X版本
  • 前端基础之《React(7)—webpack简介-ESLint集成》
  • 《 Linux 修炼全景指南: 五 》Linux 文件权限与用户管理全指南:构筑系统安全的第一道防线
  • 生命周期评价(LCA):理论、方法与工具、典型案例全解析