☰
串口分线器实战:一路物理串口如何变多路虚拟串口
2026/9/26 16:27:25 网站建设 项目流程

简介:Serial Port Splitter 是一款面向串口通信场景的系统工具,适合设备调试人员和工业控制用户,用于解决物理串口数量不足及多程序争用串口数据的问题。软件通过虚拟串口技术创建多个虚拟端口,让不同应用同时分享同一物理串口,支持读写模式和只读模式:读写模式便于双向数据交换,只读模式适合单独监控串口流量,也适用于多设备数据采集、单设备多任务处理等实际需求。压缩包仅 3 个文件,共 4.44MB,涵盖 Windows 安装程序、使用说明文档与授权协议文件,分别用于软件安装、功能了解和合法使用确认。目前已有 297 人下载学习。获取后可快速在 Windows 中部署,并参照说明配置串口分流策略,从而更高效地管理物理串口资源、简化设备接入流程。

1. Serial Port Splitter:一个串口不够用时,工程师都在用什么

老设备只有一根 RS232 串口线,但调试要开串口助手、上位机要收数据采集、日志软件还要记录存档,三套程序轮着抢同一个 COM 口,谁也开不了第二个。Serial Port Splitter 这个方向就是解决“一路物理串口,多个程序要同时读写”的尴尬:它把串口的数据流复制成多份,分发给多个应用程序,或是把多个程序的写入合并回物理口,让每个程序都以为自己在独占设备。核心价值是把串口资源从“互斥占用”变成“共享管线”,省去改设备、加网关的硬件成本。适合做设备调试、上位机开发、工业数据采集的人,尤其适合现场不能动硬件、只能靠软件救场的场景。理解它,先分清三种工作模式。

2. 串口分线的三种工作模式:哪条路最适合你的现场

2.1 应用层分线:最朴素、也最容易丢包的做法

应用层分线是理解整个 Splitter 概念的起点。做法很直白:写一个守护进程,以独占方式打开物理串口,再用程序把读到的数据转发给多个下游程序。下游程序不再直接打开物理口,而是打开转发程序提供的 TCP/Socket/管道接口。这个模式最大的优点是灵活——转发层可以加协议解析、帧过滤、数据缓存,甚至可以按设备地址把不同数据路由到不同程序。我的几个临时采集脚本就是这么干的:一会转发给 InfluxDB 采集器,一会转发给 Modbus 轮询程序,中间加一个字典变量控制路由规则,整套逻辑完全可控。

代价也非常明显:所有下游程序都依赖这个转发进程活着。进程一退,数据全断;转发程序读得慢,队列就堆积,高波特率下丢帧是家常便饭。另一个坑是应用层转发绕过了操作系统串口驱动的时间机制,字节到达间隔会被打乱,对时序敏感的上位机(比如要求字符间隔精确的称重仪表)会频繁校验失败。所以应用层分线适合调试期、数据量小、对实时性要求不高的场景,不适合直接进产线。

2.2 驱动层虚拟串口分线:业界最常用的方案

真正承担“Serial Port Splitter”这个名称的产品,绝大多数走的是驱动层。原理是在操作系统里装一个虚拟串口驱动,把一个物理 COM 口(或 USB 转串口)映射成多个额外的虚拟 COM 口。当物理串口收到数据时,驱动在自己的中断处理流程里把数据复制成多份,分发给所有已经打开的虚拟口句柄;反过来,任意一个虚拟口写入的数据,也会被驱动合并后送到物理口,让设备侧感知不到变化。对应用层来说,每个虚拟口就是一个标准串口,老程序、老库不用改一行代码,这正是企业愿意买商业分线工具的核心原因。

驱动层相对应用层的优势不只是隐蔽,更重要的是时序还原。驱动在 ring 0 层面拿到的是硬件中断原始数据,能保持字节到达的相对间隔,上位机按 Modbus 的 3.5 字符间隔判帧时基本不受影响。实际工程里可以理解为:驱动层做的是“透明硬件虚拟化”,应用层做的是“软件搬运”。常见的驱动层工具包括商业的 Eltima 分线器、Virtual Serial Port 类软件,以及开源社区基于 com0com 改造的分线驱动。选型时先看驱动是否签名,再看是否支持广播模式(后面避坑章节会细说)。

