从零开始搞串口通信:手把手带你用 QSerialPort 写出第一个上位机程序
你有没有遇到过这种情况——手头有个STM32开发板,接上传感器后想看看数据输出,结果发现电脑根本“收不到”?或者写了个小工具发指令给Arduino,可对方毫无反应?
别急。这背后大概率不是硬件坏了,而是串口没配对。
在嵌入式、工业控制和物联网的世界里,串口通信(Serial Communication)就像一条看不见的“电话线”,连接着你的PC和各种设备。它不炫酷,但极其可靠;它速度慢,却足够稳定。更重要的是——只要你懂一点Qt,就能轻松驾驭它。
今天我们要聊的就是 Qt 中那个让你秒变“软硬通吃”的利器:QSerialPort。
不用怕没基础,这篇文章就是为完全零经验者准备的。我们不堆术语,不讲理论套话,只干一件事:从新建项目开始,一步步做出一个能收能发的串口助手。
为什么是 QSerialPort?
你说现在都2025年了,Wi-Fi、蓝牙、USB Type-C满天飞,还谈什么串口?
问得好。
但现实是:你在调试任何一块单片机时,第一件事永远是打开串口打印日志。无论是 Arduino 的Serial.println(),还是 STM32 HAL 库里的printf重定向,背后走的都是 UART 协议。
而作为 PC 端开发者,你要做的就是——听懂这台设备在说什么。
这时候,QSerialPort就登场了。
它是 Qt 官方提供的串口模块,封装了 Windows 的CreateFile/ReadFile、Linux 的/dev/tty*操作,让你用同一套代码,在三个主流操作系统上都能正常工作。
而且它基于 Qt 的信号槽机制,天然适合 GUI 编程。也就是说,你不需要开线程去轮询数据,只要一有新消息进来,系统自动通知你:“嘿,有数据来了!”
这才是现代 C++ 开发该有的样子。
第一步:让工程认识 QSerialPort
新建一个 Qt Widgets Application 项目后,首先要告诉编译器:“我要用串口功能”。
打开.pro文件,加上这一行:
QT += serialport就这么简单。接下来在需要的地方引入头文件:
#include <QSerialPort> #include <QSerialPortInfo>前者负责读写操作,后者用来扫描当前有哪些串口可用。
⚠️ 注意:如果提示找不到
QSerialPort,说明你安装的 Qt 版本没带这个模块。建议使用在线安装器勾选 “Qt Serial Port” 组件。
第二步:找出你的设备插在哪个口上
想象一下:你把 Arduino 插到电脑 USB 口,系统分配了一个 COM3 或/dev/ttyACM0。但你怎么知道是哪一个?难道让用户凭空猜?
当然不行。
所以第一步应该是——枚举所有可用串口,让用户自己选。
#include <QSerialPortInfo> for (const QSerialPortInfo &info : QSerialPortInfo::availablePorts()) { qDebug() << "端口名称:" << info.portName(); qDebug() << "设备描述:" << info.description(); qDebug() << "制造商:" << info.manufacturer(); }运行这段代码,你会看到类似这样的输出:
端口名称: "COM3" 设备描述: "USB Serial Device" 制造商: "Arduino LLC"是不是立刻就知道该连哪个了?
如果你做图形界面,可以把这些信息填充进一个下拉框(QComboBox),用户点一下就能选定目标设备。
第三步:建立连接——参数必须严丝合缝
串口通信不像网络那样智能协商,它的规则非常原始:双方必须事先约定好所有参数,否则就会“鸡同鸭讲”。
这些参数包括:
| 参数 | 常见值 |
|---|---|
| 波特率 | 9600, 115200 |
| 数据位 | 8 |
| 停止位 | 1 |
| 校验位 | 无(NoParity) |
| 流控 | 无(NoFlowControl) |
其中最重要的是波特率。比如你设成 115200,而单片机那边是 9600,那收到的数据就是一堆乱码。
下面我们来配置并打开串口:
QSerialPort serial; // 设置要连接的端口名(根据用户选择) serial.setPortName("COM3"); // Windows 示例 // serial.setPortName("/dev/ttyUSB0"); // Linux/Mac 示例 // 配置通信参数 serial.setBaudRate(QSerialPort::Baud115200); serial.setDataBits(QSerialPort::Data8); serial.setParity(QSerialPort::NoParity); serial.setStopBits(QSerialPort::OneStop); serial.setFlowControl(QSerialPort::NoFlowControl); // 尝试打开 if (serial.open(QIODevice::ReadWrite)) { qDebug() << "✅ 串口已成功打开"; } else { qWarning() << "❌ 打开失败:" << serial.errorString(); }💡坑点提醒:
- 如果提示“权限被拒绝”(Permission denied),在 Linux/macOS 上可能需要将用户加入
dialout或uucp组。- 如果提示“设备正在使用”,检查是否已有其他软件(如串口助手、Arduino IDE)占用了该端口。
第四步:异步接收数据——千万别卡主线程!
很多人初学时喜欢这样写接收逻辑:
while (true) { if (serial.hasBytesAvailable()) { auto data = serial.readAll(); process(data); } }大错特错!
这种轮询方式会阻塞主线程,导致界面直接“卡死”。正确的做法是利用 Qt 的事件驱动模型,通过信号来响应数据到达。
关键信号是:readyRead
connect(&serial, &QSerialPort::readyRead, [&]() { QByteArray data = serial.readAll(); qDebug() << "📩 收到数据:" << data; // 转成字符串显示(支持中文) QString text = QString::fromUtf8(data); ui->textBrowser->append(text); // 显示在文本框中 });这个readyRead()信号会在操作系统内核通知“有新数据到达”时自动触发。你只需要快速把数据取出来处理即可。
✅ 最佳实践:
- 不要在槽函数里做耗时计算(如解析大文件),否则会影响后续数据接收;
- 使用
readAll()一次性读完缓冲区,避免遗漏;- 若需处理帧协议(如以
\n结尾),可用readLine(),但记得设置超时防止阻塞。
第五步:发送数据——你可以发文本也能发十六进制
发送比接收更简单,调用write()就行。
发送普通文本
QString msg = "开启LED\n"; serial.write(msg.toUtf8());注意要用toUtf8(),否则中文会乱码。
发送十六进制命令
有些协议要求特定字节流,比如 Modbus 或自定义二进制协议:
QByteArray cmd; cmd.append(0x01); // 地址 cmd.append(0x03); // 功能码 cmd.append(0x00); // 起始地址高 cmd.append(0x00); // 起始地址低 cmd.append(0x00); // 寄存器数量高 cmd.append(0x01); // 寄存器数量低 cmd.append(0x84); // CRC校验高 cmd.append(0x0A); // CRC校验低 serial.write(cmd);🔍 提示:若需确认数据是否真正发出,可以监听
bytesWritten(qint64 bytes)信号。
第六步:别忘了善后——错误处理与资源释放
你以为打开就完事了?错。真正的健壮程序必须考虑异常情况。
最常见的问题是:用户拔掉了USB转串口线。
此时如果不处理,再调用write()就会导致崩溃。
解决方案是绑定errorOccurred信号:
connect(&serial, &QSerialPort::errorOccurred, [&](QSerialPort::SerialPortError error) { if (error == QSerialPort::ResourceError) { // 通常是设备断开 qCritical() << "🔴 设备已断开:" << serial.errorString(); serial.close(); // 自动关闭,防止后续操作 QMessageBox::warning(this, "警告", "设备已断开,请重新连接"); } });另外,在程序退出或切换端口前,记得手动关闭:
if (serial.isOpen()) { serial.close(); }否则下次可能无法再次打开同一个端口。
实战技巧:那些没人告诉你但必须知道的事
🛑 问题1:明明发了数据,对方却没反应?
先自查以下几点:
- TX 和 RX 是否接反?记住:你的TX要连对方的RX
- GND有没有共地?没有地线,信号就没参考电平
- 波特率是否一致?建议两端都固定为 115200
- 单片机有没有启用串口外设和中断?
可以用串口助手工具反向测试:PC 发 → 单片机收 → 单片机回显 → PC 接收验证
💬 问题2:中文显示乱码?
这是编码问题的经典表现。
解决方法是在接收时明确指定 UTF-8 解码:
QString str = QString::fromUtf8(data);而不是默认的QString(data),后者按 Latin-1 处理,中文必乱。
💥 问题3:程序崩溃或无法重连?
常见原因是对象生命周期管理不当。
比如你在一个函数里声明了QSerialPort serial;,然后绑定了信号槽。但函数结束时栈对象析构,串口也被关闭,但信号还在试图调用已销毁的对象——直接段错误。
✅ 正确做法:
- 把
QSerialPort成员变量放在类中(heap or stack 不重要,作用域要够长) - 或使用
new QSerialPort(this)让父对象自动管理
架构设计建议:做一个专业级串口助手
如果你想把这个小demo升级成实用工具,这里有几个值得加的功能:
| 功能 | 实现思路 |
|---|---|
| ASCII / Hex 显示切换 | 接收时判断模式,用data.toHex(' ')转换 |
| 自动换行刷新 | 定时合并碎片数据,避免每字节刷一次UI |
| 发送历史记录 | 用QCompleter+QSettings保存最近输入 |
| 日志保存 | 将收发内容写入.log文件 |
| 自动重连 | 检测断开后启动 QTimer 延时尝试 reconnect |
甚至可以进一步集成 Modbus RTU 解析器,变成工控级 HMI 工具。
总结:你已经掌握了通往硬件世界的钥匙
看到这里,你应该已经明白:
- 如何发现可用串口
- 如何正确配置参数并打开连接
- 如何异步接收数据而不卡界面
- 如何发送文本和二进制指令
- 如何处理断开、权限等常见异常
这些技能组合起来,足以让你写出第一个真正有用的上位机程序。
无论是用来调试自己的开发板,还是帮同事读取仪器数据,这套方案都经得起实战考验。
更重要的是,你现在已经站在了一个交叉点上:
前端界面 + 后端通信 + 硬件交互
而这正是现代嵌入式软件工程师的核心竞争力。
下一步你想做什么?
- 加个定时发送按钮?
- 把收到的数据画成曲线图?
- 实现 CRC 校验自动计算?
- 支持 Modbus 协议解析?
都可以。因为现在,你已经有了最坚实的基础。
如果你正在尝试实现某个具体功能,欢迎留言交流。我们一起把想法变成现实。