资讯动态

Qt5串口调试助手V2.0开发实战:从QSerialPort到协议解析插件

发布时间:2026/10/27 1:41:34 来源:尧图企业网站定制
简介这是一份面向嵌入式与工控开发者的QT5串口调试助手V2.0完整工程含可运行程序与全部源代码适合需要快速验证串口通信、学习QT5界面与串口编程的初中级开发者。资源包共80个文件以33个png界面图标、18个h头文件与17个cpp源文件为核心另含qrc资源、ui界面、pro工程及css皮肤样式压缩包约129KB结构紧凑便于二次开发。功能覆盖刷新设备列表、选择串口并设置波特率/数据位/停止位/校验位、开关串口、数据发送与回显、数据保存及定时发送并支持自定义皮肤样式界面友好。代码中可见Modbus设备抽象、数据缓冲、高精度定时器与JSON配置等模块可作为串口通信与QT5工程组织的参考范例。目前已有130人学习下载适合拿来即用或按需裁剪。1. 串口调试助手V2.0为什么Qt5方案值得你重写一遍手头攒了一堆USB转串口模块每次调试都要在sscom串口调试助手、minicom和几个不知名小工具之间来回切换这种体验相信做嵌入式的朋友都懂。基于Qt5的串口调试助手V2.0及源代码这个方向本质上就是让你用Qt5的QSerialPort模块从零搭一个能收能发、能存日志、能画波形的桌面工具顺便把源代码攥在自己手里想加什么协议解析就加什么。它解决的不是“有没有串口工具用”的问题而是“工具不趁手、数据格式不匹配、二次开发没源码”的憋屈。适合谁刚接触Qt想找个练手项目的学生、需要定制协议解析的嵌入式工程师、以及想用C写上位机但不想从Win32 API啃起的开发者。V2.0这个版本号意味着它不是demo得有配置持久化、多编码切换、定时发送这些正经功能。2. 用QSerialPort搭最小可跑框架从工程配置到收发闭环2.1 为什么选Qt5而不是Qt6或纯Win32Qt5的QSerialPort模块在5.1之后已经相当稳定串口枚举、波特率设置、读写超时这些接口都齐了。Qt6虽然也带QSerialPort但很多工控现场的上位机还锁在Qt 5.9到5.15这个区间驱动兼容性经过大量验证。纯Win32写串口不是不行但你要自己处理OVERLAPPED结构、线程同步和消息循环一个ReadFile阻塞就能卡死界面。Qt5的信号槽机制天然适合串口这种异步IO场景readyRead信号一来就读缓冲区不用轮询。另一个现实原因是网上能搜到的Qt5串口调试助手源代码大多基于5.12到5.15你拿过来改比从Qt6迁移回去省事得多。2.2 工程文件里必须加的那几行新建一个Qt Widgets Application在.pro文件里加上串口模块声明。这一步漏了编译时就会报“QSerialPort: No such file or directory”。QT core gui serialport greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET SerialAssistantV2 TEMPLATE appQT serialport是核心它告诉qmake去链接Qt5SerialPort库。如果你在Linux下编译还需要确认libqt5serialport5-dev已经装上否则链接阶段会报undefined reference。Windows下用Qt官方安装器的话这个模块默认勾选不用额外操作。2.3 枚举串口与打开设备的代码骨架下面这段代码放在MainWindow的构造函数里负责扫描当前可用串口并填充下拉框。#include QSerialPort #include QSerialPortInfo void MainWindow::refreshSerialPorts() { ui-comboPort-clear(); const auto infos QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : infos) { // 显示端口名和描述方便区分多个USB转串口 QString label info.portName() ( info.description() ); ui-comboPort-addItem(label, info.portName()); } if (ui-comboPort-count() 0) { ui-comboPort-addItem(无可用串口); } }availablePorts()返回一个QList每个元素包含portName、description、manufacturer等字段。把portName存进userData后续打开串口时直接取避免从显示文本里截取。注意在Windows下COM号大于9时portName会带\\.\前缀QSerialPort内部已经处理了你不需要手动拼。打开串口的逻辑要检查返回值不能假设一定成功。void MainWindow::openSerialPort() { if (serial-isOpen()) { serial-close(); } serial-setPortName(ui-comboPort-currentData().toString()); serial-setBaudRate(ui-comboBaud-currentText().toInt()); serial-setDataBits(QSerialPort::Data8); serial-setParity(QSerialPort::NoParity); serial-setStopBits(QSerialPort::OneStop); serial-setFlowControl(QSerialPort::NoFlowControl); if (serial-open(QIODevice::ReadWrite)) { ui-btnOpen-setText(关闭串口); ui-statusBar-showMessage(已打开 serial-portName()); } else { QMessageBox::critical(this, 错误, serial-errorString()); } }setBaudRate接受int但底层会映射到标准波特率。如果你要设非标准值比如14400Qt5在Windows下会失败Linux下取决于驱动。open(QIODevice::ReadWrite)以读写模式打开如果设备被其他程序占用会返回falseerrorString()会给出“Permission denied”或“Access is denied”。2.4 接收数据与十六进制显示串口数据到达时QSerialPort会发readyRead信号你在槽函数里读全部缓冲区。connect(serial, QSerialPort::readyRead, this, MainWindow::handleReadyRead); void MainWindow::handleReadyRead() { QByteArray data serial-readAll(); if (ui-checkHexDisplay-isChecked()) { // 转成十六进制字符串每字节两位空格分隔 QString hexStr data.toHex( ).toUpper(); ui-textRecv-appendPlainText(hexStr); } else { // 按当前编码解析默认UTF-8可切GBK QTextCodec *codec QTextCodec::codecForName(currentEncoding.toLatin1()); ui-textRecv-appendPlainText(codec-toUnicode(data)); } recvByteCount data.size(); updateStatusBar(); }toHex( )是Qt5.9之后才有的重载低版本要用循环拼。appendPlainText会自动换行如果你不想每条都换行改用insertPlainText并手动控制光标位置。接收字节计数用于状态栏显示方便判断是否有数据丢失。3. 发送区、定时发送与配置持久化V2.0该有的样子3.1 发送模式的三种形态与实现差异发送区一般要做成三种模式ASCII发送、十六进制发送、文件发送。ASCII模式直接把QString转成QByteArray写出去十六进制模式要先把用户输入的“AA BB CC”解析成字节数组文件发送则要分块读取避免一次性加载大文件卡死界面。void MainWindow::sendData() { QByteArray payload; if (ui-radioHexSend-isChecked()) { QString hexText ui-textSend-toPlainText().remove(QRegularExpression(\\s)); if (hexText.size() % 2 ! 0) { QMessageBox::warning(this, 格式错误, 十六进制字符数必须为偶数); return; } payload QByteArray::fromHex(hexText.toLatin1()); } else { payload ui-textSend-toPlainText().toLocal8Bit(); } qint64 written serial-write(payload); if (written -1) { ui-statusBar-showMessage(发送失败: serial-errorString()); } else { sendByteCount written; updateStatusBar(); } }QByteArray::fromHex要求输入是纯十六进制字符空格和换行必须先去掉。toLocal8Bit()在中文Windows下是GBKLinux下是UTF-8跨平台时要注意。write返回实际写入的字节数如果串口缓冲区满可能小于payload长度但Qt内部有写缓冲队列一般不会丢。3.2 定时发送的QTimer用法与精度陷阱定时发送用QTimer间隔设成毫秒。但QTimer的精度受事件循环影响如果你在槽函数里做了耗时操作定时会漂移。QTimer *sendTimer new QTimer(this); connect(sendTimer, QTimer::timeout, this, MainWindow::sendData); void MainWindow::toggleTimedSend(bool enabled) { if (enabled) { int interval ui-spinInterval-value(); if (interval 10) { QMessageBox::warning(this, 警告, 间隔小于10ms可能导致界面卡顿); } sendTimer-start(interval); } else { sendTimer-stop(); } }start(interval)的参数是毫秒实际触发间隔可能比设定值大几毫秒到几十毫秒取决于系统负载。如果你需要微秒级精度QTimer做不到得用高精度计时器或硬件触发。另外定时发送时如果串口没打开sendData里的write会失败最好在toggleTimedSend里先检查serial-isOpen()。3.3 用QSettings保存上次的串口参数V2.0和V1.0的差别就在这些细节上。用户不希望每次打开都重新选COM口和波特率。void MainWindow::saveSettings() { QSettings settings(MyCompany, SerialAssistantV2); settings.setValue(portName, ui-comboPort-currentData().toString()); settings.setValue(baudRate, ui-comboBaud-currentText()); settings.setValue(encoding, currentEncoding); settings.setValue(hexDisplay, ui-checkHexDisplay-isChecked()); } void MainWindow::loadSettings() { QSettings settings(MyCompany, SerialAssistantV2); QString port settings.value(portName).toString(); int idx ui-comboPort-findData(port); if (idx 0) ui-comboPort-setCurrentIndex(idx); ui-comboBaud-setCurrentText(settings.value(baudRate, 115200).toString()); currentEncoding settings.value(encoding, UTF-8).toString(); ui-checkHexDisplay-setChecked(settings.value(hexDisplay, false).toBool()); }QSettings在Windows下写注册表Linux下写~/.config/MyCompany/SerialAssistantV2.conf。组织名和应用名自己定但一旦发布就不要改否则用户配置会丢。value的第二个参数是默认值首次运行时用这个。4. 避坑与排查串口调试助手开发中最容易翻车的五个点4.1 现象打开串口提示“No such file or directory”但设备管理器里明明有原因Linux下当前用户不在dialout组没有权限访问/dev/ttyUSB0。Windows下通常是驱动没装好或者COM口被其他程序占用。解决Linux执行sudo usermod -aG dialout $USER然后重新登录。Windows下换一个USB口试试或者在设备管理器里卸载设备后重新扫描。代码里不要只弹错误框把errorString()完整显示出来方便定位。4.2 现象接收中文显示乱码英文正常原因发送端用GBK编码接收端按UTF-8解析。或者反过来。解决在界面上加一个编码切换下拉框支持UTF-8、GBK、Latin1。用QTextCodec::codecForName动态切换。注意Qt5默认把QString转成UTF-8如果你用toLocal8Bit()在中文Windows下会得到GBK跨平台时行为不一致建议统一用toUtf8()并在接收端也按UTF-8解析。4.3 现象定时发送间隔设成1ms后界面卡死原因QTimer在1ms间隔下频繁触发timeout信号事件循环被占满界面无法响应重绘和鼠标事件。解决最小间隔限制在10ms以上并在界面上给出提示。如果确实需要高速发送把发送逻辑放到独立线程用QSerialPort的moveToThread或者直接在worker线程里操作串口。但要注意QSerialPort不是线程安全的跨线程调用需要加锁或改用信号槽排队。4.4 现象十六进制发送时数据少发或多发原因用户输入的十六进制字符串里有全角空格、制表符或换行符remove(QRegularExpression(\s))在Qt5里对全角空格无效。解决先用QString::simplified()去掉首尾空白并把连续空白替换成单个空格再手动过滤非十六进制字符。或者用QRegularExpression([^0-9A-Fa-f])把所有非十六进制字符删掉再检查长度是否为偶数。4.5 现象程序关闭后串口没释放重新打开提示被占用原因MainWindow析构时没有close串口或者串口对象没有正确delete。解决在closeEvent里调用serial-close()并把serial的父对象设为this让Qt对象树自动管理。如果用了独立线程先quit线程再wait最后delete serial。Windows下如果程序崩溃串口可能被系统锁定几秒等一会儿再试。5. 从能用到好用协议解析插件与数据落盘的进阶技巧5.1 用QPluginLoader做协议解析扩展串口调试助手V2.0如果只做收发和sscom串口调试助手差别不大。真正拉开差距的是协议解析。我一般会定义一个抽象接口类把解析逻辑做成动态库主程序用QPluginLoader加载。// protocolinterface.h class ProtocolInterface { public: virtual ~ProtocolInterface() {} virtual QString name() const 0; virtual QString parse(const QByteArray raw) 0; }; Q_DECLARE_INTERFACE(ProtocolInterface, com.mycompany.SerialAssistant.ProtocolInterface/1.0)插件项目里实现这个接口编译成.dll或.so放到主程序的plugins目录。主程序启动时扫描目录把每个插件的name()加到菜单里。用户选中某个协议后接收区的数据先经过parse()再显示。这样你加Modbus RTU解析、自定义帧头帧尾解析都不用重新编译主程序。5.2 接收数据自动落盘与滚动日志长时间抓数据时界面文本框会越来越卡。我习惯在接收槽里同时写文件界面只保留最近500行。void MainWindow::handleReadyRead() { QByteArray data serial-readAll(); if (logFile.isOpen()) { logFile.write(data); logFile.flush(); // 确保崩溃时不丢数据 } // 界面显示逻辑... if (ui-textRecv-blockCount() 500) { QTextCursor cursor ui-textRecv-textCursor(); cursor.movePosition(QTextCursor::Start); cursor.select(QTextCursor::BlockUnderCursor); cursor.removeSelectedText(); } }logFile.flush()每包都刷会降低性能但串口波特率一般不超过921600数据量不大刷盘开销可以接受。如果波特率很高改成定时flush比如每秒一次。文件名用时间戳格式recv_20250101_120000.log方便回溯。5.3 用QCustomPlot画实时波形如果串口传的是传感器数据比如每帧带一个float你可以解析出来后喂给QCustomPlot。QCustomPlot是一个单头文件库加到工程里就能用。// 在接收解析里提取数值 float value bytesToFloat(data.mid(2, 4)); ui-customPlot-graph(0)-addData(QDateTime::currentMSecsSinceEpoch() / 1000.0, value); ui-customPlot-xAxis-setRange(QDateTime::currentMSecsSinceEpoch() / 1000.0 - 10, 10); ui-customPlot-replot(QCustomPlot::rpQueuedReplot);rpQueuedReplot把重绘请求排队避免每来一个数据点就重绘一次导致CPU飙升。x轴范围设成最近10秒实现滚动效果。注意QCustomPlot的addData是O(1)复杂度但数据点太多时内存会涨定期调用graph(0)-data()-removeBefore(t)清理旧数据。5.4 一个我踩过的坑串口热插拔后句柄失效USB转串口模块拔掉再插上COM号可能变也可能不变。如果不变QSerialPort对象还持有旧句柄write会返回-1但errorString()可能不更新。我的做法是监听QSerialPort的errorOccurred信号当错误是ResourceError或DeviceNotFoundError时自动关闭串口并刷新端口列表。用户重新选一下就能恢复。不要试图在错误信号里直接重新open因为设备可能还没枚举完等500ms再试更稳。写这个工具最大的收获是串口调试助手看起来简单但每个细节都影响手感。定时发送的间隔精度、十六进制输入的容错、编码切换的默认值这些才是用户每天要碰的地方。我习惯在发布前用虚拟串口对com0com或socat跑一遍全流程从打开到收发到保存配置确认没有回归。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