基于Qt与QCustomPlot的开源串口虚拟示波器上位机开发全解析
2026/6/11 10:43:03 网站建设 项目流程

1. 开源串口虚拟示波器上位机开发概述

在嵌入式开发中,经常需要实时监测传感器数据或系统运行状态。传统示波器价格昂贵且功能固定,而基于Qt和QCustomPlot开发的串口虚拟示波器上位机,不仅成本低廉,还能灵活适配各种采集需求。我最近在做一个STM32项目时,就遇到了需要同时监测16路ADC数据的场景,通过开源方案完美解决了这个问题。

串口虚拟示波器的核心原理很简单:下位机(如STM32)通过ADC采集模拟信号,转换为数字量后通过串口发送给上位机,上位机负责解析数据并实时绘制波形。这种方案特别适合需要长时间监测、多通道采集的场景,比如环境监测、设备调试等。实测下来,一套完整的系统开发成本不到百元,却能达到专业示波器的部分功能。

2. 开发环境搭建与工具选型

2.1 Qt开发环境配置

Qt作为跨平台的C++图形界面框架,是开发上位机的理想选择。我推荐使用Qt 5.15 LTS版本,这个版本稳定且兼容性好。安装时记得勾选MSVC工具链和Qt Creator,这是后续开发的基础。对于新手来说,可能会纠结MinGW和MSVC的选择,我的经验是:如果只是开发Windows应用,MSVC编译效率更高;如果需要跨平台,MinGW更方便。

安装完成后,还需要配置一个关键组件——QCustomPlot。这是一个基于Qt的绘图库,专门用于高效绘制动态曲线。可以直接从官网下载源码,解压后把qcustomplot.h和qcustomplot.cpp两个文件添加到你的项目里就行。我在多个项目中使用过这个库,它的性能比Qt自带的QChart要好很多,特别是在高频数据刷新时。

2.2 串口通信库选择

Qt自带的QSerialPort类基本能满足大部分串口通信需求,但如果你需要更高级的功能(比如自动检测串口热插拔),可以考虑第三方库如QSerialDevice。我在实际项目中发现,原生的QSerialPort在高速传输(超过500kbps)时稳定性稍差,这时候可以调整缓冲区大小来改善:

serial->setReadBufferSize(1024 * 1024); // 设置1MB的接收缓冲区

3. 上位机核心功能实现

3.1 数据协议设计与解析

串口通信需要定义明确的数据协议。参考开源项目serial_port_plotter,我设计了一个简单高效的协议格式:

$通道1值 通道2值 ... 通道N值;

在代码实现上,数据解析主要分为三个步骤:

  1. 接收原始数据并缓存
  2. 查找帧头$和帧尾;
  3. 提取中间数据并分割到各通道
void MainWindow::readData() { static QByteArray buffer; buffer += serial->readAll(); int startIndex = buffer.indexOf('$'); int endIndex = buffer.indexOf(';', startIndex); if(startIndex != -1 && endIndex != -1) { QByteArray frame = buffer.mid(startIndex + 1, endIndex - startIndex - 1); processFrame(frame); buffer = buffer.mid(endIndex + 1); } }

3.2 实时曲线绘制优化

QCustomPlot虽然性能优异,但在绘制高频数据时还是需要一些技巧。我发现最影响性能的是下面几个因素:

  • 数据点数量:控制在500-1000个点最佳
  • 刷新频率:30-60FPS足够人眼观察
  • 绘图区域大小:避免全屏绘制

这里分享一个实用的优化技巧:使用setData()的重载版本,直接传递QVector指针,可以减少内存拷贝:

void updatePlot() { static QVector<double> x(1000), y(1000); // ...填充数据... customPlot->graph(0)->setData(x, y, true); // 最后一个参数表示不拷贝数据 customPlot->replot(); }

4. 高级功能扩展实现

4.1 多通道管理

当需要显示16路甚至更多通道时,良好的通道管理必不可少。我的做法是:

  1. 为每个通道分配唯一颜色
  2. 实现图例点击显隐功能
  3. 添加通道重命名功能
// 通道显隐控制 connect(customPlot, &QCustomPlot::legendClick, [=](QCPLegend *legend, QCPAbstractLegendItem *item) { for(int i=0; i<customPlot->graphCount(); ++i) { if(item == customPlot->graph(i)->legendItem()) { bool visible = customPlot->graph(i)->visible(); customPlot->graph(i)->setVisible(!visible); customPlot->replot(); break; } } });

4.2 数据记录与回放

对于需要长时间监测的场景,数据记录功能非常实用。我建议使用SQLite数据库存储历史数据,比直接写文件更可靠。实现时要注意:

  • 采用批量插入提高性能
  • 添加时间戳字段
  • 定期压缩旧数据
// 创建数据表 QSqlQuery query; query.exec("CREATE TABLE IF NOT EXISTS adc_data (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, " "channel1 REAL, channel2 REAL, ...)"); // 批量插入 query.exec("BEGIN TRANSACTION"); for(int i=0; i<data.size(); ++i) { query.prepare("INSERT INTO adc_data (channel1, channel2,...) VALUES (?,?,...)"); // 绑定参数... query.exec(); } query.exec("COMMIT");

5. 性能优化与调试技巧

5.1 高频数据传输优化

当波特率达到921600时,串口数据的接收和处理就成为性能瓶颈。我总结了几点经验:

  1. 使用DMA模式接收数据(下位机端)
  2. 上位机采用生产者-消费者模式,避免UI线程直接处理数据
  3. 适当降低绘图刷新频率

一个实用的性能监测方法是在状态栏显示实时数据速率:

void updateStatusBar() { static qint64 lastBytes = 0; static QElapsedTimer timer; qint64 currentBytes = serial->bytesAvailable(); double rate = (currentBytes - lastBytes) / timer.restart() * 1000.0; statusBar()->showMessage(QString("数据速率: %1 KB/s").arg(rate / 1024, 0, 'f', 2)); lastBytes = currentBytes; }

5.2 常见问题排查

在实际开发中,我遇到过几个典型问题:

  1. 数据丢包:通常是缓冲区溢出导致,增大接收缓冲区可解决
  2. 波形卡顿:检查是否在UI线程进行大量计算
  3. 坐标轴跳动:关闭自动缩放或设置合理的固定范围

一个实用的调试技巧是添加原始数据显示窗口,当波形异常时,可以首先检查原始数据是否正确:

void logRawData(const QByteArray &data) { static QFile logFile("rawdata.log"); if(!logFile.isOpen()) { logFile.open(QIODevice::WriteOnly | QIODevice::Append); } logFile.write(data); logFile.flush(); }

6. 项目实战:STM32+Qt完整方案

6.1 下位机配置

以STM32F103为例,配置16路ADC采集+DMA传输的核心代码:

// ADC初始化 ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 16; ADC_Init(ADC1, &ADC_InitStructure); // DMA配置 DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADC_Value; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // ...其他DMA配置 DMA_Init(DMA1_Channel1, &DMA_InitStructure);

6.2 上位机对接

下位机发送的数据格式要与上位机协议匹配。对于16路ADC,发送代码如:

printf("$%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d;\r\n", ADC_Value[0], ADC_Value[1], ..., ADC_Value[15]);

在上位机端,需要相应设置16个图形通道:

for(int i=0; i<16; ++i) { customPlot->addGraph(); customPlot->graph(i)->setPen(QColor::fromHsv(i*360/16, 255, 200)); }

7. 用户体验优化技巧

7.1 界面布局设计

一个好的虚拟示波器界面应该做到:

  1. 主绘图区占据至少70%空间
  2. 控制面板集中放置
  3. 状态信息实时可见

我常用的布局方式是使用QSplitter分隔控制区和绘图区:

QSplitter *splitter = new QSplitter(Qt::Vertical, this); splitter->addWidget(plotWidget); splitter->addWidget(controlPanel); splitter->setStretchFactor(0, 7); // 绘图区占7份 splitter->setStretchFactor(1, 3); // 控制区占3份

7.2 交互功能增强

除了基本的缩放拖动,还可以添加:

  1. 游标测量功能
  2. 峰值保持
  3. FFT频谱分析

以游标功能为例,实现方法如下:

// 添加游标 QCPItemStraightLine *cursorX = new QCPItemStraightLine(customPlot); cursorX->point1->setCoords(0, 0); cursorX->point2->setCoords(0, 1); // 鼠标移动时更新游标位置 connect(customPlot, &QCustomPlot::mouseMove, [=](QMouseEvent *event) { double x = customPlot->xAxis->pixelToCoord(event->pos().x()); cursorX->point1->setCoords(x, customPlot->yAxis->range().lower); cursorX->point2->setCoords(x, customPlot->yAxis->range().upper); customPlot->replot(); });

8. 项目部署与发布

8.1 跨平台编译

Qt的优势之一就是跨平台。要将项目移植到Linux或MacOS,需要注意:

  1. 串口设备名称差异(Windows是COMx,Linux是ttySx或ttyUSBx)
  2. 换行符处理(Windows是\r\n,Linux是\n)
  3. 库依赖关系

一个实用的跨平台串口初始化方法:

#ifdef Q_OS_WIN serial->setPortName("COM3"); #elif defined(Q_OS_LINUX) serial->setPortName("/dev/ttyUSB0"); #endif

8.2 打包发布

Windows平台推荐使用windeployqt工具自动打包依赖库:

windeployqt --release --no-compiler-runtime my_plotter.exe

对于不想安装Qt运行时的用户,可以静态编译Qt。不过要注意静态编译的许可问题,商业项目需要购买商业授权。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询