
简介这是一份基于Qt框架开发的串口助手可执行程序面向嵌入式开发者、硬件调试工程师及Qt初学者适用于串口参数配置、数据收发与调试信息监测等场景可快速获得可直接运行的串口调试工具。压缩包共51个文件大小23.53MB主要包含Qt5运行所需的dll动态库、qm语言翻译文件、平台与图像格式插件、主程序和卸载程序另有cpp与o源文件及dat数据文件下载解压后即可在Windows环境启动使用。已有130人学习下载说明该工具在串口调试场景中具备一定参考价值。借助这份资源使用者既能直接用于日常串口调试也可以从qrc资源编译文件、辅助工具与整体目录结构中了解Qt程序打包发布时依赖库、插件和翻译文件的组织方式适合实际开发与学习借鉴。1. Qt串口助手exe程序现场调试的“小救星”也是“大坑主”做嵌入式联调的人大概率经历过这种场景下位机临时改协议、主控板多了一字节校验手里的现成串口助手要么不支持自定义帧要么打开就卡住关键时刻掉链子。这时候一个能用、能改、能打包发给同事的“Qt串口助手exe程序”就比什么都顶用。这个标题的本质就是用 Qt 自带的 QSerialPort 模块做一个串口调试工具再借助 windeployqt 和打包脚本把它变成独立的 Windows 可执行文件。它的难点不在“写出来”而在“交付出去”从端口枚举、参数设置、数据收发到界面上不卡顿、换电脑不缺 DLL整条链路每一环都有门道。适合谁适合想摆脱现成工具限制、自己做专用串口工具的开发者这条路走通后你手里就是一个随时能分发给现场同事的落地产物。2. 串口通信模块用 QSerialPort 封装一个能用的串口类2.1 为什么是 QSerialPort 而不是第三方库串口助手最核心的部分是通信而 Qt 自带的 QSerialPort 模块在多数场景下是首选。它本身就挂在 Qt 生态里不用额外引第三方库事件驱动模型和 Qt 的信号槽机制天然契合数据到了会有 readyRead 信号通知不需要你像 Win32 API 那样去维护线程和消息循环。选择它的另一个理由是 QSerialPortInfo 提供了跨平台的端口枚举能力。在 Windows 上设备管理器里那个“COM3”对应什么设备、USB 转串口芯片的描述信息都能通过这个类读出来。自定义串口助手如果要做一个下拉框给用户选端口这一步是必修课。很多开源串口库确实也不错比如某些支持 Modbus 封装的库但那是给“协议级”场景用的。对于纯粹的原始字节收发、HEX 显示、帧调试QSerialPort 足够轻也足够稳定。我的习惯是把所有串口操作封装成一个独立类界面层只管和这个类交互不直接碰 QSerialPort 的 API。这样一来以后想替换底层库、加虚拟串口模拟都不用动界面代码。2.2 初始化与打开参数设不对后面全白搭一个串口助手的第一步是“选端口、设参数、点打开”。下面的 SerialHelper 类是我常用的封装结构把打开、关闭、发送、接收全部收拢到一个类里。// SerialHelper.h #ifndef SERIALHELPER_H #define SERIALHELPER_H #include QObject #include QSerialPort #include QSerialPortInfo class SerialHelper : public QObject { Q_OBJECT public: explicit SerialHelper(QObject *parent nullptr); bool openPort(const QString portName, int baudRate); void closePort(); bool isOpen() const; qint64 sendData(const QByteArray data); QString currentPort() const; signals: void dataReceived(const QByteArray data); // 收到一包数据 void errorOccurred(const QString message); // 串口错误统一转发 private slots: void onReadyRead(); private: QSerialPort *m_serial; }; #endif // SERIALHELPER_H// SerialHelper.cpp #include SerialHelper.h #include QSerialPortInfo SerialHelper::SerialHelper(QObject *parent) : QObject(parent) { m_serial new QSerialPort(this); connect(m_serial, QSerialPort::readyRead, this, SerialHelper::onReadyRead); connect(m_serial, QSerialPort::errorOccurred, this, [this](QSerialPort::SerialPortError error) { if (error ! QSerialPort::NoError) emit errorOccurred(m_serial-errorString()); }); } bool SerialHelper::openPort(const QString portName, int baudRate) { if (m_serial-isOpen()) m_serial-close(); m_serial-setPortName(portName); if (!m_serial-setBaudRate(baudRate)) { emit errorOccurred(波特率设置失败); return false; } m_serial-setDataBits(QSerialPort::Data8); m_serial-setStopBits(QSerialPort::OneStop); m_serial-setParity(QSerialPort::NoParity); m_serial-setFlowControl(QSerialPort::NoFlowControl); // ReadWrite应用场景大多需要同时收发 return m_serial-open(QIODevice::ReadWrite); } void SerialHelper::closePort() { if (m_serial-isOpen()) m_serial-close(); } bool SerialHelper::isOpen() const { return m_serial-isOpen(); } qint64 SerialHelper::sendData(const QByteArray data) { if (!m_serial-isOpen()) return -1; // 串口是高波特率下的字节流write 返回值是实际写入缓冲区字节数 qint64 written m_serial-write(data); return written; } void SerialHelper::onReadyRead() { QByteArray data m_serial-readAll(); if (!data.isEmpty()) emit dataReceived(data); }这段代码里有几个关键点值得单独说。setBaudRate返回 bool很多初写者会忽略这个返回值一旦串口芯片不支持某个奇葩波特率比如 3000000代码会静默失败界面上看起来打开了实际收发全是乱的。设置波特率后没有立刻 open而是先检查返回是一个低成本高收益的防御习惯。参数方面数据位、停止位、校验位是最常见的三个坑。8N18 数据位、无校验、1 停止位是绝大多数嵌入式设备的默认配置但有些老设备用 8E1个别 PLC 用 7E1。设计界面时这三个参数都必须做成下拉框不要写死。流控默认选 NoFlowControl很多 USB 转串口设备在硬件流控打开时会出现收发卡顿的玄学问题不建议默认开启。2.3 数据收发readyRead 信号与缓冲区的配合onReadyRead里直接readAll()是最直观的写法但这里有一个新手容易误解的点readyRead 信号不代表“一帧数据到了”只代表“缓冲区里有数据可读”。设备如果一次发 100 字节底层可能分 3 次触发 readyRead也可能把两帧数据合并成一次触发。这是串口通信的常态不是 Bug。所以串口助手的接收区不能简单地把每次 readAll 的结果当成一条完整消息去解析。正确的做法分两种如果只是给人看直接追加到显示区即可如果要做协议解析必须自己维护一个接收缓冲区按帧头、帧长、校验来切割数据。协议解析的具体做法放到最后一章讲这里先把“数据到达”这件事做对。发送方向的注意事项在sendData里也体现了write()把数据写入 QSerialPort 的内部发送缓冲区就返回了真正的物理发送在后台完成。对串口这种低速设备来说绝大多数情况下一次 write 就能写完整个包但如果你发送的数据很大比如几十 KBwrite 可能只写入一部分剩余数据得等 bytesWritten 信号触发后继续写。串口助手的实际使用场景很少发大包但把这个边界记住遇到批量升级固件场景时能少走弯路。3. 界面与数据流让接收区不卡、发送区不乱3.1 主窗口布局从 QMainWindow 到控件的选型串口助手的界面形态已经非常成熟左侧是参数面板右侧是接收显示区底部是发送编辑区。我用 QMainWindow 做外壳左侧放 QGroupBox 包住 QFormLayout右侧放 QTextEdit只读模式底部用 QLineEdit 加两个按钮。端口下拉框需要手动填充这就是 QSerialPortInfo 发挥作用的地方。刷新端口按钮的槽函数代码如下void MainWindow::refreshPorts() { m_ui-portCombo-clear(); const auto infos QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : infos) { // 显示格式COM3 (USB-SERIAL CH340) // description 可能为空此时只显示端口名 QString label info.description().isEmpty() ? info.portName() : QString(%1 (%2)).arg(info.portName(), info.description()); // 把端口名存到 itemData后面打开时用 data 而不是显示文本 m_ui-portCombo-addItem(label, info.portName()); } }说明一下availablePorts()返回的是当前系统里所有可用串口的信息列表每次刷新都重新枚举。注意description()在不同驱动下的表现差异很大有些国产 USB 转串口驱动返回空字符串所以代码里做了空判断。下拉框的显示文本给用户看端口名本身存进 itemData打开时用portCombo-currentData().toString()取避免因为显示文本做了格式化而传错端口名。打开和关闭按钮的逻辑很直接点打开时从界面上收集端口、波特率、数据位等参数传给 SerialHelper 的 openPort点关闭时调 closePort。这里的 UI 细节是“按钮状态切换”——打开成功后把端口下拉框和参数区全部禁用避免用户在一个已打开的连接上乱改参数。3.2 接收与显示QTextEdit 显示与 HEX 切换接收区的核心需求有两个一是大数据量下不卡二是文本模式与 HEX 模式自由切换。先看 HEX 和字符串互转的工具函数这个函数在很多地方都会用到// 把 AA 55 01 02 这样的字符串转成字节数组 QByteArray hexStringToBytes(const QString hex) { QByteArray bytes; QString clean hex.simplified(); // 去掉多余空格和换行 const QStringList parts clean.split( , Qt::SkipEmptyParts); for (const QString part : parts) { bool ok false; uint value part.toUInt(ok, 16); if (ok value 0xFF) bytes.append(static_castchar(value)); } return bytes; }注意Qt::SkipEmptyParts在 Qt 5 中写法是QString::SkipEmptyParts如果你还在用 Qt 5.15编译时会遇到枚举类型不匹配的报错这是 Qt 6 的破坏性变更之一。另外simplified()会把制表符、换行统一成空格这对“从文档粘贴一串带换行的 HEX”场景特别有用。接收区的文本追加逻辑也要讲究效率。最直接的做法是每次收到数据就append()但长时间运行后 QTextEdit 内部文档会越来越大内存和渲染都会变慢。我会在追加前检查文档块数量void MainWindow::appendReceiveData(const QByteArray data) { // 勾选了 HEX 显示时把字节转成大写十六进制文本 if (m_ui-hexReceiveCheck-isChecked()) { // toHex( ) 是 Qt 5.9 之后才有的重载每个字节后加空格 m_ui-receiveEdit-insertPlainText(QString::fromLatin1(data.toHex( ).toUpper())); m_ui-receiveEdit-insertPlainText( ); } else { // 文本模式很多下位机发的是 GBK 或 UTF-8按项目实际情况选择编码 m_ui-receiveEdit-insertPlainText(QString::fromUtf8(data)); } // 超过 2000 行时清空防止长时间挂机内存暴涨 static const int MAX_LINE 2000; if (m_ui-receiveEdit-document()-blockCount() MAX_LINE) m_ui-receiveEdit-clear(); // 自动滚动到底部 QTextCursor cursor m_ui-receiveEdit-textCursor(); cursor.movePosition(QTextCursor::End); m_ui-receiveEdit-setTextCursor(cursor); }这里直接clear()是图省事的做法代价是全部历史被清掉。真要做得精细可以把超过 2000 行的前缀通过 QTextCursor 按 block 删除保留最近的数据。接收区是否自动换行也建议做成可选项因为 HEX 模式下一行太长会自动换行反而影响阅读。3.3 定时发送与协议帧把常用调试场景变成按钮串口调试里有几个场景是高频操作定时发送某条指令、发送 HEX 报文、自动回车换行。这些功能做进 UI比让用户每次手敲命令高效得多。定时发送用 QTimer 实现// 发送区初始化、按钮绑定 m_timer new QTimer(this); m_timer-setInterval(1000); // 默认 1 秒可在界面上让用户改 connect(m_timer, QTimer::timeout, this, MainWindow::sendCurrentData); void MainWindow::sendCurrentData() { if (!m_serial-isOpen()) { m_timer-stop(); statusBar()-showMessage(串口未打开定时发送已停止, 3000); return; } QByteArray data; if (m_ui-hexSendCheck-isChecked()) data hexStringToBytes(m_ui-sendEdit-text()); else data m_ui-sendEdit-text().toUtf8(); // 勾选“发送新行”时自动补 \r\n很多 AT 指令和 Modbus 都有这个习惯 if (m_ui-appendNewLineCheck-isChecked()) data.append(\r\n); m_serial-sendData(data); }定时发送的间隔不建议做得小于 100ms。我见过有人为了测试设备极限把间隔设到 10ms结果下位机处理不过来丢包后以为是自己程序写错了查了一晚上发现是设备侧压根就处理不了那么快。定时器最小间隔建议限制在 100ms真要做压力测试请在协议层做应答确认而不是无脑刷。到这里一个串口助手的“能用”版本已经成型。但注意一个架构问题前面这些代码都在主线程里跑。串口速度一般只有 9600 到 115200bps主线程用事件驱动完全扛得住如果你的应用还同时做 TCP 转发、数据库写入、大日志文件落盘那建议把 SerialHelper 丢到子线程里用信号槽跨线程通信否则界面会在数据量上来时出现明显的卡顿感。thread 的用法进阶章再展开。4. 把程序打包成 exewindeployqt 之后才是真正的起点4.1 发布前的准备Release 构建与 Qt 模块裁剪很多人在这一步翻车因为不知道“Debug 版本不能发布”。Qt 的 Debug 版 exe 依赖一堆带 d 后缀的调试库比如 Qt6Cored.dll这些库在别人机器上不存在就算你把 Debug 版目录整个拷过去也会因为缺少运行库而启动失败。所以在打包之前先确认自己在用 Release 构建。用 qmake 工程的话构建命令是mkdir build-release cd build-release qmake ../SerialHelper.pro -spec win32-g CONFIGrelease mingw32-make -j4如果是 MSVC 环境qmake 会生成 Makefile 后用 nmake 构建命令差别不大。关键是CONFIGrelease这个标记以及构建完成后去 release 子目录里找 exe而不是去 debug 目录里找。“release 目录里也能看到 exe但 debug 目录里也有一个同名 exe”——用文件管理器顺手把 debug 目录拷走是最常见的翻车操作拷之前先看一眼路径。如果用的是 CMake则构建时指定-DCMAKE_BUILD_TYPERelease效果相同。这一步没有任何捷径构建类型错了后面 windeployqt 跑得再顺利也白搭。4.2 windeployqt 与部署清单一键收集依赖windeployqt 是 Qt 官方提供的部署工具它会扫描 exe 的导入表把用到的 Qt 模块 DLL、编译器运行库、platforms 插件等自动拷贝到 exe 所在目录。基本命令windeployqt --release --no-translations --no-system-d3d-compiler SerialHelper.exe几个参数的含义--release表示处理发布版依赖防止误拷 Debug 库--no-translations不拷贝 Qt 自带的翻译文件对串口工具来说这些语言包没意义还能省几 MB 体积--no-system-d3d-compiler跳过 D3D 编译器相关组件只有用到 Qt3D 或某些图形模块才需要它。我习惯在命令执行后做一次检查确认platforms/qwindows.dll存在。这个文件是 Qt GUI 程序在 Windows 上启动的核心插件很多人打包后 exe 在自己机器能跑、拷到别人机器双击没反应就是缺了 platforms 目录。windeployqt 正常执行时会自动生成这个目录但如果你曾经手动拷贝过运行时库、破坏过目录结构那就需要自查。进一步确认依赖有没有缺可以用 Visual Studio 自带的 dumpbin 工具dumpbin /dependents SerialHelper.exe输出会列出 exe 依赖的所有 DLL挨个和部署目录里的文件比对缺哪个补哪个。dumpbin 只能在 VS 开发人员命令行里用如果你用 MinGW也可以用objdump -p SerialHelper.exe | grep DLL Name达到同样的效果。这一步把“玄学缺库”变成“清单比对”是打包环节最值得花时间的步骤。4.3 免安装版还是安装包NSIS 的最小脚本windeployqt 执行完之后exe 所在目录已经是一个可以直接拷贝运行的免安装版了。把它压成 zip 发给同事是最快的分发方式。但如果要交付给非技术背景的人比如产线工人一个带桌面快捷方式和卸载入口的安装包会更专业。NSIS 是最常用的轻量打包工具之一它不需要额外的运行时。一个最小可用的安装脚本如下!include MUI2.nsh Name 串口助手 ; 安装过程中显示的名称 OutFile SerialHelper_Setup.exe ; 生成的安装包文件名 InstallDir $PROGRAMFILES\SerialHelper ; 默认安装目录 !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_LANGUAGE SimpChinese Section 主程序 SetOutPath $INSTDIR File /r deploy\*.* ; 把 windeployqt 处理好的整个目录拷进去 WriteUninstaller $INSTDIR\Uninstall.exe SectionEnd Section uninstall RMDir /r $INSTDIR SectionEnd注意File /r deploy\*.*这一行它会把 windeployqt 处理好的目录递归完整拷贝进安装目录。如果漏了 /r 参数子目录比如 platforms不会被带进去安装后照样缺插件跑不起来。“deploy”目录名可以按自己习惯改但和脚本里的路径必须一致。免安装版和安装包我一般让使用者二选一内部开发协作直接用 zip正式发布给外部现场用 NSIS 安装包。安装包还有一个多出来的价值是写注册表、加卸载入口后面想升级时能判断旧版本装在哪里。5. 串口助手常见问题与避坑指南现象、原因、解决5.1 打开端口失败返回 Access is denied现象端口下拉框里能看到 COM3但点打开按钮后弹错open()返回 false错误信息是“Access is denied”。原因串口被其他程序占用了。最典型的是另一个串口助手还在后台开着或者某个调试软件的监视窗口没关。串口是独占设备Windows 不允许两个句柄同时打开同一个端口。解决先关掉所有可能占用串口的软件再点打开。如果还是失败检查设备管理器里该 COM 口是否处于异常状态常见的是 USB 转串口驱动被系统更新搞挂。也有一种情况是权限问题某些工控机上串口设备被安全软件限制了访问此时用管理员身份运行一次程序验证能打开就是权限配置问题。5.2 数据乱码先看 HEX再查参数现象接收区显示的内容像“烫烫烫”一样的乱码字符或者数据断断续续出现半个汉字。原因分两种情况。第一种是真正的通信参数不匹配波特率、校验位、数据位有一项不对物理层收到的字节本身就是错的第二种是参数没问题但编码显示错了——下位机发的是 GBK你按 UTF-8 解码看到的自然是乱码。解决先勾上 HEX 显示模式看十六进制字节流是否符合预期。如果 HEX 数据完全正确那就是纯编码问题改用 GBK 解码即可如果 HEX 本身就是乱的再回头核对波特率、校验位。这个方法能把“通信问题”和“显示问题”快速分离是排查乱码最快的路径比反复改波特率盲猜高效得多。5.3 关闭串口或拔掉设备时程序崩溃现象正常关闭窗口没事但直接在设备管理器里禁用端口或者拔掉 USB 转串口线程序立刻崩溃。原因设备被拔掉时QSerialPort 内部会触发错误信号如果此时你的槽函数里操作了正在析构的窗口对象就会踩到野指针。另外如果窗口关闭时没有先 closePort析构顺序失控也可能导致崩溃。解决在关闭事件里先断开串口再关窗口。同时给 errorOccurred 的 ResourceError 单独处理connect(m_serial, QSerialPort::errorOccurred, this, [this](QSerialPort::SerialPortError error) { if (error QSerialPort::ResourceError) { // 设备拔出或掉线主动关闭端口避免内部继续尝试读写 m_serial-close(); emit serialDisconnected(); } });ResourceError 表示硬件层面的连接丢失此时串口已经不可用主动 close 是止损。界面层收到 serialDisconnected 信号后再把按钮状态复位而不是直接在错误槽里操作 UI能避免很多隐性问题。这是 Qt 信号槽异步触发的经典坑——你永远不知道一个信号会在哪个时刻抵达。5.4 打包后的 exe 在别的电脑双击没反应现象在自己机器上跑得好好的 exe拷到另一台 Windows 电脑上双击后没有任何窗口弹出任务管理器里也看不到进程。原因第一嫌疑是缺 platforms/qwindows.dll也就是前面打包章反复强调的插件缺失。第二是缺编译器运行库比如 MSVC 版本的 exe 在只有 MinGW 运行库的机器上就起不来。解决先用 dumpbin 或 objdump 查依赖清单逐个核对部署目录。确认 platforms 目录存在且 qwindows.dll 在里面。如果用的是 MSVC 编译还需要确认 vc_redist 运行库是否安装windeployqt 有时会把运行库拷进来但不是每台机器都能自动加载。把 windeployqt 生成的整个目录原封不动打 zip是规避这个问题最省事的做法千万别只拷单个 exe。5.5 界面假死串口操作卡住了主线程现象接收数据很快时窗口拖不动、按钮点不动过一会又恢复。原因在极端情况下比如下位机以 1ms 间隔连续发数据readyRead 信号触发的频率极高主线程忙于处理数据追加和界面刷新事件队列被挤爆UI 自然就卡住了。解决把 SerialHelper 移到独立线程。具体做法是创建一个 QThread把 SerialHelper 对象 moveToThread 到该线程所有信号槽连接用 QueuedConnection。串口助手这种低频工具大多数不需要这一步但如果你要长时间挂机采集数据这个改造是必须的。我一般建议在项目一开始就把 SerialHelper 设计成“无界面依赖”的纯逻辑类这样后续移线程只是改连接方式不用拆类。6. 进阶从“能用”到“好用”的三个实用技巧第一个技巧是给接收数据加协议帧解析。前面的 onReadyRead 只是把字节丢给界面真正和下位机联调时你收到的是连续的字节流需要自己切分成帧。一个简化版的帧解析器长这样void appendData(const QByteArray data) { m_buffer.append(data); // 协议示例AA 55 len payload crclen 表示 payload 长度 while (m_buffer.size() 4) { if ((quint8)m_buffer[0] ! 0xAA || (quint8)m_buffer[1] ! 0x55) { m_buffer.remove(0, 1); // 不匹配帧头逐字节丢弃 continue; } quint8 len (quint8)m_buffer[2]; int frameLen 3 len 1; if (m_buffer.size() frameLen) break; // 等待完整帧 // 校验 crc通过则取出完整帧失败则丢帧头继续找 m_buffer.remove(0, frameLen); } }这个循环可以单独封装成类界面只管显示解析出来的完整帧。对调试自定义协议来说这比盯着 HEX 数数高效太多值得花半小时做。第二个技巧是给接收数据打时间戳并写日志文件。用 QDateTime::currentDateTime().toString(yyyy-MM-dd hh:mm:ss.zzz) 拼接每条记录按天生成日志文件出错时能回看完整现场。这个功能不复杂但关键时刻能省半天排查时间。第三个技巧是“软串口”模拟。开发阶段没有硬件时用虚拟串口软件创建一对互相连通的串口一个口发给另一个口收程序不用改就能完整自测。我自己的习惯是任何串口工具都先在虚拟串口上跑一轮收发测试确认协议解析逻辑没问题再拿到现场免得现场联调成了第一次运行时的“黑匣子”。这三个技巧做完你的串口助手才算打磨成趁手的工具从能发能收到能解析、能留痕、能脱离硬件调试。串口工具这种项目没有“做完”一说每调一种设备就可能加一个功能关键是把底子打好让每个新功能都有地方放。希望帮到你。本文还有配套的精品资源点击获取