简介:面向自动化测试工程师与PyQt5开发者,这份资源提供了一套基于Python+PyQt5控制泰克MDO3014示波器的自动化测试界面方案,适用于仪器控制与界面开发结合时常见的数据显示、日志记录等场景。软件支持示波器图像实时显示与保存、日志信息实时读取与界面显示,并可保存窗口日志;支持单次循环执行自编测试用例,同时详细演示了常用Qt控件在Python中的调用方法,还附带了界面运行演示图片、日志样例与文本框输出示例,便于对照调试与二次开发。压缩包共16个文件,以Python源码、UI设计文件、XML配置及PNG图片素材为主,整体139KB,结构精简,适合快速拆解学习上位机开发与仪器通信技巧。已有3653人学习下载,适合具备一定Python基础、希望掌握PyQt5界面编程并扩展仪器自动化控制能力的开发者。 做硬件测试的朋友应该都有过这种经历:新产品调试阶段,一个人守着一台示波器,左手调旋钮,右手记数据,一个波形一个波形地量,一次测试下来几十上百个数值,记完还要手抄进Excel。这套流程不但累,而且一旦环境有干扰或者手误,前面记录的数据全得推倒重来。后来我用Python搭配PyQt5,把泰克示波器接上电脑,写了一套自动化测试工具,把整个流程从“手动测量+手工记录”变成了“一键执行+自动出报告”。
这篇文章就把这套方案的完整实现思路和关键代码拆开来讲。不管你是做硬件研发的测试工程师,还是刚接触仪器自动化的学生,只要会一点Python基础,就能照着搭一套属于自己的示波器自动化测试系统。
1. 项目整体思路与工具选型
1.1 为什么选Python和PyQt5,而不是其他组合
做仪器控制,业界常见的方案有LabVIEW、C#、MATLAB,还有Python。我最终选了Python + PyQt5,不是因为它最“高级”,而是因为它最适合快速迭代。
先说LabVIEW,很多电子实验室都在用,图形化编程上手确实直观,但有个现实问题:许可证费用不低,而且程序大了以后,图形化连线的维护成本反而比写代码更高。C#搭配VISA库也能做,Windows下表现稳定,但如果你要顺便做数据拟合、波形分析、批量数据处理,C#的生态和易用性跟Python比起来差了一截。MATLAB做信号分析很强,但做成一个给产线或者同事用的图形工具,部署起来太麻烦。
Python就简单多了。pyvisa库封装了VISA通信的细节,scipy和numpy做信号处理是标配,PyQt5提供了成熟的桌面控件。我实测下来,从零开始写到第一版能用,大概只花了一个周末的时间。PyQt5在仪器控制领域用得非常多,网上参考案例也多,踩坑时能找到答案的概率很高。
1.2 泰克示波器的通信基础:VISA和SCPI
泰克示波器和电脑通信,底层依赖的是VISA(Virtual Instrument Software Architecture)标准。你可以把VISA理解成一个通用的“翻译层”,不管仪器是USB口、GPIB口还是网口,应用层都用同一套API去读写,区别只是资源地址不同。VISA之上,示波器使用SCPI命令(Standard Commands for Programmable Instruments)来响应控制,这是一种纯文本的命令语言。
举个例子,你向示波器发送一行字符串*IDN?,示波器就会返回自己的厂商、型号和序列号信息。再比如你想读通道1的电压平均值,发送MEASUrement:IMMed:VALue?,返回的就是测量结果。这些命令都是ASCII文本,调试起来非常直观。Python通过pyvisa库把这些命令发出去,再把结果解析成数据,整个过程和后端接口调用的逻辑本质上是一回事。
1.3 系统架构分层
整套系统我按三层来设计:
- 界面层(PyQt5):负责参数输入、按钮交互、波形显示、测试结果表格展示。
- 控制层(pyvisa + SCPI):负责与示波器通信,发命令、读数据、捕获异常。
- 数据层(numpy + matplotlib/pyqtgraph + openpyxl):负责解析波形数据、计算指标、生成测试报告。
这样分层的最大好处是,以后如果换用别的品牌的示波器,只要控制层的命令做少量修改,界面和数据层基本不用动。如果要把这套系统接到其他仪器,比如电源、万用表,也是同样的架构,增加一个设备类就行。
2. 开发环境搭建与依赖安装
2.1 Python环境准备
我用的是Python 3.10,64位版本,建议别用太老的3.6以下版本,PyQt6有的库对旧版支持不好,而且新版本处理中文和编码问题更省心。在Windows下安装时,记得勾选“Add Python to PATH”,否则后续pip命令会提示找不到。
装好Python之后,建议创建一个虚拟环境,避免把系统环境搞乱。我习惯用venv,命令很简单:
python -m venv scope_env scope_env\Scripts\activate # Windows # source scope_env/bin/activate # Linux/macOS顺便提一下编辑器。用VS Code加Python插件就够用,配置好Python解释器路径,F5可以直接跑脚本。如果你更习惯PyCharm,Community版完全够用,新建项目时选择刚才创建的虚拟环境解释器即可。
2.2 PyQt5及关键依赖库安装
确认环境没问题后,直接用pip安装依赖。我项目里的依赖清单大致是这样的:
pip install pyqt5 pyvisa pyvisa-py numpy matplotlib pyqtgraph openpyxl解释一下这几个库的用途:
- pyqt5:跨平台桌面应用框架,提供了创建GUI所需的一切控件。
- pyvisa:VISA库的Python封装,用于和测量仪器通信。
- pyvisa-py:一个纯Python实现的VISA后端。如果装不上厂商的NI-VISA,这个后端配合USBTMC也够用。
- numpy:处理波形数据数组必需,尤其在做FFT、统计计算时能省大量代码。
- matplotlib和pyqtgraph:都能用来画波形。matplotlib大家比较熟,pyqtgraph则是专门为高性能实时绘图设计的,刷新速度明显更快,实际测试时我推荐用pyqtgraph。
- openpyxl:生成Excel格式的测试报告,产线或者研发存档时比较通用。
2.3 泰克VISA驱动安装与验证
在Windows下,我建议安装NI-VISA(NI公司的VISA驱动,对包括泰克在内的主流仪器都支持)。装好之后,用USB线把示波器和电脑连接起来。泰克示波器上可能需要按一下“Utility”菜单,把USB设备模式设置成“Computer”或者“Remote”,不同型号菜单项略有差异。
然后打开Python继续验证:
import pyvisa rm = pyvisa.ResourceManager() print(rm.list_resources())如果输出类似('USB0::0x0699::0x0368::C020000::INSTR',)这样的设备串,说明电脑已经识别到示波器了。注意,USB接口的示波器通常不支持热插拔后立刻自动识别,如果列不出来,试着换个USB口,或者把示波器重新上电。
如果没有安装NI-VISA,也可以完全依赖pyvisa-py。但根据我的实测,原生NI-VISA的稳定性更好,尤其在长指令交互和批量数据读取时,坑更少。所以生产环境里我更推荐先装NI-VISA,pyvisa-py作为兜底方案。
3. 示波器通信与控制模块实现
3.1 建立连接与设备识别
设备连接这一步是整个系统的基础。我的做法是封装一个ScopeDevice类,把所有和示波器相关的操作放在里面,界面层不直接调用VISA命令。这样代码清晰,以后换仪器也方便。
import pyvisa import numpy as np import struct class ScopeDevice: def __init__(self, resource_string): self.rm = pyvisa.ResourceManager() self.instr = self.rm.open_resource(resource_string) self.instr.timeout = 5000 self.instr.encoding = 'latin1' # SCPI 返回数据默认 latin1 def get_id(self): return self.instr.query('*IDN?').strip() def reset(self): self.instr.write('*RST') def close(self): self.instr.close()建立连接后,我习惯先在界面上显示*IDN?的返回结果,例如“TEKTRONIX,MSO5204B,C020000,CF:91.1CT FV:2.1.2”,这能快速确认通信链路正常。如果你的示波器返回值中文乱码,通常是编码问题,把encoding改成'latin1'或者'ascii'试试。
关于timeout参数,我做了一个小调整:普通命令设置5秒,读取波形数据时单独设置10秒以上,因为大点数的波形传输需要更长时间。单独设置的方法是self.instr.timeout = 10000,读完后可以再改回来。
3.2 常用SCPI命令整理
泰克示波器支持的命令非常全,直接把完整的命令手册拿来看会眼花缭乱,我整理了一份常用命令速查表,基本能覆盖90%的自动化测试场景:
| 命令 | 功能 | 示例 |
|---|---|---|
*IDN? | 查询设备信息 | 返回厂商、型号、序列号 |
*RST | 恢复出厂设置 | 测试开始时复位状态 |
ACQuire:STATE? | 查询采集是否完成 | 返回 1 表示采集完成 |
ACQuire:STOPAfter | 设置单次采集模式 | RUNSTop表示单次触发后停止 |
MEASUrement:IMMed:TYPe | 选择测量类型 | MEASUrement:IMMed:TYPe PK2PK |
MEASUrement:IMMed:VALue? | 读取当前测量值 | 返回测量数值 |
CH1:SCAle | 设置通道垂直刻度 | CH1:SCAle 1.0表示1V/格 |
HORIZONTAL:MAIn:SCAle | 设置水平时基 | 1E-3表示1ms/格 |
DATA:SOUrce | 设置波形数据来源通道 | DATA:SOUrce CH2 |
CURVe? | 读取波形数据 | 返回二进制数据 |
WFMOUTpre:BYT_Nr | 设置波形字节数 | 1表示每个采样点1字节 |
WFMOUTpre:ENCdg | 设置波形编码格式 | BIN表示二进制 |
SAVE:IMAGe | 保存屏幕截图 | SAVE:IMAGe 'D:\\test.png' |
这些命令不一定全都要背,实际写代码时查也能查。但有几个细节需要特别注意:
- SCPI命令不区分大小写,但参数语义要正确。比如
MEASUrement:IMMed:TYPe和MEASUrement:IMMed:VALue?是一对,先设置测量类型,再读取数值。 - 设置量程时,如果值不合法,示波器会返回错误,但不影响程序运行,只是查询结果可能为0。所以最好在测试前固定量程,而不是依赖自动量程。
- 波形通道设置
DATA:SOUrce至少要跟CURVe?搭配使用,否则你读到的可能不是你预期通道的波形。
3.3 波形读取与坐标转换
读取波形数据是自动化测试中最常遇到问题的地方,很多人卡在这一步。泰克的波形数据通过CURVe?返回,以二进制格式传输。下面这段代码是完整可用的读取流程:
def read_waveform(self, channel='CH1'): # 设置数据源和传输格式 self.instr.write(f'DATA:SOUrce {channel}') self.instr.write('DATA:WIDth 1') self.instr.write('WFMOUTpre:BYT_Nr 1') self.instr.write('WFMOUTpre:ENCdg BIN') # 获取当前量程和偏移 vscale = float(self.instr.query(f'{channel}:SCAle?')) voffset = float(self.instr.query(f'{channel}:OFFSet?')) tscale = float(self.instr.query('HORIZONTAL:MAIn:SCAle?')) # 读取波形数据(二进制块) raw = self.instr.query_binary_values('CURVe?', datatype='b', container=np.array) # 坐标转换 v_per_division = float(self.instr.query('WFMOUTpre:YMUlt?')) v_offset = float(self.instr.query('WFMOUTpre:YOFf?')) voltage = (raw - v_offset) * v_per_division x_start = float(self.instr.query('WFMOUTpre:XZEro?')) x_increment = float(self.instr.query('WFMOUTpre:XINcr?')) time = x_start + np.arange(len(voltage)) * x_increment return time, voltage这里有几个关键点:
query_binary_values是pyvisa提供的一个很方便的方法,不需要自己解析二进制块头部,直接返回数值数组。datatype='b'对应单字节有符号整数,也就是WFMOUTpre里设置的BYT_Nr=1。- 电压值换算公式是
voltage = (raw - wfmoutpre.YOFf) * wfmoutpre.YMUlt,这两个参数分别表示垂直偏移和垂直缩放因子。直接用CH1:SCAle和CH1:OFFSet换算不太准确,因为示波器内部处理时可能还有探头衰减等因素,但用WFMOUTpre返回的系数是准的。 query_binary_values返回的数组是numpy数组,这一点在处理大量数据时效率很高。如果你直接调query('CURVe?')然后手动解析,等到波形点数上了10万级别,性能差距会非常明显。
3.4 触发同步与等待采集完成
自动化测试里经常遇到的问题之一是:你发了TRIGger FORC强制触发,然后立刻去读波形,这时示波器还没采集完,读到的数据是上一次的旧数据。解决方法是轮询ACQuire:STATE?:
def wait_acquisition_done(self, timeout=10, interval=0.05): import time start = time.time() while True: state = self.instr.query('ACQuire:STATE?').strip() if state == '0': return True if time.time() - start > timeout: raise TimeoutError('采集超时') time.sleep(interval)也可以配置ACQuire:STOPAfter RUNSTop,让示波器完成一次单次采集后自动停止,然后读取数据前先检查TRIGger:STATE?是否等于READY,这更接近硬件触发的工作模式。两种方式都行,我习惯轮询ACQuire:STATE?,简单直接,不容易踩状态机边界的坑。
4. PyQt5界面设计与交互逻辑
4.1 界面布局
PyQt5界面我分了四个主要区域,从上到下依次是:
- 连接控制区:设备资源字符串输入框、连接/断开按钮,还可以放一个“读取设备ID”按钮,用来快速验证通信。
- 参数配置区:通道选择下拉框、垂直刻度下拉框、水平时基下拉框、测量项下拉框(如峰峰值、频率、平均值)、测试次数输入框。
- 波形显示区:用pyqtgraph的PlotWidget控件展示波形,同时显示测量结果的关键数值。
- 测试执行区:开始测试按钮、停止按钮、进度条、结果表格(QTableWidget)、导出报告按钮。
界面布局我用的是QMainWindow + QWidget + QVBoxLayout/HBoxLayout组合。实际经验是:不要把所有控件堆在一个窗口里,适当用QGroupBox对功能分组,界面会清晰很多,也方便后续扩展。
4.2 波形显示方案对比
波形显示是PyQt5示波器工具里最有意思的部分。我一开始用matplotlib嵌入,代码写起来很顺手,因为matplotlib大家都在用,但实际测试后发现一个问题:每秒刷新一次时,matplotlib的CPU占用率比较高,波形一多会卡。后来换成pyqtgraph,差异是肉眼可见的,刷新更顺滑,代码也不复杂。
pyqtgraph显示波形的基础代码:
import pyqtgraph as pg class WaveWidget(pg.PlotWidget): def __init__(self): super().__init__(pen='y') self.showGrid(x=True, y=True, alpha=0.6) self.setLabel('bottom', 'Time', units='s') self.setLabel('left', 'Voltage', units='V') self.curve = self.plot([], [], pen='c') def update_waveform(self, time, voltage): self.curve.setData(time, voltage)用setData更新波形而不是重复创建曲线,这是性能优化的关键。如果你用matplotlib,每次plot都会创建新的对象,旧对象不清理就会越来越卡。
4.3 通信线程与界面刷新分离
写PyQt5仪器控制程序,最典型的错误是在主线程里直接调用read_waveform()。一旦示波器响应慢,整个界面就会卡死,看起来像程序崩溃了。正确的做法是把通信操作放到QThread里。
我封装了一个TestWorker(QThread)类,run()方法里执行测试循环,通过信号把数据传回主线程:
from PyQt5.QtCore import QThread, pyqtSignal import time class TestWorker(QThread): update_wave = pyqtSignal(object, object) # time, voltage update_status = pyqtSignal(str) update_progress = pyqtSignal(int) test_result = pyqtSignal(dict) def __init__(self, scope, params): super().__init__() self.scope = scope self.params = params self._running = True def stop(self): self._running = False def run(self): for i in range(self.params['iterations']): if not self._running: break # 等待采集完成 self.scope.wait_acquisition_done() # 读取波形 time, volt = self.scope.read_waveform(self.params['channel']) # 计算测量值 self.update_wave.emit(time, volt) # 把测量值放到结果里,稍后统一处理 self.update_progress.emit(i + 1) time.sleep(0.1)这里注意信号类型:pyqtSignal(object, object)用了object类型,因为numpy数组不能直接映射为基本Python类型。如果你信号连接的是主界面的槽函数,槽函数直接在UI线程里更新界面,这是最安全的模式。
热词里有一条“pyqt5 文本框超链接点击后执行自定义操作”,这其实也是信号槽的拓展。PyQt5的QTextBrowser或者QLabel可以用setOpenExternalLinks控制链接打开方式,用anchorClicked信号自定义跳转逻辑,和我们的线程信号机制是同一个思路。
有一点要提醒:QThread停止不能靠强制terminate,那样很容易让VISA连接处于未知状态。正确做法是像上面代码一样,设置一个_running标志,循环里判断,然后让线程自然退出。
4.4 下拉框闪退的避坑经验
热词里看到很多人搜“pyqt5 下拉框闪退”,我也踩过这个坑。现象是程序运行到某个操作,比如切换下拉框选项或更新下拉框内容时,程序直接崩溃退出。
这个问题最常见的原因有两个:
第一,QComboBox的当前项引用失效。例如你在QComboBox的currentIndexChanged信号槽里执行了clear()操作,之后再访问currentText(),此时索引已经变成-1,继续用会触发运行时错误。解决方法是在清空之前先断开信号,或者用blockSignals(True)临时屏蔽:
combo.blockSignals(True) combo.clear() combo.addItems(new_items) combo.blockSignals(False)第二,下拉框里存了对象指针或者相关数据,而对应对象已经被销毁。比如数据更新太快,旧的数据模型被删除了,但下拉框还持有引用。解决方法是把数据拷贝而不是存引用,或者设置QComboBox用setItemData存值而不是存对象。
我自己养成一个习惯:凡是涉及下拉框动态更新的地方,更新前先blockSignals(True),更新完再恢复。这个习惯帮我避开了一大批闪退问题。
5. 自动化测试流程设计与报告生成
5.1 测试用例组织与批量执行
自动化测试的价值在于批量跑。我在界面层设计了一个参数表,通过QTableWidget让用户输入多组测试条件,每一行就是一条测试用例,包含通道、垂直刻度、时基、测量项、上下限、迭代次数。点“开始测试”后,程序逐条执行这些用例。
测试执行的核心逻辑如下:
def run_test_case(self, case): # 配置示波器 self.scope.instr.write(f'{case["channel"]}:SCAle {case["vscale"]}') self.scope.instr.write(f'HORIZONTAL:MAIn:SCAle {case["tscale"]}') self.scope.instr.write(f'MEASUrement:IMMed:TYPe {case["measure"]}') # 读取测量值 value = float(self.scope.instr.query('MEASUrement:IMMed:VALue?')) # 判断上下限 is_pass = case['min'] <= value <= case['max'] return {'case': case, 'value': value, 'pass': is_pass}执行多组测试还有个小技巧:每执行完一组测量,要用*OPC?(Operation Complete Query)同步一下。*OPC?会一直等待当前操作完成才返回1,这比估计sleep时间可靠得多。示波器在切换量程后,需要一点时间稳定,直接硬等容易偶发读不到正确值。
5.2 数据记录与报告生成
测试结果我统一收集到一个列表里,全部跑完后用openpyxl生成Excel报告。报告内容包括测试时间、示波器型号、每项参数设置、实测值、判定结论,还顺便做一列“波形图路径”,把每轮的波形截图文件路径写进去。
Excel生成的简化代码:
from openpyxl import Workbook def export_report(results, filename): wb = Workbook() ws = wb.active ws.title = '测试报告' ws.append(['通道', '测量项', '实测值', '下限', '上限', '结论']) for r in results: ws.append([ r['case']['channel'], r['case']['measure'], r['value'], r['case']['min'], r['case']['max'], 'PASS' if r['pass'] else 'FAIL' ]) wb.save(filename)波形图保存则是调用泰克示波器自带的截图命令SAVE:IMAGe,截图文件可以直接存到电脑上,比在电脑上重新绘制波形图更接近示波器现场画面。当然,如果你的测试结果还要做二次分析,那就建议把CURVe?读回来的时间电压数组也存一份npy或者csv,方便事后画图。我一般两种都保留,截图用于报告,原始波形数据用于复盘。
5.3 测试过程中的异常处理
自动化测试跑在无人职守状态下,必须要考虑异常情况。比如示波器偶尔不响应,或者某条测量命令超时,如果程序直接崩溃,前面所有数据就白测了。
我的策略是:在测试Worker的循环里,对每条用例做try-except,超时或者通信异常时记录错误信息,把这条用例标记为“异常”,继续执行下一条。测试跑完以后,再单独查看异常用例,决定是否重新测试。
try: value = float(self.scope.instr.query('MEASUrement:IMMed:VALue?')) except pyvisa.VisaIOError as e: self.update_status.emit(f'测量失败: {e}') self.scope.close() self.scope = reconnect_scope() # 重新连接示波器 continue重新连接示波器是一个比较实用的兜底策略。VISA连接在长时间运行后偶尔会断开,重连一次往往就能恢复正常。这也解释了为什么设备类要单独封装,重连只影响控制层,界面不用变。
6. 常见问题与排查技巧实录
6.1 通信类问题:设备找不到或返回超时
问题现象:list_resources()输出为空,或者打开资源成功但第一条查询命令就超时。
排查步骤:
- 确认示波器USB驱动已正确安装。在Windows的设备管理器里查看“通用串行总线设备”和“VISA设备”有没有感叹号。
- 换个USB口试试,有些示波器前面板和后面板的USB口功能不同,有的只是优盘口,不能做控制。
- 用厂商自带的测试软件(比如Tektronix OpenChoice或TekVISA)手动连接一次,看能否连上。如果厂商软件也连不上,说明示波器端设置或者线缆有问题。
- 在代码里设置timeout稍微大一点,比如3000毫秒。有些示波器上电后第一条命令响应比较慢。
6.2 波形数据解析不对,电压值偏差大
这个坑我遇到过不止一次。现象是读取时间轴正确,但电压数值明显偏大或偏小。
原因基本都在坐标转换参数这里。用WFMOUTpre:YMUlt和WFMOUTpre:YOFf换算,是针对所选数据通道的。如果你改了DATA:SOUrce,但忘了重新查询WFMOUTpre,那么读出来的参数还是上一个通道的,换算出来的电压当然不对。
每次读波形之前,先重新查询WFMOUTpre四个参数:XZEro、XINcr、YMUlt、YOFf。我把这个操作放在read_waveform里,每次调用都刷新,虽然多费几条SCPI命令的耗时,但换来的是数据准确,性价比很高。
6.3 PyQt5界面刷新卡顿
如果波形点数是10万级别,pyqtgraph虽然比matplotlib好,但高频刷新时仍然会有压力。优化思路:
- 降采样后再绘图。如果只是看波形形态,不需要每一点都画出来,可以用numpy的切片或者索引采样,例如
time[::10]。 - 把图形显示控件的最大点数限制一下,比如最多显示5万个点,超出部分均匀抽样。
- 不要在UI线程里做数据处理,把FFT、滤波这些都放到Worker线程里,UI只负责展示。
6.4 长时间运行的稳定性
批量测试如果跑几个小时,VISA连接偶尔会出问题。我的做法是:
- 每条用例之间加一个短sleep,比如100ms,让示波器内部状态稳定。
- 定期检查连接状态,每次读取失败后重新初始化。
- 在测试循环外层加一个整体超时保护,比如总测试时间超过预期1.5倍就自动停止,防止异常死循环。
6.5 VSCode环境配置补充
看到热搜词里有“vscode python环境配置”和“pyqt5安装教程”,这里顺带提一句调试环境的配置。在VSCode里跑PyQt5程序,需要确保虚拟环境被正确选中。按下Ctrl+Shift+P,执行“Python: Select Interpreter”,选择你创建虚拟环境里的python.exe。如果运行时报ModuleNotFoundError: No module named 'PyQt5',多半是解释器没选对,或者pip安装到了另一个Python版本下。
另外,PyQt5程序在VSCode调试时,尽量避免在程序退出时用sys.exit(),因为调试器有时会拦截退出信号导致闪退,换成直接self.close()更保险。
写在最后
做这套系统最大的收获,不是省了多少手动操作的时间,而是让测试过程变得可重复、可追溯。以前调参数是“凭手感”,现在每个数字都有记录,波形图、测量值、判定结论全部留档,出了问题能快速回溯是哪一次测试引入的偏差。回头看这个项目,从头到尾没有任何一个环节是高不可攀的技术,难点反而在细节:量程的换算、触发的时机、线程的管理,这些都要动手调试才有体会。
如果你也想做类似的仪器自动化工具,我的建议是:先不要急着写界面,先把示波器通信这条路跑通,用命令行读取一次波形,打印出来看看数据长什么样。这一步通了,再往上加界面、加批量逻辑,每一步都有反馈,做起来会顺畅很多。
本文还有配套的精品资源,点击获取