ARTICLE DETAIL

资讯详情

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

Qt Widgets从零实现串口调试助手V1.0完整指南

Qt Widgets从零实现串口调试助手V1.0完整指南 简介一份基于Qt框架的串口调试助手完整工程源码面向Qt初学者与需要快速搭建串口通信工具的开发者解决从零创建图形界面、配置串口参数以及收发数据的常见需求。压缩包共31个文件包含9个C源文件、9个头文件、3个UI界面文件以及图标、背景图片、资源文件和工程配置文件整体大小约234KB结构清晰便于直接编译运行。目前已有1164人学习/下载。源码涵盖波特率、数据位等参数设置串口打开关闭与数据收发逻辑并封装了QextSerialPort串口通信类界面部分配有UI设计文件和样式表适合对照学习串口通信、槽函数事件处理及界面布局技巧也可作为功能扩展的起点。 很多做嵌入式、硬件调试或者上位机开发的朋友第一个想动手写的桌面工具就是串口调试助手。Qt 生态里现成的例子不少但真要把功能做成自己顺手的样子还是得从零开始搭一遍。这篇文章分享我用 Qt Widgets 从零实现串口调试助手 V1.0 的完整过程包含源码级别的实现思路和踩坑记录适合想练手 Qt 串口编程、或者想定制自己调试工具的朋友参考。1. 动手之前串口调试助手的功能边界1.1 一个串口工具需要哪些功能串口调试助手的核心就一句话把电脑的串口数据收进来、发出去并且让人看得懂。围绕这个核心V1.0 我列了一个功能清单串口配置端口号、波特率、数据位、校验位、停止位开关串口打开成功后配置区锁死防止误改数据接收支持 ASCII 和 Hex 两种显示模式数据发送支持 ASCII 和 Hex 两种发送模式支持发送新行\n 和 \r\n清空接收区、清空发送区收发字节计数状态栏实时显示串口开关状态这个清单看着简单但每一项都对应着实际调试场景。比如 Hex 收发几乎每个搞单片机的人都要用因为很多下位机协议就是按字节定义的直接用 ASCII 看会乱套。再比如计数功能调试大数据量传输时用来判断有没有丢字节非常实用。1.2 V1.0 版本故意不做的事我见过不少人在第一个版本就想加各种复杂功能波形显示、Modbus 解析、自动应答、脚本执行……我的建议是第一版别碰这些。原因很简单这些功能全部依赖底层的串口收发逻辑底子没打稳上面堆再多都是空中楼阁。所以 V1.0 我刻意砍掉了这些东西不做 QThread 收发线程第一版在 UI 线程里直接读写串口串口本身有缓冲区只要不一次性读上千字节完全够用。后面发现问题再加线程也不迟。不做波形绘制那是 V2.0 的事。不做分包粘包智能分析V1.0 只做最朴素的读一段、显示一段。说白了V1.0 的目标不是炫技而是把一个串口调试助手该有的骨架搭出来跑通了、用顺了后续才有迭代的底气。2. 环境准备Qt 版本、编译器与工程文件2.1 我的开发环境选型我用的组合是 Qt 5.15.2 MinGW 64-bit。选 5.15 LTS 而不是 Qt 6主要原因有三个Qt 5.15 的 QSerialPort 模块非常稳定网上资料最多遇到问题搜一下遍地都是答案。很多嵌入式开发板厂商提供的交叉编译工具链、示例工程还是基于 Qt 5直接对齐省心。如果你还想把代码移植到 ARM 板子上跑Qt 5.15 的支持面更广。编译器我选了 MinGW。因为它是开源工具链不依赖 Visual Studio下载安装 Qt 时自带配置零成本。MSVC 版本我也试过但 Qt 对 MinGW 的适配更顺手Debug 和 Release 都不容易出幺蛾子。2.2 创建工程与 pro 文件配置打开 Qt Creator新建项目选择Application → Qt Widgets Application类名我起了MainWindow基类保持QMainWindow不变。建好工程后第一件事是改.pro文件加上串口模块QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets QT serialport TARGET SerialAssistant TEMPLATE app SOURCES \ main.cpp \ mainwindow.cpp HEADERS \ mainwindow.h FORMS \ mainwindow.ui这里有个细节QT serialport必须加否则#include QSerialPort编译直接报找不到头文件。很多新手卡在这一步其实根本不是代码问题是模块没引入。2.3 确认编译器与运行库用 MinGW 64-bit 编译运行后Release 包需要带上Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll、Qt5SerialPort.dll和platforms/qwindows.dll这几个关键文件才能在其他电脑上跑。V1.0 阶段不用折腾打包开发机上直接运行就行但心里要有个数。3. 界面设计不是简单摆控件3.1 串口配置区的交互逻辑界面我用QDesigner拖出来的但布局逻辑不是随手放的。整个窗口分成三个区域顶部配置区、中间收发区、底部状态栏。配置区我用了一个QGroupBox标题叫串口设置里面放了两行第一行端口号下拉框、波特率下拉框、数据位下拉框第二行校验位下拉框、停止位下拉框、打开/关闭按钮这几个下拉框的值要提前填好。波特率我填了这些常用值波特率典型场景9600老式设备、工业仪表默认值115200嵌入式开发最常用460800GPS/高速模块调试921600串口屏、快速固件升级注意QSerialPort有现成的QSerialPort::BaudRate枚举但我直接用QComboBox的文本再转int这样以后要加 250000、1000000 这种非标准波特率只要在界面里加一行就行不用改代码。3.2 收发区与按钮布局中间收发区我用了QSplitter分成左右两个子区域左边是接收区右边是发送区。用QSplitter的好处是用户可以自己拖分隔条接收区大了看数据舒服发送区大了编辑多行协议方便。接收区是一个只读的QTextEdit上方一行复选框Hex 显示、自动滚动。发送区是一个可编辑的QTextEdit上方一行复选框Hex 发送、追加回车换行下面还有一行按钮发送、清空发送区、清空接收区、定时发送。定时发送我加了一个QSpinBox输入框单位是毫秒旁边放一个启动/停止按钮。这个功能调试周期性的心跳包非常管用。3.3 控件禁用/启用管理的信号槽设计界面做好之后最重要的是状态管理逻辑。核心原则是配置控件只在串口关闭时可编辑打开后一律禁用发送相关只在串口打开时可操作。这一步用代码写在打开和关闭函数里void MainWindow::setSerialEnabled(bool enabled) { ui-comboBoxPort-setEnabled(!enabled); ui-comboBoxBaud-setEnabled(!enabled); ui-comboBoxDataBit-setEnabled(!enabled); ui-comboBoxParity-setEnabled(!enabled); ui-comboBoxStopBit-setEnabled(!enabled); ui-btnSend-setEnabled(enabled); ui-btnTimerSend-setEnabled(enabled); ui-spinBoxTimerInterval-setEnabled(enabled); }别小看这个集中管理的函数。一开始我是在打开串口成功后一行行去 setEnabled在关闭函数里再一行行设回来代码又长又容易漏。抽成函数之后无论从哪个入口切换串口状态界面状态都不会错乱。4. 核心实现串口打开、接收、发送与 Hex 显示4.1 打开串口前参数如何生效打开串口的代码是重点直接决定后续收发是否正常。我的实现思路是先从界面上把所有参数读出来再依次设置到QSerialPort对象上最后调用open()。void MainWindow::onBtnOpenClicked() { if (m_serial-isOpen()) { m_serial-close(); ui-btnOpen-setText(打开串口); setSerialEnabled(false); return; } m_serial-setPortName(ui-comboBoxPort-currentText().split( ).first()); m_serial-setBaudRate(ui-comboBoxBaud-currentText().toInt()); m_serial-setDataBits(QSerialPort::Data8); m_serial-setParity(QSerialPort::NoParity); m_serial-setStopBits(QSerialPort::OneStop); m_serial-setFlowControl(QSerialPort::NoFlowControl); if (m_serial-open(QIODevice::ReadWrite)) { ui-btnOpen-setText(关闭串口); setSerialEnabled(true); statusBar()-showMessage(tr(串口 %1 已打开) .arg(ui-comboBoxPort-currentText().split( ).first())); } else { QMessageBox::warning(this, 错误, tr(打开串口失败%1).arg(m_serial-errorString())); } }这里有个经验端口下拉框的显示文本我用了comName description的格式比如COM3 USB-SERIAL CH340所以在设置端口名时一定要.split( ).first()把纯净的COM3取出来否则串口打不开。还有一个细节串口打开之后如果参数设置不对很多设备不会立刻报错而是收发数据乱码。所以数据位、校验位、停止位在 V1.0 里我先固定给了最常用的 8N1 组合界面上虽然留了下拉框但默认值就是 8 位、无校验、1 位停止位避免新手乱选导致调试时出诡异问题。4.2 readyRead 信号与 readAll 的配合接收数据的核心是QSerialPort::readyRead信号。这个信号只要串口缓冲区里来了新数据就会触发一次我在构造函数里把它连到了自定义槽函数onReadyRead。connect(m_serial, QSerialPort::readyRead, this, MainWindow::onReadyRead);void MainWindow::onReadyRead() { QByteArray data m_serial-readAll(); if (data.isEmpty()) return; m_recvBytes data.size(); ui-labelRecvBytes-setText(tr(接收: %1 字节).arg(m_recvBytes)); if (ui-checkBoxHexRecv-isChecked()) { QString hex QString(data.toHex( )).toUpper(); ui-textEditRecv-insertPlainText(hex ); } else { ui-textEditRecv-insertPlainText(QString::fromUtf8(data)); } if (ui-checkBoxAutoScroll-isChecked()) { QTextCursor cursor ui-textEditRecv-textCursor(); cursor.movePosition(QTextCursor::End); ui-textEditRecv-setTextCursor(cursor); } }这里有一个非常重要的概念readyRead信号不保证一次收到完整的一帧数据。串口数据是一个字节一个字节进来的操作系统攒一批就会通知应用去读这个一批可能是半个协议帧、也可能是三五个协议帧。V1.0 我只是把收到的数据原样追加到文本框里不做帧解析所以直接readAll()没问题。如果你后面要解析协议一定要自己维护一个接收缓冲区做粘包处理这是后话。4.3 Hex 显示与编码处理Hex 显示我用了QByteArray::toHex( )这个重载函数会用空格分隔每个字节的 Hex 值显示出来像01 03 00 21肉眼很容易对齐校验。再调用.toUpper()把字母转大写视觉上更清爽。实测发现小写的de ad be ef和大写的DE AD BE EF在协议对比场景下大写明显更好核对。ASCII 显示这里有个容易忽视的坑串口下位机返回的字节不一定都是 UTF-8 编码。很多老设备用的是 GBK 或者 GB2312直接QString::fromUtf8(data)会出现乱码。V1.0 我先统一按 UTF-8 处理因为手头调试的设备都是自己写的代码控制端也用 UTF-8 发送够用了。如果哪天要接老设备再根据情况选择QString::fromLocal8Bit或者QTextCodec。你要做自己的版本务必搞清楚下位机的编码格式。4.4 发送逻辑与定时发送发送数据相对简单但要分清楚 Hex 和 ASCII 两条路径Hex 模式下用户输入的是01 03 00 00 00 01这种字符串必须用QByteArray::fromHex转成真正的字节数组再发送ASCII 模式下直接把字符串转 UTF-8 字节数组发送。void MainWindow::onBtnSendClicked() { if (!m_serial-isOpen()) { QMessageBox::warning(this, 提示, 请先打开串口); return; } QByteArray data; if (ui-checkBoxHexSend-isChecked()) { data QByteArray::fromHex(ui-textEditSend-toPlainText().toUtf8()); } else { data ui-textEditSend-toPlainText().toUtf8(); if (ui-checkBoxAppendNewline-isChecked()) { data.append(\r\n); } } if (data.isEmpty()) return; qint64 written m_serial-write(data); m_sendBytes written; ui-labelSendBytes-setText(tr(发送: %1 字节).arg(m_sendBytes)); }追加回车换行我默认用\r\n不用单独的\n。因为很多 MCU 端的串口中断处理是按行扫描的\n之前如果需要\r才能正确解析少了就出问题。\r\n是兼容性最好的选择。定时发送我用了QTimer在构造函数里创建m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, MainWindow::onBtnSendClicked);启动按钮的槽函数里void MainWindow::onBtnTimerToggled(bool checked) { if (checked) { int interval ui-spinBoxTimerInterval-value(); m_timer-start(interval); ui-btnTimerSend-setText(停止定时发送); } else { m_timer-stop(); ui-btnTimerSend-setText(启动定时发送); } }注意QTimer的最小精度受操作系统影响Windows 下如果定时间隔设成 1 毫秒实际误差可能到 15 毫秒左右。所以定时发送我建议最小间隔设 10ms实际测试大多数设备 10ms 发一包也够快了没必要追求 1ms 极值。5. 踩坑与优化V1.0 留下的经验5.1 Hex 输入校验不严谨V1.0 里有个 bug 一直没修如果用户在 Hex 发送模式下输入了非法字符比如01 03 ZZQByteArray::fromHex会把非法字符当成 0 来处理结果发出去的数据完全不是用户想要的还不好排查。我的建议是如果你要继续开发一定要在发送前校验输入合法性。简单做法是检查字符串中每个字符是否在0-9A-Fa-f和空白字符范围内不合法就弹窗提示别让数据稀里糊涂发出去了。5.2 大流量接收时界面卡顿V1.0 在接收高频数据时比如下位机每 10ms 发 100 字节QTextEdit每触发一次readyRead就插入一段文本界面刷新压力很大极端情况下 CPU 能跑到 40% 以上。这个问题最简单的缓解方案是限制接收缓冲区长度。比如只保留最近 200KB 的显示数据超过就丢弃前面的旧内容if (ui-textEditRecv-document()-characterCount() 200000) { QTextCursor cursor ui-textEditRecv-textCursor(); cursor.setPosition(0); cursor.setPosition(10000, QTextCursor::KeepAnchor); cursor.removeSelectedText(); }但即使这样V1.0 在 115200 波特率满速接收时依然会有点卡。真要长期跑高速数据还是得把接收数据处理放到QThread里UI 线程只负责更新显示。这是 V2.0 的重点优化方向。5.3 端口下拉框的刷新问题程序启动时QSerialPortInfo::availablePorts()只会获取一次端口列表。如果用户先把设备插好再打开程序一切正常但如果程序开着中途拔了 USB 转串口再插一个不同号的端口下拉框不会自动更新。最简单的人工刷新方案是加一个刷新端口按钮void MainWindow::refreshPorts() { ui-comboBoxPort-clear(); const auto infos QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : infos) { QString desc info.description(); if (desc.isEmpty()) desc info.manufacturer(); ui-comboBoxPort-addItem(info.portName() desc); } }注意处理description为空的情况很多免驱芯片在部分系统上拿不到描述信息直接拼字符串会多一个空位看起来怪怪的。5.4 串口被占用时打开失败的提示调试时最容易遇到的一个场景串口助手已经打开了某个端口你再开一个串口终端去连同一端口此时open()会失败。V1.0 的代码里已经弹了QMessageBox::warning但在实际使用中我还加了一行日志输出把errorString()打印到控制台方便对照。这里想特别提醒遇到端口打开失败的时候先检查是不是被别的工具占用了不要盲目怀疑代码。6. 一些使用层面的心得V1.0 写完之后我日常调试 STM32 和 ESP32 的串口日志、调传感器模块、看 GPS 报文用的都是这个小工具。虽然功能上完全没法跟 SSCOM、XCOM 这种老牌工具比但自己写的工具最大的优势就是听话——想加什么功能直接改。比如我后来在 V1.0 基础上加了一个定时发送递增序号的功能用来测试一个设备长时间跑稳定性。这个需求放在现成工具上很难实现但在自己的代码里只是加一个计数器拼进数据包的事情。另外一个小技巧调试时把接收区设置成自动滚动数据会一直往下刷但如果想回看之前的内容记得把自动滚动关掉。这个看似不起眼的交互细节在真实调试中能省很多事。用 Qt 做串口调试助手的这条路走到这里算是真正入门了。从打开串口到了解readyRead的触发机制、从toHex显示到QTimer定时发送每一个功能点背后都有值得深挖的原理。你把 V1.0 跑通之后再去研究 V2.0 的线程优化、协议解析、波形显示会顺畅得多。毕竟代码骨架是你的想怎么长都是你说了算。本文还有配套的精品资源点击获取
返回列表