简介:基于Python+PyQt5开发的泰克示波器自动化测试资源,面向需要将泰克MDO3014/3000系列示波器接入上位机进行实时控制与数据采集的测试工程师、自动化脚本开发人员及PyQt5学习者。软件可在界面实时显示示波器波形与图像并保存,同时支持log实时读取、显示和界面日志窗口内容保存;提供单次循环测试用例执行框架,测试逻辑需根据项目自行编写。资源包为RAR格式,共16个文件,包含Python源码、QT Designer的ui界面文件、xml配置、png图片、txt日志等,体积仅139KB,结构精简,适合快速参考与二次开发。资源中详细讲解了常用QT控件在Python中的调用方法,并给出完整的工程目录组织方式,可帮助读者理解从界面绘制到示波器通信、日志管理的整体流程。目前已有3657人学习下载,适用于入门PyQt5界面编程或搭建轻量级仪器自动化测试原型。
1. 这个项目解决什么:把示波器上的肉眼看测试搬进 PyQt5 界面
产线或者实验室里最常见的场景是:被测板子上电,测试员盯着泰克3000系列示波器屏幕,手动按 Print 键截图,再拿笔记下峰值电压、频率、上升沿时间,最后填到 Excel 表格里。一台板子测下来少说十几分钟,遇上批量测试,一整天都在跟旋钮和按键较劲。这个项目就是把这一整套搬进 PyQt5 写的 Windows 桌面程序里:示波器连上电脑后,软件自动下发配置指令、实时刷新波形图片、实时刷新打印测量信息,跑完自动生成测试记录。适合做硬件产线测试、实验室验证、以及想用自动化测试替代人工盯屏的开发者。核心价值不在“连上仪器”,而在“图片和文本两条链路同时实时刷新,测完不用人碰示波器”。
2. 系统架构与技术选型:VISA 通信层、界面线程与指令模型
2.1 整体架构:三层模型,指令全走 SCPI
这个项目本质上是一个仪器控制桌面应用,主框架是 PyQt5 + pyvisa。泰克 3000 系列示波器支持标准 SCPI 指令集,通信方式有 USB、网口和 GPIB 三种,程序里通常推荐用网口或者 USB,因为 GPIB 需要额外转接卡。
项目架构我拆成三层看:
- 界面层:PyQt5 负责主窗口、波形显示区域、测量信息打印区域、测试控制按钮。所有用户操作都在这一层,不能直接碰仪器。
- 指令层:基于 pyvisa 封装一个示波器驱动类,把
*IDN?、:MEASUREMENT:MEAS1:VALUE?、:SAVE:IMAGe这类 SCPI 指令包成 Python 方法。上层调用scope.get_voltage(),不需要知道 VISA 指令长什么样。 - 数据链路层:波形图片和测量文本是两条独立链路。图片用二进制读取,测量文本用 ASCII 解析,两条链路各自跑在线程或定时器里。
这个分层的好处是:如果后面把泰克换成是德科技或者国产示波器,只需要替换指令层,界面和测试流程不用动。
2.2 线程模型:为什么图片和打印必须分开刷新
实时刷新图片是典型的“读大块二进制数据”操作——示波器截图通常是几百 KB 到几 MB 的 PNG,VISA 总线读一次要几百毫秒。实时刷新打印信息是“读短字节文本”操作,一次就几百字节,几十毫秒能跑完。如果把两条链路都丢在主线程里,界面会周期性地卡顿,按钮点一下要等一秒多才有反应。
常见做法是开两个 QThread:一个线程专门轮询波形图片,另一个线程专门轮询测量值文本。线程拿到数据后通过pyqtSignal发回主线程,主线程只负责往控件上刷新内容。线程里不碰任何界面对象,这是写完不翻车的前提。
2.3 指令与配置设计:Tektronix3000 的常用指令集映射
泰克 3000 系列选型时通常关注这几个指令:
| 指令 | 功能 | 返回格式 |
|---|---|---|
*IDN? | 查询仪器型号 | 逗号分隔的字符串 |
:MEASUREMENT:MEAS1:SOURCE CH1 | 指定测量通道 | 无 |
:MEASUREMENT:MEAS1:MEASurement PK2PK | 指定测量类型 | 无 |
:MEASUREMENT:MEAS1:VALUE? | 查询测量数值 | ASCII 浮点 |
:SAVE:IMAGe:FILEFmt PNG | 设置截图格式 | 无 |
:SAVE:IMAGe:ACTUal | 触发截图 | 无 |
:FILESystem:READFile "路径" | 读取示波器文件 | 二进制块 |
指令层的关键点在于测量前要先配置SOURCE和MEASUREMENT,直接VALUE?会返回空值或者9.91e37(表示无有效测量值)。这个顺序写错,程序不会报错,但数据全是废的。
3. 核心功能实现:实时刷新图片与打印信息的完整逻辑
3.1 仪器连接与指令封装
第一步写示波器驱动类,所有 SCPI 指令集中在里面。连接时需要指定 VISA 地址,网口地址一般是TCPIP0::192.168.1.100::inst0::INSTR,USB 地址一般是USB0::0x0699::0x0368::C012345::INSTR。泰克官方驱动选 TekVISA,也可以装 NI-VISA,pyvisa 会自动探测后端。
import pyvisa class ScopeDriver: def __init__(self, visa_address: str): self.rm = pyvisa.ResourceManager() self.instr = self.rm.open_resource(visa_address) self.instr.timeout = 5000 # 超时设为5秒,避免卡死 self.instr.encoding = "latin-1" # 某些3000系列返回的字符编码 def check_idn(self) -> str: # 连接后先确认型号,避免误连到其他仪器 resp = self.instr.query("*IDN?") return resp.strip() def set_measure(self, channel: str, meas_type: str): # 先用SOURCE指定测量通道,再指定测量类型 self.instr.write(f":MEASUREMENT:MEAS1:SOURCE {channel}") self.instr.write(f":MEASUREMENT:MEAS1:MEASurement {meas_type}") def read_measure_value(self) -> float: # 查询测量值,注意返回的是ASCII字符串 value = self.instr.query(":MEASUREMENT:MEAS1:VALUE?") return float(value.strip()) def save_screenshot(self, remote_path: str): # 设置PNG格式并触发截图,截图会保存到示波器本地 self.instr.write(":SAVE:IMAGe:FILEFmt PNG") self.instr.write(f':SAVE:IMAGe:ACTUal "{remote_path}"') def read_file(self, remote_path: str) -> bytes: # 示波器文件系统读取,返回二进制数据 return self.instr.query_binary_values( f':FILESystem:READFile "{remote_path}"', datatype="B", is_binary=True, expect_data=True, )query_binary_values是这个项目里比较容易踩坑的地方:泰克返回的是二进制块(header 格式:#N<长度>),pyvisa 需要指定is_binary=True和expect_data=True才能正确解析。如果不加这两个参数,返回的数据会是乱码或者直接报错。超时时间设置很重要,读大文件时 5000ms 不够的话需要加大到 10000ms,但注意超时时间过大会让界面假死时间变长,通常配合线程使用。
3.2 实时刷新波形图片:QThread 轮询链路
图片刷新线程的核心逻辑:每隔一段时间截一张 PNG,读取二进制,发信号给主线程显示。线程里不能直接调用QLabel.setPixmap,必须通过信号传QImage或者bytes。
from PyQt5.QtCore import QThread, pyqtSignal import time class WaveformRefreshThread(QThread): # 信号参数是bytes类型,主线程收到后转QImage image_ready = pyqtSignal(bytes) def __init__(self, driver: ScopeDriver, remote_path: str, interval: float = 0.5): super().__init__() self.driver = driver self.remote_path = remote_path self.interval = interval self.running = True def run(self): while self.running: try: # 触发截图并读取 self.driver.save_screenshot(self.remote_path) time.sleep(0.2) # 等示波器把文件写入文件系统 data = self.driver.read_file(self.remote_path) self.image_ready.emit(data) # 发信号给主线程 except Exception as e: print(f"截图异常: {e}") time.sleep(self.interval) def stop(self): self.running = False self.wait()time.sleep(0.2)是必须的,3000 系列在:SAVE:IMAGe:ACTUal之后文件写入需要时间,立刻去读经常读到上一次的旧图或者空文件。轮询间隔建议设 0.3 到 0.8 秒,太频繁会让示波器面板操作变得迟滞,太慢则实时性太差。
主线程这边的槽函数负责把二进制 PNG 转成QPixmap:
from PyQt5.QtGui import QPixmap, QImage import io # 在MainWindow中连接信号 self.waveform_thread.image_ready.connect(self.update_waveform_image) def update_waveform_image(self, data: bytes): # 从二进制数据直接构建图片对象,不需要落到磁盘 pixmap = QPixmap() pixmap.loadFromData(data, "PNG") # 按控件宽度等比缩放,避免图片过大 scaled = pixmap.scaled( self.image_label.width(), self.image_label.height(), aspectRatioMode=1 # 保持比例 ) self.image_label.setPixmap(scaled)注意aspectRatioMode=1表示等比缩放,如果省略这个参数,图片会被强行拉伸变形,波形图这种带网格的图片一变形,幅度读数视觉上就是错的。
3.3 实时刷新打印信息:测量值文本链路
文本刷新链路更轻量,不需要线程,用QTimer+ 普通方法轮询就可以。核心是把测量值格式化后追加到QPlainTextEdit,同时保留清屏按钮。
from PyQt5.QtCore import QTimer class MainWindow: def setup_measure_timer(self): self.measure_timer = QTimer(self) self.measure_timer.timeout.connect(self.refresh_measure_info) self.measure_timer.start(300) # 300ms刷新一次 def refresh_measure_info(self): try: # 这里同时采集多个测量值,频率和幅度 freq = self.driver.read_measure_value() # 切换到第二个测量通道配置 self.driver.instr.write(":MEASUREMENT:MEAS1:MEASurement FREQUENCY") freq_val = self.driver.read_measure_value() ts = time.strftime("%H:%M:%S") line = f"[{ts}] 频率: {freq_val:.2f} Hz" self.print_text.appendPlainText(line) # 只保留最近100行,防止内存无限膨胀 if self.print_text.blockCount() > 100: self.print_text.clear() except Exception as e: self.print_text.appendPlainText(f"[采集异常] {e}")这个定时器方案有个隐藏坑:read_measure_value()里每次都切换测量类型,如果示波器不是 3000 系列或者固件版本旧,切换后立刻查询会返回 9.91e37。我一般会加一个防御性写法:查询后判断数值是不是这个特定值,是的话等 100ms 再查一次。
3.4 测试流程编排:一键跑一个完整用例
自动化测试的核心是把测步骤串起来。这里用一个 JSON 配置定义测试项,按顺序执行:
{ "test_items": [ {"name": "5V输出峰值电压", "channel": "CH1", "meas": "PK2PK", "low": 4.8, "high": 5.2}, {"name": "时钟频率", "channel": "CH1", "meas": "FREQUENCY", "low": 9900000, "high": 10100000} ] }执行循环里,每项先配置指令,再取测量值,与上下限比对,把结果写进 CSV 文件,同时把当前的波形图截图按测试项命名保存到report目录。这个流程可以在测试跑完后自然写成测试报告。
4. 环境搭建与启动:从接线到跑通一个完整测试流程
4.1 依赖安装
项目依赖不多,核心是 pyvisa、PyQt5 和 pyserial(如果用串口扩展的话)。Python 版本建议 3.8 以上,pyvisa 在 1.12 以上的版本对二进制块解析更稳。
pip install pyvisa pyvisa-py PyQt5装完验证 VISA 后端:
python -c "import pyvisa; rm = pyvisa.ResourceManager(); print(rm.list_resources())"如果输出空列表,先确认示波器网络能 ping 通(网口)或者 USB 驱动装了 TekVISA。常见做法是网口优先——USB 在截图高频轮询时偶尔会掉线。
4.2 连接配置与目录结构
项目解压后的目录结构大致如下:
scope_auto_test/ ├── main.py # 程序入口 ├── config/ │ └── device.json # VISA地址、截图保存路径、采样间隔 ├── core/ │ ├── scope_driver.py # 示波器指令封装 │ └── test_engine.py # 测试流程编排 ├── ui/ │ ├── main_window.py # PyQt5 主窗口 │ └── widgets.py # 图片显示控件、打印控件 ├── report/ # 自动生成的测试报告目录 └── requirements.txt修改config/device.json里的visa_address为自己的示波器地址:
{ "visa_address": "TCPIP0::192.168.1.100::inst0::INSTR", "screenshot_path": "C:\\temp\\scope_screen.png", "waveform_interval": 0.5, "measure_interval": 0.3 }4.3 启动程序
python main.py启动后界面分三个区域:左边是波形图实时刷新区,右边是测量信息打印区,底部是测试按钮区。先点“连接仪器”,打印区显示*IDN?返回的型号信息,然后点“开始刷新”就能看到波形图动起来。测试按钮区的“一键测试”会执行 3.4 节那个 JSON 里的全部测试项。
跑不起来时排查顺序:先单独跑scope_driver.py下的连接方法,确认 VISA 地址没写错;再跑截图方法,确认C:\temp在示波器上存在(示波器侧也需要建同名目录);最后才开界面。直接把这三步一次跑完,出问题不好定位。
5. 避坑记录:实时刷新卡顿、乱码与兼容性排查
5.1 界面假死,截图时按钮点不动
现象:点击“开始刷新”后界面卡住,鼠标转圈,几秒后恢复。
原因:截图线程没启用,save_screenshot和read_file直接跑在 UI 主线程里,一个轮询周期内阻塞 1 秒多,界面事件循环被卡死。
解决:把图片轮询放进 QThread(参考 3.2 节),主线程只处理信号槽。如果你在别的项目里用的是threading.Thread,记得在run()里不要碰任何QWidget方法,pyqtSignal 是 QThread 跨线程更新的唯一安全姿势。
5.2 中文乱码,测出来的频率显示成乱码
现象:打印区显示ç¹å¾之类的乱码,或者测量值变成负数。
原因:泰克 3000 系列在某些固件版本返回的字符集不是 UTF-8,pyvisa 默认按ascii或utf-8解码会导致字节错位。
解决:在open_resource后显式设置encoding="latin-1",或者读取后手动重新解码。我一般会做双重保险:字符串前后strip(),再判断是否含nan或9.91e37,是的话丢弃本次读数重查一次。这不是工具类代码,这是拿真实示波器测过的血泪经验。
5.3 图片刷新不及时,一直是上一张
现象:波形图内容没变化,但文件时间戳在变。
原因::SAVE:IMAGe:ACTUal触发后立刻读文件,示波器文件系统写入有延迟,读到的是上一张图。另一种可能:连了两台示波器,read_file读的是另一台的路径。
解决:截图指令后至少等 200ms,再把间隔调成 0.5 秒。另外每次read_file前先check_idn()一次确认连接的还是同一台设备。实测 3000 系列在 USB 链路下延迟最高能到 500ms,建议把time.sleep放宽到 0.3 秒更稳。
5.4 更换示波器型号后程序报错
现象:从 Tektronix3000 换成 2000 系列或者其它品牌,提示:"MEASUREMENT:MEAS1:VALUE?" 语法错误。
原因:不同系列支持的 SCPI 子集不一样,2000 系列只支持:MEASure:VPP?这种带问号的短指令,不支持:MEASUREMENT长指令加VALUE?查询的组合。
解决:在驱动类里做一个指令适配层——启动时用*IDN?识别型号,然后给指令模板字典切换。这个改造其实很轻量:每条指令从字符串变成lambda channel: f":MEASUREMENT:MEAS1:SOURCE {channel}"这样的模板,型号变了换模板加载就行。避免“换仪器就改代码”的被动局面,适配层值得一写。
6. 二次开发实战:把单机测试改成可配置的多场景测试台
最后一个改造建议:把测试流程从写死的循环改成配置驱动。我在 3.4 已经用了 JSON 定义测试项,如果要扩展,就再加一个“场景”字段,每个场景是独立的测试项数组,界面下拉框切换场景。这样产线换测试项目时不用改代码,改 JSON 就够了。
另外一个高频需求是阈值判定可视化。目前test_engine.py里测量值只在打印区显示文本,我建议把测量值同时画成实时趋势曲线:pyqtgraph库比matplotlib更适合高频刷新,plot.setData()直接更新曲线数据不重绘坐标轴。这样波形图、趋势图、文本打印三路同时跑,产线测试员一眼能看出信号漂移趋势,比只看数字直观得多。
还要加一个 QA 小技巧:测试结束自动生成报告目录,按时间戳建子目录,里面放 PNG 截图、CSV 测量记录和一个summary.txt。CSV 用csv.writer写 UTF-8 带 BOM,不然 Excel 打开中文列名会乱。截图命名带上测试项名称和数值,方便后续追溯。
从那以后我每次写这类仪器控制程序,都强制先确认“线程边界、编码、超时”三件事再动手。线程边界决定了界面会不会卡死,编码决定数据看着对不对,超时决定了异常时程序会不会挂住。这三件事花十分钟想清楚,后面能省下好几天的调试时间。希望帮到你。
本文还有配套的精品资源,点击获取