3173浏览
查看: 3173|回复: 2

[项目] 双USB串口数据交换器

[复制链接]
串口是最常见的接口,因为它足够简单,几乎在所有的调试场合都能看到它的身影。从编程的角度来说,这种接口的代码已经非常成熟,从 C Python 都能够支持这种接口。实际工作生产中最常见的就是USB转串口。美中不足的是,这USB转串口在编程的时候存在着如下缺陷:
1.     某些USB转串口设备需要额外安装驱动才能使用;
2.     当同一台电脑存在多个COMPort时,查找特定的设备会比较麻烦;
3.     打开串口后很难得知串口另外一端的设备是否已经准备好;
4.     传输速度有限制,最常见的波特率只有115200,意味着一秒只能传输11KB左右的数据。
针对上述问题,这次使用南京沁恒微电子公司出品的 CH32V208 制作一个双USB串口数据交换器(DualUSB SERIAL DATA EXCHANGER, 简称DUSDE)。CH32V208是一款基于32RISC-V设计的无线型微控制器,配备了硬件堆栈区、快速中断入口,在标准RISC-V基础上大大提高了中断响应速度。搭载V4C内核,加入内存保护单元,同时降低硬件除法周期。除了片上集成2Mbps低功耗蓝牙BLE通讯模块、10M以太网MAC+PHY模块、CAN控制器等接口之外还带有2USB2.0全速设备+主机/设备接口。这次的双USB串口数据交换器就是将自身模拟为2 USB 串口设备分别连接到两台电脑上,从而实现数据传输的功能。

双USB串口数据交换器图1
从上面的系统结构图可以看到,CH32V208WBU6支持两个USB2.0Full Speed设备,其中一个可以作为 HOST或者Device,另外一个只能作为 Device 使用。我们通过编程的方式,让他们都工作在Device模式下,下图就是我们设备的框图。
双USB串口数据交换器图2
针对前面的需要额外驱动的问题,通过编程在DUSDC上实现USBCDC协议,这样在Windows8 及其以上的系统无需额外安装驱动。Windows识别所属的 Class 之后,会自动加载内置驱动。
针对多个串口和需要检测对面设备是否准备好的问题,我们预定义了一些命令,当用户使用2022波特率打开串口后,能够响应如下命令。
  
方向
  
