双USB串口数据交换器

串口是最常见的接口,因为它足够简单,几乎在所有的调试场合都能看到它的身影。从编程的角度来说,这种接口的代码已经非常成熟,从 C 到 Python 都能够支持这种接口。实际工作生产中最常见的就是USB转串口。美中不足的是,这USB转串口在编程的时候存在着如下缺陷:

  1. 某些USB转串口设备需要额外安装驱动才能使用;
  2. 当同一台电脑存在多个COM Port时,查找特定的设备会比较麻烦;
  3. 打开串口后很难得知串口另外一端的设备是否已经准备好;
  4. 传输速度有限制,最常见的波特率只有115200,意味着一秒只能传输11KB左右的数据。

针对上述问题,这次使用南京沁恒微电子公司出品的 CH32V208 制作一个双USB串口数据交换器(Dual USB SERIAL DATA EXCHANGER, 简称DUSDE)。CH32V208是一款基于32位RISC-V设计的无线型微控制器,配备了硬件堆栈区、快速中断入口,在标准RISC-V基础上大大提高了中断响应速度。搭载V4C内核,加入内存保护单元,同时降低硬件除法周期。除了片上集成2Mbps低功耗蓝牙BLE 通讯模块、10M以太网MAC+PHY模块、CAN控制器等接口之外还带有2个USB2.0全速设备+主机/设备接口。这次的双USB串口数据交换器就是将自身模拟为2个 USB 串口设备分别连接到两台电脑上,从而实现数据传输的功能。

CH32V208 架构

从上面的系统结构图可以看到,CH32V208WBU6支持两个USB2.0 Full Speed设备,其中一个可以作为 HOST或者Device,另外一个只能作为 Device 使用。我们通过编程的方式,让他们都工作在Device 模式下,下图就是我们设备的框图。

双USB串口数据交换器架构

针对前面的需要额外驱动的问题,通过编程在DUSDC上实现USB CDC协议,这样在Windows 8 及其以上的系统无需额外安装驱动。Windows 识别所属的 Class 之后,会自动加载内置驱动。

针对多个串口和需要检测对面设备是否准备好的问题,我们预定义了一些命令,当用户使用2022波特率打开串口后,能够响应如下命令。

方向命令返回值用途
PC->Device?ACVUSB1 或者 USB2设备测试。返回当前的USB接口名称
PC->Device?VER例如:0003查询当前固件版本。返回当前固件版本信息,例如:0003 这种版本号
PC->Device?SER例如:1234查询设备序列号。返回当前设备的序列号,对于同一个设备,从USB1和USB2读取的序列号相同,例如:1234
PC->Device?CFGNRDY或者REDY查询另外一个USB端口是否已经Active。当一个USB Port 收到“?ACV” 命令后就处于 Active 状态了。之后另外一个USB Port 使用这个命令会反馈前一个USB Port的状态

例如,系统中存在 COM1 到 COM4 多个串口,程序枚举每一个串口,打开串口后设定波特率为2022,之后从该串口发送“?ACV”命令,如果能够得到“USB1”或者“USB2”这样的回复,会表明当前为1号或者2号USB端口;

再比如,我们用设备连接两台电脑,USB Port1有程序打开过端口,并且用 “?ACV”查询过当前设备,那么USB Port 2这端程序以2022波特率打开端口后,再发送“?CFG”就能得到“REDY”的回复,表示对面的端口(USB1)已经准备好,反之如果收到“NRDY”则表示另外端口没有收到过“?ACV”命令;

最后,在程序看来数据使用串口传输,但是因为所有的传输都是在USB中进行,USB端口之间的数据也是在内存中进行的交换,相比传统串口速度会有很大的提升。实际测试表明在在使用超级终端程序的zmodem 协议传输文件时,速度可达300KB/S以上。同时因为没有串口的发送和采样过程,传输过程发生错误的概率极低。

上面就是为什么要设计DUSDC,以及它的优点。下面介绍DUSDC的具体实现。

首先是硬件部分。整体设计非常简单,并没有太多的元件:

