基于Qt与QSerialPort的简易串口示波器实现详解

发布时间:2026/8/31 18:28:51
基于Qt与QSerialPort的简易串口示波器实现详解 简介这是一份基于Qt框架实现的轻量级示波器波形显示Demo面向嵌入式开发、信号处理及GUI编程初学者与中级开发者解决实时波形动态绘制、缩放平移交互及历史状态恢复等核心可视化问题。资源包共8个文件282KB含3个cpp源文件主窗口逻辑、绘图控制与主程序入口、2个头文件界面定义与QCustomPlot图表封装、1个.ui界面文件、1个.pro工程配置及1个.user用户设置文件结构清晰便于理解Qt信号槽驱动数据刷新、坐标系变换实现缩放/平移、以及操作历史栈管理“找回”功能的技术路径。已有662人学习下载提供可直接编译运行的完整工程包含QCustomPlot集成示例、实时数据模拟机制与基础交互响应代码是掌握Qt跨平台波形可视化开发的实用入门参考。 最近手里有个小项目需要把ADC采集的数据实时画出来看波形一开始用的是串口把数据导到PC再丢给Python画图来回折腾效率太低。后来干脆用Qt写了个简易示波器也就是这个plottest_QT_示波器项目。它能通过串口实时读取数据、滚动绘制波形、带电压和时间的粗略测量虽然比不上商用示波器的精度但日常调试传感器、看PWM信号、观察电源上电时序完全够用。这篇文章就把我整个实现过程、几个关键模块的设计思路和踩过的坑都整理出来给想用Qt做数据采集和波形显示的朋友做个参考。1. 项目初衷与整体设计思路1.1 为什么用Qt做示波器说实话一开始我也纠结过是用上位机配合Python还是纯C写。Python的matplotlib虽然画图方便但实时性不行刷新一快界面就卡而且打包分发环境很麻烦。Qt这边有成熟的QSerialPort串口类、QChart图表库C的性能也足够支撑高频数据刷新。另外还有一个重要的点Qt的跨平台特性。我在Windows上调试但板子那边的采集程序是跑在Linux上的以后如果想把示波器界面直接跑在配了触摸屏的Linux板卡上Qt这套代码基本不用动。这也是为什么项目名叫plottest_QT_示波器——plottest就是plot test绘图测试本质上是用Qt验证实时绘图和数据采集的可行性。1.2 核心功能模块划分整个项目我按功能拆成了四个模块每个模块各管一摊方便后续扩展。数据采集模块基于QSerialPort实现负责从串口读取下位机发来的波形数据解析成double类型的数值。数据处理模块负责把原始数据放入环形缓冲区做滑动窗口切割和数值校准比如把ADC原始值转换成实际电压值。波形绘制模块基于QChart或者自定义QPainter绘制负责把缓冲区里的数据画成波形支持暂停、缩放、滚动。交互控制模块负责按钮、旋钮、参数输入框这些UI交互比如波特率设置、触发模式选择、电压档位切换。模块之间通过信号槽通信采集模块收到新数据就发一个信号绘图模块收到信号就刷新波形。这样串口速率再快也不会因为UI卡顿导致数据丢失。1.3 技术选型考量这里重点说我为什么用QChart而不是QPainter直接画。QChart是Qt Charts模块提供的现成图表库有坐标轴、网格线、图例这些现成组件缩放和平移也自带了省去很多底层绘图的烦琐工作。但它的性能在数据量大的时候不太好实测QChart在波形数据点超过5万时刷新率会明显下降。QPainter直画则更灵活性能上限高但所有东西都要自己实现坐标轴刻度计算、网格线绘制、波形缩放、触发标记工作量翻倍。我的方案是折中先用QChart快速搭出可用版本如果后面遇到性能瓶颈再用QPainter替换绘制核心。目前项目实测串口1M波特率、每秒约2000个采样点QChart完全扛得住所以暂时没有切到QPainter。2. 环境搭建与基础框架实现2.1 Qt开发环境的选择与配置这个项目我用的版本是Qt 5.15.2 Qt Creator 4.13编译器选的MSVC 2017 64位。为什么不用最新的Qt 6这里有几个现实原因。第一Qt 5.15.2是LTS版本稳定性好网上资料多出问题容易搜到解决方案。第二QSerialPort和Qt Charts这两个模块在Qt 5.15里都是独立封装好的直接加模块就行。第三项目的目标平台既有Windows又有LinuxQt 5.15两边的兼容性都验证过很多次。下载安装时有个坑要注意Qt官方在线安装包如果不加镜像大概率会卡在下载进度条上。我用的清华镜像具体操作是在安装程序启动时添加命令行参数qt-unified-windows-x64-online.exe --mirror https://mirrors.tuna.tsinghua.edu.cn/qt/装完之后在Qt Creator里确认组件勾选情况Qt 5.15.2下的MSVC 2017 64-bit、Qt Charts、Qt SerialPort这几个模块务必选上否则后面工程文件里加QT charts serialport时会编译不通过。2.2 项目结构组织项目文件名就叫plottest_QT_示波器工程文件组织如下plottest_QT_示波器/ ├── plottest_QT_示波器.pro ├── main.cpp ├── mainwindow.h / mainwindow.cpp ├── datacapture.h / datacapture.cpp ├── databuffer.h / databuffer.cpp ├── waveformview.h / waveformview.cpp └── resources/ ├── style.qss └── icons/.pro文件里除了基础配置还要加上模块声明QT core gui charts serialport greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 SOURCES \ main.cpp \ mainwindow.cpp \ datacapture.cpp \ databuffer.cpp \ waveformview.cpp HEADERS \ mainwindow.h \ datacapture.h \ databuffer.h \ waveformview.h这样拆的目的很明确datacapture只管串口收发databuffer只管数据缓存和格式转换waveformview只负责绘图mainwindow做界面集成。模块间接口用信号槽串联任何一个模块出问题单独调试都不受牵连。2.3 主窗口UI布局设计主窗口布局我用了QMainWindow加QWidget组合的方式。左侧放参数控制区右侧是波形显示区底部是状态栏。左侧控制区包含这些控件串口选择下拉框QComboBox波特率选择QComboBox预置9600到921600连接/断开按钮QPushButton启动/停止采集按钮电压档位选择QComboBox±1V、±2V、±5V、±10V时基选择QComboBox10ms/div到1s/div触发电平输入框QDoubleSpinBox暂停显示按钮波形显示区我用一个QChartView作为主轴同时叠了一个QLabel用来显示光标读数。之所以用叠层而不是把读数放在状态栏是因为调试时视线焦点要始终在波形区域读数放在波形上比放在角落要直观得多。界面初始化时我加了一点QSS美化深色背景更适合长时间盯着波形看而且波形显示区用黑色背景、绿色波形对比度更清晰这也是大多数商用示波器的配色习惯。3. 核心细节解析与实操要点3.1 串口数据采集模块的完整实现串口模块是数据链路的第一环也是最容易出问题的地方。我的实现里QSerialPort的配置包括端口名、波特率、数据位、停止位、校验位其中端口名和波特率留给用户在界面上选其余参数固定为8-N-1。关键代码是数据接收的异步处理。我利用QSerialPort的readyRead信号每次收到数据就触发读取而不是用定时器去轮询。这样CPU占用率低响应也快。void DataCapture::onReadyRead() { QByteArray data serialPort-readAll(); // 追加到内部解析缓冲区 parseBuffer.append(data); // 尝试解析出完整的数据帧 extractFrames(); }这里有一个非常关键的坑也是很多初学者容易踩的串口数据是流式的不会保证一次readAll就拿到一帧完整数据。下位机可能发送的是帧头 数据长度 数据体 校验的封装格式而上位机每次收到的可能是半帧、一帧半甚至多帧粘在一起。如果直接按固定长度去解析肯定会出问题。我的处理方式是维护一个内部解析缓冲区void DataCapture::extractFrames() { // 寻找帧头 0xAA 0x55 while (true) { int startIndex parseBuffer.indexOf(QByteArray::fromHex(AA55)); if (startIndex 0) { // 没找到帧头丢弃多余数据 parseBuffer.clear(); return; } if (startIndex 0) { // 帧头之前的数据是无效数据丢弃 parseBuffer.remove(0, startIndex); } if (parseBuffer.size() FRAME_LENGTH) { // 缓冲区数据不足一帧等待下个readyRead return; } // 校验帧头提取ADC数据 QByteArray frame parseBuffer.left(FRAME_LENGTH); parseBuffer.remove(0, FRAME_LENGTH); bool ok verifyChecksum(frame); if (ok) { int adcValue extractAdcValue(frame); emit newSampleReady(adcValue); } } }帧格式我是这样约定的帧头2字节0xAA 0x55数据长度1字节ADC高位1字节ADC低位1字节校验和1字节前面所有字节累加取低8位。这样一帧总共6字节。下位机按这个格式发上位机按这个格式解析就能把ADC值稳定地传上来。3.2 数据缓冲与实时波形绘制核心逻辑数据采集解析的频率很高如果每收到一个数据点就刷新一次波形界面会疯狂重绘CPU占用率飙升。我的做法是利用环形缓冲区加定时刷新机制。环形缓冲区用QVector实现固定大小例如20000个点存满后覆盖最老的数据void DataBuffer::push(double value) { buffer[writeIndex] value; writeIndex (writeIndex 1) % bufferSize; count; }绘图时通过一个QTimer定时器每30ms触发一次从缓冲区读取数据并刷新波形。这样既保证了波形足够实时又不会因为高频刷新导致UI卡顿。电压值的转换也是在这个模块完成的下位机发送的是12位ADC原始值范围0到4095参考电压3.3V所以电压值 adcValue * 3.3 / 4095。再根据用户选择的电压档位做等比缩放决定纵轴显示范围。这一点必须提醒参考电压必须要准确。我用的是板载TL431基准源实测3.294V而不是理论上的3.3V直接按3.294算否则电压测量会有一个固定偏差看波形趋势无所谓但要想做精度测量就得配个万用表实测校准。绘制方面QChart的数据更新用的是QLineSeries的replace()方法而不是append()。append会反复触发重绘和坐标轴重算性能差很多。replace是整体替换数据点坐标轴范围可以保持稳定刷新速度高一个量级。这个是我实测下来最明显的性能优化点void WaveformView::updateWaveform(const QVectordouble points) { // 使用replace代替append避免坐标轴反复重算 series-replace(points); chart-axisX()-setRange(0, points.size() - 1); chart-axisY()-setRange(minVoltage, maxVoltage); }时基功能时间轴跨度是通过滑动窗口实现的。比如时基调成20ms/div窗口宽度是10格那么窗口就是200ms。根据采样率计算出窗口内应该显示多少个点然后从缓冲区中截取对应区间进行绘制。3.3 波形显示、缩放与测量功能实现波形显示增强功能我做了三件事滚动显示、拖动查看历史数据、光标测量。滚动显示是默认模式随着新数据进来波形像医院心电监护仪一样从右往左移动。实现上就是根据当前窗口内最后一个数据点的时间戳计算出窗口的起始位置不断右移窗口终点。触发功能我用了一个简单的上升沿触发触发电平设置好之后在数据中搜索第一个超过触发电平的点然后把该点放到波形窗口的左端作为触发参考点。这样波形看起来稳定了不会满屏乱飘。int WaveformView::findTriggerPoint(const QVectordouble data, double triggerLevel) { for (int i 1; i data.size(); i) { if (data[i - 1] triggerLevel data[i] triggerLevel) { return i; } } return -1; }触发模式我只实现了自动触发和正常触发两种。自动触发是找不到触发点就自由滚动正常触发是先暂停等待触发条件成立。手动测量方面我加了一个光标模式点击波形上的两个位置软件会自动计算这两点的电压差和时间差显示在波形上方。这个功能用来测量信号周期和峰峰值特别方便。4. 常见问题与排查技巧实录4.1 串口通信丢包、粘包、校验失败问题这几个问题基本是串口类上位机最容易踩的坑。我调试的时候前后折腾了两天最后总结出三个最核心的原因。第一个原因是缓冲区开得太小。QSerialPort读取时如果没有及时把数据取走数据会积压在系统串口缓冲区里超了就会丢。解决办法是把串口接收缓冲区调大同时确保readyRead信号触发后立刻readAll不要做多余的处理再读。serialPort-setReadBufferSize(64 * 1024);第二个原因是下位机发送间隔不稳。有些下位机用的是MCU串口发送时因为中断优先级的问题两帧之间的间隔会抖动上位机如果超时判断设得太短就会把一帧数据拆成两次接收拆包逻辑又没处理好就丢了。解决方案是给解析加状态机而不是固定时间片。第三个原因最隐蔽校验算法不一致。下位机用CRC16上位机用累加和两边约定的时候没对齐结果所有数据都解析失败。所以强烈建议在通信协议文档里把校验方式写得清清楚楚测试时先用一个固定数据包做联调确认校验一致了再开始全速跑。4.2 波形绘制卡顿与性能优化波形绘制卡顿本质上就是数据量和绘制频率不匹配。遇到过几次逐一排查下来除了前面提到的replace代替append还有几个关键点。第一个是QChart坐标轴智能重算。默认情况下QChart可能会根据数据自动调整坐标轴范围但滚动波形时我们并不想让它自动调整否则波形会被来回拉伸。显式使用chart-axisY()-setRange()固定住纵轴范围就能显著减少重绘计算量。第二个是定时器的刷新频率。QTimer刷新间隔设置低于16ms没有意义因为显示器的刷新率一般也就是60Hz即使能画到100fps肉眼也看不出区别反而白白消耗CPU。我的经验是30ms到50ms比较合适波形实时感和CPU占用率能取得平衡。第三个是硬件加速。如果屏幕分辨率很高可以尝试在QChartView上启用OpenGL加速chartView-setRenderHint(QPainter::Antialiasing, true); chartView-setRenderHint(QPainter::HighQualityAntialiasing, false);Antialiasing打开会让波形边缘平滑但数据量大时也费性能。实测在2万点、30fps刷新的场景下关闭抗锯齿能带来30%以上的性能提升。所以我会在性能模式中关掉它在暂停查看模式下再打开。4.3 Qt开发过程中的高频报错与编译问题这里整理几个我反复遇到的编译和运行时报错新手看到会一脸懵其实处理起来都有套路。QT unknown module multimedia这个错特别常见。原因是.pro文件里加了QT multimedia但安装Qt时根本没有勾选multimedia模块。解决办法有两个要么在Qt安装管理器里补装对应模块要么把.pro里的multimedia模块去掉。如果你只是做串口示波器multimedia模块根本用不到去掉最省事。还有cannot find -lSerialPort类似的链接错误这个多半是因为工程文件缺少QT serialport或者安装Qt时漏了SerialPort模块。安装Qt时务必在组件列表里勾选Qt SerialPort。至于Qt崩溃问题最常见的原因是在lambda表达式里捕获了栈上的局部变量然后这个局部变量已经析构了后续在信号槽触发时访问了野指针。我的排查方法非常简单粗暴编译时开启AddressSanitizer看看报错的调用栈或者检查所有connect的lambda捕获列表把捕获方式改为[this]并且在类成员里持有关键对象而不是依赖局部临时变量。还有一个老生常谈的坑用Qt Creator编译时如果出现了奇怪的报错先看看构建套件Kit是不是选对了。你代码复制过来套件用的还是别人的编译器路径不对当然编不过。切到正确的MSVC或MinGW套件后往往一下子就通了。5. 后续扩展思路与个人体会目前这个plottest_QT_示波器主干功能已经稳定日常调试够用了。但如果想做得更完善有几个方向可以考虑。一个是增加数据存储功能把采集到的波形数据导出成CSV文件。这个实现上就是在DataBuffer模块里加一个导出函数把缓冲区数据按时间戳和数值写进文件就行。做完之后配合Python做FFT频谱分析或者长时间的漂移监测就非常方便了。另一个方向是增加FFT频谱显示。Qt Charts没有现成的频谱图组件需要自己把时域数据做FFT变换再绘制频谱曲线。QT下可以用FFTW库或者自己实现一个简化版基2FFT配合现有的绘图模块就能做出一个简易的频谱分析功能。还有一个我最近正在琢磨的扩展点通过Qt UDP网络模块把波形数据推送到局域网内的网页端。这样不需要在电脑前也能远程观察设备状态。搜到的open-meteo等API场景给了我一个思路——把采集端的网络协议做成类似REST风格网页端或者小程序端拉取数据渲染波形。最后说说我个人的一点体会。用Qt做示波器这类工具软件前期的模块划分比写代码更重要。如果一开始就把串口解析、数据处理、波形绘制全部堆在一个类里后面想加功能或者修bug都会非常痛苦。我这个项目虽然叫plottest看起来只是个绘图测试但正是这种把基础模块理清楚的小项目让我后面再做复杂的数据采集系统时直接复用代码省了大量时间。另外一点就是不要迷信参数和理论值。实际调试中参考电压、波特率偏差、数据校验方式这些细节都要以实测为准。花点时间做校准和协议验证远比后来发现问题再返工来得划算。如果你也在做类似的数据采集或者波形显示项目希望这篇文章能帮你少踩几个坑。有更好的想法欢迎一起交流。本文还有配套的精品资源点击获取