命令
返回值
用途
PC->Device
?ACV
USB1 或者 USB2
设备测试。返回当前的USB接口名称
PC->Device
?VER
例如:0003
查询当前固件版本。返回当前固件版本信息,例如:0003  这种版本号
PC->Device
?SER
例如:1234
查询设备序列号。返回当前设备的序列号,对于同一个设备,从USB1和USB2读取的序列号相同,例如:1234
PC->Device
?CFG
NRDY或者REDY
查询另外一个USB端口是否已经Active。当一个USB  Port 收到“?ACV” 命令后就处于 Active 状态了。之后另外一个USB  Port 使用这个命令会反馈前一个USB  Port的状态
例如,系统中存在 COM1 COM4 多个串口,程序枚举每一个串口,打开串口后设定波特率为2022,之后从该串口发送“?ACV”命令,如果能够得到“USB1”或者“USB2”这样的回复,会表明当前为1号或者2USB端口;
再比如,我们用设备连接两台电脑,USBPort1有程序打开过端口,并且用 “?ACV”查询过当前设备,那么USBPort 2这端程序以2022波特率打开端口后,再发送“?CFG”就能得到“REDY”的回复,表示对面的端口(USB1)已经准备好,反之如果收到“NRDY”则表示另外端口没有收到过“?ACV”命令;
最后,在程序看来数据使用串口传输,但是因为所有的传输都是在USB中进行,USB端口之间的数据也是在内存中进行的交换,相比传统串口速度会有很大的提升。实际测试表明在在使用超级终端程序的zmodem协议传输文件时,速度可达300KB/S以上。同时因为没有串口的发送和采样过程,传输过程发生错误的概率极低。
上面就是为什么要设计DUSDC,以及它的优点。下面介绍DUSDC的具体实现。
首先是硬件部分。整体设计非常简单,并没有太多的元件:
双USB串口数据交换器图3
核心部分就是基于CH32V208的最小系统,其中的S4Download按钮,需要下载Firmware时,先按住按钮然后插入USB接口,之后再抬起按钮,使用 WCHISPTool即可下载。
双USB串口数据交换器图4
USB 使用的是Type-B母头,选择原因是这种结构非常稳固,最大限度保证连接的可靠。需要注意的是J1是预留的取电接口,在连接两台PC时,为了避免5V电压不同可以将J1断开,这样设备就只能通过USB2进行取电。
双USB串口数据交换器图5
通过一颗TLV1117来实现5V3.3V的转换,5V来自USB 端口。预留了UART1作为调试接口,基本上所有的问题都能通过输出 Log 来解决。
双USB串口数据交换器图6
此外,板子上预留了一个SPI1出来的SPINOR 接口,如果有记录数据的需求,可以考虑在该位置焊接SPINOR芯片或者PSRAM
双USB串口数据交换器图7
因为板子上没有多余的元件,所以布线也比较简单:
双USB串口数据交换器图8
3D渲染结果:
双USB串口数据交换器图9
双USB串口数据交换器图10
成品PCB
硬件确定之后就可以着手设计软件了。CH32V208的示例程序有两个USBUART的例子,一个是USBDB(全速设备控制器)的例子,另外一个是USBFS(全速主机/设备控制器,这里用作全速设备控制器)。因为功能上有差别,这两个控制器名称也有差别,编程比较麻烦。类似CH567的话,一个是USB1另外一个是USB2感觉就会好很多。第一个工作是将两个代码融合在一起通过编译。
例如,当前是USBDB,工作基本流程是:
1.     报告HOST当前是 USB CDC 设备;
2.     在 USBDB OUT的Endpoint收取来自HOST的数据,收下之后该OUTEndpoint设置为 NAK 回复状态,这样HOST不会继续对该OUTEndpoint 发送数据;
3.     在主循环中轮询,如果有收到数据,那么查询USBFSIN Endpoint 是否 Busy,如果Busy继续等待,否则通过USBFS的INEndpoint 将数据发送出去。
注意:USB 中描述的 OUT IN 是以HOSTCPU 为准的,对于运行着 Windowsx86来说,OUT 是指从CPU到单片机方向(Write),反之数据是从单片机到CPURead)。
上述过程中, 代码位置简述:
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记录收到的数据长度

  1. /*********************************************************************
  2. * @fn      EP2_OUT_Callback
  3. *
  4. * @brief  Endpoint 2 OUT.
  5. *
  6. * @return  none
  7. */
  8. void EP2_OUT_Callback(void) {
  9.     //ZivDebug_Start
  10.     uint32_t     Status;
  11.     Status = _GetEPRxStatus(EP2_OUT);
  12.     uint16_t USB1BufLen = GetEPRxCount( EP2_OUT & 0x7F);
  13.     PMAToUserBufferCopy(&USB1_Tx_Buf[USB1_Tx_Counter], GetEPRxAddr( EP2_OUT & 0x7F),
  14.             USB1BufLen);
  15.     if (USB1Replay!=0xFF) {
  16.         if ((USB1_Tx_Buf[0] == '?') && (USB1_Tx_Buf[1] == 'A')
  17.                 && (USB1_Tx_Buf[2] == 'C') && (USB1_Tx_Buf[3] == 'V')) {
  18.             printf("RCV ACV CMD\r\n");
  19.             USB1Replay = 0x01;
  20.         }
  21.         if ((USB1_Tx_Buf[0] == '?') && (USB1_Tx_Buf[1] == 'V')
  22.                 && (USB1_Tx_Buf[2] == 'E') && (USB1_Tx_Buf[3] == 'R')) {
  23.             printf("RCV VER CMD\r\n");
  24.             USB1Replay = 0x02;
  25.         }
  26.         if ((USB1_Tx_Buf[0] == '?') && (USB1_Tx_Buf[1] == 'S')
  27.                 && (USB1_Tx_Buf[2] == 'E') && (USB1_Tx_Buf[3] == 'R')) {
  28.             printf("RCV SER CMD\r\n");
  29.             USB1Replay = 0x03;
  30.         }
  31.         if ((USB1_Tx_Buf[0] == '?') && (USB1_Tx_Buf[1] == 'C')
  32.                 && (USB1_Tx_Buf[2] == 'F') && (USB1_Tx_Buf[3] == 'G')) {
  33.             printf("RCV CFG CMD\r\n");
  34.             USB1Replay = 0x04;
  35.         }
  36.     } else {
  37.         //printf("EPS1[%d]\r\n",GetEPRxStatus(ENDP2));
  38.         USB1_Tx_Counter=USB1BufLen;
  39.         printf("EP2O[%d]\r\n",USB1_Tx_Counter);
  40.     }
  41. }
