☰
基于PyQt5与VISA的泰克示波器自动化测试上位机开发实战
2026/10/21 3:28:27 网站建设 项目流程

简介:这份资源面向使用泰克MDO3014示波器进行自动化测试的开发者与测试工程师,提供一套基于Python与PyQt5搭建的上位机软件示例,解决示波器波形实时显示、图像保存、日志读取与测试用例循环执行等实际需求。压缩包共16个文件,约139KB,包含2个py主程序与1个ui界面文件用于逻辑与界面分离,5个xml及iml、gitignore等IDE工程配置,3张png演示截图,2个txt日志样例,以及pyc缓存文件,结构紧凑便于直接运行与二次开发。资源中详细说明了常用Qt控件在Python中的调用方式,并演示了单次循环执行测试用例的流程,测试用例需读者自行编写。目前已有3659人学习下载,适合希望快速掌握示波器自动化测试框架、界面实时刷新与日志保存思路的读者参考借鉴。

1. 自动化测试上位机:把泰克示波器变成产线工位上的“黑匣子”

产线测试工位上,一台泰克3000系列示波器、一台待测板子、一个操作员,每天要重复几百次“调参数、抓波形、看结果、抄数据”的动作。问题在于,人眼盯波形会疲劳,手动抄数据会写错,测试报告靠Excel拼凑,出了问题连原始波形都找不回来。这个项目要解决的就是这件事:用 Python 写一个上位机,通过 PyQt5 搭界面,走 VISA 协议控制泰克示波器,把“设参数、采波形、判合格、存记录”整条链路自动化。它适合两类人:一类是产线测试工程师,想把手动流程改成半自动或全自动;另一类是想学 PyQt5 上位机 + 仪器控制的开发者,拿它当模板改造成自己设备的控制端。核心看点有三个:PyQt5 实时刷新波形图片、实时刷新打印信息、以及围绕泰克3000系列示波器封装的自动化测试流程。

2. 技术选型与整体架构:为什么是 PyQt5 + VISA + 泰克3000

2.1 技术栈拆解:每一层负责什么

这个项目的技术栈不复杂,但每一层都有明确的职责边界,选错了后面改起来很痛苦。

层级技术选型职责为什么这么选
界面层PyQt5主窗口、按钮、波形显示区、日志区跨平台、控件丰富、信号槽机制适合异步刷新
仪器通信层PyVISA通过 VISA 地址与示波器通信泰克3000系列支持 USBTMC/LAN/GPIB,PyVISA 统一封装
数据采集层泰克3000系列 SCPI 指令设置时基、垂直档位、触发、读取波形SCPI 是仪器控制通用语言,泰克文档齐全
数据处理层NumPy波形数据转换、幅值/频率计算示波器返回的是二进制块,NumPy 处理效率高
图像渲染层Matplotlib 或 PyQtGraph把波形数据画成图PyQtGraph 刷新快,Matplotlib 静态图更清晰
日志与报告logging + openpyxl实时打印信息、导出测试记录标准库 logging 够用,openpyxl 生成 Excel

常见做法是:界面层不直接碰仪器,仪器操作封装成一个独立的类,界面通过信号槽接收数据。这样做的原因是,仪器通信是阻塞的,如果直接在 UI 线程里发 SCPI 指令,界面会卡死。我一般会把仪器操作放到 QThread 里,通过 pyqtSignal 把波形和日志推回主线程。

2.2 项目目录结构:一个可扩展的上位机骨架

拿到源码后,先看目录结构,这决定了你后面改代码时往哪里塞东西。

tek_auto_test/ ├── main.py # 程序入口,启动 QApplication ├── ui/ │ ├── main_window.py # 主窗口类,布局和控件定义 │ └── main_window.ui # Qt Designer 设计的界面文件 ├── instrument/ │ ├── tek_scope.py # 泰克示波器封装类 │ └── visa_manager.py # VISA 资源发现与连接管理 ├── core/ │ ├── test_runner.py # 测试流程编排,QThread 子类 │ ├── waveform.py # 波形数据解析与计算 │ └── report.py # 测试记录导出 ├── config/ │ └── settings.ini # 仪器地址、测试阈值、保存路径 └── requirements.txt # 依赖清单

这个结构的好处是:仪器相关代码全在 instrument 目录,换一台示波器只需要改 tek_scope.py;测试逻辑在 core/test_runner.py,改测试步骤不用动界面;界面在 ui 目录,改布局不影响底层。新手照着这个结构放代码,后面维护会轻松很多。

2.3 仪器连接:VISA 地址怎么找、怎么连

泰克3000系列示波器支持 USB、LAN、GPIB 三种接口。产线上最常见的是 USB 直连,VISA 地址形如USB0::0x0699::0x0401::C012345::INSTR。LAN 接口则是TCPIP0::192.168.1.100::inst0::INSTR。

