第一次汇报学习笔记:把“能用”的串口收发做扎实——端口枚举、打开关闭、参数锁定、收发计数、暂停显示、接收保存。
做串口工具时,最容易掉进两个坑:串口参数一团乱(打开后还能改参数,导致状态不一致)和收发逻辑混在 UI 里(改一个小功能牵一发动全身)。这篇记录我在 Qt(QSerialPort)里,把串口“基础能力”搭稳的过程。
1. 串口端口枚举:既要“刷新”,也要“别抖”
Qt 提供 QSerialPortInfo::availablePorts() 获取当前系统所有可用串口。最直观的做法是每次刷新就 clear() 再 addItems(),但这样会带来两个体验问题:
-
下拉框闪动:列表一刷新就跳回第一项;
-
用户选择丢失
更好的策略是:
-
先记录旧的端口名(例如把“COM3: xxx”按冒号切开);
-
构造新列表;
-
如果新列表与上次一致就不更新 UI;
-
更新后尝试恢复旧选择,恢复失败再退化到第一项。
另外,很多场景下串口会动态出现/消失(插拔 USB 转串口、创建虚拟串口),所以我会做“自动刷新”。常见做法是 QTimer 每秒刷新一次,并且在用户点开下拉框时再做一次“立即刷新”(后面第 3 篇会写到 eventFilter)。
2. 打开/关闭串口:把参数设置做成“原子操作”
串口打开流程建议按“验证 → 设置 → open → 锁 UI”组织:
-
验证是否有端口:下拉框为空就先刷新一次;仍为空则提示;
-
解析端口名:不要直接用整个下拉框文本,避免把描述带进去;
-
设置波特率/数据位/校验位/停止位;
-
open(QIODevice::ReadWrite); -
打开成功后禁用参数控件(端口、波特率等),避免打开后还改参数导致“看起来改了但实际没生效”。
关闭串口时则反过来:
-
close(); -
恢复参数控件可编辑;
-
停止依赖串口的功能(比如定时发送、文件发送、协议组帧等待等);
-
把“停止显示”这类状态也复位,避免下次打开串口 UI 处在奇怪状态。
这个思路的核心是:打开/关闭串口不仅是 I/O 行为,也是 UI 状态机的一次切换。把它当成事务去处理,后续扩展会轻松很多。
3. readyRead 接收:一次 readAll 不等于“一帧”,但计数要一致
readyRead 信号触发后,最常见代码是:
QByteArray buf = serial.readAll();
这里要注意:串口是字节流,readAll() 取的是“当前缓冲区已有的字节”,不保证是协议意义上的完整帧。即便如此,在“通用串口调试助手”的视角里,把一次 readAll 当成一帧用于统计是可接受的(至少统计口径一致、实现简单)。
因此可以做两套计数:
-
字节数:
recvBytes += buf.size(); -
帧数:
recvFrames += 1(以一次 readAll 为单位);
并同步更新界面以及状态栏的 S/R 字节显示。
4. “停止显示”与“保存接收”:让数据通路分层
接收数据通常会做三件事:
-
计数;
-
显示到接收框;
-
保存到文件。
如果把“停止显示”做成简单的 return,会导致:用户暂停显示后连保存也停了。更合理的结构是把通路分成两层:
-
底层通路:计数、保存、协议处理(不受暂停/过滤影响);
-
展示通路:仅负责把数据渲染到文本框(受暂停/过滤影响)。
这样“停止显示”只影响展示,不影响计数与保存。
保存接收数据我倾向于保存原始字节(QFile::write(buf)),而不是保存“显示字符串”。原因是:显示字符串可能是 HEX 格式、带空格、带时间戳甚至带 TX/RX 前缀;保存原始字节更利于后处理(例如用脚本解析)。
5. 自动清屏:性能优化要提前做
QPlainTextEdit 长期追加文本会越来越卡,尤其是日志量大时。常见处理:
-
提供“自动清屏”选项;
-
当
document()->characterCount()超过阈值(例如 20000)就清空; -
清空后重置一些“对齐显示/表头”的内部状态(第 4 篇会详细说)。
这属于“早做早省心”的优化:一开始数据少不明显,但工具一旦上强度(比如持续跑一小时)就会暴露。
6. 小结
这一阶段我最大的收获是:别急着做高级功能,先把串口与 UI 状态机捋清楚。端口刷新不抖、打开关闭原子化、接收通路分层、计数口径一致——这些看起来“很基础”的东西,会决定后续加功能时到底是“搭积木”还是“修泥潭”。