复制代码

1.     主机的轮询发送,在main.c中,主要动作是将数据通过 USBFS_Endp_DataUp() 函数转发给USBFS 的 Port 上。
  1.         // If USB2 is connected, we will send data to USB2
  2.         if (USBFS_DevEnumStatus == 1) {
  3.             if ((USBFS_Endp_Busy[DEF_UEP3]==0)&&(USB1_Tx_Counter!=0)&&(USB1Replay==0xFF)) {
  4.                 printf("A[%d]\r\n",USB1_Tx_Counter);
  5.                 // Send data to USB2
  6.                 USBFS_Endp_DataUp( ENDP3, &USB1_Tx_Buf[0], USB1_Tx_Counter, DEF_UEP_CPY_LOAD);
  7.                 // If data length is 64, we should send a null package
  8.                 if (USB1_Tx_Counter==DEF_USBD_UEP0_SIZE) {
  9.                     // Wait until USB2 is free
  10.                     while (USBFS_Endp_Busy[DEF_UEP3]!=0) {
  11.                     }
  12.                     // Send a NULL package
  13.                     USBFS_Endp_DataUp( ENDP3, &USB1_Tx_Buf[0], 0, DEF_UEP_CPY_LOAD);
  14.                 }
  15.                 USB1_Tx_Counter=0;
  16.                 // Enable USB1 Endpoint2
  17.                 SetEPRxValid( ENDP2);
  18.             }
  19.         } else {
  20.             //If USB2 is NOT connected, data from USB1 would be dropped
  21.             USB1_Tx_Counter=0;
  22.             SetEPRxValid( ENDP2);
  23.         }
复制代码

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


2
  1.         if ((USB1Replay>0)&&(USB1Replay<5)) {
  2.             // Reply USB1 Command
  3.             if (USB1Replay==1) {
  4.                 RPYMSG[0]='U';
  5.                 RPYMSG[1]='S';
  6.                 RPYMSG[2]='B';
  7.                 RPYMSG[3]='1';
  8.             }
  9.             if (USB1Replay==2) {
  10.                 RPYMSG[0]='0';
  11.                 RPYMSG[1]='0';
  12.                 RPYMSG[2]='0';
  13.                 RPYMSG[3]='3';
  14.             }
  15.             if (USB1Replay==3) {
  16.                 RPYMSG[0]='1';
  17.                 RPYMSG[1]='2';
  18.                 RPYMSG[2]='3';
  19.                 RPYMSG[3]='4';
  20.             }
  21.             if (USB1Replay==4) {
  22.                 if (USB2Replay!=0xFF) {
  23.                     RPYMSG[0]='R';
  24.                     RPYMSG[1]='E';
  25.                     RPYMSG[2]='D';
  26.                     RPYMSG[3]='Y';
  27.                 } else {
  28.                     RPYMSG[0]='N';
  29.                     RPYMSG[1]='R';
  30.                     RPYMSG[2]='D';
  31.                     RPYMSG[3]='Y';
  32.                 }
  33.             }
  34.             USBD_ENDPx_DataUp( ENDP3, &RPYMSG, sizeof(RPYMSG)); // Send to USB1
  35.             USB1Replay=0;
  36.             SetEPRxValid( ENDP2);
  37.         }
复制代码

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


双USB串口数据交换器图11双USB串口数据交换器图12


双USB串口数据交换器图13



zoologist  高级技匠
 楼主|

发表于 2023-2-20 09:02:18

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


本文提到的项目代码下载(CH32V208 官方编译器工程):

介绍视频:

回复

使用道具 举报

 

发表于 2024-6-14 21:43:47

有没有成品可以购买?
回复

使用道具

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail