Qt串口温度实时显示上位机:帧协议、曲线刷新与断线重连

发布时间:2026/11/5 4:34:33
Qt串口温度实时显示上位机:帧协议、曲线刷新与断线重连 简介一份基于Qt框架实现串口温度采集与实时曲线显示的小型工程源码面向正在学习Qt上位机开发、串口通信与嵌入式数据可视化的初学者及在校学生。工程围绕串口接收温度数据这一主线演示了QSerialPort的打开与参数配置、原始数据的解析与数值转换以及借助信号与槽机制把新数据推送到界面刷新的过程温度曲线依托QGraphicsView与QGraphicsScene绘制配合QTimer定时读取保证显示连续、刷新及时。压缩包共9个文件体积约11KB其中3个.h与3个.cpp承担类声明和逻辑实现1个.ui描述窗口布局1个.pro工程文件用于qmake构建导入Qt Creator即可编译运行。目前已有6220人学习下载。读者可获取串口调试、数据解析、曲线绘制与界面刷新的可复用写法并以此为骨架扩展多通道采集、数据存储或报警提示等功能。1. 需求先落地Qt 串口温度实时显示界面到底要解决什么设想一台正在运行的恒温设备本地表头只显示当前温度值历史趋势、超限报警、数据存档全靠人盯着。这类需求在课程设计和毕业设计里出现频率极高用 Qt 做一个串口温度显示界面把下位机通过串口上报的温度数据实时画成曲线同时在界面上给出当前值、极值、报警状态。听上去是拖几个控件、再调一次readAll()的事真正做起来粘包、坏帧、拔插重连、曲线卡顿会一个接一个冒出来。这套方案按可交付的标准拆开讲帧协议怎么定、线程怎么分、实时曲线怎么刷、数据怎么落盘、验收怎么测适合要交一份完整上位机设计的同学也适合需要给自研设备配一个监控界面的开发者。2. 总体设计三层结构、一条帧协议与四个模块的职责划分动手写第一行代码之前最值得花时间的是把「谁负责读串口、谁负责拆帧、谁负责画图」这三件事分开。串口是阻塞型外设界面是事件驱动两者塞进同一个线程短时间看不出问题跑上十几分钟就会出现拖动窗口时曲线停顿、拔插串口时整个界面假死的现象。方案按三层来切采集层只碰字节流解析层只认帧结构展示层只处理已经合法的温度样本。2.1 三层架构与线程模型采集层是SerialWorker内部持有QSerialPort和一个字节累积缓冲区运行在独立的工作线程里解析层与采集层同线程负责帧头定位、长度校验、CRC 校验、负载解码输出一个Frame结构体展示层是MainWindow只接收已经通过校验的样本更新数值面板、追加曲线缓存、触发报警判定和落盘。跨层的数据传递统一走信号槽连接方式用默认的Qt::AutoConnection即可对象分属两个线程时 Qt 会自动排队投递等价于Qt::QueuedConnection。这条规则带来的约束是Frame必须注册为元类型否则排队投递时参数会被丢弃并只在运行期打印一句警告——这类警告在控制台日志里很容易被忽略属于典型的前期埋雷。层次承载类所在线程输入输出采集层SerialWorker工作线程串口字节流原始QByteArray解析层FrameParser工作线程原始字节Frame已校验展示层MainWindowUI 线程Frame曲线、面板、CSV线程划分的判定标准很朴素凡是可能阻塞超过 10 毫秒的操作都不能放在 UI 线程。串口读取本身不慢但打开串口、等待设备枚举、异常重连这三件事都可能卡住几百毫秒所以它们必须留在工作线程。2.2 串口帧协议帧头、长度、命令字与 CRC16协议是整个方案的地基。下位机上报温度时如果只是简单地把浮点数转成字符串加个换行前期联调很舒服一旦线路有干扰或者缓冲区被截断界面就会出现 −127.00、85.00 这种诡异数值而且很难定位是传感器坏了还是解析错了。方案采用带帧头和校验的定长头 变长负载结构。偏移字段字节数说明0–1帧头2固定0xAA 0x55用于重同步2负载长度1取值范围 1–64负载字节数3命令字10x01温度数据0x02心跳0x03设备信息4 起负载N通道号 1 字节 float32 温度 4 字节末尾 2 字节CRC162Modbus 多项式小端存放单帧总长度 2 1 1 N 2。温度负载取 5 字节一帧 11 字节在 9600 波特率下满速上报也不会挤占带宽。命令字单独留出来的好处是后续加心跳、加设备序列号、加多点温度巡检都不需要改帧结构只在解析分支里加 case。提示CRC 的计算范围必须和下发固件严格对齐通常约定为「长度 命令字 负载」不含帧头也不含 CRC 自身。协议文档里写清楚这一句能省掉半天的联调。2.3 四个核心模块与数据流模块划分的目标是让每个类的修改理由只有一个。串口配置改了只动采集模块协议升级只动解析模块换绘图库只动展示模块换存档格式只动存储模块。模块关键类职责对外接口串口采集SerialWorker打开/关闭端口、读字节、异常重连openPort、closePort、frameReady协议解析FrameParser切帧、CRC、负载解码、坏帧统计feed(const QByteArray)、takeFrames()显示MainWindow、ChartWidget曲线、数值面板、状态灯、报警appendSample、setAlarm存储配置CsvLogger、QSettingsCSV 按天轮转、参数持久化writeRow、load/save数据流的完整路径是readyRead触发 → 字节追加进缓冲区 → 解析器循环取帧 → 合法帧通过信号发往 UI 线程 → 主窗口写入环形缓存并更新面板 → 200 毫秒定时器统一重绘曲线 → 同时把样本交给 CSV 记录器。注意最后两步是解耦的绘图有节流落盘不节流两者互不等待。2.4 工程骨架与构建配置Qt 的串口模块属于附加模块安装器和.pro里都要显式声明否则编译期报QSerialPort: No such file or directory很多同学会误以为是环境变量没配好。# TempMonitor.pro QT core gui serialport greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c17 TEMPLATE app TARGET TempMonitor SOURCES \ main.cpp \ mainwindow.cpp \ serialworker.cpp \ frame.cpp \ csvlogger.cpp HEADERS \ mainwindow.h \ serialworker.h \ frame.h \ csvlogger.h FORMS mainwindow.ui # QCustomPlot 以单文件方式引入编译进工程即可不额外依赖模块 include(qcustomplot/qcustomplot.pri)QT serialport是硬性依赖c17让std::optional、结构化绑定在解析代码里可用QCustomPlot 用.pri引入而不是编成动态库好处是发布时少一个 DLL坏处是首次编译时间长一点。如果学校机房装的是精简版 Qt串口模块很可能没勾选重新跑一遍维护工具补装即可不需要重装整个 Qt。Frame定义成值类型避免在信号里传指针带来生命周期问题// frame.h struct Frame { quint8 channel; // 通道号从 0 开始 float celsius; // 摄氏温度 qint64 timestampMs; // 上位机接收时刻用于画曲线横轴 quint8 quality; // 0 正常 1 传感器断线 2 超量程 3 CRC 曾出错 }; Q_DECLARE_METATYPE(Frame) // 必须注册否则跨线程信号会被丢弃quality字段是这套设计里比较关键的一笔。很多方案把坏数据直接丢掉结果曲线出现断点却不知道原因把质量位带上界面就能显示「数据可疑」而不是假装一切正常。3. 串口采集与协议解析从 readyRead 到一条可信的温度记录这一章是整套方案里最容易翻车的部分。串口数据不是按帧到达的可能一次来半个帧也可能一次来三个半帧还可能因为接触不良夹着随机噪声。解析器必须假设「输入永远是脏的」才能在各种现场条件下活下来。3.1 QSerialPort 初始化参数、线程归属与打开失败的判定QSerialPort对象必须在它所使用的线程里创建或者至少在被使用前moveToThread不能在 UI 线程 new 出来再拿到工作线程去读。方案的写法是把 worker 整体移到线程端口对象作为它的成员在openPort里按需创建。// serialworker.cpp void SerialWorker::openPort(const QString portName, int baudRate) { if (m_port m_port-isOpen()) m_port-close(); if (!m_port) { m_port new QSerialPort(this); // 归属工作线程随 worker 一起销毁 connect(m_port, QSerialPort::readyRead, this, SerialWorker::onReadyRead); connect(m_port, QSerialPort::errorOccurred, this, SerialWorker::onPortError); } m_port-setPortName(portName); m_port-setBaudRate(baudRate); // 与下位机一致常见 9600 / 115200 m_port-setDataBits(QSerialPort::Data8); m_port-setParity(QSerialPort::NoParity); m_port-setStopBits(QSerialPort::OneStop); m_port-setFlowControl(QSerialPort::NoFlowControl); m_port-setReadBufferSize(64 * 1024); // 防止上层处理慢时无限增长 if (!m_port-open(QIODevice::ReadWrite)) { emit portError(QStringLiteral(打开 %1 失败%2) .arg(portName, m_port-errorString())); return; } m_buffer.clear(); // 换端口必须清空残留半帧 emit portStateChanged(true); }参数里唯一需要和下位机逐字对齐的是波特率、数据位、校验位、停止位这四项任何一项不一致的表现都是「能打开但全是乱码」。setReadBufferSize设成 64 KB 是给上层留缓冲余量如果解析逻辑偶发卡顿驱动层不会立刻丢数据。open失败最常见的原因是端口被别的程序占用比如串口助手没关errorString()会明确写出来界面上原样显示即可不要自己编一句「串口错误」。3.2 粘包与半包环形缓冲 帧头重同步的解析循环解析器的核心是一个循环只要缓冲区里还存在完整且校验通过的帧就不断取出来取不出来就退出等待下一次readyRead。帧头定位用indexOf而不是假定数据从第 0 字节开始这一步是抗干扰的关键。// serialworker.cpp void SerialWorker::onReadyRead() { m_buffer.append(m_port-readAll()); Frame f; while (m_parser.tryParseOne(m_buffer, f)) { emit frameReady(f); // 队列投递到 UI 线程 } // 缓冲区保护长时间找不到帧头就丢弃避免内存缓慢增长 if (m_buffer.size() 4096) m_buffer m_buffer.right(256); }tryParseOne的逻辑按顺序做四件事每一步失败都返回 false 并保留剩余数据等待下一次// frame.cpp bool FrameParser::tryParseOne(QByteArray buf, Frame out) { // 1. 找帧头找不到就只保留最后 1 字节可能是被截断的半个帧头 const int head buf.indexOf(QByteArray::fromHex(AA55)); if (head 0) { if (buf.size() 1) buf buf.right(1); return false; } if (head 0) buf.remove(0, head); // 丢掉帧头之前的噪声 if (buf.size() 4) return false; // 头 长度 命令字 const quint8 len quint8(buf.at(2)); const int total 2 1 1 len 2; // 含 CRC 的整帧长度 if (len 0 || len 64) { // 长度字段非法跳过一个字节重找 buf.remove(0, 1); return false; } if (buf.size() total) return false; // 半包等下一次数据 // 2. CRC 校验范围是「长度 命令字 负载」 const QByteArray body buf.mid(2, 2 len); const quint16 crcRecv quint16(quint8(buf.at(total - 2))) | (quint16(quint8(buf.at(total - 1))) 8); if (crc16Modbus(body) ! crcRecv) { m_badFrameCount; buf.remove(0, 1); // 只丢一个字节允许帧头重同步 return false; } // 3. 解码负载通道号 1 字节 float32 小端 out.channel quint8(body.at(2)); const quint32 raw qFromLittleEndianquint32( reinterpret_castconst uchar *(body.constData() 3)); memcpy(out.celsius, raw, sizeof(float)); out.timestampMs QDateTime::currentMSecsSinceEpoch(); out.quality 0; buf.remove(0, total); // 4. 消费整帧 return true; }校验失败时buf.remove(0, 1)而不是丢掉整帧是一个刻意的取舍。如果噪声恰好夹在帧中间只删一个字节可以让下一次indexOf重新对齐到真正的帧头如果直接删total字节很可能把后面一帧的开头一起吃掉表现为「偶尔丢一条数据」。代价是极端情况下会多循环几次但每次循环都是内存操作开销可以忽略。float的字节序必须显式指定。下位机常见 STM32、ESP32 一类通常是小端但协议文档里不写清楚换一块板子就可能整条曲线数值离谱。用qFromLittleEndian转成quint32再memcpy到float比直接reinterpret_cast安全也避免了对齐问题。3.3 校验、超时与坏帧处理把 −127 和 85 挡在界面外面CRC 只能挡住传输错误挡不住「传感器本身出问题」。数字温度传感器断线时经常返回 −127上电瞬间返回 85这两个值如果原样画进曲线纵轴范围会被瞬间拉爆前面几十分钟的趋势全被压成一条直线。方案在解析层之后加一道合理性判定。// 合理性判定放在解析成功之后、发信号之前 static quint8 checkQuality(float t) { if (t -100.0f || t 150.0f) return 2; // 超量程多半是断线或短路 if (qFuzzyCompare(t, 85.0f)) return 1; // 上电默认值标记为可疑 return 0; }判定阈值按实际被测对象来定恒温槽一般工作在 0–100 ℃量程放宽到 −100–150 ℃ 足够如果做的是低温或高温场景把常量换成配置项。质量位非 0 的样本照常写进 CSV但在曲线上用不同颜色或断点表示数值面板显示--而不是一个假数字。另一道保险是超时看门狗。下位机可能死机、可能线被拔掉此时串口仍然打开但不再有数据。主窗口持有一个 3 秒定时器每收到一帧就重置超时未重置就把状态灯打成黄色并记录一条「数据超时」不要直接弹窗弹窗会打断正在进行的操作。3.4 断线重连errorOccurred 端口热插拔扫描USB 转串口设备被拔掉时QSerialPort会发出errorOccurred其中ResourceError表示设备消失PermissionError表示被占用DeviceNotFoundError表示端口不存在。三种情况的重连策略不同设备消失要等它回来被占用要提示用户端口不存在则刷新列表。// serialworker.cpp void SerialWorker::onPortError(QSerialPort::SerialPortError err) { if (err QSerialPort::NoError) return; const bool needReconnect (err QSerialPort::ResourceError || err QSerialPort::DeviceNotFoundError); emit portStateChanged(false); emit portError(m_port-errorString()); if (needReconnect !m_reconnectTimer-isActive()) m_reconnectTimer-start(2000); // 2 秒后重试避免高频刷日志 } void SerialWorker::tryReconnect() { const QString target m_lastPortName; if (target.isEmpty()) return; // 端口列表里重新出现同名设备才尝试打开避免无效 open 刷屏 const auto ports QSerialPortInfo::availablePorts(); for (const QSerialPortInfo info : ports) { if (info.portName() target || info.systemLocation() target) { openPort(target, m_lastBaudRate); return; } } }重连定时器跑在工作线程里重试动作也发生在工作线程界面上只反映状态变化。这里有个容易踩的坑在errorOccurred槽里直接调用open()会形成递归——打开失败又触发错误信号栈很快溢出。方案用定时器把重连推迟到下一个事件循环天然打断递归。4. 实时显示与数据落盘曲线、面板、CSV 三件事怎么不打架采集侧稳定之后界面侧的矛盾才开始显现曲线要平滑、面板要即时、CSV 要完整三者的刷新频率和开销根本不在一个量级。做法是把它们拆成三种节奏——数据到达即入缓存曲线按固定频率重绘CSV 按行追加并按批落盘。4.1 绘图库选型QCustomPlot 与 Qt Charts 的取舍Qt 生态里画实时曲线主要两条路选型直接影响后面几个月的开发体验。维度QCustomPlotQt Charts引入方式两个文件编进工程QT charts模块千点级刷新流畅支持队列重绘较流畅早期版本有抖动交互定制源码在手改起来自由受限于官方 API授权GPL / 商业双授权随 Qt 模块授权上手成本中等文档偏示例式较低官方示例多毕业设计和小型上位机场景里QCustomPlot 更常见原因是单文件引入、发布少一个依赖、遇到性能问题能直接改源码。Qt Charts 的优势是官方维护、和 Qt Designer 集成更好。两条路都能交差选一条走到底就行中途换库的代价远大于选错库。4.2 滑动窗口与刷新节流1Hz 数据画成 60fps 的手感但只花 5Hz 的算力曲线卡顿的头号原因不是数据多而是「来一帧就重绘一次」。串口一次readyRead可能带来十几帧如果每帧都调replot()绘图线程会被彻底占满。方案的写法是数据到达只进缓存重绘由固定定时器驱动。// chartwidget.cpp void ChartWidget::appendSample(qint64 ms, double value) { m_keys.append(ms / 1000.0); m_values.append(value); // 滑动窗口1Hz 上报下保留 600 点约 10 分钟趋势 constexpr int kMaxPoints 600; while (m_keys.size() kMaxPoints) { m_keys.removeFirst(); m_values.removeFirst(); } m_dirty true; // 只打标记不在这里重绘 } void ChartWidget::onRefreshTimer() { if (!m_dirty) return; // 没有新数据就不动省掉无谓重绘 m_dirty false; m_graph-setData(m_keys, m_values); m_plot-xAxis-setRange(m_keys.isEmpty() ? 0 : m_keys.last() - 600, m_keys.isEmpty() ? 60 : m_keys.last()); m_plot-replot(QCustomPlot::rpQueuedReplot); // 合并重绘请求 }刷新定时器设 200 毫秒即 5 Hz。人眼对曲线滚动的感知在这个频率下已经足够顺滑而绘图开销只有「每帧重绘」的二十分之一左右。rpQueuedReplot的作用是把多次重绘请求合并成一次连续调用不会叠加。纵轴范围不要每帧自适应否则曲线会随着一个尖峰上下猛跳看着非常晕。方案固定纵轴范围超范围的点用裁剪显示只有在用户手动缩放时才临时切换成自适应。4.3 数值面板、报警与状态灯QSS 和阈值判定面板部分要做的是「一眼能看出正常还是异常」。温度值用大字号超限变红并加粗状态灯用色块表示串口连接、数据超时、报警三种状态。样式的切换用动态属性驱动 QSS比在代码里硬编码颜色好维护。// mainwindow.cpp void MainWindow::refreshPanel(const Frame f) { const bool valid (f.quality 0); ui-tempValue-setText(valid ? QString::number(f.celsius, f, 2) : QStringLiteral(--)); const bool alarm valid (f.celsius m_lowLimit || f.celsius m_highLimit); ui-tempValue-setProperty(alarm, alarm); ui-tempValue-style()-unpolish(ui-tempValue); ui-tempValue-style()-polish(ui-tempValue); // 属性变了必须重新 polish }/* 温度数值的两种状态属性选择器直接读 setProperty 的值 */ QLabel#tempValue { font-size: 40px; color: #1f2937; } QLabel#tempValue[alarmtrue] { color: #dc2626; font-weight: bold; }unpolishpolish这两行是 QSS 动态属性的固定套路漏掉就会出现「属性改了但颜色不变」的玄学现象。报警除了变色还要记一条事件日志时间、通道、数值、阈值答辩演示时这条日志比曲线更有说服力。4.4 CSV 按天轮转与 QSettings 配置持久化存档格式选 CSV理由是简单、能直接用表格软件打开、答辩时能当场展示原始数据。按天轮转可以避免单个文件无限膨胀也方便按日期回溯。// csvlogger.cpp void CsvLogger::writeRow(const Frame f) { const QString day QDateTime::fromMSecsSinceEpoch(f.timestampMs) .toString(yyyy-MM-dd); if (day ! m_currentDay) rotate(day); // 跨天换文件并写表头 QTextStream ts(m_file); #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) ts.setCodec(UTF-8); #else ts.setEncoding(QStringConverter::Utf8); #endif ts f.timestampMs , int(f.channel) , QString::number(f.celsius, f, 3) , int(f.quality) \n; if (m_pending 20) { // 每 20 行强制落盘一次 m_file-flush(); m_pending 0; } }每 20 行flush一次是可靠性和性能的折中逐行 flush 会让磁盘 IO 成为瓶颈完全不 flush 则在程序异常退出时丢掉最后一批数据。如果对数据完整性要求更高可以改成QSaveFile或者定时 fsync。配置持久化用QSettings把串口名、波特率、报警上下限、通道名称、窗口大小全部存进 ini 文件下次启动自动恢复。这一步在答辩时体现为「关掉程序再打开参数还在」是个很直观的加分项。QSettings s(QCoreApplication::applicationDirPath() /config.ini, QSettings::IniFormat); s.setValue(serial/port, m_lastPortName); s.setValue(serial/baud, m_lastBaudRate); s.setValue(alarm/low, m_lowLimit); s.setValue(alarm/high, m_highLimit); s.setValue(ui/geometry, saveGeometry());5. 避坑与排查这套串口温度界面最容易翻车的五处下面五条都是我在类似项目里真实遇到过或者帮别人排查过的问题按「现象 → 原因 → 解决」写方便对着症状找。5.1 采集与解析链路上的三个坑坑一曲线越跑越卡最后整个界面拖不动。现象是刚启动很流畅跑十几分钟后拖动窗口有明显延迟任务管理器里 CPU 占用持续走高。原因是readyRead里直接调了replot()而串口一次可能送来十几帧等于一秒钟重绘几十次绘图开销随数据量线性增长。解决办法是把「收数据」和「画数据」拆开收到即入缓存200 毫秒定时器统一重绘并且加rpQueuedReplot合并请求。改完之后同样数据量下 CPU 从 40% 降到 5% 以内。坑二温度偶发跳到 −127 或者 85曲线被拉成一条直线。现象是趋势图每隔一段时间出现一个极端尖峰然后纵轴范围自动放大前面的正常数据被压平。原因有两层一是传感器断线或上电瞬间输出的固定值被当成真实温度二是纵轴每帧自适应一个异常点就能毁掉整张图。解决办法是解析后加合理性判定把超量程和默认值标记成质量位而不是直接画出来纵轴改用固定范围只在用户手动操作时切换。这两处一起改曲线立刻稳。坑三拔掉 USB 转串口再插回来程序崩溃或者再也打不开端口。现象是热插拔之后程序闪退或者界面上一直显示「打开失败」重启程序才能恢复。原因是重连逻辑写在errorOccurred槽里直接调open()失败又触发错误信号形成递归另外端口对象可能在错误的线程里被销毁。解决办法是用定时器把重连推迟到下一个事件循环重连前先用QSerialPortInfo::availablePorts()确认设备已经回来端口对象统一作为 worker 的成员随 worker 在线程结束时销毁不在别处delete。5.2 显示与部署上的两个坑坑四自己机器上跑得好好的拷到别人电脑上打不开串口。现象是双击 exe 之后提示找不到串口模块或者干脆启动就报缺少平台插件。原因是 Qt 的串口模块是附加模块动态链接方式下需要把对应 DLL 一起带上平台插件目录platforms/也不能少。解决办法是用windeployqt生成完整依赖目录再手动确认串口 DLL 在不在打包前在一台没装过 Qt 的机器上跑一遍这一步不能省。Linux 环境下还要确认当前用户对串口设备有读写权限。坑五连续运行一整天内存缓慢上涨CSV 少了几百行。现象是任务管理器里内存曲线一直往上爬翻看存档文件发现某些时段缺数据。原因通常是两处曲线缓存用了QVector::append但没做窗口裁剪点数无限增长CSV 用了QTextStream但从不flush缓冲区里的数据在异常退出时全部丢失。解决办法是缓存严格按固定长度裁剪CSV 按行数或时间批量落盘日志文件按天轮转。这两个问题都不会在短时间演示里暴露答辩前建议挂机跑两小时再看。6. 验收与答辩一份能当场演示的测试清单和三条讲解主线功能写完不等于能交。验收要拿清单一项一项打勾演示要能在三分钟内讲清楚「做了什么、为什么这么做、怎么证明它是对的」。下面这份清单可以直接抄进文档。编号验收项判定标准测试方法1串口打开选择端口后 1 秒内连接成功状态灯转绿逐个端口试开含被占用的情况2数据解析连续 10 分钟无错帧、无丢帧下位机 1 Hz 上报对照计数3实时曲线窗口滚动平滑CPU 占用低于 10%任务管理器观察 10 分钟4异常值处理断线时显示--曲线标记断点拔掉传感器信号线5报警超阈值 1 秒内变色并记录事件手动改阈值触发6断线重连拔插 USB 后 5 秒内自动恢复反复拔插 10 次7数据存档CSV 行数与上报帧数一致用脚本统计比对8配置持久化重启后串口、阈值、窗口位置不变关闭再打开没有硬件的时候用 Python 造帧是最快的联调手段也是答辩演示的备选方案——万一现场设备罢工直接跑脚本就能让界面动起来。# simulator.py向虚拟串口按 1Hz 发送温度帧 import struct, time, math, serial def crc16_modbus(data: bytes) - int: crc 0xFFFF for b in data: crc ^ b for _ in range(8): crc (crc 1) ^ 0xA001 if crc 1 else crc 1 return crc def build_frame(ch: int, temp: float) - bytes: payload struct.pack(Bf, ch, temp) # 通道号 float32 小端 body bytes([len(payload), 0x01]) payload # 长度 命令字 负载 return b\xAA\x55 body struct.pack(H, crc16_modbus(body)) port serial.Serial(COM3, 9600, timeout1) # 换成虚拟串口对的一端 t 0.0 while True: temp 25.0 5.0 * math.sin(t / 30.0) # 模拟缓慢升温降温 port.write(build_frame(0, temp)) t 1.0 time.sleep(1.0)脚本里的struct.pack(Bf, ...)对应解析端的「通道号 1 字节 float32 小端」CRC 计算范围是「长度 命令字 负载」和上位机完全一致。跑之前先用虚拟串口工具建一对互连端口一端给脚本一端给界面。答辩讲解我一般收成三条主线第一条讲协议为什么不用字符串而用带 CRC 的二进制帧现场展示一个被篡改的帧如何被丢弃第二条讲线程说明为什么串口读取必须离开 UI 线程展示拔插时界面不卡第三条讲数据完整性把 CSV 打开用表格统计行数对上上报帧数。三条讲完工作量和技术判断都摆在台面上了比念 PPT 有效得多。从那以后我每次做带实时曲线的上位机都会在动手写界面之前先把造帧脚本和验收清单写出来界面还没成型就能验证解析链路联调时间至少省一半。希望这套结构能帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询