核心部分就是基于CH32V208的最小系统,其中的S4 是Download 按钮,需要下载Firmware时,先按住按钮然后插入USB接口,之后再抬起按钮,使用 WCHISPTool即可下载。

USB 使用的是Type-B 母头,选择原因是这种结构非常稳固,最大限度保证连接的可靠。需要注意的是J1是预留的取电接口,在连接两台PC时,为了避免5V电压不同可以将J1断开,这样设备就只能通过USB2进行取电。

通过一颗TLV1117 来实现5V到3.3V的转换,5V来自USB 端口。预留了UART1作为调试接口,基本上所有的问题都能通过输出 Log 来解决。

此外,板子上预留了一个SPI1 出来的SPI NOR 接口,如果有记录数据的需求,可以考虑在该位置焊接SPI NOR芯片或者PSRAM。

因为板子上没有多余的元件,所以布线也比较简单:

3D渲染结果:

成品PCB:

硬件确定之后就可以着手设计软件了。CH32V208的示例程序有两个USB 转UART的例子,一个是USBDB (全速设备控制器)的例子,另外一个是USBFS(全速主机/设备控制器,这里用作全速设备控制器)。因为功能上有差别,这两个控制器名称也有差别,编程比较麻烦。类似CH567的话,一个是USB1另外一个是USB2感觉就会好很多。第一个工作是将两个代码融合在一起通过编译。

例如,当前是USBDB,工作基本流程是:

  1. 报告HOST 当前是 USB CDC 设备;
  2. 在 USBDB OUT的Endpoint收取来自HOST的数据,收下之后该OUT Endpoint设置为 NAK 回复状态,这样HOST不会继续对该OUT Endpoint 发送数据;
  3. 在主循环中轮询,如果有收到数据,那么查询USBFS IN Endpoint 是否 Busy,如果Busy继续等待,否则通过USBFS的IN Endpoint 将数据发送出去。

注意:USB 中描述的 OUT 和IN 是以HOST的CPU 为准的,对于运行着 Windows的x86 来说,OUT 是指从CPU到单片机方向(Write),反之数据是从单片机到CPU(Read)。

上述过程中, 代码位置简述:

  1. 在 usb_desc.c 有添加iSerialNumber 定义,这样当使用同样的设备时,每次插入会维持相同的串口号。例如,第一次使用Windows 分配为为 COM10,如果iSerialNumber为空,那么再次插入可能会被分配为COM11,但是iSerialNumber 不为空,再次插入仍然会是 COM10;
  2. usb_endp.c 处理USBDB 收到的主机OUT的数据,接收到的数据会放在 USB1_Tx_Buf[] 中。这部分代码可以看作是两部分:一部分是处理特别 COMMAND(以2022波特率打开串口之后进入特别 COMMAND 模式);另外一部分是处理转发数据的代码,就是在USB1_Tx_Counter=USB1BufLen 记录收到的数据长度
/*********************************************************************
 * @fn      EP2_OUT_Callback
 *
 * @brief  Endpoint 2 OUT.
 *
 * @return  none
 */
