简介面向自动化测试工程师与PyQt5开发者这份资源提供了一套基于PythonPyQt5控制泰克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泰克示波器和电脑通信底层依赖的是VISAVirtual 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.1064位版本建议别用太老的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可以直接跑脚本。如果你更习惯PyCharmCommunity版完全够用新建项目时选择刚才创建的虚拟环境解释器即可。2.2 PyQt5及关键依赖库安装确认环境没问题后直接用pip安装依赖。我项目里的依赖清单大致是这样的pip install pyqt5 pyvisa pyvisa-py numpy matplotlib pyqtgraph openpyxl解释一下这几个库的用途pyqt5跨平台桌面应用框架提供了创建GUI所需的一切控件。pyvisaVISA库的Python封装用于和测量仪器通信。pyvisa-py一个纯Python实现的VISA后端。如果装不上厂商的NI-VISA这个后端配合USBTMC也够用。numpy处理波形数据数组必需尤其在做FFT、统计计算时能省大量代码。matplotlib和pyqtgraph都能用来画波形。matplotlib大家比较熟pyqtgraph则是专门为高性能实时绘图设计的刷新速度明显更快实际测试时我推荐用pyqtgraph。openpyxl生成Excel格式的测试报告产线或者研发存档时比较通用。2.3 泰克VISA驱动安装与验证在Windows下我建议安装NI-VISANI公司的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-VISApyvisa-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 PK2PKMEASUrement:IMMed:VALue?读取当前测量值返回测量数值CH1:SCAle设置通道垂直刻度CH1:SCAle 1.0表示1V/格HORIZONTAL:MAIn:SCAle设置水平时基1E-3表示1ms/格DATA:SOUrce设置波形数据来源通道DATA:SOUrce CH2CURVe?读取波形数据返回二进制数据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, channelCH1): # 设置数据源和传输格式 self.instr.write(fDATA: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?, datatypeb, containernp.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提供的一个很方便的方法不需要自己解析二进制块头部直接返回数值数组。datatypeb对应单字节有符号整数也就是WFMOUTpre里设置的BYT_Nr1。电压值换算公式是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, timeout10, interval0.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__(peny) self.showGrid(xTrue, yTrue, alpha0.6) self.setLabel(bottom, Time, unitss) self.setLabel(left, Voltage, unitsV) self.curve self.plot([], [], penc) 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(fHORIZONTAL:MAIn:SCAle {case[tscale]}) self.scope.instr.write(fMEASUrement: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程序需要确保虚拟环境被正确选中。按下CtrlShiftP执行“Python: Select Interpreter”选择你创建虚拟环境里的python.exe。如果运行时报ModuleNotFoundError: No module named PyQt5多半是解释器没选对或者pip安装到了另一个Python版本下。另外PyQt5程序在VSCode调试时尽量避免在程序退出时用sys.exit()因为调试器有时会拦截退出信号导致闪退换成直接self.close()更保险。写在最后做这套系统最大的收获不是省了多少手动操作的时间而是让测试过程变得可重复、可追溯。以前调参数是“凭手感”现在每个数字都有记录波形图、测量值、判定结论全部留档出了问题能快速回溯是哪一次测试引入的偏差。回头看这个项目从头到尾没有任何一个环节是高不可攀的技术难点反而在细节量程的换算、触发的时机、线程的管理这些都要动手调试才有体会。如果你也想做类似的仪器自动化工具我的建议是先不要急着写界面先把示波器通信这条路跑通用命令行读取一次波形打印出来看看数据长什么样。这一步通了再往上加界面、加批量逻辑每一步都有反馈做起来会顺畅很多。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站