尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

用Qt打造轻量级实时示波器:从数据采集到波形绘制与部署

用Qt打造轻量级实时示波器:从数据采集到波形绘制与部署 简介这是一份基于Qt框架实现的轻量级波形示波器演示程序面向嵌入式开发、信号处理及GUI编程初学者与中级开发者解决实时波形动态显示、缩放平移及历史状态恢复等核心需求。资源包共8个文件282KB含3个C源文件main.cpp、mainwindow.cpp、qcustomplot.cpp负责主流程与绘图逻辑2个头文件mainwindow.h、qcustomplot.h定义界面与绘图组件接口1个UI设计文件mainwindow.ui构建可视化布局另含项目配置文件.pro、用户设置.pro.user各1个结构清晰、模块职责分明。已有662人学习下载代码完整可直接编译运行内置QCustomPlot绘图库集成方案、信号-槽驱动的实时数据刷新机制、坐标系动态缩放/平移实现逻辑以及简易‘找回’功能的堆栈式状态管理思路是理解Qt跨平台波形可视化开发的典型实践范例。 调试嵌入式设备的时候最折磨人的一件事就是看波形。前几年我习惯的做法是连一台桌面示波器或者把数据导成 CSV 丢进 Python 里用 matplotlib 画数据量一上来就卡成幻灯片现场调试根本等不起。后来我决定用 Qt 写一个轻量级的示波器测试程序内部代号就叫 plottest专门用来把串口、UDP 和本地模拟数据实时变成波形顺带做了尖峰测量、眼图观察和发布打包。这篇记录把这套项目的完整开发过程、关键代码和踩过的坑全部整理出来给想用 Qt 做简易示波器、波形分析工具或数据可视化测试台的读者当参考。这个项目本质上不是一个商业级别的数字示波器替代品它解决的是更现实的问题给嵌入式联调、传感器验证、网络包时序分析提供一个能快速打开、能即时改参数、能随便折腾的可视化工具。所以全文不会去聊什么 FPGA 采样、高速 AD 前端、硬件触发那些是专业示波器厂家的事。我会把重心放在 Qt 应用层怎么把数据引进来、怎么画得快、怎么测出有用指标、怎么打包给同事用这几个部分再配合真实项目里遇到的环境问题和性能问题一起说。1. 为什么我会自己写一个 Qt 示波器测试程序先交代一下背景。我手头经常有 ADC 采样数据、串口传感器读数、UDP 过来的设备状态包这些数据量不大但是要实时看趋势、看毛刺、看协议异常。成品示波器不是不能用但它强在高速信号对低速串口和网络日志反而不友好保存文件要插 U 盘分析数据要导出再处理根本没法做到“打开程序就看到实时曲线”的直觉体验。1.1 现有方案看着能用用起来全是别扭市面上常见的替代路线大概有三条我全都试过。第一条是直接用厂商提供的上位机软件。能看波形但几乎都是“配套”思路跟某个硬件绑定换一家芯片、换一个协议就废了而且很多上位机软件界面停留在十年前的水平想标注一个峰值都得用截图工具。第二条是拿 Python 写数据可视化脚本。pyserial 收串口、matplotlib 画图简单数据没问题但实时刷新是个大问题。matplotlib 的动画模式本质上还是定时重绘整张图数据量一上来 CPU 就吃满而且部署的时候对方机器上不一定有 Python 环境。第三条是用现成的开源示波器软件比如一些用 Python 写的或者跨平台的小工具。功能不错但扩展性很差。我要加一个私有协议的解析层要自定义触发条件要对接自己写的模拟数据源改源码的成本比重新写一个还高。所以到最后与其到处找轮子不如自己在 Qt 里造一个。1.2 我的需求清单和选型结论我开始写 plottest 之前没有着急敲代码先把需求列成了清单数据源可插拔既能读串口也能收 UDP还能内置一个波形发生器用来做功能验证。实时绘制至少 20 万点不卡顿刷新率不低于 30 帧每秒。支持自动缩放和手动缩放支持暂停/继续这样抓到异常数据后可以停下来慢慢看。带基本的测量能力电压峰值、时间间隔后续能扩展眼图分析。界面简单直接别人拿到手 5 分钟内能上手。技术选型上Qt 几乎是唯一答案。Python 的部署问题前面说过C 的 Qt 用 windeployqt 或 linuxdeployqt 打出来的程序丢到别的电脑上就能跑Qt 的 QSerialPort、QUdpSocket 又自带跨平台封装不需要额外处理底层 API 差异。界面用 Qt Widgets绘制用 QCustomPlot——这个组合我后面会单独开一节详聊因为选型不是想当然里面有性能数据和许可证两本账要算。2. 第一版草率了主线程刷波形的教训很多人写 Qt 程序第一步就是把所有逻辑塞进 MainWindow 的构造函数和定时器回调里。我现在的第一版也没能免俗。2.1 第一次把串口读数和绘图全放在 UI 线程第一版的逻辑大概是这样QTimer 每 50 毫秒触发一次在槽函数里去读串口缓冲区把读到的数据追加到 QVector 里然后调用 QCustomPlot 的 replot。代码结构长这样void MainWindow::onTimer() { QByteArray data port-readAll(); for (int i 0; i data.size(); i) { waveform.append((quint8)data.at(i)); } ui-plot-graph(0)-setData(xValues, waveform); ui-plot-replot(); // 手动重绘 }在数据量小的时候这个写法勉强能跑。串口速率 115200每次读进来的数据才几百字节界面卡顿不明显。一旦我把模拟数据源打开每帧要生成几万个点事情就变了。首先是主线程忙不过来。读取数据、解析协议、更新 QVector、重新绑定数据、重绘整个绘图区全挤在 UI 线程里。窗口拖动时明显掉帧最小化再还原有时候要等一两秒。接着是内存抖动QVector 不断 appendreplot 又强制触发整个坐标轴区域的绘制CPU 占用能到 100%。更麻烦的是串口接收事件本身是异步的如果接收频率比定时器快数据就会堆积在缓冲区里导致波形看起来一条一条地跳完全没有实时感。这个问题在 debug 版本下还能接受用 Release 跑反而更明显因为绘制速度变快之后UI 线程被绘图吃掉的占比更大。2.2 改成生产者-消费者模型后界面才活过来意识到主线程方案的瓶颈后我把架构改成了生产者-消费者模式。数据采集线程作为生产者负责从串口、UDP 或模拟数据源读数据解析成统一的采样点结构放进一个线程安全的缓冲区。UI 线程只做一个事用 QTimer 周期性从缓冲区取最新一批数据更新绘图控件。改造以后的伪代码void CaptureThread::run() { forever { QByteArray bytes readFromSource(); QVectorSamplePoint parsed parseBytes(bytes); buffer-push(parsed); } } void MainWindow::updatePlot() { QVectorSamplePoint batch; buffer-takeAll(batch); if (batch.isEmpty()) return; // 只更新尾部新数据用于增量绘制 appendToGraph(batch); ui-plot-replot(QCP::rpQueuedReplot); }关键点有两个一是缓冲区要设计成“取走即清空”避免 UI 线程拿数据时和采集线程产生竞争二是 replot 用排队重绘模式避免同一帧里被多个槽函数触发多次重复绘制。这是我整轮改造里最重要的一步。改完之后波形绘制的帧率稳定在 30~45 帧CPU 占用直接降下来一半。后面我再加 FFT、眼图分析都没有回到主线程卡死的状态。3. 绘图控件选型QPainter、QCustomPlot 和 Qt Charts 谁更适合做 Qt 波形程序绘图层选型是核心中的核心。你界面再漂亮数据画不快、画出来不跟手全是白搭。我一开始对比了三种方案直接用 QPainter 手绘、用官方 Qt Charts、用第三方 QCustomPlot。3.1 三种方案的核心差异直接上表格这样差异更直观方案绘制性能交互缩放/平移学习成本许可风险维护状态QPainter 手绘上限最高但全靠自己实现需要自己写坐标变换高无风险你维护Qt Charts中等百万点会卡原生支持但定制有限低GPL/commercial要注意动态链接和依赖透传官方维护QCustomPlot高十几万点实时无压力原生支持且定制空间大中GPLv3/commercial静态链接需要购买授权个人/企业持续维护QPainter 手绘不是不能做我也用它写过简单的滚动波形但一旦加上鼠标框选缩放、十字光标、坐标轴自适应这些交互工程量会翻好几倍。示波器项目里交互能力几乎和绘制能力一样重要没有缩放和游标的示波器没法用。Qt Charts 的问题在于它虽然是官方方案但许可证夹在 GPL 和商业协议之间如果你要发布闭源软件必须重新审视链接方式和源码是否被修改。团队里如果没有法务确认用起来会有些心虚。而且 Qt Charts 对 QML 支持好对 Widgets 支持虽然也有但在 QCustomPlot 面前它的性能和定制灵活性还是差了一截。3.2 我为什么选 QCustomPlot选 QCustomPlot说白了看中三点。第一性能。QCustomPlot 底层把图元划分成多个 QCPLayer重绘时只重绘有变化的部分它还支持 OpenGL 加速QCPGraph 的 setAntialiased 配合 QCPViewport 可以做一定的加速。在我实际测试中10 万个点的单条曲线加上抗锯齿关闭能稳定跑到 30 帧以上这对我的串口示波器场景绰绰有余。第二交互开箱即用。QCustomPlot 自带了 QCPAxisRect::setRangeZoom/SetRangeDrag鼠标滚轮缩放、左键拖拽平移、右键恢复全部内置省了我不少功夫。它的 QCPItemStraightLine 还能直接画触发电平线不用自己 drawLine。第三社区资料多。Stack Overflow 上搜 QCustomPlot 相关问题的答案质量很高版本更新也比较活跃。对于这种非核心业务但不可或缺的组件宁可选择社区撑得住的也不选孤僻的小众轮子。当然我给它加了前置条件如果是纯个人学习或者内部非商业工具GPLv3 完全没问题如果要对外发闭源软件老老实实花几百块买一个 commercial license。这个钱对比省下的开发时间太值了。4. 核心代码拆解数据接入、缓冲、缩放和触发架构定好之后剩下的就是一个个功能模块的硬啃。我按数据流动的方向来说数据源 - 缓冲区 - 波形绘制 - 测量分析。4.1 数据源虚拟波形、QSerialPort、UDP为了让项目能在没有硬件的情况下独立调试我做了一个抽象基类DataSource几个具体实现可以随时替换。虚拟波形发生器用来模拟正弦波、方波和带毛刺的随机信号。正弦波核心就是按相位生成采样点class SineDataSource : public DataSource { public: void setFrequency(double freqHz, double sampleRate) { m_phaseStep 2 * M_PI * freqHz / sampleRate; } QVectorSamplePoint readBlock(int count) override { QVectorSamplePoint out; out.reserve(count); for (int i 0; i count; i) { double v amplitude * sin(phase); phase m_phaseStep; if (phase 2 * M_PI) phase - 2 * M_PI; out.push_back({timestamp, v}); } return out; } private: double phase 0.0; double m_phaseStep 0.0; double amplitude 3.3; };串口数据源就是 QSerialPort 的事件驱动读法。这里有一个很多人第一次用会踩的坑QSerialPort 的 readyRead 信号触发频率取决于数据到达节奏不是固定周期所以不能在 readyRead 里直接做 UI 更新而应该只负责把数据塞进队列由采集线程统一取。我把串口句柄和解析器集中在 CaptureThread 里避免跨线程操作 QIODevice 导致的崩溃。UDP 数据源的逻辑也类似用 QUdpSocket 绑定端口收到数据报后拆包、解析、追加到缓冲区。我在工具里把这个模式做成了通用配置页可以指定端口号、数据格式ASCII/Hex这样调试网络设备时也能用同一套界面看波形。4.2 环形缓冲区和双缓存设计缓冲区是实时示波器的大脑设计不好很容易出现数据错位、丢点、显示滞后。我用的是一个固定容量的环形缓冲区容量按照最大采样点数来开比如 1M 采样点。写入端把新数据从尾部写入读取端从头部读走当缓冲区满时丢弃最老的数据保证永远显示最近一段时间的数据。class RingBuffer { public: RingBuffer(int capacity) : m_capacity(capacity) { m_data.resize(capacity); } void push(const QVectorSamplePoint samples) { if (samples.size() m_capacity) { m_head 0; m_size m_capacity; m_data.fill(samples.last()); return; } int tail (m_head m_size) % m_capacity; int firstPart std::min(samples.size(), m_capacity - tail); std::copy(samples.begin(), samples.begin() firstPart, m_data.begin() tail); if (firstPart samples.size()) { std::copy(samples.begin() firstPart, samples.end(), m_data.begin()); } m_size std::min(m_size samples.size(), m_capacity); } QVectorSamplePoint takeAll() { /* 取走并清空 */ } private: int m_head 0; int m_size 0; int m_capacity; QVectorSamplePoint m_data; };读写双方都只访问互斥锁保护的共享内存。读取端每次 takeAll 会拿一整批数据然后一次性追加到绘图图形里尽量减少 UI 线程的锁竞争时间。双缓存的另一点体会是不要在 UI 线程里对 QCPGraph 的 data 容器做逐点插入一次性传入 QVector 再 setData。QCustomPlot 内部会把外部数据容器拷贝到自己的 QCPDataContainer如果你传一个 vector 进去它自己处理增量和范围比逐个 addData 快一个数量级。4.3 自动缩放和手动触发示波器核心功能之一是触发。真正的硬件触发我这里不模拟但做一个软触发并不难。我实现了几种模式自动缩放每次刷新时统计当前窗口内最小值和最大值动态调整 y 轴范围适合看趋势。上升沿触发设置一个触发电平 Vt当波形从小于 Vt 穿越到大于 Vt 时把 x 轴原点重置到该位置然后显示穿越后固定时间窗的数据。下降沿触发逻辑和上升沿相反常用于观察负脉冲。上升沿触发核心代码int findTriggerIndex(const QVectorSamplePoint data, double level) { for (int i 1; i data.size(); i) { if (data[i - 1].value level data[i].value level) { return i; } } return -1; }找到触发点后把原来从缓冲区头部开始的绘制窗口顺移到触发点位置就能模拟出一张稳定的波形。实测下来对 1kHz 左右的周期性信号软触发表现很稳定切换信号源后也能快速锁定波形相位。4.4 添加尖峰电压和眼图测量热搜词里反复出现“示波器测量尖峰电压”和“示波器眼图”这两个功能我也在 plottest 里做了简化实现不需要很高深的信号处理基础。尖峰电压测量本质就是在一个时间窗口内找出最大和最小采样值然后算出峰峰值。但真正的毛刺测量不能只看单点因为噪声会造成单点抖动。所以我先做了滑动均值平滑再对平滑后的序列做波峰波谷查找最后返回峰值电压、谷值电压、峰峰值、以及尖峰出现的时间戳。眼图分析在数字信号调试中非常常用原理是把连续比特的波形按符号周期分割重叠显示到同一个周期上。简化实现可以这样做用户输入一个码元周期 symbolPeriod以每个周期的起点为对齐点把所有周期的波形片段画在同一张图上。这样能直观看到信号过冲、振铃、噪声容限。我在代码里实现了一个EyeDiagramWidget用 QCustomPlot 的多个 graph 分别绘制每个周期的曲线配合颜色渐变透明度叠加多了也不会糊成一团。5. 开发期踩过的坑qt崩溃、unknown module multimedia、release 版空白界面这个项目开发过程不是一路顺畅的中间遇到了几个特别典型的 Qt 环境问题都和热搜词里那几个高度吻合。我把排查链路写完整比直接给结论有用。5.1 qmake 环境下 unknown module multimedia我在一个老项目分支上希望把音频输出当数据源于是.pro文件里加了QT multimedia结果编译报错Project ERROR: Unknown module(s) in QT: multimedia这个报错绝大多数情况不是 Qt 本身缺模块而是你安装 Qt 的时候没有勾选 Multimedia 组件。很多人第一反应是去重装 Qt其实没必要。Qt 的安装器支持在线添加组件打开维护工具勾选对应版本的 Multimedia 模块等安装完成后重新 qmake 就能解决。但如果你用的是精简版 Qt 发行包或者系统自带的 Qt那确实可能缺。我的建议是拿到报错先查 Qt 安装目录的 modules 配置文件grep -r multimedia /path/to/Qt/5.15.2/gcc_64/mkspecs/modules/如果根本没有qt_lib_multimedia.pri这类文件说明模块缺失这时再考虑用安装器修补而不是闷头改代码。5.2 调试正常、release 崩溃的 QCustomPlot 问题这是让我印象最深的一个坑。代码在 Debug 下跑得好好的一切换到 Release 版本运行时点击按钮就崩崩溃位置时而在 QCustomPlot 内部的绘制函数里时而在 QCPAxis 的标签绘制里。排查思路我梳理了好几轮最后发现两个问题叠加在一起。第一个问题是 Release 下编译器优化可能导致未初始化指针被掩盖。我创建的 QCPGraph 指针是通过addGraph()返回值拿到并缓存的但其中一个 graph 是在构造函数里用init()函数创建的而init()被调用时 QCustomPlot 的 plot area 还没完全初始化。让我意外的是 Debug 下居然能正常运行。第二个问题是 Q_OBJECT 宏缺失。我写了一个继承 QCPGraph 的自定义类但类声明里漏了 Q_OBJECT 宏导致信号槽机制失效编译期不报错运行期行为诡异。Release 下因为链接和优化顺序不同才暴露出崩溃。排查顺序上我的建议是先清理编译缓存Release 下重新构建再看是不是缺失 Q_OBJECT然后跑一次 AddressSanitizer能直接定位到非法内存访问的代码行。ASan 输出里的堆栈比崩溃弹窗靠谱一百倍。5.3 Qt 资源文件不生效和 3D 曲线绘制的注意事项不少初学者会在资源文件上翻车。我在写菜单图标和曲面配色时也踩过把 PNG 拖进.qrc后路径写的是:/images/wave.png代码里的引用也是这个路径但是运行时就显示不出图。后来发现是资源文件改动后没有重新 qmakeQt Creator 有时候不会自动重新生成qrc_xxx.cpp需要手动执行一次 qmake 或者清掉 build 目录重新构建。如果你用QPainter绘制静态水印或图标要注意 painter 绘制过程中不能随便改变 viewport否则在字体缩放模式下会出现内容缺失。我在 Window 上还遇到过高分屏下资源图标模糊的问题解决方式是在 main 函数里启用高 DPI 设置并给图片资源提供 2x 版本。至于“qt 绘制三维曲线”如果你的示波器需要观察二维波形在时间维度上的堆叠可以用 QSurface3DSeries。但说实话Qt 自带 3D 图在实时刷新性能上不如 QCustomPlot 的 2D 多层叠图我在眼图功能里最终还是选择了 2D 方案只有做频谱图和瀑布图时才考虑 3D。6. 发布和部署从工程代码到能给别人用的工具一个 Qt 项目如果只是在开发机上跑价值打五折。把自己电脑上的程序变成别人双击就能运行的工具这里面的发布和部署问题值得单独说。6.1 CMake windeployqt 打包我项目用的是 CMake 构建。发布打包脚本大概分成三步编译出 Release 版本然后用 windeployqt 收集依赖 DLL再把自己额外的配置文件、图标和协议说明文件放进去。核心命令大致是cmake -S . -B build -DCMAKE_BUILD_TYPERelease cmake --build build --config Release windeployqt build/plottest_QT_示波器.exe mkdir deploy copy build/plottest_QT_示波器.exe deploy/ copy C:\Qt\5.15.2\mingw81_64\bin\libgcc_s_seh-1.dll deploy/ # 视编译器而定 copy config.ini deploy/windeployqt 会自动检查 exe 依赖的 Qt 模块但它不一定能识别出你自己动态加载的第三方 DLL比如 QCustomPlot 如果你用的是动态库版本就要记得单独拷贝。如果你的程序用到了串口模块可以手动加上--no-quick-import等参数来避免打进去一堆用不到的东西。一个容易被忽略的点是线上环境如果以动态库方式使用 Qt部署时记得保留下 qml 和 platforms 目录。特别是 Windows 下少了platforms/qwindows.dll程序打开时会出现“This application failed to start because no Qt platform plugin could be initialized”的报错。这个报错基本是打包操作缺目录而不是程序本身有问题。6.2 配置文件和界面适配发布给同事用之后第一个问题肯定是“端口在哪改、波特率哪调”。我把所有可调参数都放进了 config.ini用 QSettings 来读写避免每次改动都重新编译。配置文件结构如下[source] typeserial portCOM3 baudrate115200 interval_ms20 [plot] auto_scaletrue trigger_level1.65 time_window100 [udp] port8899QSettings 读 ini 的代码很简洁QSettings settings(config.ini, QSettings::IniFormat); QString sourceType settings.value(source/type, serial).toString(); int baud settings.value(source/baudrate, 115200).toInt();另外发布时要考虑 Windows 高分屏缩放。QApplication 的setAttribute(Qt::AA_EnableHighDpiScaling)要在创建窗口之前调用这个方法在 Qt 5.15 里已经默认开启不必重复设置。如果同事用老版本 Qt最好在 main 函数开头显式调用一次避免字体发虚和控件错位。6.3 自动化验证模拟鼠标点击事件做回归测试最后提一个开发提效的小技巧。项目功能越来越多之后每次手动打开软件、拖动波形、切换触发模式非常费时间我索性写了一个 Qt Test 自动化脚本通过模拟鼠标点击事件来回放动作。Qt Test 里可以用QTest::mouseClick模拟点击某个控件QTest::mouseClick(ui-triggerButton, Qt::LeftButton); QTest::keyClicks(ui-portEdit, COM4); QTest::keyClick(ui-connectButton, Qt::Key_Return);再配合QTest::qWait()等待数据流过缓冲区就能在 CI 环境里自动验证“程序能否正常连接模拟串口”、“点击触发按钮后波形是否重新对齐”这类基础回归。模拟鼠标点击在 Qt 中只需注意目标控件必须可见且可用否则 QTest 会静默失败所以测试时我会先调用QApplication::processEvents()把界面刷新出来。这一套自动化之后每次改动绘图逻辑或者数据源代码我只要跑一遍脚本就能在几分钟内确认核心功能没有被改坏不用再靠肉眼盯波形了。写到最后再分享一个我自己的习惯给 Qt 波形类项目做版本记录时不要只记 git log最好把“当时测得的性能数据”一并提交到 docs 目录比如“5 万点实时 35 帧、串口 115200 下 CPU 20%”。因为过几个月你重看代码时光看逻辑看不出它跑得快不快只有数据对比才能帮你判断优化有没有效果。plottest 这个项目从第一版的卡成 PPT 到后来能流畅跑 20 万点靠的就是一次次基准数据倒逼出来的改动。如果你也想做类似的 Qt 示波器小工具建议从一开始就把采集线程和绘图线程分开控件选型上优先考虑交互成熟的发布时提前准备打包脚本这样能少走好几段弯路。本文还有配套的精品资源点击获取
返回列表