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

复制代码
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 串口。

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

相关推荐
加油20196 天前
freertos系统中如何生成随机数以及保证随机性?
freertos·risc-v·随机数·lcg·rdcycle·周期计数器
飞睿科技7 天前
乐鑫推出的第三颗RISC-V物联网芯片ESP32-H2,融合蓝牙与Thread技术!
物联网·risc-v
云雾J视界8 天前
RISC-V开源处理器实战:从Verilog RTL设计到FPGA原型验证
fpga开发·开源·verilog·risc-v·rtl·数字系统
做一个快乐的小傻瓜9 天前
易灵思FPGA的RISC-V核操作函数
fpga·risc-v·易灵思
嵌入式Linux,19 天前
RISC-V 只会越来越好(2)
risc-v
国科安芯22 天前
抗辐照MCU芯片在低轨商业卫星原子钟中的适配与优化
单片机·嵌入式硬件·fpga开发·架构·risc-v
云澈ovo25 天前
RISC-V 架构适配:开源 AI 工具链的跨平台编译优化全流程(附实战指南)
架构·开源·risc-v
Blossom.1181 个月前
用一颗MCU跑通7B大模型:RISC-V+SRAM极致量化实战
人工智能·python·单片机·嵌入式硬件·opencv·机器学习·risc-v