
简介这是一份面向嵌入式初学者的模拟串口通信实现工程演示在缺少物理UART外设时如何通过软件编程在单片机引脚上模拟串口收发。资源基于Keil C51开发环境包含发送与接收核心代码可直接用于学习串口时序模拟、引脚电平控制及数据帧格式解析。压缩包共23个文件整体仅39KB涵盖C源码、Keil工程配置uv2、opt、plg、lnp、编译输出hex、obj、lst、m51以及调试备份DSN、DBK等完整保留了从源码编写、编译链接到烧录调试的工程链条。当前已有47人浏览学习。借助核心的发送与接收C源码可掌握模拟串口通信的软件实现思路生成的hex文件可直接烧录到单片机运行验证工程文件便于在Keil中打开修改适合作为课程设计、毕业设计或日常项目中的串口扩展方案。1. 模拟串口没有硬件时串口调试靠的是什么做嵌入式或者上位机开发最怕的不是逻辑写不出来而是手里没有串口设备。板子还在路上、仪表还在产线排队可代码已经写了一半联调窗口又紧。模拟串口就是在这种时候救场的它靠驱动在系统里虚拟出一对互相打通的 COM 口一端写入的数据原样从另一端流出表现上跟一根实体串口线一模一样。协议栈、上位机界面、设备轮询逻辑都能先在这条虚拟链路上跑通真机到手后再去做时序收尾。这篇笔记按一线实操来写先分清几种“模拟”的区别再给出 Windows 和 Linux 上从建口到 Modbus 联调的完整命令最后把常见的坑一次性讲透适合手里没有硬件、想立刻往下推的工程师。2. 先分清三种“模拟串口”虚拟串口对、调试助手与设备仿真器“模拟串口”这个词在项目里指代的东西其实有三种不少踩坑就是从称呼开始的。把这三类分清楚后面每一步选型都不会走偏更重要的是你不会拿着一个串口调试助手去充当“模拟设备”结果发现自动应答根本做不出来。2.1 串口调试助手只是“打开端口收发数据”没参与模拟SSCOM、XCOM 这类调试助手确实是大家电脑里都有的串口工具它们的本质是枚举系统里的 COM 口选一个打开然后做两件基础的事把输入框里的字符或十六进制字节发出去把接收到的字节按 ASCII 或 HEX 显示出来。很多人说“我开个模拟串口测一下”其实只是打开了某个口手动点发送这不叫模拟。调试助手适合看协议交互过程、手动触发一条报文、确认设备有没有回应。不适合当一个能自动应答的假设备因为自动应答需要按协议拆帧、算校验、定时回包这些功能调试助手基本上不具备。我见过有人拿调试助手顶了一个称重仪表的空位上位机发查询帧他只能一边盯光标一边手工把回包敲进去发一帧两帧可以跑压测根本撑不住。所以用法要摆正调试助手是观察窗不是模拟器。2.2 虚拟串口对驱动层造出一根互相打通的“线缆”这才是标题里“模拟串口”最核心的东西虚拟串口对。比如 com0com 这样的驱动装上之后系统里会多出两个互相配对的 COM 口。程序向 A 口写入的字节驱动会直接投递到 B 口的接收缓冲区反过来向 B 口写A 口的读缓冲同样能收到。整个过程没有 UART 芯片、没有电平翻转、没有物理线缆但两个口的行为表现和一根零调制解调器线接在一起完全一致。用途很直接上位机程序独占一个口另一个口挂一个我们自己写的模拟设备程序两边在同一个电脑上就能把设备交互跑起来。这个方案比调试助手强在模拟端是代码能自动应答、能按协议回包、能持续推数据。我在 Windows 上一般用 com0comLinux 上用 socat 直接造伪终端对下文会给出具体命令。2.3 设备仿真器让上位机以为对面真有一台仪表再往前走一步就是设备仿真器它不是在驱动层面做虚拟口而是把某个仪表或设备的协议应答逻辑完整实现出来。拿 Modbus Slave 来说它占一个串口等待主站发来 Modbus RTU 报文然后按配置好的寄存器表返回应答。你可以布置保持寄存器、线圈、离散输入、输入寄存器的初始值甚至周期性地改变某个寄存器的值模拟温度、液位、流量这些现场量在动。设备仿真器解决的是协议交互层的验证和虚拟串口对配合使用效果最好虚拟串口对提供通路设备仿真器提供“对面那台设备”。上位机的轮询、超时重发、告警、历史曲线逻辑都能在没有真机的情况下完整验证一遍。一个典型组合是com0com 建一对口Modbus Slave 占一端当从站上位机连另一端直接验证读寄存器逻辑如果上位机要同时连好几台设备就建多对虚拟串口每个口挂一个仿真器实例。三种东西的边界下面这张表平时选型比较直观类型本质典型工具适用场景局限串口调试助手端口收发与显示SSCOM、XCOM抓包、手动发帧、看回包不能自动按协议应答虚拟串口对驱动级虚拟线缆com0com、socat让两个程序通过 COM 口互联只提供通路不懂协议设备仿真器协议级从站/设备Modbus Slave联调协议解析、界面刷新针对特定协议通用性有限实际项目里最少要备两类虚拟串口对是底座设备仿真器或者自写模拟程序是“那台假设备”。3. 用虚拟串口对建出 COM 口Windows 与 Linux 命令与最小收发验证虚拟串口对是整套模拟方案的地基。这一章直接给可复现的建口命令和验证脚本新手照着敲就能跑通熟手可以直接跳过去看参数说明。3.1 先选对工具Windows 用 com0comLinux/macOS 用 socat接真实硬件的方案我一般固定两套。Windows 上装 com0com理由很直接免费、开源、没有试用期限制命令行可控适合脚本化。VSPD 这类商业软件功能更丰富带独立界面和串口监听功能但个人开发阶段没必要为它付费。Linux 和 macOS 上更好办socat 一条命令就能造一对伪终端无需额外驱动权限要求也简单。选工具还要看目标程序依赖什么。如果只是普通文本串口com0com 和 socat 都没问题如果程序里用了自定义波特率、硬件流控断言建议先翻驱动文档确认支持情况别等装上再后悔。常见开发场景里9600、19200、115200 这三个波特率基本都是稳的。3.2 创建一对虚拟串口并固定端口名Windows 下安装好 com0com 驱动后默认会创建一对 CNCA1、CNCB1 端口。但开发时我更习惯把端口名固定成业务化的名字比如 COM10、COM11这样测试脚本里写死就行不用每次去设备管理器里看数字。以管理员身份打开命令提示符然后执行cd C:\Program Files (x86)\com0com setupcnf.exe uninstall setupcnf.exe install portnameCOM10 portnameCOM11 setupcnf.exe第一行进到 com0com 的安装目录路径以实际安装位置为准。第二行把默认端口对清干净避免 CNCA1、CNCB1 这种名字干扰后续脚本。第三行是关键创建一对名为 COM10 和 COM11 的虚拟串口portname参数成对出现名字不能和系统里已有端口冲突。最后一行不带参数运行setupcnf.exe会打印当前所有虚拟串口对的配置用来确认这对口创建成功。创建完到设备管理器里看串行设备下会多出 COM10、COM11 两个项它们的驱动类型一样区别只在于它们是配对关系。注意不要手动去设备管理器里改这两个口的编号改乱了配对关系数据通路会变得很诡异排查起来相当难受。提示install 参数执行后当前全部虚拟端口对都会被重建如果机器上有正在跑的测试进程先停掉再执行。Linux/macOS 上需要能固定路径的伪终端对socat 一条命令就能办到socat -d -d pty,raw,echo0,link/tmp/ttyV0 pty,raw,echo0,link/tmp/ttyV1两个 pty 实例在参数上完全对称pty表示创建伪终端raw是原始终端模式不经过行编辑处理echo0禁掉本地回显避免数据到了终端又被原样弹回去造成重复。link/tmp/ttyV0和link/tmp/ttyV1是重点它会为系统动态分配的 pts 编号文件建一个固定符号链接测试程序里永远写这两个路径不用每次去/dev/pts里找编号。跑起来后这个进程会一直挂在终端前台更实用的做法是加放后台或者把命令写进启动脚本。如果机器上没有 socatDebian/Ubuntu 系用apt install socat装一下即可。3.3 pyserial 最小收发验证一端写、另一端读端口建好先用 Python 的 pyserial 做一次最小验证确认两边连通。Windows 上传 COM10、COM11Linux 上传/tmp/ttyV0、/tmp/ttyV1下面统一以 Linux 写法为例import serial import time port_a serial.Serial(/tmp/ttyV0, 115200, timeout2) port_b serial.Serial(/tmp/ttyV1, 115200, timeout2) port_b.write(bping) # 从 B 端发出一帧测试数据 time.sleep(0.2) # 给驱动一点点投递时间 resp port_a.read(4) # A 端等 4 个字节 print(收到:, resp)这里timeout2表示 read 最多等 2 秒凑不够 4 字节也会先把已有数据返回避免程序永久卡在读上。time.sleep(0.2)不是必须的但给 200ms 能让驱动调度更平稳Windows 上虚拟串口驱动的投递延迟偶尔比预期高稍等再读更稳。如果打印结果是收到: bping链路就完全通了。跑这个脚本如果遇到打不开端口先确认是不是自己的其他进程占用了端口再看权限。Linux 下权限错误一般是当前用户不在 dialout 组执行sudo usermod -aG dialout $USER后重新登录即可Windows 下优先怀疑端口被别的程序占用而不是驱动坏了。3.4 串口参数怎么设波特率、数据位、校验位、停止位与流控虚拟串口对在数据通路层面不太校验两端参数是否一致你甚至可以在 9600 上写、115200 上读数据照样通。这是个陷阱虚拟环境里参数不匹配也能跑真机一接就全乱了。建议从第一天起就把会话参数固化成真实工程参数下面是我常用的模板表参数常用值说明波特率9600 / 19200 / 115200两端必须一致虚拟环境不强制但真机强制数据位8绝大多数 UART 应用默认停止位1常见串口仪表默认校验位N / EModbus 常 8E1普通透传常 8N1流控None / RTS先用 None 调通协议真机需要再开一个容易忽略的细节是虚拟串口对在驱动层会把 RTS、CTS、DTR、DSR 这些 modem 信号按线缆语义配对所以程序里做流控断言不会报错。但这不意味着真实硬件时序被复刻了它只保证了电平状态在逻辑上联通真正的 UART 时序、FIFO 深度、驱动调度延迟都和真机有差异。后面接真机时需要重点验证的就是这些差异点。4. 把模拟串口当设备用写一个能应答的从机打通 Modbus RTU 链路测试虚拟串口对建立后只是通道通了对面还没有“设备”在监听。这一章把另一端做成能应答的从机再连到上位机做完整链路测试这是把模拟串口从“玩具”变成“测试工具”的关键一步。4.1 先做一个最简单的从机收到什么原样回什么协议联调的第一步永远是确认最底层通路主站发一帧从机能不能收到能不能回发。别一上来就上 Modbus纯粹的回显能快速把串口参数、占口这些低级问题滤掉。import serial dev serial.Serial(/tmp/ttyV0, 19200, timeout0.1) while True: data dev.read(64) # 最多读 64 字节没有就等 100ms if data: print(从机收到:, data.hex()) dev.write(data) # 原样回给主站这里timeout0.1是为了让循环空转时别把 CPU 烧满同时保证数据一到达就能在 100ms 内被取走。read(64)的单次上限刻意大于常见协议帧长度避免把一帧拆成多次返回。运行这个脚本再把上位机的串口指向/tmp/ttyV1如果上位机能收到和自己发送一致的回包说明虚拟链路和两端参数配置正确。回显跑通后把dev.write(data)这行换成协议应答逻辑就是一个最小可用的设备模拟器。4.2 升级到 Modbus RTU用 pymodbus 起一个从站回显只验证通路真正的协议交互需要从站按 Modbus RTU 报文格式应答。工程里常用 pymodbus 库直接起从站依赖一条命令装齐pip install pymodbus pyserial-asynciopymodbus 缺串口支持时会报 ImportErrorpyserial-asyncio 就是补这块的。装完用下面的脚本起一个寄存器从站以 pymodbus 3.x 的写法为准from pymodbus.server import StartSerialServer from pymodbus.framer import ModbusRtuFramer from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext store ModbusSlaveContext( diModbusSequentialDataBlock(0, [0] * 16), # 离散输入 coModbusSequentialDataBlock(0, [0] * 16), # 线圈 hrModbusSequentialDataBlock(0, [40001] * 16), # 保持寄存器 irModbusSequentialDataBlock(0, [7] * 16), # 输入寄存器 ) context ModbusServerContext(slaves{1: store}, singleTrue) StartSerialServer( context, framerModbusRtuFramer, port/tmp/ttyV0, baudrate19200, timeout1, )ModbusSlaveContext 定义从站的四个数据区每个区用 ModbusSequentialDataBlock 初始化连续地址的数据。保持寄存器初始值设成 40001模拟一台仪表上电后的默认读数slaves{1: store}表示从站地址 1 占用这个存储区singleTrue则允许所有从站地址共享同一份数据。StartSerialServer 的参数里framer必须用 ModbusRtuFramer这是 RTU 模式的帧解析器port指向虚拟串口对的一端另一端留给主站程序baudrate和上位机保持一致即可。脚本会阻塞运行CtrlC 退出。4.3 主站联调读寄存器、验证数据一致从站脚本占着/tmp/ttyV0主站程序或测试脚本就接/tmp/ttyV1。用 pymodbus 的客户端做一次最直接的读取from pymodbus.client import ModbusSerialClient client ModbusSerialClient(port/tmp/ttyV1, baudrate19200, timeout2) client.connect() rr client.read_holding_registers(0, 4, slave1) print(读到保持寄存器:, rr.registers) client.close()read_holding_registers 的第一个参数是起始地址第二个是数量slave 是从站地址必须和从站脚本里slaves{1:...}的编号对上。如果打印结果是[40001, 40001, 40001, 40001]说明主站和从站两个进程通过虚拟串口对完成了完整的 Modbus RTU 报文交互。到这里上位机剩下的工作就是把读回来的寄存器数值映射成温度、液位这些工程量。想验证写寄存器换成client.write_register(0, 888, slave1)再回头看从站存储区值已经变了。这个“从站地址 寄存器地址 数值”的三角验证是打通 Modbus 联调最快的方式。4.4 不想写代码就用 Modbus Slave 图形工具如果只是临时联调不想维护一套 Python 从站脚本业界常用做法是开一个 Modbus Slave 图形工具。它占一个串口例如 COM10在界面里填从站地址、寄存器表初始值然后运行工具就按 Modbus RTU 协议等待并应答主站。好处是寄存器值和从站地址能在界面上随时改模拟现场参数变化特别方便缺点是自动化程度有限跑不了回归。我的习惯是短期联调用图形工具长期回归测试用自写脚本两种都备着互不冲突。5. 模拟串口避坑手记端口被占、粘包丢帧与流控“玄学”这一章不是理论全是实际跑过之后留下来的血泪经验。每一条都按现象、原因、解决三个层次写你照着排查比重新翻驱动文档快得多。5.1 端口打不开不是驱动坏了是端口被占用现象脚本运行时报SerialException: could not open port或者一个程序刚打开虚拟串口另一个程序紧接着打开配对端口失败。原因最常见的不是驱动装坏了而是前一个测试进程没退干净把端口句柄占着。串口不像普通文件Windows 下同一端口默认只能被一个进程独占进程被强制结束时句柄没有释放端口就仍然处于占用状态。解决先到任务管理器里把残留的 python、串口调试助手进程都关掉再不行就用setupcnf.exe uninstall全部删除后重建。Linux 下用lsof /tmp/ttyV0查谁占着端口找到 PID 后 kill 掉再重新打开。5.2 数据收不全、粘包串口没有报文边界现象上位机发了一帧 16 字节的查询从机这边dev.read(64)有时一次收到 16 字节有时只收到 4 字节有时两帧粘在一起返回。按固定长度解析的代码就时不时翻车。原因串口是字节流没有报文边界。pyserial 的 read 返回的是当前驱动缓冲区里已有的字节数数据到达时间受驱动调度影响所以一次读到的长度不可能保证正好是一帧。解决不要按“读固定长度等于一帧”去实现要按帧协议解析。我的常用写法是先从机里读缓冲再按帧头里的长度字段把剩余字节补齐import serial dev serial.Serial(/tmp/ttyV0, 19200, timeout1) buf b while True: data dev.read(64) if not data: continue buf data while len(buf) 4: if buf[0] ! 0xAA: # 帧头校验 buf buf[1:] continue length buf[1] if len(buf) length 2: frame buf[:length 2] buf buf[length 2:] print(完整帧:, frame.hex())这段逻辑的关键先确认帧头再从长度字节算出整帧大小攒够才裁切。虚拟串口上最容易被忽略的就是这一点调通链路后一定要用协议解析去读数据而不是靠运气等整帧。真实设备上驱动中断延迟更高拆帧和粘包会更频繁迟早要面对。5.3 流控是“玄学”虚拟串口上行真机不一定行现象虚拟串口上关闭流控数据交互非常流畅没有任何丢帧。换到真实设备后同样的软件配置开始间歇性丢字节有时每帧都缺几个。原因虚拟串口对把数据直接从一个驱动的读缓冲送到另一个的写通道绕过了真实 UART 的 FIFO、超时中断和流控时序。它不产生真实的电平竞争所以流控实际上被“架空”了程序里的 RTS/CTS 断言在虚拟环境永远不会触发阻塞。解决虚拟环境只用来验证协议逻辑不拿它验证时序。如果目标设备硬件上用了硬件流控从一开始就在测试代码里打开 RTS/CTS至少让断言逻辑跑一遍同时做好心理预期接真机后的第一轮一定要专门压时序不能因为在虚拟串口上顺滑就认为大功告成。这条算是我在这上面吃过亏之后换来的习惯。5.4 com0com 安装失败驱动签名和系统架构现象设备管理器里虚拟串口项是黄色感叹号setupcnf 命令报错或者安装完系统里一个口都看不到。原因com0com 属于内核驱动Windows 对驱动签名有强制校验而旧版 com0com 驱动在新系统上过不了签名另外 32 位系统装了 64 位驱动的包也会有类似表现。解决先到设备管理器里把黄色感叹号的设备卸载重启再下载与当前系统架构匹配的版本重新安装。如果仍然提示签名问题常规做法是在 Windows 恢复环境的启动设置里选择“禁用驱动强制签名”然后重启再装一次装完恢复正常启动。注意这种方式只对调试机用别在生产环境机器上开着。5.5 虚拟串口太多系统枚举变慢程序启动卡顿现象为了模拟多台设备一口气建了七八对虚拟串口某个上位机软件启动时枚举串口明显变慢甚至卡几秒。原因不少上位机启动时会全量扫描系统里的 COM 口逐一查询每个口的状态。虚拟端口数量一大全量查询要等驱动逐个响应时间线性拉长。解决不用的虚拟串口对随手删掉别留着占资源同时把确实要用的虚拟口安排在 COM50、COM200 这样的高位。很多程序默认只枚举 COM1 到 COM32 或 COM64高位端口能避开默认枚举列表也减少对其他程序的干扰。我在自动化环境里都是按测试脚本动态建口、测试结束立即销毁保证系统里不残留一堆无主端口。6. 让模拟串口成为测试基础设施多路并联、杂波注入与回归测试脚本当模拟串口的用法不再局限于“临时拉通一根线”它就可以变成一套反复复用的测试环境。这里讲三个我常用的进阶做法。第一个是多路并联。上位机往往同时连多台设备这时脚本里一次性建多对虚拟串口每对口的从站端挂一个模拟进程做完一轮完整的联动测试。比如一台设备负责上报温度、一台负责上报报警状态把两个模拟进程分别挂到两对口的从站端上位机正常启动后看到的就是“两套仪表在线”联动逻辑在没有现场设备的情况下直接验证。第二个是杂波注入。真实串口环境里会有干扰电磁噪声、接口氧化都会让对端收到意料之外的字节。别等到现场才发现协议栈碰到垃圾数据就崩用一段注入脚本提前测import serial import random port serial.Serial(COM200, 115200, timeout1) for _ in range(2000): noise bytes(random.randrange(256) for _ in range(random.randint(1, 16))) port.write(noise)这段代码往虚拟串口一端持续写入随机长度、随机内容的字节另一端挂着自己的协议解析程序。跑完看解析程序有没有丢帧、卡死异常处理分支有没有被正确触发。我自己最常翻车的不是正常流程而是把虚拟串口调通就当任务完成真机一接就露馅杂波注入能把这类偶发问题提前暴露出来。第三个是把建口、开从站、跑用例做成一个启动脚本每次协议改动后全量回放一遍。socat 和 com0com 都支持命令行整套环境可以无人工介入搭建很适合接进自动化测试。这样模拟串口就不再是手工玩具而是真正扛得住回归压力的测试基础设施。这几个做法在我这里反复用省掉的不只是等硬件的时间还有现场排错的成本。希望帮到你。本文还有配套的精品资源点击获取