资讯详情

Python虚拟示波器实战:从声卡采集到串口接入的完整指南

📅 2026/9/10 7:49:57 | 华诺云谱 👁 阅读
Python虚拟示波器实战:从声卡采集到串口接入的完整指南
简介基于Python的虚拟示波器项目面向电子工程、科研与教育领域需要观察分析信号波形的人群借助matplotlib、numpy及PyQt等库实现信号采集、实时显示、滤波分析和多通道对比无需依赖实体示波器即可完成电路与信号实验。包内共12个文件压缩包仅20KB包含4个Python脚本主程序、波形绘制、数据处理等、PyQt界面文件、项目配置xml及pyc编译缓存结构精简。已有1155人学习下载。源码涵盖GUI控制、matplotlib波形绘制、numpy数据处理、自定义滤波、数据保存回放与多通道对比等核心逻辑可直接运行或二次扩展对想学习Python科学计算或希望用软件模拟示波器进行教学演示、课前验证的开发者具有实用参考价值。1. python虚拟示波器用笔记本替代硬件示波器靠什么在嵌入式开发和信号调试中示波器是发现问题最快的工具但实体示波器价格高、体积大存储深度还经常不够用。python虚拟示波器要做的事是把电脑自带的声卡、串口或 USB 设备当成前端采集硬件再用 PyQtGraph 或 Matplotlib 把信号实时画出来。你真正要自己实现的不只是画线还包括采样率切换、电压标定、触发和测量。下面内容从声卡采集讲到串口接入单片机最后落到触发和性能调优。适合想用笔记本替代示波器看波形、又不想买昂贵硬件的电子工程师和上位机开发者如果你刚接触 Python按章节顺序装包、跑代码就行。2. 声卡作为数据源python虚拟示波器的采集层与采样率选择虚拟示波器第一步是解决模拟信号的入口。不接外部硬件时你手里最便宜的高质量 ADC 就是笔记本声卡。声卡的采集前级自带抗混叠滤波器和可调增益比一块裸的 ADC 芯片容易驱动。这一章先把声卡采集链路跑通让后面的波形处理有稳定的数据来源。2.1 采样率与缓冲区大小的选择标准声卡常见的采样率是 44100 Hz 和 48000 Hz部分设备能到 192000 Hz。根据采样定理采样率的一半才是理论上能显示的最高频率所以声卡虚拟示波器的有效带宽通常在 20 kHz 以下。显示时还需要把数据切成固定大小的帧这就是 blocksize 的作用。采样率 (Hz)奈奎斯特频率 (kHz)blocksize1024 时单帧时长 (ms)4410022.0523.24800024.0021.319200096.005.3波形刷新率等于采样率除以 blocksize。48000 Hz 采样、1024 点一帧时每秒刷新约 47 帧肉眼已经感觉不到闪跳。如果你一直卡在“画面只有一条直线”先别怀疑绘图代码多半是设备枚举没对上采样率。还没装环境的话先从 python.org 拿 3.10 以上的官方安装包装的时候勾上 Add python.exe to PATH然后执行pip install sounddevice numpy scipy pyqtgraph pyserial这几个包覆盖了后面所有章节。2.2 用 sounddevice 打开输入流的最小代码sounddevice 是 PortAudio 的薄封装用它做采集比 pyaudio 少写一半代码。下面的脚本会打开默认输入设备把每一帧波形放进队列import sounddevice as sd import numpy as np from queue import Queue, Empty SAMPLERATE 48000 BLOCKSIZE 1024 CHANNELS 1 q Queue(maxsize8) def audio_callback(indata, frames, time_info, status): if status: print(声卡状态变化:, status) q.put(indata[:, 0].copy()) with sd.InputStream( samplerateSAMPLERATE, blocksizeBLOCKSIZE, channelsCHANNELS, dtypefloat32, callbackaudio_callback, ): while True: try: frame q.get(timeout2) except Empty: print(采集线程可能已经断开) continue print(帧长度, len(frame), 峰值, np.abs(frame).max())indata 的形状是 (blocksize, channels)所以indata[:, 0]取出单通道波形copy 是为了避免复用缓冲把旧数据覆盖。q 的 maxsize 设成 8是防止采集速度快于消费速度时内存无限膨胀。status 非空时通常意味着 underflow 或 overflow出现时先检查 blocksize 是否太小。2.3 回调、队列和 UI 线程的职责这段代码的关键不是“开一个音频流”而是把采集和显示完全解耦。PortAudio 的回调运行在实时线程里如果你在这里执行 matplotlib 绘图或文件写入声卡驱动会认为你处理不过来连续丢帧。采集线程唯一合法的动作是把数据放入线程安全的队列。UI 线程再以固定频率从队列取帧这样即使绘图卡顿 100 ms音频线程也不会被拖死。提示不要在 callback 里调用任何绘图 API。遇到线程崩溃先检查是不是把 plot.update 放进 callback。3. numpy 信号处理电压标定、去除直流与降噪滤波把声卡拿到的原始 float 数组直接画出来你会遇到三个问题波形带着稳定的直流偏移、噪声比万用表读数大、数值还停留在无量纲单位上。示波器与录音软件的区别就是显示前要对时间序列做一层校正。这一层用 numpy 写起来很短。3.1 去除直流分量避免波形整体抬高笔记本内置声卡的输入端是交流耦合但 ADC 本身会引入一个缓慢漂移的直流偏置。直接看原始数据波形中心会随着温度或增益变化上下移动。第一个处理就是按帧减均值def remove_dc(frame: np.ndarray) - np.ndarray: return frame - np.mean(frame)均值是整个数据块的直流分量减掉后波形围绕 0 V 摆动。低频信号尤其需要这步因为它的周期比一帧数据长时均值并不能代表真正的直流偏置波形会在帧边界出现跳变。更稳妥的做法是对连续数据流做一阶高通滤波保留 5 Hz 以下的成分用来跟踪偏置。滤波不是越强越好虚拟示波器追求的是“看到的波和真实信号一致”。一旦引入带内衰减相位就差出来了后面的触发判断也会偏移。这也是为什么很多开源示波器默认不做平滑只做直流移除。3.2 电压标定把无量纲数值变成伏特逐一对应FULL_SCALE_V 2.4 # 这个值由实测标定得到单位 Vpp volts processed * (FULL_SCALE_V / 2.0)声卡输入在 float32 下范围是 -1.0 到 1.0对应 ADC 满量程。如果满量程是 2.4 Vpp那么 -1.0 代表 -1.2 V1.0 代表 1.2 V换算系数就是 2.4 / 2.0。注意这里“满量程”不是声卡标称最大输入而是实际硬件接上信号后用万用表或标准信号源测出来的有效范围不同声卡差异很大。标定过程也很直接输入一个已知峰峰值的 1 kHz 正弦波把采集到的峰值记录下来用实际电压 / 采样峰值算出标定系数。把这个系数写进配置显示器上的 Y 轴就能标注为 V。3.3 降噪滤波移动平均与 Savitzky-Golay 的取舍方法适用条件参数建议简单移动平均白噪声为主波形边缘不急窗口 3~7 点Savitzky-Golay波形本身平缓但叠加噪声脉冲窗口 31阶数 3from scipy.signal import savgol_filter filtered savgol_filter( frame, window_length31, polyorder3, )window_length 必须是奇数polyorder 不能大于窗口长度。savgol_filter 对波形细节的保留比移动平均好但高频方波会被拉圆。如果你要观察的还是触发沿就别滤波先保证触发正确再谈降噪。4. PyQtGraph 绘制实时波形渲染与曲线颜色定制采集和处理都做完了接下来是让波形稳定滚动起来。Matplotlib 的 animation 在 50 FPS 下 CPU 占用很高窗口缩放还会卡顿。pyqtgraph 直接绘制 OpenGL 图形单曲线 6000 点轻松跑到 120 FPS。这一章的重点是搭建可复用的画布并解决“如何改变曲线颜色”这个高频问题。4.1 为什么选择 PyQtGraph不选 Matplotlib 动画Matplotlib 擅长出版级静态图但它的重绘机制是整体重绘波形移动时锯齿和残影明显。pyqtgraph 把数据放在场景图中调用setData只更新顶点缓冲响应速度完全不在一个量级。虚拟示波器要求的是持续滚动、低延迟、能稳定缩放这是 pyqtgraph 的主场。4.2 最小实时波形窗口下面的脚本默认你已经有了第 2 章定义的q和SAMPLERATE这里只做 UI 部分import sys import numpy as np import pyqtgraph as pg from PyQt5 import QtCore, QtWidgets from queue import Empty app QtWidgets.QApplication(sys.argv) win pg.GraphicsLayoutWidget(showTrue) plot win.addPlot(titleCH1) plot.showGrid(xTrue, yTrue, alpha0.3) curve plot.plot(penpg.mkPen(#FFD700, width2)) def poll_queue(): try: frame q.get_nowait() except Empty: return y frame[:2048] t np.arange(len(y)) / SAMPLERATE curve.setData(t, y) timer QtCore.QTimer() timer.timeout.connect(poll_queue) timer.start(30) app.exec_()poll_queue 由 Qt 的事件循环驱动每 30 ms 检查一次队列。取不到帧就直接返回不会阻塞 UI。setData是 pyqtgraph 增量更新的核心比重新创建 PlotDataItem 节省大量时间。把 y 截断到 2048 点是为了限制单帧绘制规模刷屏时 CPU 占用稳定在低位。4.3 如何改变曲线颜色QPen 颜色表与刷新逻辑颜色问题在示波器里很常见暗色背景下黄线最清楚但保存截图后黄线变成白色反而看不清。处理思路是把颜色集中管理用函数切换而不是到处硬编码。from pyqtgraph import mkPen COLOR_TABLE { yellow: #FFD700, cyan: #00FFFF, green: #00FF7F, red: #FF5500, } def set_curve_color(name: str): pen mkPen(COLOR_TABLE.get(name, #FFD700), width2) curve.setPen(pen) curve.setBrush(None)颜色常用场景黄色 #FFD700暗色背景主通道最醒目青色 #00FFFF第二通道线条细时辨识度高绿色 #00FF7F长时间观察波形时眼睛疲劳度低setBrush(None) 是为了不填充曲线下方的面积否则滚动时会残留彩色色块。颜色切换与波形缩放独立你甚至可以把它绑定到键盘快捷键方便调试时随时换色。4.4 双通道共用时间轴多通道时最怕两个通道时间轴对不上。pyqtgraph 的做法是让第二通道共享第一通道的 X 轴plot2 win.addPlot(row1, col0) plot2.setXLink(plot) curve2 plot2.plot(penmkPen(#00FFFF, width1))这样鼠标放大任何一路另一路的时间范围跟着同步移动。剩下的数据分发只需要在 poll_queue 里把帧拆成多个通道的数组分别送入 setData 即可。5. 串口数字示波器接入解析 Arduino/STM32 的采集帧不是所有信号都适合走声卡。传感器输出、电机控制、STM32 调试引脚这些都直接接在串口上。串口数字示波器最常见的工作方式是把单片机当成模拟前端。你搜索 serial_digital_scope v2 这类命名时看到的结构基本也分两层一个定时采集的固件一个解析串口的 Python 脚本。协议设计没有神秘之处难点在帧边界。5.1 单片机端的帧格式和协议选型用文本行传数据最简单比如每行发512,723\n但边界解析依赖换行符遇到高波特率时容易出现半个数据包。更稳的做法是定义二进制帧让接收方通过帧头和校验对齐。波特率文本行格式 9 字节二进制帧 8 字节115200约 1280 帧/秒1440 帧/秒921600约 10240 帧/秒11520 帧/秒二进制帧的效率和容错都更好。8 字节一帧在 115200 下等于每帧一次双通道采集对 1 kHz 内的信号足够稳定。需要更高采样率时把 STM32 接到 USB 虚拟串口波特率不再限制传输速度。一个通用帧格式建议帧头 2 字节0xAA 0x55通道 1uint16 little-endian通道 2uint16 little-endian校验通道1 与通道2 相加取低 8 位帧尾0x0D5.2 用 pySerial 读帧并校验import serial ser serial.Serial(COM16, baudrate115200, timeout0.1) def read_scope_frames(ser, max_len512): buf bytearray() while True: chunk ser.read(64) if not chunk: continue buf.extend(chunk) while len(buf) 8: if buf[0] ! 0xAA or buf[1] ! 0x55: buf.pop(0) continue ch1 int.from_bytes(buf[2:4], little) ch2 int.from_bytes(buf[4:6], little) checksum (buf[2] buf[3] buf[4] buf[5]) 0xFF if checksum ! buf[6] or buf[7] ! 0x0D: buf.pop(0) continue yield ch1, ch2 del buf[:8]buf.pop(0) 在这里是线性操作但单片机的发送频率通常不高可读性优先。int.from_bytes 的 little 参数必须和固件端一致否则两通道会互相交换。校验失败时丢掉一个字节而不是整帧是为了容错半包后的重同步。5.3 把串口数据接进第 4 章 UI串口线程和声卡线程本质相同数据进队列UI 定时取。处理完一帧后把两个通道的 numpy 数组传给两个 curvefor ch1, ch2 in read_scope_frames(ser): t np.arange(len(frame)) / SAMPLERATE curve.setData(t, ch1) curve2.setData(t, ch2)如果数据源是 MCU时间轴不再是声卡的 SAMPLERATE而是实际采样间隔。固件发回一个时间戳或按固定次数标记上位机用帧序号换算时间避免波形横向漂移。6. 触发、游标与数据导出虚拟示波器的三个实用技巧6.1 用 NumPy 实现边沿触发没有触发的虚拟示波器只是录音器波形会左右跳动。边沿触发的核心是找信号穿越阈值的点def edge_trigger(data, level0.0, moderising): sign np.sign(data - level) diff np.diff(sign) idx np.where(diff 0)[0] 1 if mode rising else np.where(diff 0)[0] 1 for i in idx: if i 64: return i return None idx edge_trigger(frame) if idx is not None: seg_start idx - 64 view frame[seg_start:seg_start 1024] if seg_start 0 else frame[:1024]level 是触发电平rising 对应上升沿diff 0 表示信号由下往上穿越。跳过前 64 点等于预触发区确保触发点在窗口左侧露出波形触发前的状态。6.2 游标与电压测量用pg.InfiniteLine加两条可拖动线vline pg.InfiniteLine(angle90, movableTrue, labelt{value:.3f}s) hline pg.InfiniteLine(angle0, movableTrue, labelU{value:.2f}V) plot.addItem(vline) plot.addItem(hline)拖动后 label 自动显示所在坐标值不需要额外写事件回调。测量频率时把两条竖线分别移到两个相邻周期的同相位点时间差取倒数就是频率。6.3 导出 CSV 验证测量结果np.savetxt( scope_export.csv, np.column_stack((t, view)), delimiter,, headertime_s,ch1, comments, )导出后用 Excel 或 pandas 打开检查第一列时间间隔是否等于 1/SAMPLERATE。如果导出的数据时间间隔不对问题一定出在采样的帧边界或滤波延迟回头查第 3 章的高通滤波参数。触发电平也建议先设到信号峰峰值的中间太小会让噪声触发抖动太大则可能错过真实沿这两种情况在文本界面上都能直接看到触发点索引的跳变。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。