手把手教程:使用QSerialPort实现简单串口收发
2026/5/17 3:56:43 网站建设 项目流程

手把手教你用 QSerialPort 写一个能跑的串口助手

你有没有遇到过这样的场景:手头一块刚焊好的开发板,连上电脑死活没反应;或者调试 GPS 模块时,串口工具一发指令就卡住?别急,今天我们就从零开始,用Qt 的 QSerialPort搭一个真正可用的串口收发程序。不是那种“能编译就行”的玩具项目,而是一个你可以直接拿去改造成自己上位机的小工具。

重点是——全程不讲废话,只说人话,代码能跑,坑我都替你踩过了。


为什么选 QSerialPort?别再用 C 风格 API 了!

你说串口通信不就是打开、读写、关闭吗?Linux 下open()read()write()不香吗?

香是香,但前提是你的程序不需要界面、不用跨平台、也不怕不同系统之间行为不一致。

一旦你要做个带按钮、文本框、能自动刷新端口列表的 GUI 工具,你会发现:

  • Windows 上 COM 口要走 Win32 API
  • Linux 要配 termios 结构体,权限还可能不够
  • macOS 又是另一套设备节点命名规则

这时候你就明白,统一抽象有多重要

而 Qt 的QSerialPort就干了这件事。它把底层这些破事全封装了,你只需要关心:

“我要连哪个口?”、“波特率设多少?”、“数据来了怎么处理?”

剩下的交给 Qt。一套代码,Windows、Linux、macOS 全都能跑。

而且它是标准 Qt 类,支持信号槽、自动内存管理、IDE 断点调试……简直是现代 C++ 开发者的福音。


第一步:先把环境搭起来

先别急着写逻辑,第一步永远是让工程能编译通过。

.pro文件里加上这一行:

QT += serialport

就这么一句。没了。

如果你用的是 Qt Creator,记得重新运行 qmake 或构建一下项目,不然会报undefined reference

头文件也简单:

#include <QSerialPort> #include <QSerialPortInfo>

前者用来操作串口,后者可以帮你枚举当前系统有哪些串口设备可用。

比如你想让用户从下拉菜单选端口,就可以这么做:

foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { qDebug() << "发现串口:" << info.portName() << "(" << info.description() << ")"; }

输出可能是:

发现串口: "COM3" ("USB Serial Port")

这样你就知道该连哪个口了。


第二步:参数配不对,通信全白费

串口通信就像两个人打电话,必须约定好“语速”和“暗号”,否则听到的全是乱码。

最常见的配置是这组黄金组合:

115200-8-N-1

翻译成人话就是:

  • 波特率:115200(每秒传这么多比特)
  • 数据位:8 位(一个字节)
  • 校验位:无(现在设备都挺稳,基本不用校验)
  • 停止位:1 位(表示一个字符结束)

代码怎么写?

QSerialPort serial; serial.setPortName("COM3"); // 端口号 serial.setBaudRate(QSerialPort::Baud115200); // 波特率 serial.setDataBits(QSerialPort::Data8); // 8位数据 serial.setParity(QSerialPort::NoParity); // 无校验 serial.setStopBits(QSerialPort::OneStop); // 1位停止 serial.setFlowControl(QSerialPort::NoFlowControl); // 不用流控

注意!所有参数都有对应的枚举值,不要自己填数字。比如Baud115200是 Qt 定义的常量,比你写115200更安全,编译器还能帮你检查拼写错误。


第三步:打开串口,别忘了信号槽

串口打开了就能收发了吗?错。关键在于——你怎么知道有数据来了?

传统做法是开个线程不停read(),累不累?

Qt 的做法优雅得多:事件驱动 + 信号槽

只要调用了open(),串口就会在后台监听数据。一旦收到新内容,立刻触发readyRead()信号。

所以我们这么写:

if (serial.open(QIODevice::ReadWrite)) { qDebug() << "✅ 串口已打开"; connect(&serial, &QSerialPort::readyRead, this, &MainWindow::onDataReceived); connect(&serial, &QSerialPort::errorOccurred, this, &MainWindow::onSerialError); } else { qWarning() << "❌ 打不开串口:" << serial.errorString(); }

这里有两个重点:

  1. readyRead信号会在有数据可读时自动触发,不会阻塞主线程
  2. errorOccurred能捕获断开连接、权限问题等异常,让你及时响应。

⚠️ 坑点提醒:有些新手喜欢在while(1)里循环read(),结果 UI 直接卡死。记住,GUI 程序永远不要做阻塞操作!


第四步:发数据很简单,但有个细节要注意

发送数据用write(),返回值是写入缓冲区的字节数:

void MainWindow::sendData(const QString &text) { QByteArray data = text.toUtf8(); // 推荐 UTF-8 编码 qint64 result = serial.write(data); if (result == -1) { qWarning() << "❌ 发送失败:" << serial.errorString(); } else { qDebug() << "📤 已发送" << result << "字节"; } serial.flush(); // 强制立即发送(可选) }

关于flush()的说明:

  • 大多数情况下不用调,操作系统会自动调度。
  • 如果你对实时性要求极高(比如控制机械臂),可以加一句强制刷出。

另外,如果是发送 HEX 数据(比如0x02 0x03),那就别用QString,直接构造QByteArray

QByteArray hexData; hexData << 0x02 << 0x03 << 0xFF; serial.write(hexData);

第五步:收数据最难的地方——别以为 readAll() 就完事了

很多人以为readAll()一调,数据就完整了。错得很离谱。

真实世界中,串口数据是“流式到达”的。比如你发了一个 JSON 包:

{"temp":25,"hum":60}

可能第一次readyRead收到{\"temp\":25,
第二次才收到"hum\":60}

这就叫粘包/拆包

所以你在onDataReceived里不能直接解析,得缓存起来,等一整帧收完再说:

private slots: void onDataReceived() { QByteArray chunk = serial.readAll(); receiveBuffer.append(chunk); // 累积到缓冲区 // 示例:以换行符为分隔符判断是否接收完成 while (receiveBuffer.contains('\n')) { int index = receiveBuffer.indexOf('\n'); QByteArray frame = receiveBuffer.left(index + 1); receiveBuffer.remove(0, index + 1); processFrame(frame); // 解析这一帧 } }

当然,实际协议可能是:

  • 固定长度帧
  • 带帧头帧尾(如0x7E ... 0x7E
  • 包含长度字段

那你就要根据具体协议来判断什么时候“收完了”。

✅ 秘籍:宁可在readyRead里多调几次readAll(),也不要试图在里面 sleep 或做耗时计算,否则会影响后续数据接收。


第六步:那些没人告诉你但必须知道的事

1. 权限问题(Linux/macOS 常见)

插上 USB 转串口模块,发现打不开/dev/ttyUSB0

大概率是权限不够。

解决方法有两个:

  • 临时方案:sudo chmod 666 /dev/ttyUSB0
  • 永久方案:写 udev 规则,把用户加入 dialout 组

推荐后者:

sudo usermod -aG dialout $USER

然后重启生效。


2. 串口被占用怎么办?

另一个程序(比如串口助手、Arduino IDE)开着,你就打不开。

解决办法:

  • 提示用户先关闭其他程序
  • 在打开前尝试探测是否已被占用

可以用QSerialPort::open()返回值判断。


3. 如何实现自动重连?

设备突然拔掉 USB,下次插回来希望自动连上?

思路是监听errorOccurred中的DeviceNotFoundError,然后启动一个定时器轮询:

void MainWindow::onSerialError(QSerialPort::SerialPortError error) { if (error == QSerialPort::DeviceNotFoundError) { QTimer::singleShot(2000, this, &MainWindow::tryReconnect); } }

当然,别无限重试,加个计数上限更稳妥。


4. 显示模式切换:ASCII 还是 HEX?

调试时经常需要看原始字节流。建议你的界面提供两种显示方式:

  • ASCII:适合看日志、JSON、AT 指令
  • HEX:适合看二进制协议、Modbus、加密数据

转换也很简单:

QString hexStr = receiveData.toHex(' ').toUpper(); // → "02 03 FF"

最后一点建议:别把简单事搞复杂

我见过太多“高级”串口工具,功能一堆,结果连基本收发都不稳定。

刚开始学,记住几个原则:

  1. 先让它通起来:哪怕只有一个输入框、一个输出框、两个按钮(打开/发送)
  2. 参数可配置:波特率、端口名能改
  3. 日志要清晰:打印时间戳、方向(TX/RX)、数据内容
  4. 别迷信多线程QSerialPort本身是非阻塞的,除非特殊需求,根本不需要另开线程

等你把这个最简版本跑通了,再慢慢加功能:

  • 自动识别设备
  • 协议解析面板
  • 数据绘图
  • 日志保存

写在最后

到现在为止,你应该已经掌握了如何用QSerialPort实现一个真正可用的串口通信程序。

它不只是“技术demo”,而是你能拿去实战的基础框架。

无论你是要做温湿度监控上位机、PLC 调试工具,还是无人机地面站,底层通信逻辑都逃不开这几个步骤:

枚举 → 配置 → 打开 → 发送 ← 接收 → 解析

QSerialPort把这套流程变得异常简洁。

下次当你面对一块沉默的开发板时,不要再靠别人的串口工具碰运气了。

自己动手,写一个属于你的通信桥梁。

如果你实现了基础功能,欢迎留言交流你遇到的问题,我可以帮你一起优化架构。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询