news 2026/9/27 1:52:29

Qt串口通信实战:Keysight 34401A工业数据采集系统设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt串口通信实战:Keysight 34401A工业数据采集系统设计

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 状态机核心流转逻辑

状态编号状态名称触发条件执行动作退出条件
S0Idle用户点击“连接”按钮初始化QSerialPort实例,设置波特率/数据位/停止位serialPort->open()成功
S1HandshakeserialPort->isWritable()为 true发送*IDN?,启动QTimer::singleShot(200, this, &Worker::checkIdn)收到*IDN?响应且含 "34401A"
S2ConfigS1 成功发送CONF:VOLT:DC 10,0.001(设 10V 量程,0.001V 分辨率)收到OK或超时(300ms)
S3ReadyS2 成功发送TRIG:SOUR IMM(触发源设为立即)收到OK
S4AcquireS3 成功发送READ?,启动QTimer::singleShot(150, this, &Worker::parseRead)收到有效浮点数(正则匹配)
S5StoreS4 成功写入 CSV 文件(带时间戳),触发QChart曲线更新文件写入完成
S6ErrorRecovery任意状态超时/响应非法发送*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 占用
11520082ms1722%
1920095ms011%
9600108ms08%

选 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); // 一次性替换全部点,非逐个 append

4. 实操全流程:从 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。

解决方案分三步:

  1. 关闭冲突软件:任务管理器结束io_library.exe、visa32.exe等进程;
  2. 重置 COM 端口:设备管理器 → 端口(COM 和 LPT)→ 右键COM3→ 属性 → 高级 → 将“IRQ”改为0(禁用中断),勾选“使用 FIFO 缓冲区”;
  3. 以管理员身份运行 Qt 程序:右键 exe → “以管理员身份运行”,或在.pro文件加:
RC_FILE = app.manifest

app.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);
  • 验证步骤:
    1. AccessPort 连接 COM3,波特率 9600,发送*IDN?\n(注意勾选“Send CR/LF”);
    2. 若收到KEYSIGHT,34401A,0,1-02-02\n,说明硬件链路正常;
    3. 发送CONF:VOLT:DC 10,0.001\n,再发READ?\n,应返回+1.23456789E+00\n;
    4. 若 AccessPort 正常,Qt 不行,问题必在 Qt 代码的write()或readAll()逻辑。

实操心得:我曾因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.ico

5. 常见问题与排查技巧实录:产线踩过的 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, &
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 1:52:27

C++17 filesystem 实战:路径拼接、目录遍历和错误处理

C17 filesystem 实战&#xff1a;路径拼接、目录遍历和错误处理 手工拼路径字符串容易混淆斜杠、目录和文件名。C17 的 std::filesystem 提供 path 与一组文件系统操作&#xff0c;让代码直接表达“路径”“文件大小”“遍历目录”等意图。最低标准&#xff1a;C17。下面示例只…

作者头像 李华
网站建设 2026/9/27 1:52:05

人脸表情识别考试防作弊系统:从关键点检测到风险评分

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:51:52

Android本地跑LLaMA-7B:QNN框架部署实战与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:51:09

等保2.0内存安全基线检测:用Shell脚本自动核查Linux内核配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:50:54

RK3588网络启动指南:TFTP+NFS部署openEuler 24.03

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:50:13

VMware Workstation Pro 17 安装配置全指南:从下载到网络调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华