# instrument/visa_manager.py import pyvisa class VisaManager: def __init__(self): # 创建 VISA 资源管理器,这是所有仪器通信的入口 self.rm = pyvisa.ResourceManager() def list_resources(self): """扫描当前连接的所有 VISA 设备,返回地址列表""" return self.rm.list_resources() def connect(self, address): """根据 VISA 地址打开仪器连接,超时设为 5 秒""" try: inst = self.rm.open_resource(address) inst.timeout = 5000 # 单位毫秒,防止仪器无响应时卡死 inst.read_termination = '\n' inst.write_termination = '\n' return inst except pyvisa.VisaIOError as e: print(f"连接失败: {e}") return None

逻辑说明:list_resources()返回的是当前系统识别到的所有 VISA 地址,界面上可以做一个下拉框让用户选。open_resource()打开连接后,必须设timeout,否则仪器掉线时程序会一直等。read_termination和write_termination设成换行符,是因为泰克示波器的 SCPI 指令以换行结尾。

参数说明:timeout单位是毫秒,产线环境建议 3000 到 5000,太短会误判超时,太长会拖慢测试节拍。VISA 地址里的0x0699是泰克的厂商 ID,0x0401是型号 ID,后面的序列号每台机器不同。

3. 核心模块拆解:波形采集、实时刷新与日志打印

3.1 泰克3000系列 SCPI 指令封装:从设参数到读波形

示波器控制的核心是一组 SCPI 指令。泰克3000系列的指令集在官方程序员手册里有完整列表,这里只列自动化测试最常用的几条。

# instrument/tek_scope.py import numpy as np class TekScope: def __init__(self, inst): self.inst = inst # PyVISA 的仪器对象 def set_channel(self, ch, scale, offset=0, coupling='DC'): """设置通道垂直档位和偏移 ch: 通道号,1-4 scale: 每格电压,如 1 表示 1V/div offset: 垂直偏移,单位 V coupling: 耦合方式,DC/AC """ self.inst.write(f'CH{ch}:SCALE {scale}') self.inst.write(f'CH{ch}:OFFSET {offset}') self.inst.write(f'CH{ch}:COUPLING {coupling}') self.inst.write(f'CH{ch}:PROBE 1') # 探头衰减比,1X 或 10X def set_timebase(self, scale): """设置水平时基,scale 单位秒/格""" self.inst.write(f'HORizontal:SCALE {scale}') def set_trigger(self, source='CH1', level=1.0, edge='RISe'): """设置边沿触发 source: 触发源通道 level: 触发电平,单位 V edge: 上升沿 RISe 或下降沿 FALL """ self.inst.write(f'TRIGger:A:EDGE:SOURce {source}') self.inst.write(f'TRIGger:A:EDGE:SLOPe {edge}') self.inst.write(f'TRIGger:A:LEVel {level}') self.inst.write('TRIGger:A:MODe NORMal') # 普通触发模式 def read_waveform(self, ch): """读取指定通道的波形数据,返回时间轴和电压数组""" # 选择数据源 self.inst.write(f'DATa:SOUrce CH{ch}') # 设置数据格式为二进制,传输效率最高 self.inst.write('DATa:ENCdg RIBinary') self.inst.write('DATa:WIDth 2') # 2 字节采样 # 读取前导信息,包含采样率、点数、垂直缩放系数 preamble = self.inst.query('WFMPre?') # 读取原始波形数据块 raw = self.inst.query_binary_values('CURVe?', datatype='h', is_big_endian=True) # 解析前导信息 parts = preamble.split(';') ymult = float(parts[13]) # 垂直缩放系数 yoff = float(parts[14]) # 垂直偏移 xincr = float(parts[9]) # 水平采样间隔 yzero = float(parts[15]) # 垂直零点 # 转换为实际电压值 voltage = (np.array(raw) - yoff) * ymult + yzero time = np.arange(len(voltage)) * xincr return time, voltage

逻辑说明:set_channel和set_timebase是测试前的参数配置,产线换型号时改这几个值就行。read_waveform是核心,先设DATa:SOUrce选通道,再设ENCdg RIBinary用二进制传输,比 ASCII 快几十倍。WFMPre?返回的前导信息里包含缩放系数,必须用它把原始整数转成电压值,否则读出来的数据没有物理意义。

参数说明:ymult是每个 ADC 码对应的电压值,yoff是垂直偏移的码值,yzero是零点电压。这三个参数每次改垂直档位都会变,所以每次读波形都要重新查WFMPre?。xincr是采样间隔,乘以点数就是整个屏幕的时间窗口。

3.2 实时刷新波形图片:QThread + pyqtSignal 的正确用法

