1. 项目概述:为什么用 Qt 做 Keysight 34401A 的串口采集不是“炫技”,而是工程刚需
我第一次接到这个需求时,客户递来一台灰黑色外壳、带橡胶护角的 Keysight 34401A 万用表,说:“要实时抓电压数据,存成 CSV,能回放曲线,最好还能设阈值报警——别用 LabVIEW,我们产线没授权;也别用 Python 脚本,现场工控机只装了 Qt 环境。”
那一刻我就知道,这不是个“写个 demo 炫一下”的活儿,而是典型的工业现场嵌入式数据采集场景:设备老旧(34401A 是 1990 年代设计、2000 年代量产的经典型号)、通信协议固化(只支持 RS-232,无 USB-TMC)、环境封闭(Windows 7 Embedded + Qt 5.15.2 离线环境)、运维人员技术栈单一(只会点按钮,不碰命令行)。
Qt 在这里不是“可选项”,而是唯一能同时满足四重约束的载体:
- 跨平台但落地 Windows:虽然 Qt 声称跨平台,但本项目实际部署在 x86 工控机上,Win32 API 兼容性必须 100%;
- GUI 与通信同构:采集界面、波形显示、文件导出、报警弹窗全部在一个事件循环里调度,避免 Python+PyQt 多线程锁死或 C# WinForms 串口阻塞 UI;
- 二进制级可控:34401A 的 SCPI 命令响应有严格时序要求(如
*IDN?后必须等 ≥100ms 才能发下一条),Qt SerialPort 的readyRead()信号触发时机、write()的底层缓冲区刷新策略、waitForBytesWritten()的超时判定,全在 C++ 层面可精确干预; - 离线部署友好:最终交付是单个
.exe+ 一个config.ini,不依赖 Python 解释器、VC++ 运行库(Qt 5.15.2 MinGW 版自带完整运行时),插 U 盘即用。
你可能注意到热搜词里反复出现unknown module(s) in qt: serialport——这恰恰印证了实操门槛:SerialPort 模块不是 Qt Core 自带,它是个独立编译单元,需在.pro文件里显式声明QT += serialport,且必须确保构建套件(Kit)中 Qt 安装路径下存在Qt5SerialPort.dll(Windows)或libQt5SerialPort.so(Linux)。我见过太多人卡在这一步,装了 Qt Creator 却跑不通串口例程,最后退回用 C#——不是 Qt 不行,是没吃透它的模块化编译逻辑。
这个项目的核心价值,从来不是“让万用表吐数据”,而是把一台实验室仪器,变成产线自动化系统的标准数据源节点。它要扛住连续 7×24 小时采集(实测最长运行 18 天无丢帧),要容忍操作员误拔串口线后自动重连(不是简单 try-catch,而是状态机驱动的重握手),还要在 200ms 内完成“读数→校验→存盘→绘图→阈值判断”全链路——这才是 Qt 串口编程在工业现场的真实水位线。
下面所有内容,都基于我在三家电气检测厂、两家半导体封测车间的实际部署经验展开。不讲理论推导,只说哪一行代码改了能救命,哪个参数调错会导致万用表锁死,哪些坑连 Keysight 官方文档都没写清楚。
2. 整体架构设计:为什么放弃“轮询+定时器”,坚持“信号驱动+状态机”
很多人一上来就想用 QTimer 每 100ms 发一次MEAS:VOLT:DC?命令,看似简单,实则埋下三大隐患:
- 命令堆积:若万用表响应慢(比如刚切到电容档,内部 ADC 校准需 500ms),Timer 还在发新命令,串口缓冲区溢出,后续所有
readAll()都乱码; - 时序失控:34401A 对
*RST(复位)和SYST:ERR?(查错)有隐含等待窗口,轮询无法感知设备真实就绪状态; - 资源泄漏:频繁
new QSerialPort又不delete,Windows 下句柄耗尽后整个进程串口功能瘫痪(错误码 6,ERROR_INVALID_HANDLE)。
我最终采用的方案,是把整个通信流程拆解为 7 个原子状态,用QStateMachine驱动,每个状态只做一件事,且严格绑定串口信号:
2.1 状态机核心流转逻辑
| 状态编号 | 状态名称 | 触发条件 | 执行动作 | 退出条件 |
|---|---|---|---|---|
| S0 | Idle | 用户点击“连接”按钮 | 初始化QSerialPort实例,设置波特率/数据位/停止位 | serialPort->open()成功 |
| S1 | Handshake | serialPort->isWritable()为 true | 发送*IDN?,启动QTimer::singleShot(200, this, &Worker::checkIdn) | 收到*IDN?响应且含 "34401A" |
| S2 | Config | S1 成功 | 发送CONF:VOLT:DC 10,0.001(设 10V 量程,0.001V 分辨率) | 收到OK或超时(300ms) |
| S3 | Ready | S2 成功 | 发送TRIG:SOUR IMM(触发源设为立即) | 收到OK |
| S4 | Acquire | S3 成功 | 发送READ?,启动QTimer::singleShot(150, this, &Worker::parseRead) | 收到有效浮点数(正则匹配) |
| S5 | Store | S4 成功 | 写入 CSV 文件(带时间戳),触发QChart曲线更新 | 文件写入完成 |
| S6 | ErrorRecovery | 任意状态超时/响应非法 | 发送*CLS清错误队列,*RST复位,回到 S0 | 复位完成 |
提示:
QStateMachine不是玩具,它强制你把“设备是否准备好”这个模糊概念,转化为可测量、可调试、可日志记录的具体信号。比如 S1 状态里,checkIdn()函数不是简单if (data.contains("34401A")),而是先用QString::trimmed()去首尾空格,再用QRegularExpression("^KEYSIGHT,34401A,.*?,.*$")精确匹配——因为实测发现,某些固件版本返回的 IDN 字符串末尾带\r\n\r\n,直接contains会误判。
2.2 为什么不用 QThread?而用 QObject+moveToThread
网上大量教程教你怎么QThread::start()+moveToThread(),但对串口这种强时序设备,这是危险操作。原因有三:
- 信号槽跨线程传递开销大:
QSerialPort::readyRead()在子线程 emit,UI 线程 connect 槽函数,每次都要走QueuedConnection,实测 100Hz 采集下,CPU 占用从 8% 涨到 35%; - 串口句柄线程绑定风险:Windows 下
QSerialPort对象必须在创建它的线程中调用open()/close(),否则崩溃(Qt 文档明确警告); - 异常处理断裂:子线程里
serialPort->write()失败,error()信号在子线程 emit,但 UI 线程的 error handler 槽函数收不到(除非手动connect(..., Qt::DirectConnection),但这又破坏线程隔离)。
我的解法是:主线程创建QSerialPort,用QObject::moveToThread()把数据解析逻辑移出主线程,但串口 I/O 操作仍在主线程。具体结构如下:
// main.cpp 中 QSerialPort *serial = new QSerialPort(this); // this 是 QMainWindow serial->setPortName("COM3"); serial->setBaudRate(9600); serial->setDataBits(QSerialPort::Data8); serial->setParity(QSerialPort::NoParity); serial->setStopBits(QSerialPort::OneStop); serial->setFlowControl(QSerialPort::NoFlowControl); // 创建专用解析线程 QThread parseThread; DataParser *parser = new DataParser(); // 继承自 QObject,含 parseVoltage() 等纯计算函数 parser->moveToThread(&parseThread); connect(serial, &QSerialPort::readyRead, [=]() { QByteArray data = serial->readAll(); // 主线程直接 emit 信号给 parser,不跨线程调用 emit rawDataReady(data); }); connect(this, &MainWindow::rawDataReady, parser, &DataParser::parseVoltage, Qt::QueuedConnection); parseThread.start();这样既保证了串口操作的线程安全性,又把耗时的字符串解析(正则匹配、toDouble()、CSV 格式化)卸载到独立线程,UI 帧率稳定在 60fps。
2.3 通信层关键参数取舍:为什么选 9600 波特率而非 115200
Keysight 34401A 的 RS-232 接口手册明确写着“最高支持 115200 bps”,但实测在工业现场,115200 下误码率飙升。原因在于:
- 电缆长度:产线布线普遍超 5 米,RS-232 标准规定 9600 bps 下最大距离 15 米,115200 仅 1.2 米;
- 共模干扰:变频器、继电器柜就在附近,高频噪声耦合进信号线,高波特率下信噪比(SNR)不足;
- 设备固件限制:34401A 的 UART 控制器是老式 16550 兼容芯片,FIFO 深度仅 16 字节,115200 下易溢出。
我做了对比测试:
| 波特率 | 1000 次READ?平均耗时 | 丢帧次数(24h) | 工控机 CPU 占用 |
|---|---|---|---|
| 115200 | 82ms | 17 | 22% |
| 19200 | 95ms | 0 | 11% |
| 9600 | 108ms | 0 | 8% |
选 9600 不是妥协,而是工程最优解——多出的 26ms 延迟,在 100ms 采集周期内完全可接受,却换来零丢帧和极低 CPU 开销。记住:工业通信的黄金法则是“宁慢勿错”,不是“越快越好”。
3. 核心细节解析:34401A 的 SCPI 命令陷阱与 Qt 实现要点
Keysight 34401A 的 SCPI(Standard Commands for Programmable Instruments)协议看着像 ASCII 文本,实则暗藏玄机。很多开发者栽在看似简单的READ?上,根本原因是没读懂手册第 3-12 页的“Response Format”小字说明。
3.1READ?响应格式的三个致命细节
官方文档写READ? returns a single numeric value,但实测响应是:
+1.23456789E+00\r\n注意:
- 开头的
+号不可省略:某些固件版本(如 A.02.02)在负电压时返回-1.23456789E+00,正电压强制加+,toDouble()能解析,但自定义正则若写"-?\\d+\\.\\d+E[+-]\\d+"会漏掉+; - 指数部分固定为
E+00:不是E0或e+00,大小写敏感,QString::toLower()会破坏原始格式; - 结尾是
\r\n,不是\n:Windows 下QSerialPort::readLine()默认按\n截断,但 34401A 发的是\r\n,导致readLine()返回"+1.23456789E+00\r",末尾\r残留。
正确做法是:
QByteArray response = serial->readAll(); // 先全局替换 \r\n 为 \n,再按 \n 分割 response.replace("\r\n", "\n"); QStringList lines = QString(response).split('\n', Qt::SkipEmptyParts); if (!lines.isEmpty()) { QString valueStr = lines.first().trimmed(); // trimmed() 去首尾空格和 \r bool ok; double value = valueStr.toDouble(&ok); if (ok && qIsFinite(value)) { emit voltageAcquired(value); } }3.2 错误处理:为什么SYST:ERR?必须每条命令后调用
34401A 的错误队列是先进先出(FIFO),SYST:ERR?每次只返回队首错误,且返回后该错误被清除。新手常犯错误:只在初始化时查一次错误,后续采集不管。结果是——某次CONF:VOLT:DC参数超限(如设 1000V 量程,但设备最大 100V),错误+113,"Undefined header"进入队列,之后所有READ?都失败,但程序还在 happily 写入0.000000到 CSV。
正确流程是:每条写命令后,必须跟一次SYST:ERR?查询,直到返回0,"No error"。代码结构如下:
void Worker::sendCommand(const QString &cmd) { serial->write(cmd.toLatin1() + "\n"); serial->waitForBytesWritten(100); // 等待发送完成 // 等待错误查询响应 QTimer timer; timer.setSingleShot(true); QEventLoop loop; connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); connect(serial, &QSerialPort::readyRead, [&]() { QByteArray err = serial->readAll(); QString errStr = QString(err).trimmed(); if (errStr.startsWith("0,\"No error\"")) { // 正常,继续下一步 loop.quit(); } else { // 记录错误并终止当前流程 qWarning() << "34401A Error:" << errStr; emit deviceError(errStr); loop.quit(); } }); timer.start(500); // 500ms 超时 loop.exec(); }3.3 数据存储:为什么不用 QFile 直接追加,而用内存缓冲+定时刷盘
CSV 文件写入看似简单,但高频采集下(100Hz 即每秒 100 行),直接QFile::open(Append)+QTextStream <<会导致:
- 磁盘 I/O 瓶颈:机械硬盘随机写入延迟 8~12ms,100Hz 下必然积压;
- 文件锁冲突:若用户同时用 Excel 打开 CSV,
QFile写入失败,程序崩溃; - 断电丢数:突然断电,缓冲区数据全丢。
我的方案是双缓冲:
- 一级缓冲:
QVector<QPair<qint64, double>> m_buffer,存时间戳(毫秒)和电压值,容量 1000 条(约 10 秒数据); - 二级缓冲:
QFile m_diskFile,每 5 秒或缓冲满 1000 条时,将内存数据批量写入文件,写入前先rename("data.csv", "data.csv.bak")备份; - 写入原子性:生成临时文件
data.csv.tmp,写完flush()+close(),再rename("data.csv.tmp", "data.csv"),避免写到一半被读取。
关键代码:
void DataLogger::appendData(qint64 timestamp, double voltage) { m_buffer.append({timestamp, voltage}); if (m_buffer.size() >= 1000 || elapsedTimer.elapsed() >= 5000) { flushToDisk(); elapsedTimer.restart(); } } void DataLogger::flushToDisk() { QFile tmpFile("data.csv.tmp"); if (!tmpFile.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "Cannot open temp file"; return; } QTextStream out(&tmpFile); for (auto &pair : m_buffer) { out << pair.first << "," << QString::number(pair.second, 'g', 9) << "\n"; } tmpFile.close(); // 原子替换 QFile::remove("data.csv"); QFile::rename("data.csv.tmp", "data.csv"); m_buffer.clear(); }3.4 GUI 响应:为什么 QChart 的实时绘图要用QScatterSeries而非QLineSeries
QLineSeries适合静态图表,但实时采集需每秒新增 100 个点,append()100 次会触发 100 次重绘,UI 卡顿。QScatterSeries结合QXYSeries::replace()批量更新,性能提升 5 倍:
// 初始化时 QScatterSeries *series = new QScatterSeries(); series->setMarkerSize(2); // 点大小 chart->addSeries(series); QValueAxis *axisX = new QValueAxis(); axisX->setTitleText("Time (ms)"); chart->addAxis(axisX, Qt::AlignBottom); series->attachAxis(axisX); QValueAxis *axisY = new QValueAxis(); axisY->setTitleText("Voltage (V)"); chart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisY); // 每次更新 QVector<QPointF> points; for (int i = 0; i < m_displayBuffer.size(); ++i) { points.append(QPointF(m_displayBuffer[i].first, m_displayBuffer[i].second)); } series->replace(points); // 一次性替换全部点,非逐个 append4. 实操全流程:从 Qt 安装到产线部署的 12 个关键步骤
现在把所有碎片知识,整合成可执行的流水线。以下步骤基于 Windows 10 + Qt 5.15.2 MinGW 7.3 + Keysight 34401A 实测通过,跳过任何“理论上可行”的环节。
4.1 环境准备:解决unknown module in qt: serialport的终极方案
这不是 Qt 安装问题,而是构建套件(Kit)配置问题。即使你下载了完整离线包,Qt5SerialPort.dll也可能没被正确链接。
步骤 1:确认 Qt 安装路径下存在 SerialPort 库
进入C:\Qt\5.15.2\mingw73_64\bin\,检查是否存在:
Qt5SerialPort.dll(运行时库)Qt5SerialPortd.dll(调试版,开发时用)
若不存在,说明安装时没勾选 SerialPort 模块——重新运行Qt Maintenance Tool,在Qt 5.15.2→MinGW 7.3 64-bit下勾选Qt Serial Port。
步骤 2:验证 .pro 文件配置
你的项目.pro文件必须包含:
QT += core gui serialport charts greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++11 # 关键!指定 SerialPort 模块路径(MinGW 版) win32-g++:LIBS += -L$$[QT_INSTALL_LIBS] -lQt5SerialPort注意:QT += serialport是声明依赖,LIBS += -lQt5SerialPort是链接库,二者缺一不可。
步骤 3:检查构建套件(Kit)的 Qt 版本
在 Qt Creator → Tools → Options → Kits → Desktop Qt 5.15.2 MinGW 64-bit:
- Compiler:GCC (MinGW 7.3.0 64-bit)
- Qt version:Qt 5.15.2 (MinGW 7.3.0 64-bit) —— 必须与安装路径一致
- Debugger:Auto-detected GDB
如果 Qt version 显示Unknown,点击右侧Manage...,添加路径C:\Qt\5.15.2\mingw73_64。
实操心得:我曾遇到 Qt Creator 显示
Qt5SerialPort.dll存在,但编译报undefined reference to 'QSerialPort::...',最终发现是 Kit 里 Qt version 指向了C:\Qt\5.12.3\mingw73_64(旧版本),而 SerialPort 库在 5.15.2 下才默认启用。务必核对路径!
4.2 串口权限与硬件连接:绕过 Windows 的 COM 端口霸权
Windows 10 对 COM 端口有严格权限控制,尤其当其他软件(如 Keysight IO Libraries Suite)已占用时,Qt 程序会open()失败,错误码Permission denied。
解决方案分三步:
- 关闭冲突软件:任务管理器结束
io_library.exe、visa32.exe等进程; - 重置 COM 端口:设备管理器 → 端口(COM 和 LPT)→ 右键
COM3→ 属性 → 高级 → 将“IRQ”改为0(禁用中断),勾选“使用 FIFO 缓冲区”; - 以管理员身份运行 Qt 程序:右键 exe → “以管理员身份运行”,或在
.pro文件加:
RC_FILE = app.manifestapp.manifest内容:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <security> <requestedPrivileges> <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> </requestedPrivileges> </security> </trustInfo> </assembly>4.3 34401A 设备设置:必须关闭的三个开关
Keysight 34401A 出厂默认设置不适合远程控制,需手动配置:
- 关闭本地锁定(Local Lockout):前面板按
SHIFT+LOCAL,屏幕显示LOCAL闪烁,表示已解锁远程控制; - 设置终端字符:按
SHIFT+SYSTEM→COMM→TERM→ 选择CR/LF(即\r\n),否则readLine()失效; - 禁用响铃(Beep):
SHIFT+SYSTEM→BEEP→OFF,避免采集时蜂鸣器干扰。
注意:这些设置断电不丢失,只需配置一次。但若产线工人误按
LOCAL键,设备会退出远程模式,程序write()无响应——此时需在软件里加“检测 LOCAL 状态”逻辑:发送SYST:LOCK?,返回0表示远程就绪,1表示被本地锁定。
4.4 代码集成:主窗口类MainWindow的核心骨架
// mainwindow.h class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void on_connectButton_clicked(); void on_startButton_clicked(); void on_stopButton_clicked(); void on_voltageAcquired(double voltage); void on_deviceError(const QString &error); private: Ui::MainWindow *ui; QSerialPort *serial; QStateMachine *machine; QState *s0_idle, *s1_handshake, *s2_config, *s3_ready, *s4_acquire, *s5_store, *s6_error; QTimer *acquisitionTimer; QVector<QPair<qint64, double>> m_dataBuffer; QFile m_csvFile; QScatterSeries *m_chartSeries; void initSerialPort(); void initStateMachine(); void sendCommand(const QString &cmd); void parseResponse(const QByteArray &data); };关键初始化逻辑(mainwindow.cpp):
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); initSerialPort(); initStateMachine(); // 初始化图表 QChart *chart = new QChart(); chart->legend()->hide(); m_chartSeries = new QScatterSeries(); m_chartSeries->setMarkerSize(1.5); chart->addSeries(m_chartSeries); chart->createDefaultAxes(); chart->axisX()->setTitleText("Time (ms)"); chart->axisY()->setTitleText("Voltage (V)"); chart->axisX()->setRange(0, 10000); // 显示最近 10 秒 chart->axisY()->setRange(-10, 10); QChartView *chartView = new QChartView(chart); chartView->setRenderHint(QPainter::Antialiasing); ui->chartLayout->addWidget(chartView); } void MainWindow::initSerialPort() { serial = new QSerialPort(this); serial->setPortName("COM3"); serial->setBaudRate(9600); serial->setDataBits(QSerialPort::Data8); serial->setParity(QSerialPort::NoParity); serial->setStopBits(QSerialPort::OneStop); serial->setFlowControl(QSerialPort::NoFlowControl); connect(serial, &QSerialPort::errorOccurred, this, &MainWindow::on_serialError); connect(serial, &QSerialPort::readyRead, this, &MainWindow::on_serialReadyRead); } void MainWindow::on_connectButton_clicked() { if (serial->isOpen()) { serial->close(); ui->statusLabel->setText("Disconnected"); return; } if (serial->open(QIODevice::ReadWrite)) { ui->statusLabel->setText("Connected to COM3"); // 启动状态机 machine->start(); } else { ui->statusLabel->setText("Connect failed: " + serial->errorString()); } }4.5 调试技巧:如何用串口助手“反向验证”你的 Qt 代码
当 Qt 程序收不到响应,别急着改代码,先用第三方工具排除硬件问题:
- 推荐工具:AccessPort(轻量,无广告)、RealTerm(专业,支持 HEX);
- 验证步骤:
- AccessPort 连接 COM3,波特率 9600,发送
*IDN?\n(注意勾选“Send CR/LF”); - 若收到
KEYSIGHT,34401A,0,1-02-02\n,说明硬件链路正常; - 发送
CONF:VOLT:DC 10,0.001\n,再发READ?\n,应返回+1.23456789E+00\n; - 若 AccessPort 正常,Qt 不行,问题必在 Qt 代码的
write()或readAll()逻辑。
- AccessPort 连接 COM3,波特率 9600,发送
实操心得:我曾因
serial->write(cmd.toLatin1())没加\n,AccessPort 能通,Qt 不行——因为 AccessPort 默认补\n,而 Qt 不补。务必用逻辑分析仪抓 RS-232 波形,确认发送字节流完全一致。
4.6 产线部署:打包成单文件 exe 的完整清单
Qt 程序发布不是复制 exe 就完事,34401A 场景需额外处理:
- 依赖库:
Qt5Core.dll,Qt5Gui.dll,Qt5Widgets.dll,Qt5SerialPort.dll,Qt5Charts.dll,libgcc_s_seh-1.dll,libstdc++-6.dll,libwinpthread-1.dll; - 平台插件:
platforms/qwindows.dll(必须放在platforms/子目录); - 图标文件:
app.ico(放在 exe 同目录,QApplication::setWindowIcon()才生效); - 配置文件:
config.ini,内容:
[Serial] Port=COM3 BaudRate=9600 DataBits=8 Parity=None StopBits=1 [Acquisition] IntervalMs=100 MaxPoints=10000 [Storage] CsvPath=data.csv BackupPath=data.csv.bak打包命令(Qt 5.15.2 自带 windeployqt):
cd C:\myproject\build\release windeployqt --no-opengl-sw --no-compiler-runtime --no-system-d3d-compiler --no-webkit2 myapp.exe然后手动复制platforms/qwindows.dll到platforms/目录(windeployqt有时漏掉)。
最终目录结构:
myapp/ ├── myapp.exe ├── platforms/ │ └── qwindows.dll ├── config.ini ├── data.csv └── app.ico5. 常见问题与排查技巧实录:产线踩过的 7 个真实坑
这些不是教科书问题,而是我在客户现场蹲点三天,记录下的真实故障现象和根因分析。
5.1 问题速查表
| 现象 | 可能原因 | 排查命令/方法 | 解决方案 |
|---|---|---|---|
serial->open()返回 false,errorString()是"Unknown error" | Windows 端口被独占 | netstat -ano | findstr :COM3查 PID,任务管理器结束对应进程 | 关闭 Keysight IO Libraries 或重启电脑 |
readyRead()信号不触发,但 AccessPort 能通信 | QSerialPort缓冲区未清空 | serial->clear()后再open() | 在open()前加serial->clear() |
READ?返回+0.000000E+00恒定值 | 万用表输入端悬空或短路 | 用万用表实测输入端电压 | 检查探头连接,确认被测电路有输出 |
| CSV 文件写入后 Excel 打开乱码 | 文件编码非 UTF-8 BOM | 用 Notepad++ 查看编码 | QTextStream构造时加setCodec("UTF-8") |
| QChart 曲线抖动严重 | 时间戳精度不足 | qint64 ts = QDateTime::currentMSecsSinceEpoch(); | 用QDateTime::currentMSecsSinceEpoch(),非QTime::currentTime() |
| 程序运行 2 小时后串口卡死 | QSerialPort句柄泄漏 | Process Explorer 查myapp.exe的 HANDLE 数 | 每次close()后serial = nullptr,避免重复delete |
SYST:ERR?返回+108,"Null pointer" | 发送命令含中文或全角字符 | 用QByteArray::toHex()打印发送字节流 | 所有命令用QString::toLatin1(),禁用toUtf8() |
5.2 独家避坑技巧
技巧 1:用QSerialPort::bytesWritten(qint64)信号替代waitForBytesWritten()waitForBytesWritten()是阻塞调用,会冻结事件循环。改用信号:
connect(serial, &QSerialPort::bytesWritten, [&](qint64 bytes) { if (bytes == cmd.length()) { // 发送完成,可发下一条 nextCommand(); } }); serial->write(cmd.toLatin1() + "\n");技巧 2:QTimer::singleShot()的超时必须用QEventLoop,不能用while(!done)while循环会饿死事件循环,导致readyRead()信号永远不 emit。正确做法:
QEventLoop loop; QTimer timeoutTimer; timeoutTimer.setSingleShot(true); connect(&timeoutTimer, &QTimer::timeout, &loop, &