void EP2_OUT_Callback(void) {

    //ZivDebug_Start
    uint32_t     Status;
    Status = _GetEPRxStatus(EP2_OUT);
    uint16_t USB1BufLen = GetEPRxCount( EP2_OUT & 0x7F);
    PMAToUserBufferCopy(&USB1_Tx_Buf[USB1_Tx_Counter], GetEPRxAddr( EP2_OUT & 0x7F),
            USB1BufLen);

    if (USB1Replay!=0xFF) {
        if ((USB1_Tx_Buf[0] == '?') && (USB1_Tx_Buf[1] == 'A')
                && (USB1_Tx_Buf[2] == 'C') && (USB1_Tx_Buf[3] == 'V')) {
            printf("RCV ACV CMD\r\n");
            USB1Replay = 0x01;
        }
        if ((USB1_Tx_Buf[0] == '?') && (USB1_Tx_Buf[1] == 'V')
                && (USB1_Tx_Buf[2] == 'E') && (USB1_Tx_Buf[3] == 'R')) {
            printf("RCV VER CMD\r\n");
            USB1Replay = 0x02;
        }
        if ((USB1_Tx_Buf[0] == '?') && (USB1_Tx_Buf[1] == 'S')
                && (USB1_Tx_Buf[2] == 'E') && (USB1_Tx_Buf[3] == 'R')) {
            printf("RCV SER CMD\r\n");
            USB1Replay = 0x03;
        }
        if ((USB1_Tx_Buf[0] == '?') && (USB1_Tx_Buf[1] == 'C')
                && (USB1_Tx_Buf[2] == 'F') && (USB1_Tx_Buf[3] == 'G')) {
            printf("RCV CFG CMD\r\n");
            USB1Replay = 0x04;
        }
    } else {
        //printf("EPS1[%d]\r\n",GetEPRxStatus(ENDP2));
        USB1_Tx_Counter=USB1BufLen;
        printf("EP2O[%d]\r\n",USB1_Tx_Counter);
    }

}
  • 主机的轮询发送,在main.c中,主要动作是将数据通过 USBFS_Endp_DataUp() 函数转发给USBFS 的 Port 上。

     

        // If USB2 is connected, we will send data to USB2
        if (USBFS_DevEnumStatus == 1) {
            if ((USBFS_Endp_Busy[DEF_UEP3]==0)&&(USB1_Tx_Counter!=0)&&(USB1Replay==0xFF)) {
                printf("A[%d]\r\n",USB1_Tx_Counter);
                // Send data to USB2
                USBFS_Endp_DataUp( ENDP3, &USB1_Tx_Buf[0], USB1_Tx_Counter, DEF_UEP_CPY_LOAD);
                // If data length is 64, we should send a null package
                if (USB1_Tx_Counter==DEF_USBD_UEP0_SIZE) {
                    // Wait until USB2 is free
                    while (USBFS_Endp_Busy[DEF_UEP3]!=0) {
                    }
                    // Send a NULL package
                    USBFS_Endp_DataUp( ENDP3, &USB1_Tx_Buf[0], 0, DEF_UEP_CPY_LOAD);
                }
                USB1_Tx_Counter=0;
                // Enable USB1 Endpoint2
                SetEPRxValid( ENDP2);
            }
        } else {
            //If USB2 is NOT connected, data from USB1 would be dropped
            USB1_Tx_Counter=0;
            SetEPRxValid( ENDP2);
        }

此外,特别命令模式处理代码如下,通过USBD_ENDPx_DataUp() 将数据返回到USBDB对的USB Port上。

        if ((USB1Replay>0)&&(USB1Replay<5)) {
            // Reply USB1 Command
            if (USB1Replay==1) {
                RPYMSG[0]='U';
                RPYMSG[1]='S';
                RPYMSG[2]='B';
                RPYMSG[3]='1';
            }
            if (USB1Replay==2) {
                RPYMSG[0]='0';
                RPYMSG[1]='0';
                RPYMSG[2]='0';
                RPYMSG[3]='3';
            }
            if (USB1Replay==3) {
                RPYMSG[0]='1';
                RPYMSG[1]='2';
                RPYMSG[2]='3';
                RPYMSG[3]='4';
            }
            if (USB1Replay==4) {
                if (USB2Replay!=0xFF) {
                    RPYMSG[0]='R';
                    RPYMSG[1]='E';
                    RPYMSG[2]='D';
                    RPYMSG[3]='Y';
                } else {
                    RPYMSG[0]='N';
                    RPYMSG[1]='R';
                    RPYMSG[2]='D';
                    RPYMSG[3]='Y';
                }

            }
            USBD_ENDPx_DataUp( ENDP3, &RPYMSG, sizeof(RPYMSG)); // Send to USB1
            USB1Replay=0;
            SetEPRxValid( ENDP2);
        }

       

上面就是USBDB 的USB Port 处理流程,USBFS的USB Port处理逻辑相同,但是代码表达上差异很大。下面是实物:

介绍的视频

https://www.bilibili.com/video/BV1484y1H7UM/

本文提到的电路图和PCB设计下载:

本文提到的完整代码下载:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注