简介:这是一套面向自动化测试初学者与仪器控制开发者的Python实战项目,围绕泰克MDO3014示波器实现上位机软件,解决波形实时显示、图像保存与日志读取等测试场景需求。资源包共16个文件,约139KB,包含2个py主程序与1个ui界面文件,另有5个xml配置、3张png界面截图、2个txt日志样例及pyc缓存等,结构紧凑,便于直接运行与二次开发。项目支持示波器界面实时刷新图片、实时打印并保存log窗口信息,可单次循环执行自编测试用例,同时详细演示常用QT控件在Python中的调用方式。目前已有3659人学习下载,适合想掌握PyQt5界面开发与仪器通信的读者参考,能从中获得可复用的界面框架、日志处理思路与示波器联调经验,快速搭建自己的自动化测试工具。1. 从手动按示波器到自动化测试:这套方案要解决什么如果你在实验室里做过硬件测试,大概率经历过这样的场景:一块板子上电后,你蹲在泰克示波器前面,手动调时基、调触发电平、截图、记数据,测一轮下来半小时没了,测十轮就是五个小时,中间还容易漏记某个通道的波形。更麻烦的是,测试报告要手动整理,截图要一张张命名归档,一旦发现异常波形,想回溯当时的参数配置只能靠记忆。这套基于 Python + PyQt5 + 泰克示波器的自动化测试方案,要解决的就是这个问题:把「配置仪器 → 采集波形 → 实时显示 → 记录数据 → 生成报告」整条链路串起来,用一个桌面端界面统一控制。它适合正在做硬件测试毕设的学生、需要搭建产线自动化测试工位的工程师,以及任何想把泰克 3000 系列示波器从「手动工具」变成「可编程测试节点」的人。核心关键词就三个:PyQt5 做界面、Tektronix3000 做数据源、实时刷新图片和打印信息做交互反馈。2. 需求拆解与技术选型:为什么是 PyQt5 加泰克 3000 系列2.1 测试场景的真实需求先把这个项目的需求说清楚,不然后面选型没有依据。一个典型的自动化测试场景通常包含这几个动作:第一,通过程控接口连接示波器,设置通道、时基、触发模式等参数;第二,启动采集,获取波形数据;第三,把波形画成图,实时显示在界面上;第四,把关键测量值(频率、峰峰值、上升时间等)打印到日志区域;第五,保存数据和截图,供后续生成报告。这五个动作里,前两个是仪器控制,第三个是 GUI 绘图,第四个是日志输出,第五个是文件 IO。手动操作时,这些动作是串行且依赖人的;自动化方案要做的,是把它们变成可配置、可循环、可无人值守的流程。这里有一个容易被忽略的点:实时性。很多同学一开始觉得「采集完再画图」就行,但实际测试中,你需要一边采集一边看波形变化,否则等一轮跑完才发现触发没设对,时间全浪费了。所以「实时刷新图片」和「实时刷新打印信息」不是锦上添花,而是这个方案能不能真正替代手动操作的关键。2.2 为什么选 PyQt5 而不是 Web 或命令行技术选型上,仪器控制层用 Python 是共识,因为 PyVISA 这个库把 VISA 通信封装得很干净,几行代码就能跟示波器对话。争议点在界面层:有人用 Web(Flask + ECharts),有人用命令行,有人用 PyQt5。我选 PyQt5 的理由很实际。第一,部署简单。Web 方案要起服务、开浏览器,实验室的测试电脑往往不联网,多一层就多一个故障点。PyQt5 打包成 exe 后双击就能跑,对不写代码的测试人员友好。第二,绘图性能。PyQt5 的 QGraphicsView 或者 pyqtgraph 在刷新率上比 Web 的 Canvas 重绘更可控,尤其是波形数据点上千的时候,Web 方案容易卡。第三,线程模型清晰。PyQt5 的 QThread 和信号槽机制,天然适合「采集线程 + UI 线程」分离,避免界面假死。命令行方案虽然轻,但没法直观展示波形,测试人员不买账。至于示波器选泰克 3000 系列,是因为这个系列在高校和中小型实验室里保有量高,支持 LAN、USB、GPIB 多种接口,程控指令集(基于 SCPI)文档齐全,PyVISA 能直接识别。你换成其他支持 SCPI 的示波器,代码结构基本不用大改,改一下资源地址和部分指令即可。2.3 整体架构:三层分离这套方案的架构我一般会分成三层。最底层是仪器通信层,负责跟示波器建立连接、发送 SCPI 指令、读取波形数据,用 PyVISA 实现。中间层是业务逻辑层,负责测试流程编排、数据解析、异常判断,比如「设置触发 → 等待触发 → 读取波形 → 计算参数 → 判断是否合格」。最上层是界面层,用 PyQt5 实现,包含波形显示区、日志打印区、参数配置区和控制按钮区。三层之间通过信号槽和队列通信,采集线程不直接操作 UI 控件,而是发信号让 UI 线程更新。这样做的好处是,即使采集过程中示波器响应慢,界面也不会卡死。下面这张表是三层各自的职责和关键技术点。层级职责关键技术仪器通信层连接示波器、发送 SCPI、读取波形PyVISA、SCPI 指令业务逻辑层流程编排、数据解析、合格判断Python 多线程、NumPy界面层波形显示、日志打印、参数配置PyQt5、pyqtgraph3. 仪器通信与数据采集:把示波器变成可编程节点3.1 连接示波器与基础 SCPI 指令连接示波器是第一步,也是最容易翻车的一步。泰克 3000 系列支持 LAN 和 USB 两种常用接口。LAN 方式下,你需要知道示波器的 IP 地址,然后在代码里用 TCPIP 资源地址连接。USB 方式下,资源地址是一串包含厂商 ID 和序列号的字符串,通常用 PyVISA 的列表功能自动发现。import pyvisa # 创建资源管理器 rm = pyvisa.ResourceManager() # 列出所有可用仪器,确认示波器被识别 resources = rm.list_resources() print("可用仪器:", resources) # 连接示波器,LAN 方式示例 # 实际使用时把 IP 换成你示波器的地址 scope = rm.open_resource("TCPIP0::192.168.1.100::inst0::INSTR") scope.timeout = 10000 # 超时设 10 秒,波形数据大时默认 2 秒不够 # 查询仪器标识,确认通信正常 print(scope.query("*IDN?"))这段代码里,list_resources()是排查连接问题的第一手段,如果列表里没有你的示波器,后面都不用谈。timeout参数建议设大一点,因为读取长波形数据时,默认超时经常不够,会报 VisaIOError。*IDN?是 SCPI 的通用查询指令,返回厂商、型号、序列号、固件版本,能返回就说明通信链路通了。3.2 配置采集参数与读取波形通信通了之后,要设置采集参数。泰克 3000 系列的 SCPI 指令集里,常用的有通道开关、垂直档位、时基、触发模式等。下面这段代码演示了配置通道 1、设置时基、单次触发采集、读取波形数据的完整流程。# 配置通道 1:打开、直流耦合、垂直档位 1V/格 scope.write("SELECT:CH1 ON") scope.write("CH1:COUPLING DC") scope.write("CH1:SCALE 1") # 设置时基为 1ms/格,共 10 格 scope.write("HORIZONTAL:SCALE 0.001") # 设置触发:边沿触发,通道 1,上升沿,电平 0.5V scope.write("TRIGGER:A:TYPE EDGE") scope.write("TRIGGER:A:EDGE:SOURCE CH1") scope.write("TRIGGER:A:EDGE:SLOPE RISE") scope.write("TRIGGER:A:LEVEL 0.5") # 单次采集 scope.write("ACQUIRE:STATE ON") scope.write("ACQUIRE:STOPAFTER SEQUENCE") # 等待采集完成,查询状态 import time while True: state = scope.query("ACQUIRE:STATE?") if state.strip() == "0": break time.sleep(0.1) # 读取波形数据 scope.write("DATA:SOURCE CH1") scope.write("DATA:ENCDG ASCII") # 用 ASCII 编码,方便解析 scope.write("DATA:WIDTH 2") raw_data = scope.query("CURVE?")这里有几个关键点。DATA:ENCDG ASCII让示波器返回文本格式的波形数据,解析简单,但数据量大时传输慢;如果追求速度,可以用二进制编码(DATA:ENCDG RIBINARY),但解析要处理字节序。ACQUIRE:STOPAFTER SEQUENCE表示采集一次就停,适合单次测试;如果要连续采集,改成RUNSTOP或者循环触发。等待采集完成的轮询里,ACQUIRE:STATE?返回 0 表示停止,返回 1 表示运行中,这个判断逻辑要写对,否则会读到不完整的波形。3.3 数据解析与参数计算读回来的CURVE?是一串用逗号分隔的数值,需要转成 NumPy 数组,再结合示波器的垂直和水平刻度换算成实际电压和时间。泰克示波器的波形数据通常需要这样换算:电压 = (原始值 - 垂直偏移) × 垂直档位 / 垂直分辨率;时间 = 水平偏移 + 索引 × 水平档位 / 水平分辨率。具体公式要看型号手册,但思路是一样的。import numpy as np # 解析 ASCII 波形数据 values = np.array([float(v) for v in raw_data.split(",")]) # 查询换算参数 y_mult = float(scope.query("WFMPRE:YMULT?")) y_off = float(scope.query("WFMPRE:YOFF?")) y_zero = float(scope.query("WFMPRE:YZERO?")) x_incr = float(scope.query("WFMPRE:XINCR?")) # 换算成实际电压和时间 voltage = (values - y_zero) * y_mult - y_off * y_mult time_axis = np.arange(len(voltage)) * x_incr # 计算峰峰值和频率 vpp = np.max(voltage) - np.min(voltage) print(f"峰峰值:{vpp:.3f} V")这段代码里,YMULT、YOFF、YZERO、XINCR是泰克示波器波形前导码里的标准参数,分别代表垂直缩放因子、垂直偏移、垂直零点、水平采样间隔。不同型号的指令名可能略有差异,但逻辑一致。算出来的voltage和time_axis就是后面画图要用的数据。4. PyQt5 界面与实时刷新:让波形和日志动起来4.1 界面布局与控件划分界面部分我一般会分成四个区域。左上角是波形显示区,用 pyqtgraph 的 PlotWidget,因为它比 Matplotlib 嵌入 PyQt5 更流畅,刷新率更高。右上角是参数配置区,包含 IP 地址输入框、通道选择下拉框、时基和触发设置输入框。下方是日志打印区,用 QTextEdit 只读模式,实时追加测试信息。最底部是控制按钮区,包含「连接」「开始采集」「停止」「保存数据」几个按钮。布局用 QGridLayout 或者 QVBoxLayout 嵌套,保证窗口缩放时各区域比例合理。波形显示区要设置最小高度,否则窗口拉小时波形会被压扁。日志区要设置自动滚动到底部,方便看最新信息。4.2 多线程采集与信号槽通信PyQt5 的界面刷新必须在主线程,而采集是耗时操作,如果直接在按钮点击事件里写采集循环,界面会卡死。标准做法是把采集逻辑放到 QThread 里,通过信号把数据和日志发回主线程。from PyQt5.QtCore import QThread, pyqtSignal import numpy as np class AcquireThread(QThread): # 定义信号:波形数据、日志信息 wave_ready = pyqtSignal(np.ndarray, np.ndarray) log_ready = pyqtSignal(str) def __init__(self, scope): super().__init__() self.scope = scope self.running = True def run(self): while self.running: try: # 触发采集 self.scope.write("ACQUIRE:STATE ON") self.log_ready.emit("开始采集...") # 等待采集完成 while self.scope.query("ACQUIRE:STATE?").strip() != "0": self.msleep(50) # 读取并解析波形 raw = self.scope.query("CURVE?") values = np.array([float(v) for v in raw.split(",")]) # 换算逻辑省略,同上一章 voltage = values # 占位,实际要换算 time_axis = np.arange(len(voltage)) * 0.001 # 发信号给 UI 线程 self.wave_ready.emit(time_axis, voltage) self.log_ready.emit(f"采集完成,点数:{len(voltage)}") except Exception as e: self.log_ready.emit(f"采集异常:{str(e)}") self.msleep(500) # 采集间隔 def stop(self): self.running = False self.wait()这段代码的核心是pyqtSignal和emit。wave_ready信号携带两个 NumPy 数组,log_ready携带字符串。在 UI 线程里,把这两个信号连接到对应的槽函数,槽函数里更新波形图和日志框。注意msleep是 QThread 的方法,比time.sleep更适合 Qt 环境,因为它不会阻塞事件循环。4.3 波形实时刷新与日志打印UI 线程里的槽函数负责把数据画出来、把日志打出来。波形刷新用 pyqtgraph 的setData方法,它比清空重画快得多。日志打印用append方法,配合ensureCursorVisible自动滚动。from PyQt5.QtWidgets import QMainWindow, QTextEdit, QPushButton, QVBoxLayout, QWidget import pyqtgraph as pg class MainWindow(QMainWindow): def __init__(self): super().__init__() # 波形显示控件 self.plot_widget = pg.PlotWidget() self.plot_widget.setLabel("left", "电压", units="V") self.plot_widget.setLabel("bottom", "时间", units="s") self.curve = self.plot_widget.plot(pen="y") # 日志控件 self.log_text = QTextEdit() self.log_text.setReadOnly(True) # 按钮 self.btn_start = QPushButton("开始采集") self.btn_start.clicked.connect(self.start_acquire) # 布局 layout = QVBoxLayout() layout.addWidget(self.plot_widget) layout.addWidget(self.btn_start) layout.addWidget(self.log_text) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) def start_acquire(self): self.thread = AcquireThread(scope) self.thread.wave_ready.connect(self.update_wave) self.thread.log_ready.connect(self.append_log) self.thread.start() def update_wave(self, time_axis, voltage): # 实时刷新波形,setData 比重画快 self.curve.setData(time_axis, voltage) def append_log(self, msg): self.log_text.append(msg) # 自动滚动到底部 self.log_text.ensureCursorVisible()这里的关键是setData而不是clear后重画。pyqtgraph 的setData只更新数据点,不重建图形对象,刷新率能到几十帧每秒,完全满足实时显示需求。日志的ensureCursorVisible保证每来一条新日志,视图自动滚到最新一行,测试人员不用手动拖滚动条。注意:采集线程里不要直接操作 UI 控件,否则会报「QObject::setParent: Cannot set parent, new parent is in a different thread」之类的错误。所有 UI 更新必须通过信号槽回到主线程。5. 避坑与常见问题:那些让我熬夜的翻车点5.1 连接超时与资源被占用现象:程序运行几次后,突然连不上示波器,报「VI_ERROR_RSRC_BUSY」或者超时。原因通常是上一次连接没有正确关闭,VISA 资源被占用。解决方法是每次用完调用scope.close(),并且在程序退出时用try...finally保证关闭。如果已经占用了,重启示波器或者换一个资源地址(比如从 LAN 换成 USB)能临时解决。5.2 波形数据读取不完整现象:画出来的波形只有前半段,或者数值明显不对。原因可能是DATA:WIDTH和DATA:ENCDG设置不匹配,或者读取时超时导致数据截断。解决方法是把timeout设大,并且确认DATA:START和DATA:STOP覆盖了整个记录长度。另外,ASCII 编码下数据量大时传输慢,可以改用二进制编码,但解析要处理字节序。5.3 界面卡死与信号槽连接错误现象:点击「开始采集」后界面无响应,或者日志不更新。原因通常是采集逻辑写在了主线程,或者信号槽连接时参数类型不匹配。解决方法是把采集放到 QThread,并且检查pyqtSignal定义的类型和emit传入的数据类型是否一致。NumPy 数组要用np.ndarray声明,不能写list。5.4 触发设置不当导致采集不到波形现象:程序一直在等待采集完成,但示波器没有触发。原因可能是触发电平设得过高或过低,或者触发源选错了通道。解决方法是先用示波器面板手动调好触发,确认能稳定触发后,再用query读回当前设置,照着写到代码里。不要凭感觉设触发参数。5.5 打包成 exe 后找不到 VISA 库现象:在开发环境跑得好好的,打包成 exe 后报「找不到 visa32.dll」或者「No module named pyvisa」。原因是 PyInstaller 没有自动打包 VISA 的动态库。解决方法是在打包命令里加--add-binary把 VISA 库带上,或者让目标机器先安装泰克官方的 VISA 驱动。6. 验收与答辩:怎么证明这套方案真的能跑验收这套方案,我一般会从四个维度来检查。第一,连接稳定性:连续运行 30 分钟,每隔 2 秒采集一次,看是否出现掉线或超时。第二,数据准确性:用信号发生器输出一个已知频率和幅度的正弦波,看程序算出来的频率和峰峰值跟信号发生器显示的是否一致,误差控制在 5% 以内算合格。第三,界面响应:采集过程中拖动窗口、点击按钮,界面不卡死,波形刷新流畅。第四,异常处理:拔掉网线或者关掉示波器,程序应该给出明确错误提示,而不是直接崩溃。答辩的时候,评委通常会问三个问题:为什么用 PyQt5 不用 Web?多线程怎么保证数据安全?如果换一台示波器,代码要改多少?第一个问题按第 2 章的理由答。第二个问题答「采集线程只发信号不碰 UI,数据通过信号槽传递,天然线程安全」。第三个问题答「只要支持 SCPI,改资源地址和少量指令即可,架构不变」。下面这张表是我常用的验收检查清单,你可以直接拿去用。检查项合格标准测试方法连接稳定性30 分钟无掉线连续采集 900 次数据准确性频率误差 < 5%信号发生器对比界面响应无卡死,刷新流畅采集时操作界面异常处理有明确提示不崩溃拔网线/关仪器报告完整性数据+截图齐全检查输出目录最后说一个我自己的习惯。每次交付这类测试方案前,我都会强制走一遍「冷启动」流程:关掉所有设备,重启电脑,从零开始连接、采集、保存、生成报告,全程不参考任何笔记。这一步能暴露 90% 的环境依赖问题,比如某个库没装、某个路径写死了、某个驱动没打包。从那以后我每次验收都强制走一遍冷启动,省了很多返工。希望帮到你。本文还有配套的精品资源,点击获取
简介:这是一套面向自动化测试初学者与仪器控制开发者的Python实战项目,围绕泰克MDO3014示波器实现上位机软件,解决波形实时显示、图像保存与日志读取等测试场景需求。资源包共16个文件,约139KB,包含2个py主程序与1个ui界面文件,另有5个xml配置、3张png界面截图、2个txt日志样例及pyc缓存等,结构紧凑,便于直接运行与二次开发。项目支持示波器界面实时刷新图片、实时打印并保存log窗口信息,可单次循环执行自编测试用例,同时详细演示常用QT控件在Python中的调用方式。目前已有3659人学习下载,适合想掌握PyQt5界面开发与仪器通信的读者参考,能从中获得可复用的界面框架、日志处理思路与示波器联调经验,快速搭建自己的自动化测试工具。1. 从手动按示波器到自动化测试:这套方案要解决什么如果你在实验室里做过硬件测试,大概率经历过这样的场景:一块板子上电后,你蹲在泰克示波器前面,手动调时基、调触发电平、截图、记数据,测一轮下来半小时没了,测十轮就是五个小时,中间还容易漏记某个通道的波形。更麻烦的是,测试报告要手动整理,截图要一张张命名归档,一旦发现异常波形,想回溯当时的参数配置只能靠记忆。这套基于 Python + PyQt5 + 泰克示波器的自动化测试方案,要解决的就是这个问题:把「配置仪器 → 采集波形 → 实时显示 → 记录数据 → 生成报告」整条链路串起来,用一个桌面端界面统一控制。它适合正在做硬件测试毕设的学生、需要搭建产线自动化测试工位的工程师,以及任何想把泰克 3000 系列示波器从「手动工具」变成「可编程测试节点」的人。核心关键词就三个:PyQt5 做界面、Tektronix3000 做数据源、实时刷新图片和打印信息做交互反馈。2. 需求拆解与技术选型:为什么是 PyQt5 加泰克 3000 系列2.1 测试场景的真实需求先把这个项目的需求说清楚,不然后面选型没有依据。一个典型的自动化测试场景通常包含这几个动作:第一,通过程控接口连接示波器,设置通道、时基、触发模式等参数;第二,启动采集,获取波形数据;第三,把波形画成图,实时显示在界面上;第四,把关键测量值(频率、峰峰值、上升时间等)打印到日志区域;第五,保存数据和截图,供后续生成报告。这五个动作里,前两个是仪器控制,第三个是 GUI 绘图,第四个是日志输出,第五个是文件 IO。手动操作时,这些动作是串行且依赖人的;自动化方案要做的,是把它们变成可配置、可循环、可无人值守的流程。这里有一个容易被忽略的点:实时性。很多同学一开始觉得「采集完再画图」就行,但实际测试中,你需要一边采集一边看波形变化,否则等一轮跑完才发现触发没设对,时间全浪费了。所以「实时刷新图片」和「实时刷新打印信息」不是锦上添花,而是这个方案能不能真正替代手动操作的关键。2.2 为什么选 PyQt5 而不是 Web 或命令行技术选型上,仪器控制层用 Python 是共识,因为 PyVISA 这个库把 VISA 通信封装得很干净,几行代码就能跟示波器对话。争议点在界面层:有人用 Web(Flask + ECharts),有人用命令行,有人用 PyQt5。我选 PyQt5 的理由很实际。第一,部署简单。Web 方案要起服务、开浏览器,实验室的测试电脑往往不联网,多一层就多一个故障点。PyQt5 打包成 exe 后双击就能跑,对不写代码的测试人员友好。第二,绘图性能。PyQt5 的 QGraphicsView 或者 pyqtgraph 在刷新率上比 Web 的 Canvas 重绘更可控,尤其是波形数据点上千的时候,Web 方案容易卡。第三,线程模型清晰。PyQt5 的 QThread 和信号槽机制,天然适合「采集线程 + UI 线程」分离,避免界面假死。命令行方案虽然轻,但没法直观展示波形,测试人员不买账。至于示波器选泰克 3000 系列,是因为这个系列在高校和中小型实验室里保有量高,支持 LAN、USB、GPIB 多种接口,程控指令集(基于 SCPI)文档齐全,PyVISA 能直接识别。你换成其他支持 SCPI 的示波器,代码结构基本不用大改,改一下资源地址和部分指令即可。2.3 整体架构:三层分离这套方案的架构我一般会分成三层。最底层是仪器通信层,负责跟示波器建立连接、发送 SCPI 指令、读取波形数据,用 PyVISA 实现。中间层是业务逻辑层,负责测试流程编排、数据解析、异常判断,比如「设置触发 → 等待触发 → 读取波形 → 计算参数 → 判断是否合格」。最上层是界面层,用 PyQt5 实现,包含波形显示区、日志打印区、参数配置区和控制按钮区。三层之间通过信号槽和队列通信,采集线程不直接操作 UI 控件,而是发信号让 UI 线程更新。这样做的好处是,即使采集过程中示波器响应慢,界面也不会卡死。下面这张表是三层各自的职责和关键技术点。层级职责关键技术仪器通信层连接示波器、发送 SCPI、读取波形PyVISA、SCPI 指令业务逻辑层流程编排、数据解析、合格判断Python 多线程、NumPy界面层波形显示、日志打印、参数配置PyQt5、pyqtgraph3. 仪器通信与数据采集:把示波器变成可编程节点3.1 连接示波器与基础 SCPI 指令连接示波器是第一步,也是最容易翻车的一步。泰克 3000 系列支持 LAN 和 USB 两种常用接口。LAN 方式下,你需要知道示波器的 IP 地址,然后在代码里用 TCPIP 资源地址连接。USB 方式下,资源地址是一串包含厂商 ID 和序列号的字符串,通常用 PyVISA 的列表功能自动发现。import pyvisa # 创建资源管理器 rm = pyvisa.ResourceManager() # 列出所有可用仪器,确认示波器被识别 resources = rm.list_resources() print("可用仪器:", resources) # 连接示波器,LAN 方式示例 # 实际使用时把 IP 换成你示波器的地址 scope = rm.open_resource("TCPIP0::192.168.1.100::inst0::INSTR") scope.timeout = 10000 # 超时设 10 秒,波形数据大时默认 2 秒不够 # 查询仪器标识,确认通信正常 print(scope.query("*IDN?"))这段代码里,list_resources()是排查连接问题的第一手段,如果列表里没有你的示波器,后面都不用谈。timeout参数建议设大一点,因为读取长波形数据时,默认超时经常不够,会报 VisaIOError。*IDN?是 SCPI 的通用查询指令,返回厂商、型号、序列号、固件版本,能返回就说明通信链路通了。3.2 配置采集参数与读取波形通信通了之后,要设置采集参数。泰克 3000 系列的 SCPI 指令集里,常用的有通道开关、垂直档位、时基、触发模式等。下面这段代码演示了配置通道 1、设置时基、单次触发采集、读取波形数据的完整流程。# 配置通道 1:打开、直流耦合、垂直档位 1V/格 scope.write("SELECT:CH1 ON") scope.write("CH1:COUPLING DC") scope.write("CH1:SCALE 1") # 设置时基为 1ms/格,共 10 格 scope.write("HORIZONTAL:SCALE 0.001") # 设置触发:边沿触发,通道 1,上升沿,电平 0.5V scope.write("TRIGGER:A:TYPE EDGE") scope.write("TRIGGER:A:EDGE:SOURCE CH1") scope.write("TRIGGER:A:EDGE:SLOPE RISE") scope.write("TRIGGER:A:LEVEL 0.5") # 单次采集 scope.write("ACQUIRE:STATE ON") scope.write("ACQUIRE:STOPAFTER SEQUENCE") # 等待采集完成,查询状态 import time while True: state = scope.query("ACQUIRE:STATE?") if state.strip() == "0": break time.sleep(0.1) # 读取波形数据 scope.write("DATA:SOURCE CH1") scope.write("DATA:ENCDG ASCII") # 用 ASCII 编码,方便解析 scope.write("DATA:WIDTH 2") raw_data = scope.query("CURVE?")这里有几个关键点。DATA:ENCDG ASCII让示波器返回文本格式的波形数据,解析简单,但数据量大时传输慢;如果追求速度,可以用二进制编码(DATA:ENCDG RIBINARY),但解析要处理字节序。ACQUIRE:STOPAFTER SEQUENCE表示采集一次就停,适合单次测试;如果要连续采集,改成RUNSTOP或者循环触发。等待采集完成的轮询里,ACQUIRE:STATE?返回 0 表示停止,返回 1 表示运行中,这个判断逻辑要写对,否则会读到不完整的波形。3.3 数据解析与参数计算读回来的CURVE?是一串用逗号分隔的数值,需要转成 NumPy 数组,再结合示波器的垂直和水平刻度换算成实际电压和时间。泰克示波器的波形数据通常需要这样换算:电压 = (原始值 - 垂直偏移) × 垂直档位 / 垂直分辨率;时间 = 水平偏移 + 索引 × 水平档位 / 水平分辨率。具体公式要看型号手册,但思路是一样的。import numpy as np # 解析 ASCII 波形数据 values = np.array([float(v) for v in raw_data.split(",")]) # 查询换算参数 y_mult = float(scope.query("WFMPRE:YMULT?")) y_off = float(scope.query("WFMPRE:YOFF?")) y_zero = float(scope.query("WFMPRE:YZERO?")) x_incr = float(scope.query("WFMPRE:XINCR?")) # 换算成实际电压和时间 voltage = (values - y_zero) * y_mult - y_off * y_mult time_axis = np.arange(len(voltage)) * x_incr # 计算峰峰值和频率 vpp = np.max(voltage) - np.min(voltage) print(f"峰峰值:{vpp:.3f} V")这段代码里,YMULT、YOFF、YZERO、XINCR是泰克示波器波形前导码里的标准参数,分别代表垂直缩放因子、垂直偏移、垂直零点、水平采样间隔。不同型号的指令名可能略有差异,但逻辑一致。算出来的voltage和time_axis就是后面画图要用的数据。4. PyQt5 界面与实时刷新:让波形和日志动起来4.1 界面布局与控件划分界面部分我一般会分成四个区域。左上角是波形显示区,用 pyqtgraph 的 PlotWidget,因为它比 Matplotlib 嵌入 PyQt5 更流畅,刷新率更高。右上角是参数配置区,包含 IP 地址输入框、通道选择下拉框、时基和触发设置输入框。下方是日志打印区,用 QTextEdit 只读模式,实时追加测试信息。最底部是控制按钮区,包含「连接」「开始采集」「停止」「保存数据」几个按钮。布局用 QGridLayout 或者 QVBoxLayout 嵌套,保证窗口缩放时各区域比例合理。波形显示区要设置最小高度,否则窗口拉小时波形会被压扁。日志区要设置自动滚动到底部,方便看最新信息。4.2 多线程采集与信号槽通信PyQt5 的界面刷新必须在主线程,而采集是耗时操作,如果直接在按钮点击事件里写采集循环,界面会卡死。标准做法是把采集逻辑放到 QThread 里,通过信号把数据和日志发回主线程。from PyQt5.QtCore import QThread, pyqtSignal import numpy as np class AcquireThread(QThread): # 定义信号:波形数据、日志信息 wave_ready = pyqtSignal(np.ndarray, np.ndarray) log_ready = pyqtSignal(str) def __init__(self, scope): super().__init__() self.scope = scope self.running = True def run(self): while self.running: try: # 触发采集 self.scope.write("ACQUIRE:STATE ON") self.log_ready.emit("开始采集...") # 等待采集完成 while self.scope.query("ACQUIRE:STATE?").strip() != "0": self.msleep(50) # 读取并解析波形 raw = self.scope.query("CURVE?") values = np.array([float(v) for v in raw.split(",")]) # 换算逻辑省略,同上一章 voltage = values # 占位,实际要换算 time_axis = np.arange(len(voltage)) * 0.001 # 发信号给 UI 线程 self.wave_ready.emit(time_axis, voltage) self.log_ready.emit(f"采集完成,点数:{len(voltage)}") except Exception as e: self.log_ready.emit(f"采集异常:{str(e)}") self.msleep(500) # 采集间隔 def stop(self): self.running = False self.wait()这段代码的核心是pyqtSignal和emit。wave_ready信号携带两个 NumPy 数组,log_ready携带字符串。在 UI 线程里,把这两个信号连接到对应的槽函数,槽函数里更新波形图和日志框。注意msleep是 QThread 的方法,比time.sleep更适合 Qt 环境,因为它不会阻塞事件循环。4.3 波形实时刷新与日志打印UI 线程里的槽函数负责把数据画出来、把日志打出来。波形刷新用 pyqtgraph 的setData方法,它比清空重画快得多。日志打印用append方法,配合ensureCursorVisible自动滚动。from PyQt5.QtWidgets import QMainWindow, QTextEdit, QPushButton, QVBoxLayout, QWidget import pyqtgraph as pg class MainWindow(QMainWindow): def __init__(self): super().__init__() # 波形显示控件 self.plot_widget = pg.PlotWidget() self.plot_widget.setLabel("left", "电压", units="V") self.plot_widget.setLabel("bottom", "时间", units="s") self.curve = self.plot_widget.plot(pen="y") # 日志控件 self.log_text = QTextEdit() self.log_text.setReadOnly(True) # 按钮 self.btn_start = QPushButton("开始采集") self.btn_start.clicked.connect(self.start_acquire) # 布局 layout = QVBoxLayout() layout.addWidget(self.plot_widget) layout.addWidget(self.btn_start) layout.addWidget(self.log_text) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) def start_acquire(self): self.thread = AcquireThread(scope) self.thread.wave_ready.connect(self.update_wave) self.thread.log_ready.connect(self.append_log) self.thread.start() def update_wave(self, time_axis, voltage): # 实时刷新波形,setData 比重画快 self.curve.setData(time_axis, voltage) def append_log(self, msg): self.log_text.append(msg) # 自动滚动到底部 self.log_text.ensureCursorVisible()这里的关键是setData而不是clear后重画。pyqtgraph 的setData只更新数据点,不重建图形对象,刷新率能到几十帧每秒,完全满足实时显示需求。日志的ensureCursorVisible保证每来一条新日志,视图自动滚到最新一行,测试人员不用手动拖滚动条。注意:采集线程里不要直接操作 UI 控件,否则会报「QObject::setParent: Cannot set parent, new parent is in a different thread」之类的错误。所有 UI 更新必须通过信号槽回到主线程。5. 避坑与常见问题:那些让我熬夜的翻车点5.1 连接超时与资源被占用现象:程序运行几次后,突然连不上示波器,报「VI_ERROR_RSRC_BUSY」或者超时。原因通常是上一次连接没有正确关闭,VISA 资源被占用。解决方法是每次用完调用scope.close(),并且在程序退出时用try...finally保证关闭。如果已经占用了,重启示波器或者换一个资源地址(比如从 LAN 换成 USB)能临时解决。5.2 波形数据读取不完整现象:画出来的波形只有前半段,或者数值明显不对。原因可能是DATA:WIDTH和DATA:ENCDG设置不匹配,或者读取时超时导致数据截断。解决方法是把timeout设大,并且确认DATA:START和DATA:STOP覆盖了整个记录长度。另外,ASCII 编码下数据量大时传输慢,可以改用二进制编码,但解析要处理字节序。5.3 界面卡死与信号槽连接错误现象:点击「开始采集」后界面无响应,或者日志不更新。原因通常是采集逻辑写在了主线程,或者信号槽连接时参数类型不匹配。解决方法是把采集放到 QThread,并且检查pyqtSignal定义的类型和emit传入的数据类型是否一致。NumPy 数组要用np.ndarray声明,不能写list。5.4 触发设置不当导致采集不到波形现象:程序一直在等待采集完成,但示波器没有触发。原因可能是触发电平设得过高或过低,或者触发源选错了通道。解决方法是先用示波器面板手动调好触发,确认能稳定触发后,再用query读回当前设置,照着写到代码里。不要凭感觉设触发参数。5.5 打包成 exe 后找不到 VISA 库现象:在开发环境跑得好好的,打包成 exe 后报「找不到 visa32.dll」或者「No module named pyvisa」。原因是 PyInstaller 没有自动打包 VISA 的动态库。解决方法是在打包命令里加--add-binary把 VISA 库带上,或者让目标机器先安装泰克官方的 VISA 驱动。6. 验收与答辩:怎么证明这套方案真的能跑验收这套方案,我一般会从四个维度来检查。第一,连接稳定性:连续运行 30 分钟,每隔 2 秒采集一次,看是否出现掉线或超时。第二,数据准确性:用信号发生器输出一个已知频率和幅度的正弦波,看程序算出来的频率和峰峰值跟信号发生器显示的是否一致,误差控制在 5% 以内算合格。第三,界面响应:采集过程中拖动窗口、点击按钮,界面不卡死,波形刷新流畅。第四,异常处理:拔掉网线或者关掉示波器,程序应该给出明确错误提示,而不是直接崩溃。答辩的时候,评委通常会问三个问题:为什么用 PyQt5 不用 Web?多线程怎么保证数据安全?如果换一台示波器,代码要改多少?第一个问题按第 2 章的理由答。第二个问题答「采集线程只发信号不碰 UI,数据通过信号槽传递,天然线程安全」。第三个问题答「只要支持 SCPI,改资源地址和少量指令即可,架构不变」。下面这张表是我常用的验收检查清单,你可以直接拿去用。检查项合格标准测试方法连接稳定性30 分钟无掉线连续采集 900 次数据准确性频率误差 < 5%信号发生器对比界面响应无卡死,刷新流畅采集时操作界面异常处理有明确提示不崩溃拔网线/关仪器报告完整性数据+截图齐全检查输出目录最后说一个我自己的习惯。每次交付这类测试方案前,我都会强制走一遍「冷启动」流程:关掉所有设备,重启电脑,从零开始连接、采集、保存、生成报告,全程不参考任何笔记。这一步能暴露 90% 的环境依赖问题,比如某个库没装、某个路径写死了、某个驱动没打包。从那以后我每次验收都强制走一遍冷启动,省了很多返工。希望帮到你。本文还有配套的精品资源,点击获取