2、Python基础回顾:数据类型、控制流、函数、面向对象编程、异常处理、文件操作
在测控技术仪器驱动开发中,Python 之所以成为首选语言,不仅因为其语法简洁,更在于它提供了从底层寄存器操作到上层测试流程管理的完整生态。本章并非简单的语法罗列,而是围绕“仪器控制编程”这一实际场景,系统梳理六大核心语法模块:数据类型、控制流、函数、面向对象编程、异常处理与文件操作。我们将重点讨论这些语法特性如何映射到 GPIB 总线读写、SCPI 命令解析、仪器状态轮询、测试数据记录等真实工程任务中。
上图展示了本章六大知识模块与仪器控制应用场景之间的映射关系。在后续内容中,我们将逐一深入每个模块,并结合数字万用表(DMM)、示波器、信号源等典型仪器的控制代码示例,帮助您建立从 Python 语法到测控工程实践的完整知识链。
2.1 数据类型:仪器数据的表达与转换
核心原理
仪器控制编程中,数据类型的选择直接影响通信效率和代码健壮性。Python 内置的 int、float、str、bytes、list、dict 等类型覆盖了绝大多数测控场景。例如,SCPI 命令通常以字符串形式发送,而仪器返回的测量值可能是 ASCII 编码的浮点数字符串,也可能是二进制数据块。理解这些类型的底层存储方式和转换规则,是避免数据解析错误的前提。
| 数据类型 | 典型仪器控制场景 | 注意事项 |
|---|---|---|
int | 设置仪器地址、读取状态寄存器值 | Python 3 中 int 无上限,但仪器协议可能限制为 16/32 位 |
float | 读取电压/电流测量值、设置频率/幅度 | 注意浮点精度,比较时使用 math.isclose() |
str | SCPI 命令构造、错误信息解析 | 编码格式统一为 ASCII 或 UTF-8 |
bytes | 二进制波形数据、固件升级文件 | 注意字节序(大端/小端) |
list | 批量读取多点测量数据 | 内存占用随数据量线性增长 |
dict | 仪器配置参数映射、状态码查询 | 键值对访问速度快,适合查表 |
判断方法或操作流程
在编写仪器驱动时,应遵循以下数据转换流程:
- 识别数据来源:是用户输入、配置文件、还是仪器返回?
- 确定目标类型:根据后续操作(算术运算、字符串拼接、二进制传输)决定转换方向。
- 执行转换并校验:使用
int()、float()、str()、bytes()等内置函数,必要时包裹try-except处理异常。 - 验证边界值:例如仪器返回
"1.234E+03",需确认float()能正确解析科学计数法。
案例拆解:解析数字万用表返回的测量值
假设通过 GPIB 总线读取一台数字万用表(DMM)的直流电压读数,仪器返回的原始数据为 b'1.23456789E+00\r\n'。我们需要将其转换为浮点数并四舍五入到 4 位小数:
raw_data = b'1.23456789E+00\r\n'
# 去除尾部换行符并解码为字符串
str_data = raw_data.decode('ascii').strip()
# 转换为浮点数
voltage = float(str_data)
# 四舍五入到 4 位小数
voltage_rounded = round(voltage, 4)
print(f"测量电压: {voltage_rounded} V")
此案例展示了 bytes → str → float 的典型转换链路。实际工程中,还需考虑仪器返回 "NAN" 或 "INF" 等特殊值,此时 float() 会抛出 ValueError,应提前判断。
bytes 类型执行字符串操作(如 split())会导致 TypeError。务必先解码为 str。另外,不要使用 == 直接比较浮点数,应使用 abs(a-b) < 1e-6 或 math.isclose()。2.2 控制流:仪器状态轮询与条件判断
核心原理
仪器控制中,控制流用于实现三种基本逻辑:顺序执行(初始化→配置→触发→读取)、条件分支(根据仪器状态决定下一步操作)、循环迭代(连续采样、等待操作完成)。Python 的 if/elif/else、for、while 以及 break/continue 提供了完整的流程控制能力。
判断方法或操作流程
设计仪器控制流程时,建议遵循以下步骤:
- 绘制状态图:明确仪器的空闲、测量、完成、错误等状态。
- 选择循环类型:已知迭代次数用
for,未知次数用while。 - 设置超时机制:避免因仪器无响应导致程序死循环。
- 处理嵌套逻辑:保持代码缩进清晰,避免过深嵌套。
案例拆解:等待示波器测量完成
示波器执行单次采集后,需要查询其操作完成状态。以下代码使用 while 循环轮询状态寄存器:
import time
import pyvisa
rm = pyvisa.ResourceManager()
scope = rm.open_resource('USB0::0x1234::0x5678::INSTR')
# 发送单次触发命令
scope.write(':SINGLE')
# 轮询操作完成状态
timeout = 5 # 5秒超时
start = time.time()
while time.time() - start < timeout:
status = scope.query('*OPC?').strip()
if status == '1':
print("测量完成")
break
time.sleep(0.1) # 避免占用过多CPU
else:
print("超时,仪器未响应")
scope.write('*RST') # 复位仪器
此案例中,while...else 结构在循环正常结束时执行 else 块,用于处理超时场景。这是 Python 特有的语法,非常适合仪器控制中的超时检测。
pyvisa 的 wait_for_srq() 方法实现中断驱动。2.3 函数:模块化仪器操作
核心原理
函数是代码复用的基础单元。在仪器驱动开发中,将重复出现的操作(如发送命令、读取响应、解析数据)封装为函数,可以显著提高代码可维护性。Python 函数支持位置参数、关键字参数、默认参数、可变参数(*args/**kwargs),以及 lambda 匿名函数。
判断方法或操作流程
设计仪器控制函数时,应遵循单一职责原则:
- 识别原子操作:例如
send_command()、read_response()、parse_float()。 - 定义参数与返回值:明确输入(仪器地址、命令字符串)和输出(测量值、状态码)。
- 添加文档字符串:说明函数功能、参数类型、返回值含义。
- 考虑异常传播:函数内部不捕获所有异常,让上层调用者决定处理策略。
案例拆解:封装 SCPI 命令发送函数
import pyvisa
def send_scpi(inst, command, check_error=True):
"""
向仪器发送 SCPI 命令并检查错误状态。
参数:
inst: pyvisa 仪器对象
command: SCPI 命令字符串
check_error: 是否查询错误队列
返回:
bool: 命令执行是否成功
"""
try:
inst.write(command)
if check_error:
error = inst.query('SYST:ERR?').strip()
if error.startswith('0'):
return True
else:
print(f"仪器错误: {error}")
return False
return True
except pyvisa.Error as e:
print(f"VISA 通信错误: {e}")
return False
# 使用示例
rm = pyvisa.ResourceManager()
dmm = rm.open_resource('GPIB0::22::INSTR')
send_scpi(dmm, ':CONF:VOLT:DC 10')
send_scpi(dmm, ':TRIG:SOUR BUS')
该函数封装了命令发送和错误检查两个步骤,调用者无需关心底层 VISA 细节。通过 check_error 参数,可以灵活控制是否进行错误查询,减少不必要的总线通信。
print() 输出调试信息,应使用 logging 模块。否则在生产环境中难以关闭调试输出,且无法记录到日志文件。2.4 面向对象编程:构建仪器驱动类
核心原理
面向对象编程(OOP)是仪器驱动开发的核心范式。通过将仪器抽象为类,可以封装硬件连接、命令集、状态管理等属性与方法,实现代码的高内聚低耦合。Python 的类支持继承、多态、封装等特性,适合构建从通用仪器基类到具体型号子类的层次结构。
判断方法或操作流程
设计仪器驱动类时,建议遵循以下流程:
- 定义基类:包含通用方法(
open()、close()、write()、read())。 - 实现子类:针对具体仪器型号,添加特有命令方法(如
measure_voltage())。 - 使用属性装饰器:将仪器状态(如
idn)定义为只读属性。 - 重写特殊方法:实现
__enter__/__exit__以支持上下文管理器。
案例拆解:数字万用表驱动类
import pyvisa
class DMM:
"""数字万用表驱动类"""
def __init__(self, resource_string):
self.rm = pyvisa.ResourceManager()
self.inst = self.rm.open_resource(resource_string)
self.inst.timeout = 3000 # 3秒超时
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def close(self):
self.inst.close()
self.rm.close()
@property
def idn(self):
"""返回仪器标识字符串"""
return self.inst.query('*IDN?').strip()
def measure_dc_voltage(self, range_v=10):
"""测量直流电压"""
self.inst.write(f':CONF:VOLT:DC {range_v}')
self.inst.write(':TRIG:SOUR IMM')
value = self.inst.query(':READ?')
return float(value)
def measure_resistance(self, range_ohm=1000):
"""测量电阻"""
self.inst.write(f':CONF:RES {range_ohm}')
value = self.inst.query(':READ?')
return float(value)
# 使用示例
with DMM('GPIB0::22::INSTR') as dmm:
print(f"仪器型号: {dmm.idn}")
v = dmm.measure_dc_voltage(10)
print(f"电压: {v} V")
r = dmm.measure_resistance(1000)
print(f"电阻: {r} Ω")
该驱动类通过上下文管理器确保资源正确释放,即使发生异常也能自动关闭连接。属性 idn 使用 @property 装饰器,使其像属性一样访问,但内部执行查询操作。
__repr__ 方法,便于调试时打印对象信息。例如:def __repr__(self): return f"DMM({self.inst.resource_name})"。2.5 异常处理:保障仪器通信的稳定性
核心原理
仪器控制中,异常处理是保证程序稳定运行的关键。常见的异常包括:VISA 通信超时(pyvisa.errors.VisaIOError)、仪器返回错误代码、数据解析失败(ValueError)、文件读写错误(OSError)等。Python 的 try/except/else/finally 结构提供了完整的异常捕获机制。
判断方法或操作流程
设计异常处理策略时,应遵循以下原则:
- 捕获具体异常:避免裸
except:,应明确异常类型。 - 分级处理:底层函数抛出异常,上层函数决定是否重试或终止。
- 使用
finally释放资源:确保仪器连接被关闭。 - 记录异常上下文:使用
logging.exception()记录完整堆栈。
案例拆解:带重试机制的仪器读取
import time
import pyvisa
import logging
logging.basicConfig(level=logging.INFO)
def read_with_retry(inst, command, retries=3, delay=0.5):
"""读取仪器响应,带重试机制"""
for attempt in range(retries):
try:
response = inst.query(command)
return response
except pyvisa.errors.VisaIOError as e:
logging.warning(f"第{attempt+1}次读取失败: {e}")
if attempt == retries - 1:
raise # 最后一次失败,向上抛出
time.sleep(delay)
except ValueError as e:
logging.error(f"数据解析失败: {e}")
raise
# 使用示例
rm = pyvisa.ResourceManager()
scope = rm.open_resource('TCPIP0::192.168.1.100::INSTR')
try:
data = read_with_retry(scope, ':WAV:DATA?')
print(f"波形数据长度: {len(data)}")
except pyvisa.errors.VisaIOError:
print("通信失败,请检查网络连接")
finally:
scope.close()
rm.close()
该案例展示了如何通过重试机制应对瞬态通信故障。注意 finally 块确保资源释放,即使发生异常也不会泄漏连接。
2.6 文件操作:测试数据的持久化
核心原理
仪器控制程序通常需要将测量结果保存到文件,或从配置文件读取仪器参数。Python 的文件操作支持文本(str)和二进制(bytes)两种模式,配合 csv、json、numpy 等库,可以高效处理结构化数据。
判断方法或操作流程
设计文件操作流程时,应遵循以下步骤:
- 选择文件格式:CSV 适合表格数据,JSON 适合配置信息,二进制适合波形数据。
- 使用上下文管理器:
with open(...) as f:确保文件关闭。 - 设置编码:文本文件明确指定
encoding='utf-8'。 - 处理大文件:使用
for line in f:逐行读取,避免一次性加载。
案例拆解:保存多点测量数据到 CSV 文件
import csv
import time
# 模拟从仪器读取 5 个测量点
measurements = []
for i in range(5):
voltage = 1.0 + i * 0.1 # 模拟数据
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
measurements.append((timestamp, voltage))
time.sleep(0.2)
# 写入 CSV 文件
with open('measurements.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['时间戳', '电压(V)'])
writer.writerows(measurements)
print("数据已保存到 measurements.csv")
# 读取并验证
with open('measurements.csv', 'r', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader)
print(f"表头: {header}")
for row in reader:
print(f"时间: {row[0]}, 电压: {row[1]} V")
该案例展示了使用 csv 模块进行结构化数据读写。注意 newline='' 参数避免 Windows 平台下出现空行。
numpy.savetxt() 或 pandas.to_csv(),它们提供更丰富的格式控制和性能优化。对于配置文件,使用 json 模块可以保留数据类型。2.7 综合实战:构建完整的仪器控制脚本
将上述六大模块整合,我们可以构建一个完整的自动化测试脚本。以下示例演示了从配置读取、仪器初始化、数据采集、异常处理到结果保存的完整流程:
import json
import csv
import time
import pyvisa
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class InstrumentController:
"""仪器控制主类"""
def __init__(self, config_file):
with open(config_file, 'r', encoding='utf-8') as f:
self.config = json.load(f)
self.rm = pyvisa.ResourceManager()
self.inst = None
def connect(self):
"""连接仪器"""
try:
self.inst = self.rm.open_resource(self.config['resource'])
self.inst.timeout = self.config.get('timeout', 3000)
idn = self.inst.query('*IDN?').strip()
logging.info(f"已连接: {idn}")
except pyvisa.errors.VisaIOError as e:
logging.error(f"连接失败: {e}")
raise
def measure_sequence(self, points):
"""执行测量序列"""
results = []
for i in range(points):
try:
value = float(self.inst.query(':READ?'))
results.append((time.time(), value))
logging.info(f"点{i+1}: {value}")
except (ValueError, pyvisa.errors.VisaIOError) as e:
logging.error(f"测量失败: {e}")
results.append((time.time(), None))
return results
def save_results(self, results, filename):
"""保存结果到CSV"""
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['时间戳', '测量值'])
for ts, val in results:
writer.writerow([time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)), val])
logging.info(f"结果已保存至 {filename}")
def close(self):
"""关闭连接"""
if self.inst:
self.inst.close()
self.rm.close()
# 主程序
if __name__ == '__main__':
controller = InstrumentController('config.json')
try:
controller.connect()
data = controller.measure_sequence(10)
controller.save_results(data, 'test_results.csv')
except Exception as e:
logging.exception("程序异常终止")
finally:
controller.close()
该综合案例展示了如何将配置管理(JSON)、仪器控制(pyvisa)、异常处理、文件操作(CSV)和日志记录整合到一个完整的测试流程中。通过面向对象设计,代码结构清晰,易于扩展和维护。