拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用Qt Creator打造自己的串口调试助手:QSerialPort实战与踩坑全记录

简介基于Qt Creator的Serial Port串口调试助手项目代码面向需要快速搭建串口通信调试工具或学习Qt SerialPort与实时数据可视化的开发者。项目不仅实现常规串口数据收发还集成波形显示功能模拟VOFA上位机Plot效果适合硬件调试、信号监控等场景。资源包共53个文件约22.31MB包含cpp/h源代码、ui界面文件、png图标资源、exe可执行程序、qrc资源文件及pro工程配置等目录结构清晰便于直接参考或二次开发。已有378人学习下载代码中使用了qcustomplot绘图库并提供完整界面设计与串口逻辑实现。无论是希望快速上手串口编程还是需要实现复杂数据波形展示的开发者都能从中获得可复用的代码和设计思路。 做串口调试这块的工程师手里没几个顺手的工具还真不行。市面上现成的串口调试助手一抓一大把sscom、xcom、友善、常兴这些我都用过但用归用总有些场景觉得不够顺手——比如想加个自动回复、想按自己的协议格式解析数据、想把接收到的十六进制流直接按帧拆开看。后来干脆用Qt Creator自己写了一个基于Serial Port的串口调试助手整个过程走下来对Qt的串口模块、信号槽机制、UI布局都有了更深的理解。这篇文章就把整个项目的设计思路、核心代码、踩过的坑完整记录一遍适合正在学Qt的开发者也适合想自己定制串口工具的嵌入式工程师参考。1. 项目整体设计与思路拆解1.1 为什么选Qt Creator和QSerialPort串口上位机方案其实有不少C# WinForms写得快、Python pyserial也简单但我最终选了Qt Creator QSerialPort原因有三第一个是跨平台。今天在Windows上调完明天可能要在Linux或者Mac上跑Qt的代码基本不用改。QSerialPort是Qt官方提供的串口模块封装了底层平台差异Windows下走的是Win32 APILinux下走的是termios但对上层来说接口完全一致这就省去了大量适配工作。第二个是信号槽机制。串口通信本质上是异步的你不知道设备什么时候会发数据过来。QSerialPort的readyRead信号配合槽函数天然就是事件驱动的写法比在循环里轮询优雅得多。而且Qt的信号槽是线程安全的后面如果想把串口收发丢到子线程也不用重构代码。第三个是Qt Creator的UI设计能力。用Qt Designer拖拽控件就能把整个界面搭好串口参数区、数据收发区、日志区一目了然再加上setStyleSheet就能做出像模像样的深色主题非常适合做工具类软件。1.2 项目功能规划与数据流设计既然是调试助手核心功能必须有这几块扫描可用串口、配置波特率/数据位/停止位/校验位、打开关闭串口、收发数据、ASCII/HEX切换显示、清空接收区、发送计数。再往后可以做扩展比如定时发送、自动保存日志。整个数据流其实很清晰。发送链路用户在发送区输入内容程序按选项把文本转成字节数组通过串口写出去。接收链路设备发来数据串口缓存区触发readyRead信号程序用readAll()读出来再按显示模式ASCII还是HEX格式化成字符串追加到接收区文本框中。界面层只负责数据展示和用户操作业务逻辑集中在串口管理类里这样的分层让后续扩展维护都方便。2. 核心功能模块解析与关键实现2.1 串口参数配置与端口枚举串口打开前的准备工作是枚举端口和设置参数。枚举端口用的是QSerialPortInfo::availablePorts()它会返回当前系统里所有可用串口的列表。这里有个容易忽略的细节要在程序启动时刷新一次端口列表但也必须提供“手动刷新”按钮因为USB转串口设备是热插拔的设备已经插上之后再启动程序列表是准的但程序运行中插拔设备列表不会自动更新必须重新枚举。参数设置走的是QSerialPort的各个setter方法代码不多但顺序有讲究serial-setPortName(ui-comboBoxPort-currentText()); serial-setBaudRate(ui-comboBoxBaud-currentText().toInt()); serial-setDataBits(QSerialPort::Data8); serial-setStopBits(QSerialPort::OneStop); serial-setParity(QSerialPort::NoParity);这里面有两个坑。第一个是波特率常见的115200、9600直接toInt()没问题但如果下拉框里加了那些非标准的波特率比如7500、125000某些USB转串口芯片不一定支持打开会直接报错。第二个是打开串口前建议先close()一次防止上次的残留状态影响这次打开。我的建议是把参数配置和打开操作拆成两个步骤先配置、再打开。因为打开失败时要保留配置界面方便用户调整参数后重试。2.2 数据接收、发送与缓冲区清理数据接收的核心代码不长但细节都在里面connect(serial, QSerialPort::readyRead, this, []() { QByteArray data serial-readAll(); if (ui-checkBoxHexReceive-isChecked()) { QString hexStr data.toHex( ).toUpper(); ui-textEditReceive-append(hexStr); } else { ui-textEditReceive-append(QString::fromLocal8Bit(data)); } });readAll()会把串口缓冲区里当前所有可读的数据一次性读出来所以不用担心一次readyRead信号只能读一个字节。但要注意串口数据是分帧到达的一个完整的数据帧可能被拆成好几次readyRead触发如果做协议解析需要自己维护一个接收缓冲等完整帧凑齐了再处理。这里就涉及到缓冲区清理的问题。很多人问过我在Qt/C里清空buffer有哪几种方式我整理一下QSerialPort::clear()清空串口底层缓冲区分InputDirection和OutputDirection一般用AllDirections。QByteArray::clear()清空自定义的字节数组比如你用来攒数据帧的临时buffer。循环readAll()读到空这个方式可以用来“排空”串口缓冲区但要注意别在槽函数里死循环。实际项目中我习惯定义一个QByteArray m_recvBuffer每收到一段数据就追加进去同时检查是否凑齐一帧处理完之后再m_recvBuffer.clear()。这样的好处是协议解析不会被串口分帧打断。发送侧相对简单serial-write(data)就行。但有几个细节发送HEX数据时要先把字符串转成字节数组比如FF 01 02要转成QByteArray而不是直接按ASCII发发送文本时要注意换行符是\r\n还是\n很多设备对换行符敏感。至于校验和、CRC这类计算可以在发送前统一处理把计算逻辑封装成一个函数。2.3 界面布局与状态反馈界面我用的是Qt Designer整体布局采用左右分栏左边是串口配置区包括端口号、波特率、数据位、停止位、校验位、打开/关闭按钮右边是收发区上方接收显示下方发送输入中间一排功能按钮清空接收、发送、定时发送等。打开串口成功的瞬间记得把状态同步到界面上——端口配置控件全部置灰按钮文字从“打开串口”变成“关闭串口”状态栏显示当前使用的参数。这个细节体验差别很大用户一眼就知道当前是什么状态。关闭串口时再恢复回来。如果数据量很大接收区文本框会越积越长影响性能。加一个判断超过一定行数自动截断前面的内容只保留最新的一部分if (ui-textEditReceive-document()-blockCount() 1000) { QTextCursor cursor ui-textEditReceive-textCursor(); cursor.movePosition(QTextCursor::Start); cursor.select(QTextCursor::LineUnderCursor); cursor.removeSelectedText(); cursor.deleteChar(); }3. 实操过程从零搭建完整项目3.1 创建项目与.pro文件配置打开Qt Creator新建Qt Widgets Application类的名字我用的是MainWindow。创建好之后第一件事是打开.pro文件把串口模块加进去QT core gui serialport greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET SerialDebugger TEMPLATE app SOURCES main.cpp mainwindow.cpp HEADERS mainwindow.h FORMS mainwindow.ui这里有个关键点如果不加serialport后面所有#include QSerialPort都会报“找不到头文件”而且Qt Creator不会自动帮你加得手动编辑.pro再重新qmake。3.2 核心代码逐步实现我直接给出mainwindow.h和mainwindow.cpp里最核心的部分完整逻辑都在里面。// mainwindow.h #include QMainWindow #include QSerialPort #include QSerialPortInfo class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private slots: void on_btnRefresh_clicked(); void on_btnOpen_clicked(); void on_btnSend_clicked(); void on_btnClear_clicked(); void on_readyRead(); private: Ui::MainWindow *ui; QSerialPort *serial; };// mainwindow.cpp 构造与初始化 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui-setupUi(this); serial new QSerialPort(this); connect(serial, QSerialPort::readyRead, this, MainWindow::on_readyRead); on_btnRefresh_clicked(); }端口刷新和打开串口的实现void MainWindow::on_btnRefresh_clicked() { ui-comboBoxPort-clear(); foreach (const QSerialPortInfo info, QSerialPortInfo::availablePorts()) { ui-comboBoxPort-addItem(info.portName() - info.description()); ui-comboBoxPort-setItemData(ui-comboBoxPort-count() - 1, info.portName()); } } void MainWindow::on_btnOpen_clicked() { if (serial-isOpen()) { serial-close(); ui-btnOpen-setText(打开串口); ui-groupBoxConfig-setEnabled(true); ui-btnSend-setEnabled(false); return; } serial-setPortName(ui-comboBoxPort-currentData().toString()); serial-setBaudRate(ui-comboBoxBaud-currentText().toInt()); serial-setDataBits(QSerialPort::Data8); serial-setStopBits(QSerialPort::OneStop); serial-setParity(QSerialPort::NoParity); serial-setFlowControl(QSerialPort::NoFlowControl); if (serial-open(QIODevice::ReadWrite)) { ui-btnOpen-setText(关闭串口); ui-groupBoxConfig-setEnabled(false); ui-btnSend-setEnabled(true); statusBar()-showMessage(串口已打开: serial-portName()); } else { QMessageBox::critical(this, 错误, 串口打开失败: serial-errorString()); } }接收和发送接口void MainWindow::on_readyRead() { QByteArray data serial-readAll(); if (data.isEmpty()) return; if (ui-checkBoxHexReceive-isChecked()) { QString hexStr data.toHex( ).toUpper(); ui-textEditReceive-append(hexStr); } else { ui-textEditReceive-append(QString::fromLocal8Bit(data)); } } void MainWindow::on_btnSend_clicked() { if (!serial-isOpen()) return; QByteArray data; if (ui-checkBoxHexSend-isChecked()) { QString hexStr ui-textEditSend-toPlainText(); hexStr.remove(QRegExp(\\s)); data QByteArray::fromHex(hexStr.toLatin1()); } else { data ui-textEditSend-toPlainText().toLocal8Bit(); if (ui-checkBoxNewLine-isChecked()) { data.append(\r\n); } } qint64 written serial-write(data); statusBar()-showMessage(QString(已发送 %1 字节).arg(written)); }QByteArray::fromHex把字符串按十六进制解析成字节数组toHex反过来。这两个函数处理HEX模式非常方便省去了自己逐字节拼接的麻烦。3.3 编译运行与验证写完代码后直接构建。如果一切顺利运行程序点击刷新能看到端口列表接着就是实测环节。没有真实设备也能测。Windows上可以用虚拟串口软件比如VSPD或com0com虚拟出一对互联的串口COM3和COM4。程序打开COM3用另一个串口工具打开COM4两边就能互通。手里有USB转TTL模块的话直接把TXD和RXD短接做回环测试发什么就收到什么验证收发链路是否正常。实测时建议先用ASCII模式发一串Hello Serial看接收区是否原样返回。正常后再切HEX模式发01 02 03确认收到的也是01 02 03。都通过了说明最小功能已经可用。4. 常见问题与排查技巧实录4.1 编译报错cannot run compiler clQt Creator最让人头疼的报错之一就是qt creator:-1: error: cannot run compiler cl. output:。这个cl是微软MSVC编译器的可执行文件报这个错说明Qt Creator当前使用的工具链是MSVC但系统里找不到对应的编译器环境。一般原因有两个一是只装了Qt自己的MinGW版本却在构建套件Kit里选了MSVC二是装了MSVC但缺少VS的Build Tools或者环境变量没配置好。解决办法也直接。如果不需要用到MSVC特有的功能最简单的是在“选项 → Kits → 构建套件”里把编译器切换成MinGW然后重新构建。如果必须用MSVC那就去安装Visual Studio Build Tools确保勾选“使用C的桌面开发”装完重启Qt Creator即可。装完后如果还是报错可以检查cl.exe的路径是否有中文或空格这类路径问题也会导致Qt Creator找不到编译器。4.2 Qt Creator编译输出窗口显示乱码很多中文Windows系统下Qt Creator的编译输出窗口会显示乱码。这个问题的根源是编码不匹配MSVC编译器输出的是GBK编码的文本而Qt Creator默认用UTF-8去解释两者不一致自然就会乱码。解决办法看情况。如果用的是MinGW一般不会遇到如果用的MSVC可以在Qt Creator的“工具 → 选项 → 环境 → 接口”里调整编码设置或者把输出编码改成“System”让Qt Creator跟随系统代码页。项目源码文件里如果有中文注释建议统一以UTF-8保存并在.pro文件里加上msvc { QMAKE_CXXFLAGS /utf-8 }这行让MSVC编译器把源文件按UTF-8读取从编译层面解决中文字符串和注释的乱码问题。我之前写的串口助手如果收到设备发来的中文数据接收区显示乱码多半也是编码问题——设备发的是GBK而程序按UTF-8解析了。解决方法是用QTextCodec显式指定解码方式而不是依赖默认编码。4.3 USB转串口设备识别问题PL2303GS热词里出现prolific pl2303gs usb serial com port这个我太有体会了。PL2303GS是Profilic旺玖的一款USB转串口芯片市面上大量便宜的USB转TTL模块用的就是它。这类模块最常遇到的问题就是驱动装不上、设备管理器里识别成未知设备或者识别出来后一打开就被占用。解决办法分享几个首先驱动尽量去官网下对应型号的国内那些驱动管理软件容易装错版本其次检查是不是进入了“仅充电”模式有些劣质线材只有电源和数据中的一路正常工作再有就是COM口编号冲突在设备管理器里手动改一个未被占用的COM口。前面串口打不开时报Permission Denied或Access is denied十有八九是端口被其他软件占用了关掉占用程序再试就好。4.4 常见故障速查表现象可能原因处理方式编译报错cannot run compiler clMSVC工具链损坏或缺失切换MinGW编译套件或安装VS Build Tools编译输出中文乱码源文件编码与编译器不一致统一UTF-8MSVC加/utf-8参数串口打开失败Access denied端口被占用或无权限关闭占用软件或改用其他COM口设备管理器识别不到USB转串口驱动错误、劣质线材更换驱动、检查芯片型号匹配接收显示乱码设备发送字节与显示解码不一致按实际编码用QTextCodec转换收到数据不完整/丢帧接收后没做粘包处理用缓冲拼帧按帧头/长度解析4.5 清空buffer的几种方式对比顺着刚才提到的buffer问题这里做个汇总。在实际写代码时清空数据的方式取决于你想清哪一层清串口底层缓存的残留数据用serial-clear(QSerialPort::AllDirections)。这个在打开串口后、开始正常收发前特别有用能把之前的残留数据一次性清干净避免读到脏数据。清自定义数据拼接buffer用QByteArray::clear()。实时刷新时每次协议解析完就调用一下保证下一帧数据从头存起。暴力排空方式循环调用readAll()直到返回空。但要注意加跳出条件避免数据量大的时候卡住主线程。5. 扩展玩法与工程化经验5.1 给串口助手接入大模型热词里有个“qt creator怎么接入大模型”这个方向现在很火。串口调试助手接收到的往往是二进制、十六进制、传感器日志人眼去看确实费劲如果能把串口数据自动发给大模型做解析、总结输出一份人类能直接看懂的说明那调试效率会高不少。思路很简单。在接收数据的槽函数里把原始数据格式化成文本通过QNetworkAccessManager发送HTTP请求到提供大模型API的服务端。考虑到主界面不能卡住这个HTTP请求必须走异步或者在子线程里执行。返回结果再通过信号回到主线程显示到界面的一个单独区域。Qt的QNetworkAccessManager本身就是异步的用起来很顺手。这里要提醒一点串口数据是持续不断的不能每收到一帧就调用一次大模型接口一方面是费用扛不住另一方面是接口限流。我的做法是维护一个日志缓冲区点击“智能分析”按钮时才把最近一段数据批量送出去这样请求频率可控、上下文也完整。5.2 项目结构整理与代码复用做到后面你会发现单纯一个串口助手的代码量不算大但功能一多文件一多结构就会乱。热词里那条“【框架调整】记录一次整理项目结构把框架层代码放到私库其他模块依赖jar包”看着像Java项目但这个思路完全适用于Qt项目。早期我写串口助手时所有代码都堆在MainWindow里端口枚举、数据收发、HEX转换全在一块儿改一个功能就要在几万行里翻找。后来重构时我把代码拆成三层界面层只管UI展示和用户交互业务层封装串口管理类SerialManager负责端口扫描、打开关闭、数据收发工具层放HEX转换、CRC校验、日志保存这些通用函数。这样拆完之后再做一个基于串口的项目比如RFID读写器上位机只需要把SerialManager和工具类几个文件拷过去再换个界面就行。项目管理还有个建议用Git做版本管理时构建生成的build目录、*.user文件都不要提交到仓库在.gitignore里配好省的每次clean都误删或冲突。最后说点体会写这个串口调试助手最大的收获不是Qt API有多熟而是真正理解了串口通信的异步模型和工具软件该怎么设计。串口本身不复杂复杂的是在各种边界条件下稳定工作设备突然断开怎么办、数据帧拆包怎么拼、中文编码怎么解、非标准波特率怎么处理。每一个问题都是在实际使用中才会遇到的。这套代码现在已经成为我的基础工具箱后面做各种设备调试时都会先拿出来改一改。如果你也在做类似的项目建议先从最小功能跑通再逐步加特性别一开始就想着功能堆满。项目代码是完全可以跑起来的遇到问题欢迎多交流。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门