实时刷新是这类上位机最容易翻车的地方。新手常犯的错误是在 UI 线程里循环读波形,结果界面直接卡死。正确做法是把采集放到 QThread 里,通过信号把数据推回主线程渲染。

# core/test_runner.py from PyQt5.QtCore import QThread, pyqtSignal import time class TestRunner(QThread): # 定义信号:波形数据、日志文本、测试结果 waveform_ready = pyqtSignal(object, object) # time, voltage log_message = pyqtSignal(str) test_finished = pyqtSignal(bool, str) def __init__(self, scope, interval=0.5): super().__init__() self.scope = scope self.interval = interval # 刷新间隔,单位秒 self._running = True def run(self): """线程主循环:采集波形并发射信号""" while self._running: try: time_data, voltage = self.scope.read_waveform(1) self.waveform_ready.emit(time_data, voltage) # 计算峰峰值和频率,用于判定 vpp = voltage.max() - voltage.min() self.log_message.emit(f'Vpp={vpp:.3f}V, 点数={len(voltage)}') time.sleep(self.interval) except Exception as e: self.log_message.emit(f'采集异常: {e}') self.test_finished.emit(False, str(e)) break def stop(self): """安全停止线程""" self._running = False self.wait()

逻辑说明:pyqtSignal(object, object)里的 object 是为了传 NumPy 数组,PyQt5 原生信号不支持 ndarray,用 object 包一层。run()里是死循环,每次采集完emit信号,主线程收到后刷新图表。stop()里先置标志位再wait(),保证线程安全退出。

参数说明:interval控制刷新频率,产线测试一般 0.2 到 1 秒。太快了示波器响应不过来,太慢了操作员觉得卡。waveform_ready信号连接主窗口的槽函数,槽函数里用setData更新曲线,不要重新创建图表对象,否则内存会涨。

主窗口里的连接方式:

# ui/main_window.py class MainWindow(QMainWindow): def __init__(self): super().__init__() self.runner = None # 初始化图表 self.plot_widget = pg.PlotWidget() self.curve = self.plot_widget.plot(pen='y') # 日志文本框 self.log_text = QTextEdit() self.log_text.setReadOnly(True) def start_test(self): """点击开始按钮时调用""" self.runner = TestRunner(self.scope, interval=0.5) self.runner.waveform_ready.connect(self.update_waveform) self.runner.log_message.connect(self.append_log) self.runner.test_finished.connect(self.on_finished) self.runner.start() def update_waveform(self, time_data, voltage): """槽函数:在主线程刷新波形""" self.curve.setData(time_data, voltage) def append_log(self, msg): """槽函数:追加日志,自动滚动到底部""" self.log_text.append(f'[{time.strftime("%H:%M:%S")}] {msg}') self.log_text.moveCursor(QTextCursor.End)

逻辑说明:update_waveform和append_log都在主线程执行,因为信号槽默认是队列连接,跨线程时自动排队。setData只更新数据不重建对象,这是 PyQtGraph 高性能刷新的关键。append_log里加时间戳,方便回溯。

3.3 实时刷新打印信息:日志分级与界面同步

实时打印信息不只是往文本框里塞字符串,产线环境需要区分普通信息、警告和错误。用 Python 标准库 logging 做分级,再通过自定义 Handler 把日志推到界面。

# core/logger.py import logging from PyQt5.QtCore import QObject, pyqtSignal class QtLogHandler(logging.Handler, QObject): """自定义日志处理器,把 logging 记录转发到 PyQt 信号""" log_signal = pyqtSignal(str, str) # level, message def __init__(self): logging.Handler.__init__(self) QObject.__init__(self) def emit(self, record): msg = self.format(record) self.log_signal.emit(record.levelname, msg) def setup_logger(): logger = logging.getLogger('tek_test') logger.setLevel(logging.DEBUG) handler = QtLogHandler() handler.setFormatter(logging.Formatter('%(message)s')) logger.addHandler(handler) return logger

逻辑说明:QtLogHandler同时继承logging.Handler和QObject,这样既能接收 logging 的记录,又能发射 PyQt 信号。emit方法里把日志级别和内容一起发出去,界面根据级别用不同颜色显示。

参数说明:levelname是 DEBUG/INFO/WARNING/ERROR,界面上可以用setTextColor区分。setFormatter里可以加时间、模块名,产线调试时建议加上。

界面里的颜色区分:

def append_log(self, level, msg): color_map = {'DEBUG': '#888888', 'INFO': '#000000', 'WARNING': '#FF8800', 'ERROR': '#FF0000'} self.log_text.setTextColor(QColor(color_map.get(level, '#000000'))) self.log_text.append(f'[{level}] {msg}')

这样操作员一眼就能看到红色错误,不用在几百行日志里找问题。

4. 避坑与常见问题:仪器控制上位机的血泪经验

4.1 波形读回来全是零或者乱码

