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 ,它有的类型如下:
| 值 | 宏定义名(可选) | 含义 | 
|---|---|---|
| 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 里实现的:
__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 串口。
好了,本文就到这里,谢谢大家!
