资讯详情

资讯详情

从源码拆解VC虚拟示波器:缓冲区、双缓冲与触发测量

简介一份基于C/VC开发的虚拟示波器完整源码资源适合电子工程、物理及计算机专业学生也适合希望深入理解上位机信号采集与波形显示的C开发者。项目利用MFC框架构建界面涵盖数据采集、信号处理、波形绘制、触发同步、测量分析与参数调节等典型功能模块。压缩包共74个文件包含19个C源程序、19个头文件、17个位图资源、5个图标及VC工程配置文件整体仅429KB轻量便于对照阅读。已有548人学习下载。通过研读源码可掌握虚拟仪器设计思路、MFC绘图与消息机制、实时数据刷新等实践技能同时能学习示波器触发、频谱分析等电子测量基础原理是结合编程与硬件知识的优质入门资料。1. VC虚拟示波器源码从工程结构到信号链路的完整拆解拿到这份vcxunishbo.rar压缩包里不是一两个文件而是一个完整的 Visual C 6.0 工作区Vsbq工程、12.ico图标资源和若干源文件。把它解压、编译、跑起来之后你会看到一个能显示模拟波形的窗口纵轴是电压横轴是时间能调采样率、能改曲线颜色甚至能计算频率和幅度。这个源码的价值不是“仿一个示波器”而是把虚拟仪器最常见的信号链路——数据生成、缓冲、触发、渲染、测量——用 C 和 MFC 完整走了一遍。适合三类人刚学完 C 想碰 MFC 的初学者需要给上位机加波形监控界面的工程师以及想理解示波器内部原理但买不起硬件的电子爱好者。下面按我拆项目时的顺序从数据缓冲区讲到最后能拿出去用的测量函数。2. 数据采集与缓冲虚拟示波器的第一公里2.1 信号源从哪里来硬件采集还是软件模拟真实示波器靠 ADC 采样VC 虚拟示波器受限于环境最稳妥的做法是生成波形数据。源码里常见的信号源有两种一种是内部函数发生器通过正弦、方波、三角波的数学公式计算每个采样点另一种是从文件或串口读数据。实际工程里虚拟示波器做得更像罗技那种串口调试助手加波形视图数据来自下位机通过串口或 TCP 上传。如果你打算把这份源码改造成串口虚拟示波器核心不是画图而是数据包的解析。我见过很多人在串口中断里直接画点结果界面卡死。正确姿势是串口线程只负责把字节流塞进缓冲区UI 线程定时取出数据渲染。源码里如果没有专门的采集线程你也应该自己补一个CWinThread否则波形刷新超过 20 Hz 就会肉眼可见撕裂。2.2 环形缓冲区避免内存拷贝和抖动示波器要求连续采样且不丢点用std::vector不断push_back会导致频繁扩容和拷贝。我一般用环形缓冲区源码里如果能找到CArray或自定义数组也是同理。#define BUFFER_SIZE 4096 // 总采样点2 的幂便于取模 class CControllerBuffer { public: CControllerBuffer() : head_(0), tail_(0) {} bool Push(float value) { unsigned int next (head_ 1) (BUFFER_SIZE - 1); if (next tail_) return false; // 缓冲满丢弃或覆盖 data_[head_] value; head_ next; return true; } bool Pop(float out) { if (head_ tail_) return false; // 空缓冲 out data_[tail_]; tail_ (tail_ 1) (BUFFER_SIZE - 1); return true; } private: float data_[BUFFER_SIZE]; volatile unsigned int head_; // 写索引 volatile unsigned int tail_; // 读索引 };要点在于head_和tail_都对齐到BUFFER_SIZE - 1做位与运算比%原指令快一个数量级。生产环境里这两个索引要加内存屏障VC6 下可用InterlockedExchange一类的函数保证多线程可见性。缓冲区大小的选取直接决定能显示的波形时长采样率 1 MHz、缓冲 4096 点则单屏波形只有 4.096 ms。如果源码里默认缓冲偏小跑低频信号会看到波形被截断这时不要急着改绘图先加大BUFFER_SIZE。2.3 采样率和时基的换算关系示波器面板上有“时基”旋钮单位是 s/div。横轴分成 10 格总显示时长 时基 × 10。采样率必须满足奈奎斯特条件否则频谱混叠会出现假频率。源码里如果写了m_dSampleRate和m_dTimeBase需要注意它们之间是反比关系// 设屏幕横轴分 10 格每格 100 像素 double timePerDiv 0.001; // 1 ms/div double totalTime timePerDiv * 10; // 10 ms 屏幕总时长 double sampleRate 1000000; // 1 MSa/s int pointsPerScreen (int)(totalTime * sampleRate); // 10000 点 // 如果缓冲区只有 4096就得降低采样率或提高时基 if (pointsPerScreen BUFFER_SIZE) { sampleRate (totalTime * BUFFER_SIZE) / totalTime; // 小心这里逻辑要按需求修正 }这段代码是我随手写的示意真实计算要加溢出保护。你从源码里看到timebase的调整函数时注意它有没有同步更新采样率。很多虚拟示波器录波时出现“显示波形变慢”就是因为两个参数不同步导致同一个缓冲里重复画了几屏。提示修改采样率时一定要连带清空缓冲区。否则老的采样点和新的采样点混在一起波形会像断线一样乱跳。3. 波形显示与双缓冲绘图MFC 界面不闪的根基3.1 GDI 绘制波形的传统方式MFC 的CClientDC可以直接在窗口上画线但如果每帧都擦除背景再重画屏幕会剧烈闪烁。虚拟示波器这种需要实时刷新 30 fps 以上的界面必须用内存 DC 先画好整幅波形再一次性BitBlt到窗口。源码里如果看到CreateCompatibleDC和CreateCompatibleBitmap多半就是这么做的。void CScopeView::DrawWaveform(CDC *pDC, const float *data, int pointCount, COLORREF color) { CDC memDC; CBitmap memBitmap; CRect rect; GetClientRect(rect); memDC.CreateCompatibleDC(pDC); memBitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); CBitmap *pOldBitmap memDC.SelectObject(memBitmap); // 用背景色填充避免上一帧残留 memDC.FillSolidRect(rect, RGB(0, 0, 0)); memDC.SetViewportOrg(0, rect.Height() / 2); // 将原点移到中间Y 轴向上为正 CPen pen(PS_SOLID, 1, color); CPen *pOldPen memDC.SelectObject(pen); // 坐标换算横轴均匀映射纵轴按垂直灵敏度 V/div 缩放 int nPos 0; int xPrev 0, yPrev 0; for (int i 0; i pointCount; i) { int x (int)(i * rect.Width() / (pointCount - 1)); int y (int)(-data[i] / m_dVoltPerDiv * (rect.Height() / 10.0)); if (i 0) { memDC.MoveTo(xPrev, yPrev); memDC.LineTo(x, y); } xPrev x; yPrev y; } pDC-BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldPen); memDC.SelectObject(pOldBitmap); }这里m_dVoltPerDiv是垂直灵敏度每格代表的电压值。很多初学者直接把采样值映射到像素导致波形超出窗口或缩成一团。正确做法是y -data / voltPerDiv * pixelsPerDiv负数是因为屏幕坐标系 Y 轴向下。此外波形颜色可通过参数color传入这就解决了热搜里“虚拟示波器如何改变曲线颜色”的问题——只要把RGB()宏的值用CColorDialog弹出来让用户选即可。3.2 网格与坐标轴的绘制顺序信号波形前面先画网格网格必须是最底层。网格线间距最好固定为像素值例如每格 40 像素然后通过缩放系数适配窗口大小。源码里如果没有网格坐标系你补一个也不费力void DrawGrid(CDC *pDC, CRect rect, int pixelsPerDiv) { pDC-FillSolidRect(rect, RGB(10, 10, 10)); CPen pen(PS_SOLID, 1, RGB(60, 60, 60)); CPen *pOldPen pDC-SelectObject(pen); // 时间轴纵向线 for (int x rect.left; x rect.right; x pixelsPerDiv) { pDC-MoveTo(x, rect.top); pDC-LineTo(x, rect.bottom); } // 电压轴横向线 for (int y rect.top; y rect.bottom; y pixelsPerDiv) { pDC-MoveTo(rect.left, y); pDC-LineTo(rect.right, y); } pDC-SelectObject(pOldPen); }一个容易踩的坑窗口 resize 时如果布局是固定 10 格那么每格像素会变。此时要重算pixelsPerDiv clientWidth / 10并且保证横轴点数与缓冲长度一致。如果窗口拉宽后波形拉伸变形就是因为没有按比例重新采样——这时要么增大采样点数要么把波形中间段插值不要简单地把点按新宽度铺开。3.3 颜色与主题满足 “改曲线颜色” 的需求用户问的“虚拟示波器如何改变曲线颜色”本质是绘图函数里的COLORREF参数从哪来。最简单的做法是在设置对话框里加一个颜色选择按钮保存一个m_ColorCurve成员变量每次重绘时读取。如果源码里写死了RGB(0,255,0)你只需要在OnLButtonDown里弹出颜色对话框void CScopeView::OnPickColor() { CColorDialog dlg(RGB(0, 255, 0), CC_FULLOPEN, this); if (dlg.DoModal() IDOK) { m_ColorCurve dlg.GetColor(); Invalidate(FALSE); // 触发重绘 } }注意Invalidate(FALSE)是只重绘不擦除背景如果配合双缓冲可以稍微提高效率。但下一帧最终还是要FillSolidRect清屏所以TRUE和FALSE区别不大。真正影响刷新率的是波形点数、格数以及是否调用了UpdateWindow。点超过一万时GDI 的MoveTo/LineTo会明显变慢可以考虑把波形数据先压缩成“每像素一列只画最大值和最小值”的峰值包络这也是数字示波器的真实设计。4. 触发与测量让波形稳定显示的关键4.1 触发的本质先找特征点再对齐绘制示波器如果不加触发波形会在屏幕上乱滚。触发的作用是——每当信号跨过某个电平阈值且方向符合要求时才开始绘制下一屏。源码里常见的是上升沿触发检测相邻两个采样点一个低于阈值、一个高于阈值。int FindTriggerIndex(const float *data, int count, float threshold, bool risingEdge) { for (int i 1; i count; i) { if (risingEdge) { if (data[i-1] threshold data[i] threshold) { // 找到上升沿触发点通常还要回退若干点以显示触发电平位置 return i - 1; } } else { if (data[i-1] threshold data[i] threshold) { return i - 1; } } } return -1; // 未找到等下一帧 }触发逻辑放在数据缓冲填充之后、绘图之前。绘制时从触发索引triggerIndex开始取后面pointsPerScreen个点。这样每一屏都从相同的相位开始波形看起来就静止了。如果信号频率远高于屏幕分辨率触发点会被跳过看起来像乱码——这是采样率不足的表现而不是触发逻辑的错。4.2 自动触发与普通触发源码里可能只做了“自动触发”找不到触发点也照样画波形等待下一次。真正的示波器还有“普通触发”和“单次触发”。普通触发是找不到就黑屏单次触发是抓到一次就停止采集。如果你在做逻辑分析仪单次触发非常有用。实现方式也很简单加一个m_nTriggerMode枚举enum TriggerMode { TRIGGER_AUTO 0, TRIGGER_NORMAL, TRIGGER_SINGLE };在刷新循环里根据模式决定要不要拒绝绘制当前帧。同时把“等待触发”状态显示在界面上免得用户以为程序死了。这个状态机的复杂度不高但能看出一个虚拟示波器能不能进入工程实用。4.3 频率、周期和幅度的测量代码测量功能是示波器的刚需。频率测量的朴素做法是统计相邻上升沿之间的采样点数再用采样率换算时间间隔double MeasureFrequency(const float *data, int count, float sampleRate) { int firstRise -1, secondRise -1; for (int i 1; i count; i) { if (data[i-1] 0.0f data[i] 0.0f) { // 零电平上升沿简单阈值 if (firstRise -1) { firstRise i; } else { secondRise i; break; } } } if (firstRise ! -1 secondRise ! -1) { double period (double)(secondRise - firstRise) / sampleRate; return 1.0 / period; } return 0.0; }实际工程里不能用零电平做固定阈值因为信号可能带直流偏移。标准做法是取整个缓冲数据的平均电压作为中间电平然后把阈值的滞回区间设为平均电平上下 2%5%避免噪声造成的毛刺触发。测量幅度则需要计算数据的最大值和最小值void MeasureAmplitude(const float *data, int count, float vmax, float vmin) { vmax vmin data[0]; for (int i 1; i count; i) { if (data[i] vmax) vmax data[i]; if (data[i] vmin) vmin data[i]; } }如果你要求有效值或频率更准可以对整段数据做 FFT。MFC 环境里可以直接用 FFTW 或自实现 1024 点基-2 FFT开源库常见但 VC6 下配置稍麻烦。源码里如果没集成 FFT建议先用过零检测法够用。注意测量结果显示的是十六进制浮点值要在界面用CString::Format(%.2f, value)格式化。否则 0.000001 这种值会显示成科学计数法用户看着别扭。4.4 参数调整垂直灵敏度、水平时基和偏移示波器控制参数包括 V/div垂直灵敏度、s/div水平时基、垂直偏移和触发电平。源码里一般用滚动条或编辑框实现。一个常见的失误是改了m_dVoltPerDiv但绘图时仍用固定缩放因子。正确做法是所有绘图函数都用成员变量换算像素int y (int)(-(data[i] - m_dVerticalOffset) / m_dVoltPerDiv * pixelsPerDiv centerY);垂直线性偏移量m_dVerticalOffset可以让信号上下平移等价于硬件示波器旋钮“挡位”对应的直流电平。耦合方式如果加了 AC/DC 切换AC 耦合要在采样后先减去平均值。5. 源码编译与功能扩展VC6 工程在 Win10 上的生存指南5.1 Visual C 6.0 的兼容性问题这份源码用 VC6 创建的.dsp/.dsw工程文件在 Win10 上直接用 Visual Studio 2022 打开会报错。最简单的方案是安装 Windows XP 模式的 VC6或者把源码文件.cpp/.h导入到 VS2010 以上版本重新建工程。常见的坑是#include afxwin.h顺序问题以及WCHAR和char*混用。热搜里总有“VC常见 ANSI 和 Unicode 函数”这个问题本质是 VC6 默认 ANSI 字符集而现代系统默认 Unicode。源码里如果有CString的GetBuffer()再传给 Windows API必须检查是TCHAR版本还是A/W版本。// 兼容写法使用 TCHAR 宏 CString strInfo; strInfo.Format(_T(采样率: %d Sa/s), sampleRate); SetWindowText(strInfo.GetBuffer()); strInfo.ReleaseBuffer();这段代码同时能在 ANSI 和 Unicode 编译选项下工作因为_T和CString::Format会根据工程字符集自动选择。要是源码里直接写波形这种窄字符串在 Unicode 环境下会编译错误要么把所有字符数组改为_T()要么干脆把所有代码统一成 UTF-8 编码并在工程属性里定义/utf-8。VS2022 里这一步很简单但 VC6 里改字符集极容易崩建议直接换 IDE 迁移。5.2 快速提取可复用模块我拆这份源码时最常用的方式是把CScopeCore.cpp里所有不依赖 MFC 的类抽出来封装成独立的“波形处理库”再给 C# 或 Qt 调用。这样做的好处是采集、触发、测量逻辑全在纯 C 层界面换掉不影响算法。具体操作删除所有CString、CWnd等 MFC 依赖换用std::string和标准库。把窗口相关的成员变量改成回调函数或事件接口。保留FindTriggerIndex、MeasureFrequency、MeasureAmplitude这类纯函数。用.def文件或__declspec(dllexport)导出这几个接口。// 纯 C 接口方便其他语言绑定 extern C __declspec(dllexport) double __stdcall MeasureFreqFromBuffer( const float *data, int count, float sampleRate) { return MeasureFrequency(data, count, sampleRate); }做完之后你可以在 C# 的 WinForms 里用stringBuilder绘制波形也可以用 Qt 的QPainter直接复用采样数据。这份源码的价值就从“一个 VC6 示波器 Demo”提升为“一套跨语言的测量算法组件”。5.3 性能验证30000 点波形的刷新率测试如果你想确认自己的双缓冲优化是否有效可以写一个简单的性能计数。在OnTimer里重绘并统计耗时DWORD start GetTickCount(); DrawWaveform(...); DWORD elapsed GetTickCount() - start; CString msg; msg.Format(_T(刷新耗时: %d ms), elapsed); SetWindowText(msg);我测试的基线是10000 点波形用 GDI 双缓冲在 i5 六代机型上耗时约 15~25 ms对应 40~60 fps。如果耗时超过 50 ms优先检查是否每帧都CreateCompatibleBitmap——这是最常见的性能杀手。把这行代码提到初始化里只在窗口尺寸变化时重新创建位图性能能翻三倍以上。另外BitBlt前可以只拷贝波形区域CRgn把无效区域排除掉对大窗口很管用。最后再提醒一个细节VC6 的编译器对for (int i 0; ...)的变量作用域处理跟标准 C 不同代码如果从旧工程迁到新编译器会报“i 未声明”。批量改for循环的变量定义就能解决这份源码里的Vsbq工程几乎都是这种写法迁移时先全局搜索for (int i再逐个改别偷懒。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →