资讯动态

Python VISA控制USB可编程电源:SCPI设压回读与产线自动化

发布时间:2026/11/6 6:06:33 来源:尧图企业网站定制
简介面向电子测试与自动化测量场景的USB通讯控制艾德克斯ITECH直流电源实践工程适合已具备C#基础的测试工程师、实验室开发者与仪器程控学习者用于解决电脑远程设定电压电流、回读测量数据并搭建自动化测试流程的问题。压缩包为rar格式共14个文件、约19KB以cs源码与resx窗体资源为核心辅以csproj项目文件、sln解决方案、config配置及settings、suo等工程附属文件构成一个可直接打开的Visual Studio解决方案整体结构紧凑、轻量。已有3951人学习下载。包内包含仪器USB通讯封装、界面与主程序入口等模块可作为电源指令收发、恒压恒流设置、数据采集与过压过流保护监控的参考实现便于改写为批量测试脚本或集成进产线测试系统理解USB通讯流程与软件分层思路。1. 从一台USB可编程电源说起这个项目能解决什么产线老化架上摆着几台可编程直流电源测试员每天手动改电压、限流、开关输出USB通讯本来能把这些动作串成脚本但资源字符串、指令终止符、回读校验任何一处没对齐自动化就会翻车。这个项目把“发现设备、设置电压电流、开输出、回读校验、异常复位”做成一条可复用链路适合两类人一类是要把手动调电源变成自动测试序列的测试开发另一类是想拿现成工程改造成多型号工装的熟手。它不是驱动开发教程而是一套能落地的USB仪器控制骨架。2. 技术选型与整体架构VISA 会话、USBTMC 和 SCPI 怎么串起来USB 可编程电源的控制链路表面看只是“打开设备、发几条指令”真正落到产线工装里麻烦的是设备发现、会话生命周期、超时重试和异常恢复。这个项目选的是 Python 侧的 VISA 绑定库加 USBTMC 传输再用 SCPI 文本指令控制电源。VISA 负责把不同厂商、不同物理接口的仪器抽象成统一资源字符串USBTMC 是 USB 设备类协议负责把 SCPI 指令封装成 USB 事务SCPI 则是电源能听懂的指令集比如设压、限流、开输出、读回值。三层各管一件事代码才不会写成一大坨write/read。2.1 为什么用 VISA 会话而不是直接写 USB 裸包直接写 USB 裸包不是不行但代价很高。USB 描述符、端点读写、超时、控制传输、设备重连都要自己处理换一台同类型电源VID/PID 和端点可能又变了。VISA 会话把这件事收拢成资源字符串和open_resource业务代码只关心“我要给哪台设备发什么 SCPI”。对于产线工装稳定比炫技重要能少维护一层 USB 协议栈就少一层。常见做法是用 VISA 运行时提供底层驱动Python 侧只保留薄薄一层封装如果机器上没装完整运行时也可以走纯 Python 后端但 USBTMC 设备兼容性要逐台验证不能假设所有电源都一样。这套工程把通信层和业务层拆开通信层只管打开、关闭、写入、查询、重试业务层只管“先关输出、再设限流、再设电压、最后开输出”这类安全顺序。这样拆的好处是后面换型号时只改命令映射不动测试序列。血泪经验是一旦把time.sleep和query散落在业务脚本里后面查超时问题会像在黑匣子里摸线。2.2 分层架构配置、会话、设备、序列四层工程按四层来摆。配置层放资源字符串、超时、重试次数和测试步进会话层封装 VISA 打开、终止符、清缓冲和重连设备层把 SCPI 指令包成set_voltage、set_current、output_on、measure_voltage这些方法序列层读配置按步骤执行并落日志。这个分层不复杂但能让“设备连不上”和“测试逻辑写错”分开排查。比如资源列表为空查会话层和系统权限回读值和设定值偏差大查设备层量程和线损步骤顺序不对查序列层配置。资源发现我一般单独做成一个小命令不跟业务序列混在一起。先列出所有 USB 资源再对每个资源发*IDN?确认身份。不要一上来就写死资源字符串换一台机器、换一个 USB 口字符串可能就变了。下面这段是发现逻辑示例里的 VID/PID 和序列号都是占位实际以现场枚举结果为准。import pyvisa rm pyvisa.ResourceManager() for res in rm.list_resources(): if not res.startswith(USB): continue inst None try: # 先给短超时避免某个异常设备卡住整个枚举 inst rm.open_resource(res, timeout1500) inst.write_termination \n inst.read_termination \n idn inst.query(*IDN?).strip() print(fresource{res}) print(fidn{idn}) except Exception as exc: print(fskip {res}: {exc}) finally: if inst is not None: try: inst.close() except Exception: pass这段代码只做三件事过滤 USB 资源、设置读写终止符、用*IDN?确认设备身份。timeout1500是毫秒枚举阶段不宜太长否则一台掉线设备会把整个列表拖住write_termination和read_termination都设为换行是因为多数电源的 SCPI 解析按行结束但少数型号要求\r\n现场要以手册和实测为准。finally里关闭会话是为了避免枚举几次之后句柄泄漏。2.3 设备身份确认与资源字符串的取舍资源字符串通常长这样USB0::0x0000::0x0000::SN0001::INSTR其中 VID、PID、序列号都是示例占位。实际工程里我不建议把整串写死在业务代码里而是放到配置文件启动时先按*IDN?匹配型号和序列号匹配不上就报错退出。这样换机器时只改配置不改代码多台电源同型号时用序列号区分不靠 USB 口顺序。USB 口顺序在重启、换 hub 之后可能变化靠口序号认设备是典型的踩坑点。设备层的方法命名要直白不要用do_cmd这种万能入口。set_voltage里就只做设压output_on里就只做开输出异常在会话层统一重试。这样日志里能看到“哪一步失败”而不是只看到一条长指令。序列层则把一次测试拆成多个步骤每一步记录设定值、回读值、时间戳和错误队列。后面做产线追溯这些日志比截图有用。3. 目录结构与通信底座资源发现、超时重试和配置驱动这套工程不大但目录要一开始就分清否则脚本长到几百行之后改一个超时参数都要翻半天。下面是我会用的目录摆法核心原则是“仪器通信”和“业务序列”分开“配置”和“代码”分开“测试”单独放。usb-power-tool/ ├── app/ │ ├── main.py # 命令行入口discover、run、check │ ├── visa_session.py # VISA 会话、超时、重试、重连 │ ├── power_supply.py # 电源设备层设压、限流、开关、回读 │ ├── sequence.py # 测试序列执行与日志落盘 │ └── config.py # 配置加载与校验 ├── config/ │ └── bench.yaml # 资源字符串、超时、步进 ├── tests/ │ ├── test_parse_idn.py # IDN 解析与型号匹配 │ └── test_sequence_order.py ├── requirements.txt └── README.mdapp/main.py只做参数解析和命令分发不写具体 SCPIvisa_session.py管会话生命周期power_supply.py管设备动作sequence.py管步骤顺序和结果记录。这个摆法看起来多几个文件但排查时很省事资源列表为空就看会话层回读不对就看设备层步骤顺序错就看序列层。3.1 会话封装超时、终止符、清缓冲和重试会话层要处理四件事打开资源、设置终止符、清输入缓冲、失败重连。超时不能太短设压之后电源内部需要响应时间query还没返回就超时会误判也不能太长设备掉线时脚本会卡住。我一般把普通写入超时放在 3000 毫秒回读查询放在 5000 毫秒枚举阶段单独给 1500 毫秒。重试次数不要无限加两次足够第三次还失败就说明不是偶发问题应该让脚本停下来报错。import time import threading import pyvisa class VisaSession: def __init__(self, resource: str, timeout_ms: int 3000, read_timeout_ms: int 5000, retries: int 2): self.resource resource self.timeout_ms timeout_ms self.read_timeout_ms read_timeout_ms self.retries retries self.rm None self.inst None self.lock threading.Lock() def open(self): self.rm pyvisa.ResourceManager() self.inst self.rm.open_resource( self.resource, timeoutself.timeout_ms ) self.inst.write_termination \n self.inst.read_termination \n self.inst.clear() # 用一次轻量查询确认链路真的通了 self.inst.query(*IDN?) def write(self, cmd: str): with self.lock: last_exc None for attempt in range(self.retries 1): try: self.inst.write(cmd) return except pyvisa.errors.VisaIOError as exc: last_exc exc if attempt self.retries: raise self.reopen() def query(self, cmd: str) - str: with self.lock: last_exc None for attempt in range(self.retries 1): try: self.inst.timeout self.read_timeout_ms return self.inst.query(cmd).strip() except pyvisa.errors.VisaIOError as exc: last_exc exc if attempt self.retries: raise self.reopen() raise last_exc def reopen(self): # 重连前先尽量关掉旧会话避免句柄泄漏 try: if self.inst is not None: self.inst.close() except Exception: pass time.sleep(0.3) self.inst self.rm.open_resource( self.resource, timeoutself.timeout_ms ) self.inst.write_termination \n self.inst.read_termination \n self.inst.clear() def close(self): if self.inst is not None: self.inst.close() self.inst None这段会话封装的重点不在重试本身而在锁和重连顺序。lock保证同一时刻只有一个线程操作会话产线工装里如果多个工位共用一个进程不加锁会出现“查询返回上一条指令结果”的玄学问题。reopen里先关旧句柄再睡 300 毫秒是给系统一点释放时间如果不关旧句柄直接开新会话某些系统上会报资源被占用。write和query分别设置超时是因为查询通常比写入慢统一超时会让回读容易误超时。参数上timeout_ms管写入read_timeout_ms管查询retries管单次操作的重试次数不建议超过 3。3.2 配置驱动把电压电流步进从代码里挪出去测试步骤写死在代码里改一个电压就要重新发版这种设计在实验室能忍在产线不能忍。配置用 YAML 就够字段不要多能表达资源、超时和步骤即可。下面是一个示例resource仍然是占位实际填现场枚举到的字符串。步骤里保留hold_s是因为电源输出建立和负载稳定需要时间设完立刻回读往往读到旧值。resource: USB0::0x0000::0x0000::SN0001::INSTR timeout_ms: 3000 read_timeout_ms: 5000 retries: 2 steps: - name: 低压预置 voltage: 5.0 current: 0.2 hold_s: 0.5 - name: 满载点 voltage: 12.0 current: 1.5 hold_s: 1.0 - name: 回零 voltage: 0.0 current: 0.1 hold_s: 0.2加载配置时要做两件事检查voltage和current是否在电源量程内检查步骤名是否重复。量程检查不能省设一个超过额定值的电压有的电源会直接报错有的会钳位到最大值后者更危险因为日志看起来“设置成功”实际输出不是你要的。配置里的hold_s不是随便写的延时它是回读稳定窗口后面做回读断言时要和容差一起看。参数上timeout_ms控制写入响应read_timeout_ms控制回读等待retries控制偶发 USB 抖动的重试次数。提示配置文件和代码分开之后现场改参数不需要动 Python 文件但一定要在启动时做一次 schema 校验别让拼错的字段静默变成默认值。4. 核心模块拆解限流、设压、开输出与回读校验的安全顺序电源控制最怕的不是指令发不出去而是顺序错了还把错误当成成功。比如先开输出再设电压负载端可能先吃到上一次残留电压设完电压立刻回读读到的是旧值回读值偏差大却只打印不报错后面整批数据都不可信。这个项目的设备层把动作收成几个方法序列层只按安全顺序调用。下面这段是设备层的核心骨架指令是常见 SCPI 写法实际型号如果有差异就在命令映射里改不要把差异散落到业务代码。import time class PowerSupply: def __init__(self, session, voltage_tol0.05, current_tol0.02): self.session session self.voltage_tol voltage_tol self.current_tol current_tol def identify(self): return self.session.query(*IDN?) def output_off(self): self.session.write(OUTP OFF) def output_on(self): self.session.write(OUTP ON) def set_current_limit(self, current: float): # 先设限流避免设压瞬间电流失控 self.session.write(fCURR {current:.3f}) def set_voltage(self, voltage: float): self.session.write(fVOLT {voltage:.3f}) def measure_voltage(self) - float: return float(self.session.query(MEAS:VOLT?)) def measure_current(self) - float: return float(self.session.query(MEAS:CURR?)) def safe_set(self, voltage: float, current: float, hold_s: float 0.5): self.output_off() self.set_current_limit(current) self.set_voltage(voltage) time.sleep(0.05) self.output_on() time.sleep(hold_s) v self.measure_voltage() i self.measure_current() if abs(v - voltage) self.voltage_tol: raise RuntimeError( fvoltage mismatch set{voltage} read{v} ) if i current self.current_tol: raise RuntimeError( fcurrent over limit set{current} read{i} ) return v, isafe_set的顺序是“关输出、设限流、设压、开输出、等稳定、回读、断言”。关输出放在最前面是为了清掉上一次步骤的残留输出先设限流再设压是为了在设压瞬间把电流上限压住开输出后等hold_s是给负载和电源环路建立时间。voltage_tol和current_tol是容差不能设成 0线损、采样精度和负载波动都会让回读有偏差。电压容差 0.05V、电流容差 0.02A 只是示例现场要按电源精度和线缆压降调。回读电流只做上限判断是因为负载电流由负载决定不能要求它等于设定限流值。4.1 错误队列要主动读不要等下一次查询翻车SCPI 电源一般有错误队列指令语法错、参数超范围、输出被保护都可能进队列。如果只发不读错误会堆着下一次query可能返回一个莫名其妙的结果。我一般在每个步骤结束后读一次SYST:ERR?读到 0 就停最多读 5 条防止设备异常时死循环。下面这段是错误队列清理逻辑放在safe_set之后或者序列步骤结束时调用都行。def drain_errors(session, limit5): errors [] for _ in range(limit): raw session.query(SYST:ERR?) code_str, msg raw.split(,, 1) code int(code_str) if code 0: break errors.append((code, msg.strip())) return errorslimit5是保护上限正常步骤不会有这么多错误SYST:ERR?返回格式通常是“错误码,描述”不同型号描述可能带引号所以只按第一个逗号切分。读到 0 表示队列空直接退出。这个函数返回错误列表序列层把它写进日志不要只打印到控制台。产线追溯时错误码比“失败了”三个字有用得多。4.2 回读断言与日志别把“设过了”当成“设对了”回读断言要分清楚“设定值”和“测量值”。设压成功只代表指令被接受不代表输出端真的到了目标值。负载很轻时电源可能进入恒压模式电压接近设定值负载很重时可能进入恒流模式电压被拉低这时候电压断言会失败但失败是合理的说明测试条件不对。日志里要同时记录设定电压、设定限流、回读电压、回读电流、输出状态和错误队列。输出状态可以用OUTP?查询返回 0/1 或 OFF/ON不同型号格式不同解析时要做兼容。序列层执行时我一般把每一步包在try/except里异常时先关输出再读错误队列最后把步骤名和异常写进结果文件。关输出放在异常处理里是为了不让故障状态一直挂在负载上。结果文件用 CSV 或 JSON 都行字段固定后面做数据分析不用再猜列名。参数上hold_s不要拍脑袋负载是大电容还是纯阻性稳定时间差很多先用示波器或电源本身的高速采样看一眼建立过程再定这个值。5. 避坑与排查USB 权限、指令终止符和回读异常的五条记录USB 仪器控制的问题现场表现往往很像“玄学”有时能连有时连不上有时查询正常有时返回上一条结果有时跑一晚没事第二天一上电就超时。下面五条是这类项目里最常见的踩坑记录每条按现象、原因、解决写。先把排查顺序固定下来先看资源列表再看会话能否打开再看*IDN?再看单条设压回读最后才跑完整序列。顺序反过来容易在业务代码里绕圈。5.1 资源字符串与权限类问题现象一list_resources()返回空列表或打开资源时报“设备未找到”。原因通常不是代码而是系统层没认到 USBTMC 设备或者当前用户没有访问权限。Windows 上常见是驱动没装好设备管理器里能看到未知设备Linux 上常见是 udev 规则没配普通用户没有 USB 设备节点权限。解决方法是先用系统工具确认设备枚举再装对应 VISA 运行时或 USBTMC 驱动Linux 下把当前用户加到有权访问该设备的组或者补一条只针对该 VID/PID 的 udev 规则然后重新插拔。不要用sudo跑整个测试脚本权限问题是解决了但日志和文件所有者会乱后面更麻烦。现象二昨天还能用的资源字符串今天打开就报错。原因多半是资源字符串写死了 USB 口顺序或序列号变了。USB 口换位置、经过 hub、设备重启后枚举顺序变化USB0、USB1这样的前缀可能对不上。解决方法是启动时先枚举资源对每个 USB 资源发*IDN?按型号和序列号匹配目标设备匹配成功后再把实际资源字符串写进会话。配置文件里可以保留一个“期望序列号”但不要只依赖端口号。多台同型号电源时这一步尤其重要否则脚本可能把指令发到隔壁工位那台电源上。5.2 指令与回读类问题现象三query超时或者读回来的值是上一条指令的结果。原因通常是终止符不一致、发送后没等设备处理完、输入缓冲里有残留。有的电源要求\n有的要求\r\n还有的查询必须先write再read不能直接用query。解决方法是先确认手册里的终止符统一设置write_termination和read_termination打开会话后做一次clear()查询前如果怀疑有残留先读一次或清缓冲。超时时间不要一刀切写入和查询分开设。还有一点query之后不要立刻发下一条设压指令给电源几十毫秒处理时间尤其是老型号。现象四设压成功但回读电压偏差很大或者电流一开就超限。原因可能是输出还没开、量程不对、线损太大、负载进入恒流模式或者回读的是远端采样点而不是输出端。解决方法是先查OUTP?确认输出状态再查设定值和量程短线测试时把万用表接到负载端比对判断是电源采样偏差还是线损如果负载电流达到限流值电压被拉低是正常现象应该改测试条件而不是改容差。回读断言不要只比电压还要看电流是否超限。容差设得太紧线损和纹波会让脚本频繁误报设得太松故障又漏过去我一般先用一组已知负载标定再定容差。5.3 稳定性与恢复类问题现象五短时间跑正常连续跑一两个小时偶发断联重插 USB 又好了。原因可能是 USB 线材质量、hub 供电、电源本身 USB 接口休眠或者脚本里会话没有正确关闭导致句柄泄漏。解决方法是换短一点的屏蔽线绕开无源 hub优先直连主机后置 USB 口在会话层加重连和重试但重试次数不要无限每次异常恢复后先关输出、读错误队列、再重新*IDN?确认身份。长跑测试还要记录每次重连的时间点如果重连越来越频繁多半是硬件链路问题不是软件重试能兜住的。注意异常恢复时不要只重开会话就继续跑下一步先把输出关掉确认负载安全再决定是从当前步骤重试还是整段序列重跑。6. 二次开发从单台电源脚本改成产线工装的三步改造单台电源脚本跑通之后二次开发最值得做的不是加界面而是把“型号差异”和“测试序列”拆开。第一步做命令适配器按*IDN?里的型号前缀匹配一组 SCPI 命令把set_voltage、output_on、measure_voltage映射到不同写法业务层只调统一接口。第二步把序列配置化YAML 里只写步骤名、电压、限流、保持时间和容差执行器负责顺序和日志。第三步加结果落盘和模拟器没有电源时用模拟会话返回固定回读值先把序列逻辑和异常分支跑通再上真机。改造点目的验证方法命令适配器同型号差异不污染业务代码用两台不同型号各跑一遍设压回读序列配置化现场改步骤不改代码改 YAML 后不重启进程也能重载模拟会话无硬件时测异常分支人为让查询超时看是否关输出并记日志结果落盘产线追溯和数据分析每步都有设定值、回读值、错误码、时间戳改造顺序我一般按“先日志、再适配器、最后界面”排。日志先行是因为没有日志的工装就是黑匣子适配器第二是因为多型号是迟早的事界面放最后命令行能跑通再包一层返工最少。压缩包里保留了这套目录和一个可跑的示例配置先把资源发现和回读校验跑通再改业务序列比一上来就接产线负载稳得多。从那以后我每次接新电源都先跑一遍资源发现和回读校验再让脚本碰输出开关。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