
简介这是一份面向仪器通信和数据采集开发者的代码工程用于读取三和数字显示表与固纬数字万用表GDM-8261A的测量数据经过格式转换后发送至东华测试系统3815N适用于实验室多源数据采集、自动化测试与仪器系统集成等场景。压缩包共25个文件以C源码为主包含8个cpp与8个头文件覆盖主窗口、串口通信、输入输出数据、协议解析等模块另附有配置文件、工程文件、界面资源与软件更新日志整体仅32KB轻量且目录结构清晰方便快速定位和二次开发。目前已有144人学习下载适合具备串口编程基础或正在搭建类似数据采集链路的工程师参考。代码中串口通信基于qextserialport实现支持默认与自定义配置数据协议、格式转换和发送逻辑均有明确划分可帮助使用者理解从仪表读取到上报东华测试系统的完整流程减少重复调试时间。1. 两块仪表拼一路数据三和数字显示表与 GDM-8261A 接入 3815N 的中间层方案在结构试验台现场经常遇到这样的组合一路用三和数字显示表看应变或位移一路用固纬 GDM-8261A 万用表采电压、电阻最后所有数据都要汇总进东华测试系统 3815N 的时间序列。官方软件各管各的数据对不齐导出的时间轴也对不上。这套基于 Qt 与 qextserialport 的多源数据采集软件multiSourceDAQ就是为了解决这个问题两个串口分别读仪表解析成统一的数值结构再按 3815N 能识别的帧格式转发。它的核心价值不是读串口本身而是把仪表协议和采集系统协议解耦设备更换或通道扩展只改配置不动代码。2. GDM-8261A 的 SCPI 指令解析与三和显示表 ASCII 流采集2.1 GDM-8261A 串口初始化的参数选择GDM-8261A 是固纬 6 位半数字万用表支持 RS-232 和 USB 虚拟串口两种物理连接。上电后仪表默认串口参数是 9600 波特率、8 数据位、无校验、1 停止位。如果之前被人改过按面板I/O键进设置菜单把波特率、校验位和通信协议改回与代码定义一致。常见做法是把串口对象创建为轮询模式而不是事件驱动模式原因是 GDM-8261A 在连续测量时每秒能回传几十帧数据事件驱动模式在 Qt 事件循环繁忙时会产生信号积压轮询模式能更稳定地控制读取节奏。// mainwindow.cpp 中 GDM-8261A 串口的初始化片段 Win_QextSerialPort *gdmPort new Win_QextSerialPort(COM4, QextSerialBase::Polling); gdmPort-setBaudRate(BAUD9600); gdmPort-setDataBits(DATA_8); gdmPort-setParity(PAR_NONE); gdmPort-setStopBits(STOP_1); gdmPort-setFlowControl(FLOW_OFF); gdmPort-setTimeout(50); // 单次读超时 50ms if (!gdmPort-open(QIODevice::ReadWrite)) { qWarning() [GDM-8261A] 串口打开失败检查 COM 口号与占用状态; return; }这段代码里值得注意的参数是setTimeout(50)。轮询模式下readAll()调用最多阻塞 50ms这个值既不会让界面线程长时间卡住又给仪表返回数据留出足够的时间窗口。波特率如果改用 19200setTimeout可以缩到 30ms数据吞吐更紧凑代价是抗干扰能力下降串口线超过 2 米时建议保持 9600。2.2 SCPI 测量指令与返回值解析GDM-8261A 支持标准 SCPI 指令软件里最常见的做法是定时发送测量指令然后读取返回的 ASCII 字符串。指令与返回值的关系如下表指令功能典型返回*IDN?查询设备标识GW INSTEK,GDM-8261A,GDM-8261A xxxxx,1.0MEAS:VOLT:DC?测量直流电压3.2987654E00MEAS:RES?测量电阻9.8765432E02MEAS:CURR:DC?测量直流电流1.2345000E-02FETCh?读取最近一次测量结果3.2987654E00测量指令返回的都是科学计数法 ASCII 串以回车换行结尾。解析这一步用QByteArray::toDouble()即可但要注意仪表刚切换量程时可能返回9.9E37这样的溢出占位值需要在解析后做数值范围过滤。// inputdata.cpp 中 SCPI 返回串的处理逻辑 QByteArray raw gdmPort-readAll(); raw raw.trimmed(); // 去掉 \r\n bool ok false; double reading raw.toDouble(ok); if (!ok) { // 非数值返回一般是量程切换或指令错误直接丢弃这一帧 return; } if (fabs(reading) 1e37) { // 溢出占位值标记为无效数据 emit valueReady(GDM_8261A_ID, qQNaN()); return; } emit valueReady(GDM_8261A_ID, reading);这段解析代码的关键在于对非数值和溢出占位值的两层过滤。实际联调中发现GDM-8261A 在自动量程切换时偶发返回9.9E37如果不做过滤这帧数据会直接转发到 3815N在应变曲线上形成明显毛刺。2.3 三和数字显示表的 ASCII 流读取三和数字显示表SV 系列面板表是常见型号的串口输出和 SCPI 完全不同它通常是周期性主动推送 ASCII 字符串例如01234.5\r\n不依赖查询指令。读取这类数据流时不能简单地每次readAll()后调用toDouble()处理因为串口缓冲可能把两帧数据粘在一起也可能把一帧数据切成两半。// inputdata.cpp 中针对连续 ASCII 流的缓冲解析 static QByteArray sanwaBuffer; sanwaBuffer.append(sanwaPort-readAll()); int newlineIdx -1; while ((newlineIdx sanwaBuffer.indexOf(\n)) 0) { QByteArray line sanwaBuffer.left(newlineIdx); sanwaBuffer.remove(0, newlineIdx 1); line line.trimmed(); bool ok false; double value line.toDouble(ok); if (ok) { emit valueReady(SANWA_ID, value); } }这里的关键是把readAll()得到的数据先追加到缓冲区再按换行符切帧每切出一行就解析一行剩余数据留在缓冲区等下一轮。与直接readAll()后立即解析相比这种缓冲方式能正确处理半帧、粘帧的情况在低速仪表上尤其有效。三和显示表的输出格式与型号有关配置customConfig.txt里的model字段可以切换不同的帧解析器。3. qextserialport 双串口轮询调度与 UI 刷新隔离3.1 为什么选 qextserialport 而不是 Qt SerialPortQt 5 自带的QSerialPort功能完整但 multiSourceDAQ 这个工程用的是 qextserialport 这套老牌第三方库目录里也带了qextserialport.cpp、win_qextserialport.cpp的完整源码。原因很实际项目早期基于 Qt 4那时QSerialPort还没进入官方库qextserialport 是 Linux 和 Windows 下唯一能稳定编译的选择而且它的Win_QextSerialPort对 Windows 底层做了封装读写性能很稳定。如果你的开发环境是 Qt 5.10 以上可以考虑换成官方库但要注意两者的事件模型差异QSerialPort的readyRead信号是异步的qextserialport 的 Polling 模式是同步阻塞的混用时容易踩坑。3.2 定时器驱动的轮询读取软件在mainwindow.cpp里用QTimer驱动读取周期默认interval_ms是 200ms对应 5Hz 采样率。对静态应变和电压采集足够对 GDM-8261A 的高速测量模式则偏慢可调至 50ms。轮询模式下的代码结构是// mainwindow.cpp 中采集周期定时器的启动逻辑 QTimer *pollTimer new QTimer(this); connect(pollTimer, QTimer::timeout, this, MainWindow::collectOneCycle); pollTimer-start(config.pollIntervalMs);collectOneCycle()里依次完成三件事给 GDM-8261A 发送MEAS:VOLT:DC?、读取返回并解析、读取三和显示表异步推送的数据。因为两个串口对象都是轮询模式这一步和 C# 循环采集时的 UI 刷新卡顿问题不太一样。Qt 的QTimer回调本身运行在 GUI 线程单次回调如果执行超过 100ms界面刷新依然会掉帧所以解析后的数值不要直接操作控件而是先存到成员变量再调用update()触发一次异步重绘。3.3 工作线程中的读取逻辑更稳妥的版本是把collectOneCycle()放到QThread里跑主界面只管展示。线程化的关键点有三个串口对象的创建、打开、读取必须在同一线程内完成跨线程传数值用 signal/slot 的 QueuedConnection程序退出时先停定时器再关串口最后销毁线程对象。// 工作线程入口循环读取直至停止标志置位 void CollectorThread::run() { while (!m_stopFlag.load()) { // 给 GDM-8261A 发查询指令 m_gdmPort-write(MEAS:VOLT:DC?\r\n); m_gdmPort-flush(); // 等待返回并解析 QByteArray resp m_gdmPort-readAll(); double gdmValue parseScpi(resp); // 读取三和显示表缓冲 double sanwaValue parseAsciiStream(m_sanwaPort-readAll()); // 通过信号传回 GUI 线程 emit dataReady((float)gdmValue, (float)sanwaValue); msleep(m_intervalMs); } }这个循环背后有一个容易被忽略的细节发送查询指令后没有立刻readAll等待而是靠下一轮循环重新进入时读取上一轮的响应。这样设计规避了串口读超时阻塞带来的节奏抖动但代价是数据滞后一个采集周期。如果测量对象是缓慢变化的应变或温度200ms 的滞后完全可以接受如果是快速变化的电压信号就需要把查询改为连续测量模式让 GDM-8261A 自身按固定速率输出软件只收不查。4. 数据帧封装与发送到东华 3815N 的协议适配4.1 dataprotocol.h 中的帧结构定义东华 3815N 是静态应变与应力测试分析系统配套上位机软件一般通过以太网接收外部数据。multiSourceDAQ 的dataprotocol.h里定义了统一的数据集结构把所有仪表数据打包成 3815N 可解析的帧这是整个转换层最核心的部分。帧结构常见做法如下// dataprotocol.h 中的上行数据帧定义 #pragma pack(push, 1) #define FRAME_HEADER 0xA5 #define CMD_REALTIME_DATA 0x10 #define MAX_CHANNELS 32 typedef struct { quint8 header; // 帧头固定 0xA5 quint8 command; // 命令字0x10 表示实时数据 quint8 deviceId; // 设备编号1三和2GDM-8261A quint8 channelIndex; // 通道序号写入 3815N 的物理通道号 quint8 year, month, day; // 时间戳日期 quint8 hour, minute, second; // 时间戳时刻 float dataValue; // 浮点数值单位由配置决定 quint8 checksum; // 校验和逐字节异或 } RealtimeDataFrame; #pragma pack(pop)这个结构里deviceId和channelIndex是两个容易混淆的字段。deviceId标识数据源是哪台仪表channelIndex才是写入 3815N 的通道号。比如三和显示表接 3815N 的第 3 通道GDM-8261A 接第 4 通道那么两者channelIndex分别为 3 和 4deviceId分别为 1 和 2。两组仪表的数据集必须严格按通道索引对应错一位就会导致整条曲线数据张冠李戴。4.2 customConfig.txt 驱动通道映射customConfig.txt是运行时的配置入口软件启动时读取并初始化串口和网络连接。一个典型的配置内容如下[gdm8261a] portCOM4 baudrate9600 interval_ms200 timeout_ms50 [sanwa] portCOM3 baudrate9600 modelSV-10 interval_ms200 [donghua3815n] modetcp_client ip192.168.0.88 port2101 channel_gdm4 channel_sanwa3配置解析在inputdata.cpp中实现用QSettings按节读取。核心参数含义见下表配置节点参数含义[gdm8261a]port/interval_msGDM-8261A 串口号与读取周期[sanwa]port/model三和显示表串口号与协议型号[donghua3815n]mode连接模式tcp_client或tcp_server[donghua3815n]ip/port3815N 上位机地址与端口[donghua3815n]channel_gdm/channel_sanwa两台仪表数据写入的通道号其中mode参数需要多说一句。3815N 的配套软件有些版本做 TCP Server 监听有些版本做 TCP Client 主动连接。联调前先确认上位机软件的监听方式把mode配成对应的角色。如果两边都是 Client或者都是 Server连接永远建立不起来报错还容易误判成网线或防火墙问题。4.3 TCP 发送与断线重连数据发送用QTcpSocket在outputdata.cpp里封装了一个发送方法处理断线重连// outputdata.cpp 中发送一帧数据到 3815N void OutputData::sendFrame(const RealtimeDataFrame frame) { if (m_socket-state() ! QAbstractSocket::ConnectedState) { // 尝试重连失败则丢弃本轮数据 m_socket-connectToHost(m_cfg.ip, m_cfg.port); if (!m_socket-waitForConnected(200)) { qWarning() 3815N 连接失败等待下一个采集周期; return; } } QByteArray payload; payload.append((const char*)frame, sizeof(RealtimeDataFrame)); m_socket-write(payload); }发送前的状态检查很关键。3815N 上位机在采集过程中偶尔会重启采集会话导致 TCP 连接断开。这里的策略是检测到非连接状态就尝试重连重连失败直接丢弃这帧数据等下一个周期再试。不要在sendFrame里做循环重连否则会阻塞采集主循环导致仪表端数据积压缓冲区的旧数据混入新帧。channel_gdm4这类配置的意义在于当 GDM-8261A 测量电压、三和显示表测量应变时两者物理含义不同通道号必须严格对应 3815N 的通道定义。改接仪表或调换通道顺序时只改配置文件不用动代码这正是协议解耦带来的收益。5. 联调技巧三源数据对齐、异常过滤与日志验证5.1 三源数据的时间对齐GDM-8261A 是查询-响应模式三和显示表是主动推送模式两者天然不同步。软件中的处理方式是统一打上软件时间戳而不是依赖仪表自身的时间标记。RealtimeDataFrame里的日期和时间字段就在封帧时取自QDateTime::currentDateTime()。如果现场对时间轴有更高精度的同步需求可以把dataValue换成结构体增加一个quint32 millisecond字段在每次collectOneCycle()入口处取毫秒级时间这个改动对 3815N 端完全透明只是帧长增加 4 字节。5.2 异常值过滤的阈值策略联调时最容易出现的问题是某台仪表在量程切换瞬间输出一个极端值比如 GDM-8261A 返回9.9E37或者三和显示表偶发输出0.0000。处理方式是做一阶防抖对连续两帧数据求差值如果差值超过配置的阈值则判定为瞬时毛刺并替换为上一帧有效值。// outputdata.cpp 中的防抖过滤逻辑 static double lastGdmValue 0.0; static bool lastGdmValid false; if (qIsNaN(val) || fabs(val - lastGdmValue) maxDelta) { if (lastGdmValid) { val lastGdmValue; // 用上一有效值替代 } else { return; // 首帧异常直接丢弃 } } else { lastGdmValue val; lastGdmValid true; }maxDelta的取值依赖被测对象的物理特性。测量静态应变时两帧之间不应超过 5 个微应变测量缓慢升温的电压信号时相邻两帧差值通常小于 0.5mV。典型做法是先在无激励状态下跑 5 分钟记录最大噪声幅值再把maxDelta设为噪声幅值的 5 到 10 倍。阈值设得太小会把正常信号当毛刺削掉设得太大则起不到过滤作用。5.3 用更新日志定位帧丢失工程里带了《多源数据采集软件》软件更新日志的 log 文件建议保持这种按时间追加运行日志的习惯不依赖调试器就能定位问题。每封装一帧数据在日志里写一行时间、设备 ID、通道号、数值与校验结果用文本编辑器直接打开就能核对哪一帧丢了、哪一帧校验失败。我在解析入口习惯加一行日志同时打出 GDM-8261A 和三和显示表两侧的原始读数与发送结果连续运行几小时后统计两个通道的帧计数是否一致就能定位是哪一侧串口丢失数据还是 3815N 的 TCP 接收端丢包。这个方法在现场没有抓包工具的环境里往往是唯一可靠的排障手段能把联调时间缩短数倍。本文还有配套的精品资源点击获取