2.3 合并回流模式:把多路写入汇成一路

分线这个词容易让人只想到“一对多”,但实际现场还有反向需求:多台工控机要往同一台仪表发指令,仪表也只有一路串口。这时候 Splitter 需要工作在“合并模式”。多个虚拟口各自接收应用写入,驱动按到达顺序把它们合并写入物理串口。表面看很简单,实际是三个模式里最容易出事的:两个程序同时写,指令会在物理口上交错,如果协议没有“一问一答”机制,设备会直接忽略。另一个隐患是同一个虚拟口被多个线程同时写时,驱动底层没有原子保护,字节会互相穿插,造成设备收到非法帧。

所以合并模式的适用面很窄:指令频率低、协议有明确应答和重试、总流量不超过物理口波特率能力的 40%。我的习惯是:只要现场有两个程序要主动写设备,宁愿多加一台串口服务器走 IP 分线,也不在产品上用合并模式硬扛。如果你确实只能用 Splitter,务必在应用层做写锁,把同一时刻的写者限制为一个。

3. Windows 上落地:用 com0com 和转发脚本五分钟跑通分线

3.1 用 com0com 建虚拟串口对:命令与参数

Windows 下最常见的免费路线是 com0com,它的核心功能是创建成对的虚拟串口:一个串口对里有两个端点,写入 A 端的数据从 B 端读出来,写入 B 端的数据从 A 端读出来。第一次接触很容易误以为它能直接把物理口和虚拟口绑定,实际上它只管虚拟口,需要一个转发脚本承担物理口与虚拟口之间的搬运。先安装驱动,再在管理员命令行里创建端口对:

com0com --install com0com --create "CNCA0,CNCB0" com0com --change "CNCA0,PortName=COM4" com0com --change "CNCB0,PortName=COM5"

参数含义很直白:CNCA0、CNCB0是驱动内部端点的全局名,PortName指定对外暴露的 COM 端口号。创建时务必确认 COM4、COM5 没有被系统占用,否则驱动会创建失败,但命令不报错,只在设备管理器里看到黄色感叹号。--install只需要执行一次,重启后驱动会自动加载,端口对也会保留——这是它比纯软件方案省心的地方。

端口对建好后,写一个 Python 搬运脚本,把物理口 COM3 的读数据搬到 COM4:

import serial # 物理串口和虚拟端点分别打开 physical = serial.Serial('COM3', 9600, timeout=0.1) virtual_tx = serial.Serial('COM4', 9600, timeout=0.1) while True: # 从物理口读原始字节,原样搬到虚拟口 data = physical.read(4096) if data: virtual_tx.write(data)

这段代码是“读方向”的最小闭环:设备发来的数据进了虚拟口,业务程序只要打开 COM5,就能通过驱动内部配对读到这份数据。同时需要在另一个线程里把业务程序写到 COM5 的指令反向搬回 COM4,再到物理口。实际工程里我一般把串口对象包成双向类,加上异常重连,否则只要一个打开失败,整个链路就僵死。注意timeout=0.1意味着最多延迟 100 毫秒才把数据从驱动缓存里读出来,想追实时性可以调到 0.01,但会提高 CPU 占用。

3.2 用商业分线工具把物理 COM3 拆成 COM4/COM5

如果你不想自己维护搬运脚本,或者现场有非技术同事要操作,商业工具是更稳的选择。以典型的 Serial Port Splitter 图形工具为例,安装后界面里会列出当前系统所有串口,选择物理 COM3,再点一下 Split 操作,工具自动生成 COM4、COM5 两个虚拟口。这两个虚拟口由工具自带的驱动管理,业务程序直接打开任意一个虚拟口,就能同时收物理口数据。商业工具通常还会提供两个可切换模式:只读模式——所有虚拟口只能收,不能写物理口;读写模式——第一个打开虚拟口的程序有写权限,其余只能读。