现象:read_waveform返回的数组全是 0,或者数值明显不对。

原因:最常见的是DATa:ENCdg设成了 ASCII 但用二进制方式解析,或者WFMPre?的前导信息字段位置对不上。泰克不同型号的前导字段顺序略有差异,3000系列和2000系列就不完全一样。

解决:先打印WFMPre?的原始返回,数一下分号分隔的字段。ymult一般在第 13 个字段,但保险做法是用WFMPre:YMUlt?、WFMPre:YOFF?、WFMPre:YZERO?单独查询,不依赖整体解析。

4.2 界面刷新几次后卡死

现象:程序跑几分钟后界面无响应,日志也不再更新。

原因:在update_waveform里每次clear()再plot(),导致图表对象不断创建,内存和绘图开销暴涨。或者TestRunner线程里直接操作了 UI 控件。

解决:曲线对象只创建一次,后续只调setData。所有 UI 操作必须通过信号槽回到主线程,线程里只做数据采集和计算。

4.3 VISA 连接超时或找不到设备

现象:list_resources()返回空列表,或者open_resource报VisaIOError。

原因:USB 驱动没装(需要泰克官方驱动或 NI-VISA),或者仪器被其他程序占用(比如泰克自己的软件还开着)。

解决:先确认设备管理器里能看到示波器,再确认没有其他程序占用。LAN 连接时检查 IP 是否可达,防火墙是否拦了 VISA 端口。

4.4 测试节拍太慢,产线等不及

现象:单次测试要 3 到 5 秒,产线要求 1 秒以内。

原因:每次读波形都重新查WFMPre?,或者用了 ASCII 编码传输,或者timeout设得太长。

解决:参数不变时缓存前导信息,只在改档位后重新查。用RIBinary二进制传输。timeout设 2000 毫秒够用。另外把interval降到 0.2 秒,但要注意示波器采集需要时间,太快会读到重复数据。

4.5 打包成 exe 后仪器连不上

现象:源码运行正常,PyInstaller 打包后找不到 VISA 库。

原因:PyVISA 依赖的 VISA 动态库没有被正确打包,或者打包时没包含pyvisa的 backend。

解决:在 spec 文件里加hiddenimports=['pyvisa', 'pyvisa_py'],并把 VISA 的 DLL 一起打包。更稳妥的做法是目标机器单独装 VISA 运行时,exe 只带 Python 依赖。

5. 二次开发与改造:从“能跑”到“好用”的几个关键动作

拿到这个项目后,最值钱的改造方向不是加功能,而是让它适应你自己的测试场景。我一般会从三个地方下手。

第一,把测试参数和判定阈值抽到配置文件。项目里config/settings.ini只放了仪器地址,实际上应该把通道档位、时基、触发电平、Vpp 上下限、频率范围全放进去。这样换产品型号时只改配置文件,不用动代码。用configparser读进来,在TestRunner初始化时传进去。

第二,把单次采集改成“采集-判定-记录”的完整闭环。现在的TestRunner只负责刷新波形,实际产线需要每次采集后自动判定合格与否,并把波形图片、测量值、时间戳存到指定目录。可以在run()里加判定逻辑:

def judge(self, voltage): """根据阈值判定合格与否,返回 (结果, 描述)""" vpp = voltage.max() - voltage.min() if vpp < self.vpp_min: return False, f'Vpp {vpp:.3f}V 低于下限 {self.vpp_min}V' if vpp > self.vpp_max: return False, f'Vpp {vpp:.3f}V 高于上限 {self.vpp_max}V' return True, f'Vpp {vpp:.3f}V 合格'

判定结果通过test_finished信号发出去,界面用绿色/红色背景显示。同时把不合格的波形数据存成.npy或.csv,方便后面追溯。

第三,加一个“历史记录”页面。产线最怕的是出了问题找不到原始数据。用 SQLite 存每次测试的记录,字段包括时间、产品序列号、Vpp、频率、判定结果、波形文件路径。PyQt5 的QTableWidget直接展示,双击某一行能回放当时的波形。这个功能加上去,整个项目的价值就从“自动化测试工具”变成了“测试数据管理系统”。

验证改造是否成功,我习惯用三步:第一步,用信号发生器产生一个已知幅值和频率的正弦波,看测量值是否在误差范围内;第二步,故意把阈值设成不可能通过的值,确认判定逻辑会报不合格;第三步,连续跑 100 次,看内存有没有涨、日志有没有丢、波形文件有没有漏存。

从那以后我每次改完仪器控制代码,都强制走一遍“已知信号验证 + 边界判定 + 连续跑”的流程,不再凭感觉觉得“应该没问题”。希望这个项目的拆解能帮到你,拿到源码后先跑通连接和波形刷新,再按自己的产线需求改判定和记录,比一上来就大改架构要稳得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询