4. 性能分析工具入门
串口编程,说白了就是让Qt程序和外部设备「对话」。QSerialPort这个类,我用了快十年,踩过的坑比写过的代码还多。今天咱们就把它的核心API、枚举串口、打开配置关闭的正确姿势,以及信号槽机制,一次性讲透。
本章核心脉络:QSerialPortInfo枚举 → QSerialPort实例化 → 配置参数 → 打开端口 → 读写数据 → 错误处理 → 优雅关闭。每一步都有坑,每一步都有解法。
4.1 QSerialPortInfo:如何找到可用的串口?
你想想看,程序跑起来,怎么知道电脑上插了几个串口?哪个是USB转串口?哪个是板载串口?QSerialPortInfo就是干这个的。
// 枚举所有可用串口
QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
foreach (const QSerialPortInfo &info, ports) {
qDebug() << "端口名:" << info.portName();
qDebug() << "描述:" << info.description();
qDebug() << "制造商:" << info.manufacturer();
qDebug() << "序列号:" << info.serialNumber();
qDebug() << "系统位置:" << info.systemLocation();
qDebug() << "是否可用:" << info.isBusy();
}
我个人习惯,在程序启动时先枚举一遍,把结果塞到QComboBox里让用户选。这样用户不用记COM口号,直接看描述就知道哪个是目标设备。
小技巧:用info.description()和info.manufacturer()做过滤。比如只显示「USB Serial Port」或者「CP2102」之类的设备,把蓝牙虚拟串口过滤掉。我在做工业采集项目时,就靠这个过滤掉了系统自带的调试串口。
4.2 QSerialPort核心API:你必须掌握的接口
QSerialPort的API其实不多,但每个参数都关乎生死。我列个表,你一看就明白。
| API / 属性 | 作用 | 常见取值 | 我的建议 |
|---|---|---|---|
setPortName() |
设置串口名 | "COM3", "/dev/ttyUSB0" | 从QSerialPortInfo获取,别硬编码 |
setBaudRate() |
波特率 | 9600, 115200, 921600 | 115200最常用,高速用921600 |
setDataBits() |
数据位 | Data8, Data7 | 99%的情况用Data8 |
setParity() |
校验位 | NoParity, EvenParity, OddParity | 无校验最通用 |
setStopBits() |
停止位 | OneStop, TwoStop | OneStop就够了 |
setFlowControl() |
流控制 | NoFlowControl, HardwareControl, SoftwareControl | NoFlowControl,除非设备明确要求 |
open() |
打开串口 | QIODevice::ReadWrite | 读写模式打开 |
close() |
关闭串口 | — | 析构前必须调用 |
write() |
发送数据 | QByteArray | 注意数据完整性 |
read() |
读取数据 | qint64 maxSize | 配合readyRead使用 |
bytesAvailable() |
可读字节数 | — | 读取前先检查 |
clear() |
清空缓冲区 | — | 异常恢复时调用 |
error() |
获取错误码 | QSerialPort::Error | 信号槽中处理 |
4.3 打开/配置/关闭串口的正确姿势
嗯,这里要注意。很多新手上来就写:
// 错误示范
QSerialPort *serial = new QSerialPort("COM3");
serial->setBaudRate(115200);
serial->open(QIODevice::ReadWrite);
这段代码能跑,但经不起折腾。正确的姿势应该是这样:
// 正确示范
QSerialPort *serial = new QSerialPort(this);
// 1. 先配置,再打开
serial->setPortName("COM3");
serial->setBaudRate(QSerialPort::Baud115200);
serial->setDataBits(QSerialPort::Data8);
serial->setParity(QSerialPort::NoParity);
serial->setStopBits(QSerialPort::OneStop);
serial->setFlowControl(QSerialPort::NoFlowControl);
// 2. 打开并检查返回值
if (!serial->open(QIODevice::ReadWrite)) {
QMessageBox::critical(this, "错误",
"无法打开串口: " + serial->errorString());
delete serial;
serial = nullptr;
return;
}
qDebug() << "串口打开成功";
警告:千万不要在构造函数里打开串口!我曾经在一个项目中,把串口打开操作写在窗口构造函数里,结果设备没插,程序直接崩了。正确的做法是:提供一个「打开串口」按钮,让用户确认设备已连接后再操作。
关闭串口也有讲究:
// 安全关闭
void closeSerialPort() {
if (serial && serial->isOpen()) {
serial->clear(); // 清空缓冲区
serial->close(); // 关闭串口
qDebug() << "串口已关闭";
}
}
我个人习惯在窗口关闭事件里也调用一次关闭,防止用户直接点X导致资源泄漏。
4.4 错误处理与信号槽机制
串口通信最怕什么?设备突然断开、数据线松动、波特率不匹配。这些错误如果不处理,程序就会卡死或者数据错乱。
QSerialPort提供了两个关键信号:
readyRead():有数据可读时触发errorOccurred(QSerialPort::SerialPortError):发生错误时触发
看一个完整的信号槽绑定示例:
// 在初始化中绑定信号
connect(serial, &QSerialPort::readyRead,
this, &MainWindow::onReadyRead);
connect(serial, &QSerialPort::errorOccurred,
this, &MainWindow::handleError);
// 读取数据槽函数
void MainWindow::onReadyRead() {
QByteArray data = serial->readAll();
// 注意:这里不要假设一次读完
// 实际项目中需要组包处理
qDebug() << "收到数据:" << data.toHex();
}
// 错误处理槽函数
void MainWindow::handleError(QSerialPort::SerialPortError error) {
switch (error) {
case QSerialPort::NoError:
break;
case QSerialPort::DeviceNotFoundError:
QMessageBox::warning(this, "错误", "设备未找到");
break;
case QSerialPort::PermissionError:
QMessageBox::warning(this, "错误", "权限不足,可能被其他程序占用");
break;
case QSerialPort::OpenError:
QMessageBox::warning(this, "错误", "串口已被打开");
break;
case QSerialPort::TimeoutError:
qDebug() << "读取超时";
break;
default:
QMessageBox::critical(this, "未知错误",
"错误码: " + QString::number(error));
break;
}
// 严重错误时自动关闭
if (error != QSerialPort::NoError &&
error != QSerialPort::TimeoutError) {
closeSerialPort();
}
}
避坑指南:我曾经在readyRead里直接调用serial->readAll(),以为一次就能读完一帧数据。结果发现数据被拆成了两段,导致解析失败。后来我改用缓冲区+超时机制,才彻底解决。具体做法是:收到数据先存到QByteArray缓冲区,等100ms没新数据了,再当完整一帧处理。
4.5 知识体系总览
下面这张图,把QSerialPort的完整工作流程画出来了。你照着这个思路写代码,基本不会出大问题。
4.6 实战中的几个关键点
最后,分享几个我这些年总结的经验:
- 不要在主线程里读写大量数据。串口通信虽然快,但如果数据量大,还是建议扔到QThread里。否则界面会卡顿。
- 每次打开前先检查串口是否被占用。用
QSerialPortInfo::isBusy()判断,避免打开失败。 - 关闭串口后记得置空指针。防止野指针导致程序崩溃。
- 错误信号一定要处理。哪怕只是打印日志,也比什么都不做强。我在现场调试时,就靠错误日志定位过无数次问题。
血的教训:有一次我在产品发布前夜,发现串口偶尔打不开。查了半天,原来是上一个窗口关闭时没调用close(),导致串口资源没释放。从那以后,我每次关闭串口都会加日志,确认资源确实释放了。