ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

串口模拟工具:虚拟串口对与应用层从机实现自动化测试

串口模拟工具:虚拟串口对与应用层从机实现自动化测试 串口这东西看着简单两根线一接波特率对上就能跑数据但真到了要反复测试、要自动化回归的时候物理硬件就成了最烦人的那一环——板子只有一块被测固件三天两头要重烧测试同学排队等设备。我最早做串口协议测试的时候桌上摆着四五块开发板加一堆 USB 转串口线谁抢占哪个口全靠一张便签纸记着出了问题是设备坏了还是协议错了都分不清。后来我干脆自己写了个串口模拟工具把从机那一端用软件演出来从此测试不用等硬件通宵跑回归也不心疼。这篇就把串口模拟工具从设计到实现、再到怎么验证它自己靠不靠谱完整讲一遍适合做嵌入式、工控、IoT 固件和上位机测试的朋友参考。1. 没有真设备为什么还要造一个串口假货1.1 硬件依赖把自动化测试卡在门口串口测试和网络接口测试最大的区别在于网络测试你可以随时起一个本机服务串口测试你默认必须有一根真实的线、一个真实的对端。这个必须在手工调试阶段还能忍一旦要做持续回归就彻底崩了。我统计过我们早期一个项目的测试成本每次回归需要 3 块板子、2 个 USB 转串口模块、1 台固定主机还要人工确认板子是不是处于干净状态平均一次全量回归的准备工作就要 20 分钟而这 20 分钟里有 15 分钟纯粹是在伺候硬件。更要命的是硬件本身引入了不可控噪声。真实串口链路上线的质量、供电、晶振偏差、驱动芯片的时序都会影响结果。你以为是协议解析出了问题结果换一根线就好了你以为是代码 bug结果发现是对面板子温度上来了波特率飘了。测试结论因此变得极不可信——问题到底出在被测代码还是出在硬件链路没人说得清。模拟工具的第一个价值就在这里把链路这一段的不确定性拿掉让测试结论只指向被测对象。1.2 模拟器要解决的是通和像两件事很多人一提串口模拟脑子里想的就是让端口能收发数据就行。这只解决了通。但真正能替代硬件的模拟器必须同时解决像——也就是说它要像一个真实的、有脾气的外设那样工作。所谓像体现在三个层面第一是协议层要像帧头帧尾、校验方式、超时重发、异常应答码全都要按真实设备的行为来第二是时序层要像设备收到命令到给出应答之间的延迟、字节间间隔、不完整帧的处理都要贴近实物第三是故障层要像真实设备会丢帧、会返回错误码、会在特定条件下长时间不应答模拟器得能把这些情况演出来否则你测出来的永远是理想路径。我见过太多模拟工具只做了通这一层结果测试全绿一上真设备就翻车。原因很简单真实世界里的从机不会永远按你期望的顺序秒回。所以从设计之初就要明确——模拟器不是个 echo 服务器它是个可编排的从机。这一点想通了后面的架构和实现才有方向。2. 虚拟串口对和应用层模拟器选哪条路2.1 内核/驱动层虚拟串口对让两端都以为是真口实现串口模拟有两条主流路线第一条是造一对虚拟串口在操作系统层面创建两个互相连通的伪终端设备一端写入的数据会从另一端读出就像两个物理口用线短接了一样。在 Linux 上可以用socat直接起一对伪终端socat -d -d pty,raw,echo0 pty,raw,echo0执行后它会打印出两个设备路径比如/dev/pts/3和/dev/pts/4你把上位机指到其中一个、把从机程序指到另一个两边就像插了一根线。这种方式的优点是对上层完全透明——被测程序不需要任何改动它面对的就是一个真正的/dev/tty*设备open、termios配置、read/write全都照常。Windows 上也有类似的虚拟串口对驱动方案装完后在设备管理器里能看到一对关联的 COM 口两端接起来就能通。但这条路有个明显的限制它只提供了通道没提供从机。你仍然需要自己写另一端跑什么。也就是说虚拟串口对解决的是物理链路的问题不解决设备行为的问题。2.2 应用层从机模拟把协议逻辑握在自己手里第二条路线干脆不碰驱动直接在应用层用一个程序假装从机。这个程序通过真实的物理串口或虚拟串口打开一端然后自己解析收到的帧、按照脚本决定回什么。它的核心是一个帧解析状态机 应答决策表。相比虚拟串口对它多出来的正是设备行为这一层什么时候应答、应答什么、什么时候故意延迟、什么时候返回错误码全在你掌控之中。我最终选的是两者组合用虚拟串口对造通道用应用层程序当从机。这样上位机那侧零改动从机那侧的行为又完全可编程。如果只有一台机器又想彻底脱离硬件就把虚拟串口对和应用层从机跑在同一台机器上上位机连一个伪终端从机连另一个伪终端整套闭环不依赖任何物理设备。2.3 对比与组合使用的判断依据维度虚拟串口对驱动/伪终端层应用层从机模拟对被测程序透明完全透明像真口需要对端程序配合能否模拟设备行为不能只是通道可以完全可编程部署复杂度需要装驱动或起 socat一个脚本即可稳定性压测通道本身很稳取决于脚本实现质量异常注入能力无强可编排任意异常判断依据其实就一句话如果你只是想让程序能跑起来用虚拟串口对就够了如果你想测协议逻辑和异常路径就必须上应用层从机。我的建议是别二选一把虚拟串口对当基础设施把应用层从机当核心资产两者叠加才是完整的模拟方案。下面重点讲应用层从机怎么实现。3. 用 Python 搭一个能按剧本应答的从机模拟器3.1 串口对象初始化里最容易设错的几个参数选 Python 是因为 pyserial 生态成熟、改起来快做测试工具没必要上 C。初始化一个串口对象看起来就一行但参数设错会导致非常隐蔽的问题import serial slave serial.Serial( port/dev/pts/4, baudrate115200, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout0.05, # 读超时控制轮询节奏 inter_byte_timeout0.005, # 字节间超时用于切分不完整帧 xonxoffFalse, rtsctsFalse, dsrdtrFalse, )这里timeout和inter_byte_timeout是两个最容易被忽视的参数。timeout决定read()在没有数据时最多阻塞多久设太长会让异常注入的响应变迟钝设太短会导致 CPU 空转。inter_byte_timeout更关键它决定了一帧什么时候算结束——如果你的协议没有明确长度字段靠的就是字节间静默来分帧这个值必须小于真实设备的最短帧间隙否则你会把两帧粘在一起或者把一帧切开。我的经验值是把它设成传输 2 个字节所需时间的 1.5 倍左右115200 波特率下大约 150 微秒到 200 微秒。3.2 帧同步与状态机别用 readline 处理二进制协议新手最容易犯的错是用readline()去读数据指望着遇到换行符就返回一帧。这在文本协议里勉强能用在二进制协议里必炸——因为数据里到处都可能是 0x0A。正确做法是自己维护一个接收缓冲区用状态机逐字节推进。下面是一个针对帧头 长度 载荷 校验结构的解析骨架HEADER 0xAA55 buf bytearray() def feed(data): frames [] buf.extend(data) while True: # 同步到帧头 idx buf.find(HEADER.to_bytes(2, big)) if idx 0: buf.clear() break if idx 0: del buf[:idx] if len(buf) 4: # 帧头2 长度2 break length int.from_bytes(buf[2:4], big) total 4 length 1 # 载荷 1字节校验 if len(buf) total: break # 帧还没收全等下一批 frame bytes(buf[:total]) del buf[:total] if verify(frame): frames.append(frame) return frames这个循环里有个关键设计收不全就退出循环、保留缓冲区、等下一批数据。很多模拟器丢帧就是因为收到一半就急着解析。另外verify里要处理校验失败的情况——是丢弃还是记录真实设备通常丢弃并可能回一个错误码模拟器要能配置这两种行为。3.3 剧本化的应答与异常注入有了帧解析接下来是怎么回。我把它抽象成一张规则表每条规则包含匹配条件 → 应答内容 → 延迟 → 是否注入异常rules [ {match: lambda f: f[4] 0x03, # 功能码 reply: build_read_reply, delay: 0.002, flaky: 0.0}, {match: lambda f: f[4] 0x10, reply: build_write_reply, delay: 0.005, flaky: 0.1}, # 10% 概率丢应答 ]delay用来模拟设备处理时间flaky用来按概率丢帧或超时不应答——这正是手工测试做不到、而真实场景里一定会出现的情况。比如模拟一个车载传感器正常 2 毫秒回但每隔 100 帧会有一次 200 毫秒的卡顿上位机的超时重发逻辑只有在这种场景下才能被测出来。有了剧本表你甚至可以把整个会话录下来作为回归的固定输入。3.4 接进 pytest让每次提交都跑一遍链路模拟器最大的价值是能进 CI。做法是起一个 fixture把从机线程拉起来让上位机逻辑连上去跑断言import pytest, threading pytest.fixture def slave_sim(): stop threading.Event() t threading.Thread(targetrun_slave, args(stop,), daemonTrue) t.start() yield stop.set() t.join(timeout1) def test_read_command(slave_sim, upper): assert upper.read_register(0x10) 0x1234注意从机要跑在独立线程或独立进程里别和被测逻辑抢同一个串口对象的读。另外每个用例结束要确保端口被干净释放否则下一个用例打开会报设备忙。我习惯在 fixture 里加一段重试等待等伪终端真正就绪再返回不然偶发的启动竞态会让 CI 间歇性飘红。4. 怎么验证模拟器演得像数据、时序、边界4.1 数据完整性校验与时序抖动测量模拟器自己也是代码也会出错所以必须有一套验证模拟器本身的方法。第一个维度是数据完整性让模拟器连续收发大量已知模式的随机数据两端各自计算校验和或哈希比对是否一致。这一步能抓出缓冲区处理、分帧、粘包这些底层 bug。我一般跑 10 万个随机长度的帧覆盖从 1 字节到 512 字节的各种情况确保每一个字节都对得上。第二个维度是时序。在模拟器里给每个帧打上发送时间戳在接收端打上到达时间戳统计端到端延迟的均值和抖动。这一步的目的是确认我配置的delay参数是否真的生效以及伪终端通道本身会不会引入额外的排队延迟。实测下来伪终端通道在空闲时延迟基本在亚毫秒级但一旦发送端写得太快、接收端读得太慢内核缓冲就会堆积延迟会突然飙升——这个现象在压测里非常明显。4.2 边界值和异常帧的注入测试真正考验模拟器的是异常路径。要专门构造一批坏帧喂给它校验错误、长度字段超大、长度字段为零、帧头缺失、半截帧后长时间无后续、连续两帧粘在一起。好的模拟器对每种情况都有明确且可预期的行为而不是抛异常崩掉。我建议把这些坏帧整理成一张测试用例表异常类型期望行为常见错误实现校验错误丢弃并可选回错误码直接当正常帧处理长度字段过大丢弃或限长保护内存无限增长直到崩溃帧头缺失滑动窗口重新同步缓冲区一直不清半截帧 超时超时后丢弃残留残留污染下一帧两帧粘连正确切分成两帧只解析出第一帧就停这张表基本上就是模拟器质量的分水岭。能把最后三行处理对的实现才敢放进持续回归。4.3 长时间压测里盯哪些指标短跑快不代表长跑稳。模拟器经常要在 CI 里连续跑几小时所以要关注几个长期指标内存占用是否随时间增长缓冲区有没有泄漏、文件描述符是否稳定有没有忘记 close、CPU 占用是否恒定有没有忙等。我踩过一次坑——模拟器在收不到数据时用了while True: read()的忙等结果单个测试进程把 CPU 拉满跑多了把 CI 机器拖垮。改成阻塞读加超时之后问题就没了。这些指标不需要专门的工具ps、/proc/pid/status里就能看到。5. 实测踩过的坑与对应的解法5.1 缓冲、flush 时机与发出去对方收不到最早遇到的一个怪现象模拟器明明调用了write()对端却半天收不到数据。排查下来是缓冲没刷。串口对象在某些模式下带写缓冲数据先攒在用户态缓冲里得等缓冲区满或者显式flush()才真正推到底层。对于实时性要求高的应答写完就该 flush或者关掉写缓冲。另一头也有坑读的时候如果开了行缓冲模式二进制数据里的换行符会让读取提前返回。我的做法是收发都走原始模式自己控制分帧完全绕开这些缓冲策略。还有一个更隐蔽的问题伪终端默认可能带回显echo。也就是你往一端写的数据会原样被回显回来。如果没关掉你的模拟器会收到自己发出的内容导致误判成对端的应答。用 socat 起伪终端时务必加raw,echo0自己写程序打开伪终端时也要记得把ECHO关掉。这个坑我见过不止一个人踩表现为数据莫名其妙重复出现。5.2 Linux 权限、设备名漂移与 udev 固定真机上用 USB 转串口时设备名会漂移——今天插上是/dev/ttyUSB0重启后可能变成/dev/ttyUSB1脚本里写死端口号迟早出事。靠谱的做法是用udev规则按芯片序列号把设备绑定到固定的软链接名比如/dev/ttySlave脚本只认这个名字。权限方面普通用户默认打不开串口设备得把用户加进对应组或者配置规则否则脚本在 CI 里跑就是权限拒绝。另外还有一个和驱动相关的经典问题某些 USB 转串口芯片在系统里需要额外驱动插上后设备名出现的方式和时序都不确定。脚本启动时如果立刻去找设备很可能扑空。我通常加一个轮询等待设备出现的小循环最多等几秒而不是一上来就open然后报错退出。5.3 流控、波特率和 485 方向脚这些隐藏变量最后说几个配置层面的隐性坑。第一是流控如果你不打算用硬件流控务必把rtscts和xonxoff都关掉否则某些情况下发送会被莫名其妙地挂起。第二是波特率看起来对了其实没对两端配置一致才通但更隐蔽的是某些伪终端不真正遵循波特率也就是说你在伪终端上设 115200 或者 9600数据传输速度其实不受影响——测试逻辑层够用了但如果你想测高波特率下的吞吐极限就得用真实硬件伪终端测不出来。第三是RS485 半双工的方向控制。485 是半双工发送和接收共用一对差分线需要靠一个方向脚在收发之间切换。模拟 485 设备时如果方向切换的时机不对切换太早会把最后一个字节切掉切换太晚会和对方的应答撞上就会出现丢尾字节或总线冲突。用 USB 转 485 模块时方向通常由模块自动控制但你仍然要确认它的自动切换延迟够不够小。这一块是模拟工具和真实链路差异最大的地方我的建议是协议逻辑用模拟器测但方向切换相关的时序问题最好还是留一到两个真机用例兜底。整套下来串口模拟工具真正省的不是那点插线时间而是把测试结论到底指向谁这件事彻底理清了。我自己最受用的一点是把异常注入做成剧本表之后那些以前要靠碰运气才能复现的偶发丢帧问题终于变成了每次提交都会跑的固定用例。你在做类似工具的时候也别急着追求功能多先把分帧和异常这两块打磨稳其余的都是锦上添花。
返回列表