这里要特别提醒选型细节:买工具前先确认它是否支持“广播”能力。有些便宜的分线工具生成的多个虚拟口,实际只把数据发给“最后打开”的那个程序,之前打开的程序收不到,这本质上是单播转发,不符合共享语义。测试方法很简单:把物理口接一个一直发数据的设备,先后打开 COM4、COM5 两个串口助手,如果两边数据同时滚动,才是真正的广播分线。数据不回滚的话,这个工具的设计就不满足需求,趁早退货。

商业工具还会引入一个虚拟串口驱动,安装后设备管理器里会出现新的串口设备节点。装完必须重启一次系统,否则驱动态缓存的端口信息可能不生效,这是 Windows 内核串口驱动常见的“装机第一步必须重启”的老规矩,别省。

3.3 双开监听测试:确认分线真的在按预期工作

分线工具装好、端口配好,不代表链路就是通的,我习惯按下面三步做一次完整验收。先做回环测试:物理口上一端插一个自环头(把 2、3 脚短接),或者直接接一个真实设备,让它在固定周期回发一帧数据。再用两个串口助手分别打开 COM4 和 COM5,观察两边收到的数据是否逐字节一致,滚动速度是否同步。第二步做写入测试:先在 COM4 的助手里发一条写指令,看设备是否有动作;再在 COM5 的助手里发同样指令,确认第二个程序也能写。第三步做并发冲击:让两个助手同时发送大量数据,看设备端是否有卡顿、乱码。

这个测试同时隐含了一个关键参数:Windows 驱动默认的串口接收缓存是 4096 字节。如果设备以高波特率持续发数据,而业务程序读得不及时,缓冲满了之后新数据会被丢弃,现象就是分线后丢失尾帧。对于 115200 波特率、10ms 间隔的数据流,这个缓存通常够用;如果跑 921600 或者有突发大帧,就要在设备管理器里把“接收缓冲区”拉高到 8192 或 16384,否则后面怎么调都白搭。

4. Linux 上用 socat 与自写脚本实现串口分线

4.1 socat 建 pty 端点:先造虚拟串口再谈分发

Linux 下没有开箱即用的分线工具,常规路由是 socat 提供虚拟串口端点,自己写守护进程做数据搬运。先用 socat 生成一个伪终端(pty),把它的地址固定到一个可预期的路径:

socat -d -d pty,raw,echo=0,link=/tmp/ttyV0,perm=0666 tcp-listen:4100,reuseaddr,fork

这条命令的逻辑是:在/tmp/ttyV0创建一个对外可见的串口符号链接,同时监听 4100 端口。任何程序打开/tmp/ttyV0就能读写串口,而 TCP 客户端连到 4100 端口后,驱动的字节流和 TCP 流相互桥接。参数里raw表示不经过终端行规则处理,echo=0防止收到数据再回显,perm=0666给足权限避免权限坑,fork允许每个 TCP 连接受一个独立处理进程。如果你现场有多台机器要共享同一路串口,这个 pty 端点就相当于一个网络化的虚拟 COM 口。

但要注意,socat 的单条命令是“一对一”桥接,不是广播。想要一对多,常见做法是同一个物理串口的数据源,由你的守护进程统一读取,再分别写入多个ttyV端点,这样每个端点各自拥有独立的数据副本,不依赖 socat 的转发语义。这个组合比单独跑多个 socat 实例更稳,因为多个 socat 会互相竞争打开物理口,后启动的进程会直接报Device or resource busy。

4.2 Python 多路分发器:线程加队列,把一路读到 N 路

Linux 下我一般直接用 Python 写一个常驻分发器,核心只有三步:一个线程读物理串口,把原始数据放进广播队列;N 个写线程各消费同一个队列,把数据写入各自的 pty 端点;反过来,业务程序写入 pty 的数据也要搬回物理口,这一步走独立队列,但只有一个写者能发送,避免指令交错。最小可运行版本如下:

#!/usr/bin/env python3 import serial import threading import queue import fcntl PHYS_PORT = '/dev/ttyUSB0' BAUD = 115200 VIRT_PORTS = ['/tmp/ttyV0', '/tmp/ttyV1'] # 先用 socat 建好 q_out = queue.Queue(maxsize=4096) # 物理口 -> 虚拟口广播队列 def read_physical(): ser = serial.Serial(PHYS_PORT, BAUD, timeout=0.05) while True: data = ser.read(1024) if data: q_out.put(data) def broadcast_to_virtual(path): # 打开虚拟串口,不阻塞等待数据,不断消费广播队列 while True: data = q_out.get() try: fd = os.open(path, os.O_WRONLY | os.O_NOCTTY) os.write(fd, data) os.close(fd) except OSError: pass # 下游程序没打开时静默丢弃 threading.Thread(target=read_physical, daemon=True).start() for v in VIRT_PORTS: threading.Thread(target=broadcast_to_virtual, args=(v,), daemon=True).start() threading.Event().wait()

这段代码的关键在队列设计:maxsize=4096是一个缓冲上限,物理口瞬时涌入超过 4096 条数据时,入队动作会阻塞读线程——这比无限队列更安全,至少不会内存爆炸。写线程里每次打开、写入、关闭虚拟口是比较粗暴的做法,好处是下游程序反复开关端口不会引发句柄泄露;代价是频繁 open/close 有开销,适合下游程序数量少、数据频率不高的现场。如果你要低延迟,就把虚拟口的 fd 常驻,用select/epoll监控可写状态。

有人会把这段逻辑塞进 systemd 服务里开机自启,这是对的,但注意不要把多个虚拟口线程的写入放在同一个队列消费者里,否则某个下游处理慢就会拖累所有下游。每个虚拟口一个独立消费者线程,才能做到单个下游卡死不影响其他路,这是分线器最容易踩的并发设计坑之一。

4.3 必调参数:波特率、字符间隔、缓冲上限

参数推荐值设置理由
波特率与物理设备一致分线器不改协议,误设则整条链路乱码
物理口 timeout0.05 秒兼顾实时性与 CPU 占用,低波特率可放到 0.1
广播队列 maxsize4096防内存溢出;数据突发大时按峰值提高
虚拟口写入模式O_NOCTTY防止虚拟口变成进程控制终端导致信号中断
系统串口 low_latency开启setserial /dev/ttyUSB0 low_latency减少调度延迟

额外提醒:串口分线器是字节搬运工,不要在里面做协议解析、不要试图按帧缓存。帧边界应该由上位机程序自己处理,你只保证“进多少、出多少、顺序不变”。这个原则在 Windows 和 Linux 通用,很多人分线后乱码,不是线的问题,是自己在分线层加了粘包拆包逻辑。

5. 避坑:Serial Port Splitter 常见问题与排查

5.1 程序 A 能收、程序 B 收不到:广播模式没开

现象:用同一个分线器工具建了两个虚拟口,先打开的程序 A 数据正常,后打开的程序 B 界面永远空白。

原因:相当一部分分线工具的驱动实现不是数据广播,而是“最后打开的句柄接管”。它内部维护了一个活跃接收者列表,新句柄打开后,数据只发给最后一个句柄。这是很多商业工具为了省驱动资源刻意为之的设计,不会在界面标注。

解决:回到选型时提到的广播模式测试,用两个串口助手同时打开验证。如果工具确认不支持广播且不允许升级,换用 com0com + 自写转发脚本,让脚本用独立线程向每个虚拟口单独write(),这是开源路线里最可控的方案。

5.2 虚拟口打开失败:权限、占用、命名空间

现象:Linux 下打开/tmp/ttyV0报Permission denied;Windows 下打开 COM10 报“系统找不到指定的文件”。

原因:Linux 侧是用户不在dialout组或 udev 规则没给设备节点权限;Windows 侧则是 COM10 以上的高级端口号在某些精简版驱动里没有注册命名空间。

解决:Linux 把用户加入组并完善 udev 规则后重插设备;Windows 在设备管理器里把虚拟口映射强制改到 COM1~COM8 范围,或者用mode COM10命令确认系统是否识别。检查端口是否被占用用mode最直接,会列出当前已分配的所有 COM 口。

5.3 装完工具后系统不稳定:驱动签名问题

