CH585 高速USB 模拟CDC串口应用 ...... 矜辰所致
前言
上一篇文章我们介绍了 CH585 串口的使用,里面提供了一个一般应用的示例,正好在那个示例基础上有了一个新的需求,需要使用 高速 USB 模拟 CDC 串口实现串口数据交互功能。
所以本文我们就来讲一讲如何使用 CH585 的高速 USB 模拟 CDC串口进行数据交互。
相关文章:
CH58x/CH59x 蓝牙芯片 UART 使用.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- [一、 基础知识](#一、 基础知识)
-
- [1.1 什么是 CDC 串口](#1.1 什么是 CDC 串口)
- [1.2 官方示例](#1.2 官方示例)
- [二、 CDC串口应用](#二、 CDC串口应用)
-
- [2.1 CDC 串口数据收发实现](#2.1 CDC 串口数据收发实现)
-
- [2.1.1 端点号的确定](#2.1.1 端点号的确定)
- [2.1.2 数据交互的建立](#2.1.2 数据交互的建立)
- [2.1.3 收发中断处理](#2.1.3 收发中断处理)
- [2.2 一般应用](#2.2 一般应用)
-
- [2.2.1 基本框架](#2.2.1 基本框架)
- [2.2.2 接收处理](#2.2.2 接收处理)
- [2.2.3 没有标志结尾数据接收](#2.2.3 没有标志结尾数据接收)
- [2.2.4 发送](#2.2.4 发送)
- [2.2.5 效果图](#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 串口做数据收发,这里面可以不需要。
整个示例的实现,其实就是在主循环中一直查询收发:
c
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 ,它有的类型如下:
| 值 | 宏定义名(可选) | 含义 |
|---|---|---|
| 0x01 | USB_DESCR_TYP_DEVICE |
设备描述符 |
| 0x02 | USB_DESCR_TYP_CONFIG |
配置描述符 |
| 0x03 | USB_DESCR_TYP_STRING |
字符串描述符 |
| 0x04 | USB_DESCR_TYP_INTERFACE |
接口描述符 |
| 0x05 | USB_DESCR_TYP_ENDPOINT |
端点描述符 |
| 0x06 | USB_DESCR_TYP_DEVICE_QUALIFIER |
设备限定描述符(高速用) |
| 0x07 | USB_DESCR_TYP_OTHER_SPEED_CONFIG |
其他速率配置 |
| 0x08 | USB_DESCR_TYP_INTERFACE_POWER |
接口功耗(已废弃) |
| 0x0F | USB_DESCR_TYP_BOS |
BOS(USB 3.0 才用) |
2.1.2 数据交互的建立
上面的描述符是让主机知道 USB 设备的 EP2 是 CDC 数据口。
那我们还需要让 USB 控制器知道收到数据放在哪里,发送数据读哪里。
这部分的确定是在工程中ch585_usbhs_device.c 文件里面的函数USBHS_Device_Endp_Init 里实现的:
c
__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 缓存 → 上传给 PC
R32_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 里实现的,在这段代码中:
c
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 ) 函数,简单的示例如下:
数据发送
c
/* 仅等上一次发完(不会永久阻塞) */
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 基本框架
串口缓冲区:
c
#define cmd_max_len 100
typedef struct
{
uint8_t rx_buff[cmd_max_len];
uint8_t rx_count;
uint8_t rx_state;
uint8_t rx_back;
}uart_rx_buff;
数据处理:
c
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);
}
}
主循环调用:
c
void Main_Circulation()
{
while(1)
{
TMOS_SystemProcess();
app_uart_process();
}
}
清除缓存不需要用到。
移植的时候,我们直接把下面 4个文件拷贝到自己的工程 里面:

初始化,直接按照例程调用:
c
CH58x_BLEInit();
HAL_Init();
RFRole_Init();
cmd_uart_init();
#ifdef USE_CDC_UART
USBHS_Device_Init(ENABLE);
PFIC_EnableIRQ( USB2_DEVICE_IRQn );
#endif
Main_Circulation();
接下来还需要进行数据的收发处理。
2.2.2 接收处理
我们还需要处理下接收函数ch585_usbhs_device.c 文件,首先把示例中关于 UART2 的相关代码去掉,换成自己的代码。
我们先定义一个自己的收发缓存:
c
__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 函数中,把缓存用我们自己定义的缓存:
c
/* 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 是没问题,但是无法切换其他波特率 ):
c
/* 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 中断处理一样的原理:
c
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 事件,如果事件触发了,就表示收到了一帧数据,示意代码如下:
c
#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 函数:
c
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_SIZE
return;
/* 仅等上一次发完(不会永久阻塞) */
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 串口。
好了,本文就到这里,谢谢大家!