
简介基于Qt5.14.2在Windows 10环境下开发的串口温度采集上位机专门用于通过串口接收多路温度数据以5通道方式实时显示并绘制动态变化曲线波特率可配置同时支持温度数据的文本记录。整个压缩包共49个文件约11.83MB核心为h/cpp源码、pro工程配置和ui界面文件方便开发者阅读理解与二次修改同时附带编译好的exe可执行程序、debug/release构建目录、效果图以及pdf/doc说明文档既能直接运行查看功能也可结合说明快速定位工程结构。适用人群覆盖从入门到进阶的Qt开发者尤其适合正在做串口上位机、实时曲线监控、课程设计或毕业设计的读者可从中学习串口初始化与协议解析、多通道数据更新、实时绘图、文本存储等完整实现思路也可作为小型温度监控项目的直接参考或基础框架。已有338人学习下载工程自带Release与Debug版本兼顾联调测试与发布部署需求。1. Qt串口采集实时温度显示上位机5通道温度、曲线与文本记录一次打通之前给某台热处理设备做监测5路热电偶温度只能靠人工定时抄表每隔半分钟记一次下班前用表格软件手工补曲线既容易漏记也没法定位哪一路在夜班期间超了温。后来用Qt串口采集实时温度显示上位机把这套流程自动化了界面实时显示5通道温度曲线跟着数据走文本记录自动落盘串口波特率参数可设置。这个方案解决的不只是“少抄几个数”而是把现场异常从“事后翻记录”变成“当时看到”。适合做设备调试、产线温度监测、传感器标定的开发者直接照着复现也适合刚接触Qt串口编程的人作为一套完整样板。2. 想清楚再动手5通道温度帧协议与Qt采集架构2.1 为什么选QSerialPort官方串口模块比老方案更适合跨平台Qt做串口上位机第一个选择就是串口模块。老一批教程还在用第三方串口类那是Qt早期官方没有串口支持时的产物。现在直接用官方串口模块就好接口稳定、跨平台、和Qt事件循环天然配合不需要额外发库。它的两个核心类是串口对象和串口信息类前者负责打开端口、读写数据后者负责枚举当前机器上有哪些串口、厂商编号、端口名。五通道温度采集上位机里串口对象是整条数据链路的起点。我在项目里一般这样初始化串口参数QSerialPort m_serial; QSerialPortInfo info; const QListQSerialPortInfo portList QSerialPortInfo::availablePorts(); for (const QSerialPortInfo item : portList) { ui-comboPort-addItem(item.portName()); }代码逻辑很简单枚举系统里所有可用串口把端口名填进下拉框。这样做的好处是上位机不用写死串口号现场换USB转串口设备后重新插拔再刷新下拉框就能看到新端口。“串口波特率参数可设置”也是从这里展开波特率不要写死在代码里而是做成下拉框运行时再赋值。波特率、数据位、校验位、停止位这四件套直接决定了串口能不能和温度采集设备对上。下位机那边是8数据位、无校验、1停止位的设备上位机就照此配置。很多设备默认参数就是8N1但不要默认——接上设备后第一件事是确认它的实际参数不是抄上次项目的。2.2 5通道温度帧协议帧头、通道号、校验和与0.1℃分辨率上位机要实时显示温度并画曲线前提是能正确识别每一路温度数据。设备端通常轮询5个通道每路温度打包成一帧通过串口发出来。帧格式没有行业统一标准但常见做法是“帧头 通道号 温度值 校验和”。下面是我惯用的一套6字节帧格式可以直接照抄字节序号内容说明0帧头1固定0xAA1帧头2固定0x552通道号0x000x04对应5个通道3温度低字节int16小端单位0.1℃4温度高字节int16小端单位0.1℃5校验和前5字节累加取低8位温度用int16表示值为实际温度乘以10。比如通道1实测26.4℃帧里就存264解析时再除以10得到26.4。用0.1℃分辨率够绝大多数温度监测场景又比浮点数传输省字节也方便下位机用单片机处理。校验和只取低8位能防单字节错误和少量噪声简单可靠。C侧用结构体表示这一帧最直观struct TempFrame { quint8 head1; // 0xAA quint8 head2; // 0x55 quint8 channel; // 0~4 quint8 tempLo; // 温度低字节 quint8 tempHi; // 温度高字节 quint8 checksum; // 前5字节求和取低8位 };定义结构体是给解析程序看的真正从缓冲区解析还要处理一个关键问题串口数据是字节流不是按帧来的。设备一次可能发来半帧、一帧半、甚至多帧粘连。解析时必须用“缓冲区累积 逐字节扫描帧头 校验通过才消费帧”的思路这部分在3.2里实现。2.3 为什么采集要放线程事件循环不被阻塞粘包才有得解有人会问串口收发本来就有信号通知放主线程不就行了短数据确实能跑但温度采集上位机通常7x24小时运行还要同时刷新曲线、写文本记录。如果串口接收、数据处理全在主线程一次大批量数据到达、一次文件写入卡顿整个界面就会冻结。常见的做法是让串口工作在一个独立线程里用信号槽把解析结果发回主线程更新界面。Qt的信号槽是队列连接跨线程时接收方在自己的事件循环里执行槽函数天然线程安全不需要自己加锁。这个架构只有一条铁律串口对象必须在工作线程里创建不要在main window里先建对象再挪线程——串口底层句柄绑定了创建它的线程。线程还有一个好处拆包逻辑可以放心处理。粘包、半包在独立线程里慢慢磨不会把界面拖垮。下面3.2的代码就是在工作线程里完成串口读写和帧解析的完整结构。3. Qt上位机落地实现串口配置、5通道实时显示与温度曲线绘制3.1 主窗口布局与串口参数设置区串口号、波特率、数据位与停止位界面布局按数据流向排最上面是串口配置区中间是5通道实时温度显示下面是曲线绘制区最底部放文本记录控制与状态日志。这个顺序符合操作直觉先连串口再观察数据再看趋势最后管理记录文件。串口配置区至少要有端口选择下拉框、波特率下拉框、连接/断开按钮三个控件。波特率下拉框我是这样填充的// 把常用波特率放进下拉框数据区存波特率数值 const QListint baudList {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200}; for (int baud : baudList) { ui-comboBaud-addItem(QString::number(baud), baud); } ui-comboBaud-setCurrentIndex(3); // 默认选中9600addItem的第二个参数是Qt内置的“数据角色”把波特率数值挂在下拉项上读取时用currentData()取出来避免再解析显示文本。默认选中9600因为大多数工业温度采集设备出厂波特率就是9600。如果设备用了非标波特率把下拉框设为可编辑再用QValidator限制输入为纯数字就行。连接按钮的槽函数要做三件事取端口名和波特率、调用工作线程的启动函数、更新按钮文字。打开失败时要弹明确提示不是只打日志。串口被占用、设备未插紧、驱动未装好都会导致打开失败错误信息里能看出原因。3.2 串口接收与温度解析实现缓冲拼接、校验与信号转发串口工作线程的核心是串口接收和帧解析。我先写启动函数void SerialWorker::startSerial(const QString portName, qint32 baudRate) { if (m_serial.isOpen()) { m_serial.close(); // 先关闭残留连接 } m_serial.setPortName(portName); m_serial.setBaudRate(baudRate); m_serial.setDataBits(QSerialPort::Data8); m_serial.setParity(QSerialPort::NoParity); m_serial.setStopBits(QSerialPort::OneStop); m_serial.setFlowControl(QSerialPort::NoFlowControl); if (!m_serial.open(QIODevice::ReadOnly)) { emit errorOccurred(m_serial.errorString()); return; } connect(m_serial, QIODevice::readyRead, this, SerialWorker::onReadyRead); }这段有两个细节值得说明。setFlowControl(QSerialPort::NoFlowControl)必须显式设置不设的话Qt用默认值不同平台上默认值可能不同现场就会遇到“同一套代码换台工控机就乱码”的怪事。连接信号用的QIODevice::readyRead是因为这个信号声明在基类里这样写Qt5、Qt6都能编译。接下来是接收与解析void SerialWorker::onReadyRead() { m_buffer.append(m_serial.readAll()); while (m_buffer.size() 6) { int idx indexOfFrame(); if (idx 0) { m_buffer.remove(0, 1); // 没找到帧头逐字节丢 continue; } if (idx 0) { m_buffer.remove(0, idx); // 丢弃帧头前的杂质 } const QByteArray frame m_buffer.left(6); m_buffer.remove(0, 6); quint8 sum 0; for (int i 0; i 5; i) { sum static_castquint8(frame[i]); } if (sum ! static_castquint8(frame[5])) { continue; // 校验失败放弃本帧 } quint8 channel static_castquint8(frame[2]); if (channel 5) { continue; // 通道号非法防脏数据 } qint16 rawTemp static_castqint16( (quint8)frame[3] | ((quint16)(quint8)frame[4] 8)); emit temperatureReady(channel, rawTemp / 10.0); } }indexOfFrame()的职责是找到第一个0xAA 0x55帧头位置。帧头校验放在while循环里刷找不到就前进1字节这个“逐字节滑动”策略能处理任意粘连和错位是最稳的写法。校验和计算是用无符号字符累加防止符号位干扰结果。温度两个字节按小端拼装再除以10还原成实际温度。校验失败直接丢弃这6字节不中断循环因为后面可能还有完整帧。解析完成后通过temperatureReady信号把通道号和温度值发给主线程主线程槽函数只负责刷界面不做串口操作。这就是2.3说的线程分工。3.3 5通道实时温度显示与曲线绘制时间轴滑动与刷新策略主线程收到temperatureReady信号后要同时更新两块通道温度显示区和曲线图。温度显示我用五组QLCDNumber每组一个标签标明“通道1通道5”。QLCDNumber显示数字清晰近距离看效果好适合工控界面。要注意setDigitCount设够位数温度显示到小数一位比如“126.4”就设5位。显示代码很简单void MainWindow::onTemperatureReady(int channel, qreal value) { ui-lcdTemp[channel]-display(value); // 可加阈值判断超温时把显示值变红 }曲线绘制是整个上位机最出效果的部分。Qt里画实时曲线常见两条路用官方图表模块或者引入轻量第三方绘图库。实时追加数据的场景我用第三方绘图库更多因为它的addData接口配合运行时追加数据比官方模块更顺手绘制性能也够用。初始化5条曲线的代码是这样// m_plot是绘图控件初始化5条曲线 for (int i 0; i 5; i) { m_plot-addGraph(); m_plot-graph(i)-setName(QString(通道%1).arg(i 1)); m_plot-graph(i)-setPen(QPen(QColor::fromHsv(i * 50, 255, 180))); } m_plot-xAxis-setLabel(时间); m_plot-yAxis-setLabel(温度 ℃); m_plot-legend-setVisible(true);5条曲线用hsv色相错开颜色保证相邻通道颜色差别明显现场不戴老花镜也分得清楚。接收温度时追加数据点并让x轴跟随时间滑行void MainWindow::onTemperatureReady(int channel, qreal value) { const double t QDateTime::currentMSecsSinceEpoch() / 1000.0; m_plot-graph(channel)-addData(t, value); m_plot-xAxis-setRange(t - 60, t); // 固定显示最近60秒 }x轴显示最近60秒数据点永远向右推进看起来就是实时刷新的动态曲线。如果你希望停下来看历史曲线可以再加一个“暂停自动滚动”按钮暂停后用户手动拖拽查看。曲线区还有一个隐藏宝藏在图上点出来的悬浮数值提示用起来会让现场操作员很舒服这里按下不表到第6章再提。4. 温度文本记录按日期写CSV、追加入盘与防卡顿写入设计4.1 记录文件格式与命名CSV字段顺序、按天分文件、表格软件直开温度要留痕文本记录是最简单可靠的形式。文件格式我用CSV一个字段一个通道表格软件可以直接打开也方便做后续分析。每条记录的时间戳统一格式精确到毫秒。字段顺序定为时间戳、通道1温度、通道2温度、通道3温度、通道4温度、通道5温度。文件按日期分片一个文件只存一天的数据文件名带日期就能看出哪天有问题。文件第一行写表头后面每行一条采样记录。这样一个文件几千行以内打开、筛选、排序都不卡。CSV有一个隐蔽问题如果温度是小数不同电脑的表格软件打开时可能因为区域设置不同把小数点解释成千位分隔符。解决办法是写入时统一用qSetRealNumberPrecision固定小数位数我在导出时用“f”参数转换成固定位数的字符串比如QString::number(value, f, 1)保证写入文件的是“26.4”这种纯文本不带区域差异。4.2 文本追加写入的实现QFile QTextStream 与定时落盘最简单的写法是每收到一条温度就打开文件、写一行、关文件。代码少但现场跑久了会暴露性能问题频繁open/close文件在机械硬盘上会产生大量IO操作长时间运行会拖慢其他操作。我惯用的做法是“缓冲写入 定时落盘”数据先攒在内存列表里定时器每2秒批量写一次文件保持打开状态用QTextStream把数据格式化后直接写入。代码如下void MainWindow::appendTemperatureRecord(const QDateTime ts, const QVectorqreal temps) { if (!m_recordEnabled) { return; } if (!m_recordFile.isOpen()) { m_recordFile.setFileName( QString(temp_%1.csv).arg(ts.toString(yyyyMMdd))); m_recordFile.open(QIODevice::Append | QIODevice::Text); QTextStream head(m_recordFile); head time,ch1,ch2,ch3,ch4,ch5\n; } m_pendingCount; m_pendingData.append(tempLine(ts, temps)); // tempLine返回一行文本 if (m_pendingCount 20) { flushRecordFile(); } } void MainWindow::flushRecordFile() { if (m_pendingData.isEmpty() || !m_recordFile.isOpen()) { return; } QTextStream out(m_recordFile); for (const QString line : m_pendingData) { out line; } out.flush(); m_pendingData.clear(); m_pendingCount 0; }appendTemperatureRecord在串口信号槽里调用但真正写文件的是flushRecordFile它由界面定时器驱动同样的数据批量写入比逐条open/close快得多。还有一个安全细节flush()只把Qt缓冲区交给操作系统不保证立即落盘到物理磁盘。如果现场对数据可靠性要求高可以在定时器里调用out.flush()后让操作系统把文件刷到磁盘。掉电不丢数据这件事是需要单独评估的不能只用定时flush就声称“绝对可靠”。4.3 防卡顿设计文件写入和UI刷新要分开文本记录最容易翻车的地方是写入线程。串口线程已经承担了收数据和拆帧的职责再把文件写入塞进去会拖慢后续帧的接收。体重一点的写法是单独开一个记录线程界面线程把待写文本丢进队列记录线程排队写文件。实际项目里怎么取舍我的经验是分两级数据量不大时定时器驱动批量写入就够了不用额外线程数据量大、或者串口波特率拉满、同时又要写多路数据时就把写入逻辑挪到独立线程用Qt的队列信号传递文本行。经过这两层隔离界面线程、串口线程、文件线程各干各的活线程之间只有信号联系不会互相阻塞。有一个地方容易忽略定时器flush的周期和采样率需要匹配。如果每100毫秒来一帧、每2秒才写一次那内存里最多累积20条左右数据完全可控。如果采样率提高到每10毫秒一帧2秒就是200条写入时会产生一次明显的批量IO这时要么把写入周期缩短到500毫秒要么走独立线程。文本记录的意义不在写得多快而在稳定不丢、可追溯。5. 串口采集上位机常见问题与避坑乱码、卡死、曲线丢点与重连失败5.1 串口打开失败或收到全乱码先查波特率再查数据位停止位现象打开串口报错或者界面显示的温度数据是乱码曲线像心电图一样疯跳。原因排查顺序很重要。第一查物理层串口是否被占用、USB转串口是否插紧。第二查参数层波特率、数据位、停止位、校验位是否与设备一致。第三查协议层设备实际用的帧格式和上位机解析逻辑是否匹配。我有一次在现场调了一个小时最后发现是设备出厂波特率是19200而上位机用9600打开改动一个下拉框就好了——经验是上位机的波特率参数可设置意味着你要主动核对不能改完界面就以为万事大吉。解决先看设备说明书或问设备方确认实际串口参数再用串口调试助手先手动发帧验证最后才用上位机接收。这三级排查能筛掉90%的乱码问题。另外要注意Qt5和Qt6里波特率枚举的写法不同老代码里如果直接传枚举常量升级Qt版本后可能会编译不过。统一改成传健康int类型两个版本都友好。5.2 界面卡死文件写入和UI刷新挤在串口线程里现象程序刚启动正常运行几小时后界面点击没反应窗口标题显示“未响应”过一会儿又恢复或者彻底卡死。原因串口线程的槽函数里如果直接调用了UI控件或者做了文件写入、阻塞网络请求串口线程就会被拖住。槽函数返回前后面来的串口数据全部堵在缓冲区里线程事件循环被卡界面的信号槽也等不到响应最终表现就是界面假死。解决串口线程只做两件事——读数据、拆帧、发信号。所有UI更新都在主线程槽函数里做所有文件写入都丢给定时器或者独立记录线程。还有一个常见翻车点主线程槽函数里也别忘了控制耗时。比如曲线replot是重操作一秒钟几十次地调用肯定扛不住把不必要的重绘去掉只在数据到达时追加并setRange最后统一replot一次就能减轻压力。这条踩坑经验几乎做过实时绘制的人都会遇到。5.3 曲线越画越卡无限追加数据让绘图控件内存膨胀现象程序运行头半小时曲线流畅几小时后鼠标拖动卡顿内存占用一直涨最后曲线绘制明显迟钝。原因5通道曲线每通道每秒几十个点一小时就是十几万个点。如果绘图库只是不断addData而不清理旧数据所有点都保存在内存里每次重绘都要遍历全量数据性能和内存双双告警。解决限制曲线数据点数。常见做法是设定最大点数比如每条曲线最多保留7200个点一小时数据超过就移除最早的点。绘图库通常提供数据清除或按范围删除的接口在定时器里周期性裁剪一次即可。如果你需要完整历史曲线那就把裁剪前的数据全部落到文本文件里界面上只保留最近一段这样既流畅又不丢记录。曲线本身只是观察窗口文本记录才是唯一权威的数据备份。5.4 设备断开再重连失败串口状态没有完全复位现象程序刚启动能正常连接设备断电重启或者拔掉USB串口线再插回去点“重新连接”没反应偶尔报端口被占用。原因串口重连时旧连接没有彻底释放底层句柄还没归还给系统新连接就被拒绝了。另外串口线重新插拔后设备在系统中的端口名可能变化上位机还在用原来的端口名去打开。解决重连前先做两件事。第一关闭旧串口连接并清理串口对象的错误状态第二重新枚举当前系统里的可用端口更新下拉框列表。打开失败时读取串口对象的错误字符串给用户明确提示到底是不存在端口、还是权限不足、还是被占用。重连逻辑如果只重试不清理那下一次连接大概率还是失败。把“关、清、列、开”四步串成完整流程断线重连就不再玄学。6. 进阶用虚拟串口对模拟温度数据把采集到绘图链路全部验证通6.1 虚拟串口对模拟设备不碰硬件也能完整验证上位机没有温度采集硬件在手边时怎么验证上位机逻辑答案是虚拟串口对。用一个能成对创建虚拟串口的工具它会生成两个互联的虚拟端口一端给模拟程序写数据另一端给上位机读数据完全模拟真实串口链路。模拟程序用Python写最省事pyserial库按第2章的帧格式发数据。脚本里温度值用随机数加缓慢漂移模拟真实温度变化import serial import time import struct import random ser serial.Serial( portCOM_B, # 虚拟串口对的一端另一端给上位机 baudrate9600, bytesize8, parityN, stopbits1, ) temp_per_ch [25.0 i * 3.0 for i in range(5)] while True: for ch in range(5): temp_per_ch[ch] random.uniform(-0.3, 0.3) # 模拟温度漂移 frame bytearray() frame.append(0xAA) frame.append(0x55) frame.append(ch) # 通道号 0~4 frame struct.pack(h, int(temp_per_ch[ch] * 10)) # 0.1℃分辨率 frame.append(sum(frame[0:5]) 0xFF) # 前5字节校验和 ser.write(frame) time.sleep(0.02) # 每通道约50ms模拟20Hz轮询这段脚本对照着第2章的协议写每帧6字节校验算法和上位机解析完全一致。运行后上位机打开COM_A端就能看到5条温度曲线缓慢漂移文本记录文件按天生成CSV。整条链路——串口、拆帧、显示、绘图、落盘——全部在不碰硬件的情况下验证完毕。这套模拟方法价值很大它把协议调试从“对着现场设备瞎猜”变成“在电脑前可控复现”协议改一版、模拟脚本改一版、上位机改一版快速闭环。6.2 验证重点与日常维护习惯用虚拟串口对验证时重点测试几个场景第一正常流程打开串口后看5通道曲线是否平滑、文本记录是否有内容第二模拟粘包把脚本改成一次写两帧数据观察上位机是否不乱第三模拟半包把一帧拆成两次write中间加delay观察解析是否仍然完整第四关闭模拟程序再打开测试重连逻辑。这四个场景能覆盖现场80%的异常。我个人已经习惯把这份模拟脚本留着每次改完上位机协议都先跑一遍再上真设备效果比直接拿现场设备调试高效很多。另外建议在曲线区加上点悬浮查询功能鼠标停在曲线上就能读当前时刻温度值这对事后追溯“某个异常点发生在哪一秒”非常有用——曲线图上肉眼看不出精确数值文本记录里能查到但鼠标直接悬停读取更快现场人员的感受完全不同。这套方向的投入产出比很高硬件成本为零软件工作量集中在协议定义和线程架构上一旦跑通后续加通道、改波特率、扩展报警功能都很顺。希望帮到你。本文还有配套的精品资源点击获取