
简介这份资源面向具备一定C基础、希望快速上手串口通信上位机开发的Qt5学习者与嵌入式工程师围绕QSerialPort模块解决设备短距离数据交互问题。压缩包共5个文件约4KB包含2个cpp源文件、1个h头文件、1个pro工程文件与1个ui界面文件分别对应串口逻辑实现、类声明、工程配置与可视化界面布局结构精简便于直接导入Qt Creator编译运行。资源完整呈现串口初始化、参数配置、读写操作、错误处理与资源释放等关键环节并借助信号与槽机制将界面控件与串口事件联动读者可据此理解上位机与外部设备通信的基本流程。目前已有3749人学习下载适合作为串口调试工具开发的入门参考也可在此基础上扩展数据解析与格式转换功能。1. 串口调试工具满地跑为什么还要自己写一个上位机手头有个单片机或者工控板子每次联调都得开第三方串口助手波特率、校验位、停止位来回点数据格式还只能看十六进制。更麻烦的是产品要交付给现场人员用总不能让人家对着一个通用调试工具手动算校验和。这时候就需要一个自己的上位机打开就是几个按钮、几个输入框点一下发指令下面实时刷数据。QT5 的QSerialPort模块就是干这个的跨平台Windows 和 Linux 下代码几乎不用改编译出来一个可执行文件丢过去就能跑。这篇笔记拆的就是一个最小可用的 QT5 串口上位机项目适合刚接触 QT 又想快速做出能交付工具的人也适合想从 MFC 或 C# 转过来的老手看看 QT 的信号槽在串口场景里到底怎么用。2. 环境搭建与工程骨架从零建一个能编译的 QT5 串口工程2.1 安装 QT5 与串口模块的坑装 QT 的时候很多人只勾了默认的 MinGW 组件结果编译时提示Unknown module(s) in QT: serialport。这是因为QSerialPort在 QT5 里是独立模块不在核心库里。用在线安装器的话在组件树里展开Qt 5.15.2或你选的版本把Qt Serial Port勾上。如果已经装完了可以单独运行MaintenanceTool补装。Linux 下更简单直接包管理器装libqt5serialport5-dev就行。装完之后在.pro文件里加一行QT serialport这行不加后面所有#include QSerialPort都会报找不到头文件。我见过有人手动去改INCLUDEPATH其实没必要qmake 的模块机制已经处理好了。2.2 工程目录结构一个干净的上位机工程不需要太复杂我一般按下面这样分SerialHost/ ├── SerialHost.pro ├── main.cpp ├── mainwindow.h ├── mainwindow.cpp ├── mainwindow.ui └── resources/ └── icons/mainwindow.ui用 QT Designer 拖控件串口选择下拉框、波特率下拉框、打开/关闭按钮、发送输入框、接收显示区这几个是标配。如果不想用.ui文件纯代码手写布局也行但后期改界面会累一点。2.3 串口枚举与参数配置打开软件第一件事是列出当前可用的串口。QSerialPortInfo::availablePorts()返回一个列表每个元素包含端口名、描述、制造商等信息。下面这段代码放在MainWindow构造函数里初始化下拉框// 枚举可用串口填充下拉框 void MainWindow::initSerialPorts() { ui-portBox-clear(); const auto infos QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : infos) { // 显示端口名和描述方便用户识别 QString label info.portName(); if (!info.description().isEmpty()) label ( info.description() ); ui-portBox-addItem(label, info.portName()); } }这里把portName作为userData存进去后面打开串口时直接取currentData().toString()不用再去解析显示文本。波特率下拉框可以预设 9600、115200 这些常用值也可以让用户手动输入。数据位、校验位、停止位一般默认 8N1但工控场景里偶校验用得不少所以还是留出选择框。参数配置的核心是QSerialPort::setBaudRate()等一系列 setter设置完再open()// 根据界面选择配置并打开串口 void MainWindow::openSerialPort() { serial new QSerialPort(this); serial-setPortName(ui-portBox-currentData().toString()); serial-setBaudRate(ui-baudBox-currentText().toInt()); serial-setDataBits(QSerialPort::Data8); serial-setParity(QSerialPort::NoParity); serial-setStopBits(QSerialPort::OneStop); serial-setFlowControl(QSerialPort::NoFlowControl); if (serial-open(QIODevice::ReadWrite)) { // 连接 readyRead 信号数据到达时自动触发 connect(serial, QSerialPort::readyRead, this, MainWindow::readData); ui-openBtn-setText(关闭串口); } else { QMessageBox::critical(this, 错误, 无法打开串口 serial-errorString()); } }注意serial对象在关闭时要deleteLater()不然反复打开关闭会泄漏。另外readyRead信号是 QT 事件循环驱动的不需要自己开线程去轮询这是 QT 串口编程比裸写read()循环舒服的地方。3. 数据收发核心信号槽机制与粘包处理3.1 接收数据与缓冲区管理readyRead触发时数据可能是一帧完整的也可能是半帧甚至多帧粘在一起。串口是字节流没有消息边界这是新手最容易翻车的地方。下面是一个带缓冲的接收实现// 接收缓冲区处理粘包和半包 void MainWindow::readData() { QByteArray data serial-readAll(); recvBuffer.append(data); // 简单协议以 0x0D 0x0A 结尾为一帧 while (recvBuffer.contains(\r\n)) { int idx recvBuffer.indexOf(\r\n); QByteArray frame recvBuffer.left(idx); recvBuffer.remove(0, idx 2); handleFrame(frame); } }recvBuffer是QByteArray成员变量。每次把新数据追加进去然后循环查找帧尾。找到就截取出来交给handleFrame处理剩下的留在缓冲区等下一批数据。如果协议是定长的就判断recvBuffer.size() frameLen再取。如果协议带长度字段先解析长度再判断。总之不能假设一次readAll()就是完整一帧。handleFrame里可以做解析、显示、存文件。显示的时候如果数据是 ASCII 就转字符串如果是二进制就转十六进制// 根据显示模式格式化帧数据 void MainWindow::handleFrame(const QByteArray frame) { QString text; if (ui-hexDisplay-isChecked()) { text frame.toHex( ).toUpper(); } else { text QString::fromLocal8Bit(frame); } ui-recvEdit-appendPlainText(text); }toHex( )会在每个字节之间加空格toUpper()转大写看起来更符合工控习惯。3.2 发送数据与校验和计算发送比接收简单但要注意写入的字节数。serial-write(data)返回实际写入的字节数如果串口缓冲区满了可能只写一部分。稳妥做法是判断返回值或者连接bytesWritten信号确认发完了。下面是一个带校验和的发送函数// 发送一帧帧头 0xAA 长度 数据 校验和 void MainWindow::sendFrame(const QByteArray payload) { QByteArray frame; frame.append(static_castchar(0xAA)); frame.append(static_castchar(payload.size())); frame.append(payload); // 累加和校验取低 8 位 quint8 sum 0; for (char c : frame) sum static_castquint8(c); frame.append(static_castchar(sum)); qint64 written serial-write(frame); if (written ! frame.size()) { statusBar()-showMessage(发送未完成请检查串口状态); } }校验和算法要和下位机约定一致累加和、异或和、CRC16 都常见。这里用累加和只是示例实际项目里 CRC16 更可靠。发送前记得判断serial-isOpen()没打开就写会直接失败。3.3 定时发送与线程安全有些场景需要周期性发心跳包用QTimer最合适// 定时发送心跳 heartbeatTimer new QTimer(this); connect(heartbeatTimer, QTimer::timeout, this, [this]() { if (serial serial-isOpen()) sendFrame(QByteArray::fromHex(01)); }); heartbeatTimer-start(1000); // 1 秒一次QT 的信号槽默认在同一个线程里执行QTimer和串口都在主线程所以不存在竞态。但如果把串口操作放到工作线程就要注意跨线程调用write()是线程安全的但readyRead信号会在线程的事件循环里触发界面更新必须用QMetaObject::invokeMethod切回主线程。新手建议先单线程跑通数据量大了再考虑多线程。4. 避坑与常见问题排查4.1 打开串口失败提示“No such file or directory”现象点击打开按钮弹窗报错errorString()返回“No such file or directory”。原因通常是串口名不对比如 Linux 下是ttyUSB0而不是COM3或者设备根本没插好。解决先调initSerialPorts()刷新列表确认端口名和系统设备管理器里一致。Linux 下还要检查当前用户有没有dialout组权限没有就sudo usermod -aG dialout $USER然后重新登录。4.2 能打开但收不到数据现象串口显示已打开下位机也在发但接收区一直空白。原因可能是波特率不匹配或者readyRead信号没连上。解决先确认双方波特率、数据位、校验位、停止位完全一致。然后在openSerialPort()里加一句qDebug() serial-isOpen() serial-baudRate();看实际生效的参数。如果参数对但还是没数据检查connect语句有没有写错readyRead拼成readyread编译能过但运行时不会触发。4.3 接收区显示乱码现象收到的数据是一堆问号或方块。原因下位机发的是二进制但显示模式选成了 ASCII。解决勾选十六进制显示或者用QString::fromLatin1而不是fromLocal8Bit。如果下位机发的是 GBK 编码的中文fromLocal8Bit在 Windows 下能正确解码Linux 下可能不行最好统一用 UTF-8 或者纯 ASCII。4.4 反复打开关闭后程序崩溃现象点几次打开/关闭按钮程序闪退。原因每次打开都new QSerialPort关闭时只close()没delete旧对象还在信号槽重复连接。解决在openSerialPort()开头判断if (serial) { serial-deleteLater(); serial nullptr; }或者把serial作为成员只创建一次打开关闭只调open()和close()。我一般用后者更省事。4.5 发送中文时下位机收到乱码现象发送“启动”两个字下位机收到的是乱码。原因QT 的QString默认 Unicode直接toLocal8Bit()在 Windows 下是 GBKLinux 下是 UTF-8下位机如果按固定编码解析就会错。解决和硬件端约定好编码统一用 UTF-8 或者 GBK发送前显式转换QByteArray data QString(启动).toUtf8();。如果下位机只认 GBK就用QTextCodec::codecForName(GBK)-fromUnicode()。5. 二次开发改造从能用到好用5.1 协议插件化上面写的收发逻辑把协议硬编码在MainWindow里换一个项目就要改代码。更好的做法是抽一个ProtocolBase接口class ProtocolBase : public QObject { Q_OBJECT public: virtual QByteArray pack(const QByteArray payload) 0; virtual bool unpack(QByteArray buffer, QByteArray frame) 0; signals: void frameReady(const QByteArray frame); };然后实现SimpleProtocol、ModbusProtocol等子类。主窗口只负责串口开关和界面刷新协议解析交给插件。这样新增协议不用动主窗口代码符合开闭原则。5.2 数据持久化与回放调试时经常需要把接收数据存下来事后分析。可以在handleFrame里加一个QFile写入按时间戳命名// 追加写入日志文件 void MainWindow::logFrame(const QByteArray frame) { if (!logFile.isOpen()) return; QString line QDateTime::currentDateTime() .toString(yyyy-MM-dd HH:mm:ss.zzz) | frame.toHex( ).toUpper() \n; logFile.write(line.toUtf8()); logFile.flush(); }回放功能就是读文件按时间戳逐条sendFrame中间用QTimer::singleShot控制间隔。这个功能在现场复现问题时特别有用比口头描述“当时发了什么”靠谱得多。5.3 界面响应优化接收数据量大时appendPlainText每来一帧就刷一次界面CPU 占用会飙升。我一般用一个QTimer做批量刷新比如 50ms 一次把缓冲区里的新数据一次性追加到显示区。另外显示区用QPlainTextEdit而不是QTextEdit前者为大量文本做了优化几万行也不卡。5.4 跨平台打包Windows 下用windeployqt把依赖的 DLL 拷到发布目录Linux 下用linuxdeployqt打 AppImage。注意串口模块的 DLL 也要带上windeployqt一般会自动处理但保险起见检查一下Qt5SerialPort.dll在不在。如果目标机器没有 QT 环境静态编译也行但要注意 LGPL 协议问题。从那以后我每次新建串口上位机项目都强制先把readyRead的粘包处理写进去再写界面。因为血泪经验告诉我界面再好看数据收不全就是白搭。希望帮到你。本文还有配套的精品资源点击获取