现象:装了某款分线工具,重启后虚拟口创建失败,设备管理器里分线驱动设备显示黄色感叹号,甚至系统频繁蓝屏。

原因:Windows 10/11 对内核态串口驱动有强制签名要求,未签名的分线驱动会被系统直接禁用,但驱动初始化逻辑已经被加载,导致资源冲突。

解决:生产机器不要尝试关闭签名强制加载,这是让整个系统变成黑匣子的风险动作。换用有微软 WHQL 签名的商业版本,或者直接走 com0com——它的驱动在国内社区验证非常广泛,签名完备。这是我在这个项目上翻车最狠的一次:贪便宜装了个绿色版,结果整机蓝屏,现场设备全断。

5.4 数据乱码、半帧:字节间隔被打破

现象:物理口设备数据正常,但业务程序通过虚拟口收到的帧偶尔乱码,或者 Modbus 报文在分线后频繁报 CRC 错误。

原因:分线器把数据从物理口读出来再写入虚拟口,两个动作之间存在时间差。如果业务程序按“字符间隔超过 3.5 字符时间”判帧,这个时间差会让一帧数据被切开,上位机就会把半帧误判为完整帧。

解决:优先选择驱动层分线,它在中断上下文里复制数据,时间间隔保持得最好。如果只能用应用层脚本,把读循环 timeout 调到 10ms 以内,并且不要在分线代码里做time.sleep(),数据一律走队列直通。乱码问题 80% 是分线层引入延迟,不是波特率错误。

5.5 业务程序重启后,物理口数据消失

现象:分线器服务还在,虚拟口也在,但程序重启后物理口设备不再响应,数据全断。

原因:部分分线工具把“虚拟口连接数量”当成了物理口共享的生命周期。当最后一个虚拟口被关闭(业务程序退出)时,驱动认为没有接收者,主动把物理口也关闭掉,设备侧看到 DTR/RTS 变化,自然就不发了。

解决:把物理口的两根流控线强制拉高保持设备在线。在自写脚本里打开物理口时设置dsrdtr=False, rtscts=False,并在初始化后手动把 RTS 置为 True。如果是商业工具,找设置里的“保持物理口连接”开关,没有就换工具。

6. 进阶:用数据镜像与帧过滤把分线做成可观测管线

6.1 按帧头过滤,把调试流量和业务流量分开

很多现场的分线需求不只是复制数据,还想让不同下游看到不同数据流。我一般会在分发器里加一层按帧头过滤:比如设备周期上报的标定帧(帧头 0xAA 0x55)送给标定软件,运行帧(帧头 0x68)送给监控后台。过滤逻辑放在分发器里能减少下游无效处理,但别做完整拆包,只做头部匹配和粗粒度裁剪,避免误杀变长帧:

def dispatch_by_header(data): if data.startswith(b'\xaa\x55'): q_calibration.put(data) # 标定软件队列 elif data.startswith(b'\x68'): q_scada.put(data) # 监控后台队列 else: q_debug.put(data) # 调试镜像队列

这个函数每次读到一个物理口数据块就调用一次,数据块边界不等于业务帧边界,所以下游拿到后仍要做完整组帧。过滤的价值在于降低下游程序的工作量,不是替代它。

6.2 把分线器做成 systemd 服务,掉线自动重连

分发器是现场链路的地基,不能跟着会话退出。我习惯把它固定成系统服务,加入自动重启参数,并且关闭标准输入输出避免被挂起:

[Unit] Description=Serial Port Splitter Service After=network.target [Service] ExecStart=/usr/local/bin/serial_splitter.py Restart=always RestartSec=3 StandardOutput=journal [Install] WantedBy=multi-user.target

Restart=always配合RestartSec=3防止脚本崩溃后拖垮整条数据链路;日志走 journal,排查时用journalctl -u serial-splitter直接看。现场跑了一年,最大的教训是:不要试图在分线器里处理所有协议,它只是搬运工;也不要相信任何不经过双开测试的分线工具。做这个方向,先把广播模式验证清楚,再谈并发和调优,永远别让一个临时脚本直接上产线。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询