第2章:Python环境与仪器通信
说实话,很多做仪器分析的朋友,一听到「编程」两个字就头大。我当年也是这样。但后来我发现,真正难的不是Python本身,而是不知道怎么把Python和仪器连起来。这一章,我们就来解决这个问题。
2.1 Python科学计算环境搭建(Anaconda)
我个人习惯用Anaconda来管理Python环境。为什么?因为它把Python解释器、常用科学计算库、还有包管理器都打包好了。你不需要一个个去装,省心。
安装完成后,打开Anaconda Prompt,输入以下命令验证:
conda --version
python --version
看到版本号,说明环境搭好了。接下来,我们需要安装几个核心库:
conda install numpy scipy matplotlib
pip install pyserial
这里要注意一点:numpy和scipy用conda装,pyserial用pip装。为什么?因为conda的源里不一定有最新版的pyserial,而pip的更新更及时。我在项目中遇到过因为版本太旧导致串口通信丢包的问题,后来换成pip安装就解决了。
2.2 PySerial库基础
PySerial是Python和串口设备通信的桥梁。说白了,它就是帮你把数据从串口读出来、写进去的工具。
先看一个最简单的例子:
import serial
# 打开串口
ser = serial.Serial(
port='COM3', # Windows下用COM口,Linux下用/dev/ttyUSB0
baudrate=9600, # 波特率,要和仪器一致
timeout=1 # 超时时间,单位秒
)
# 发送指令
ser.write(b'*IDN?\n')
# 读取响应
response = ser.readline()
print(response.decode().strip())
# 关闭串口
ser.close()
这段代码看起来简单,但坑不少。我曾经在调试一台老旧的频谱仪时,发现发送指令后死活收不到回复。折腾了半天,才发现是换行符的问题——仪器只认\r\n,而我发的是\n。
\n,而一些国产仪器可能只认\r\n。我建议你在发送指令前,先查一下仪器的编程手册。
2.3 串口通信协议解析
串口通信协议,说白了就是「仪器和电脑之间怎么说话」。常见的协议有两种:
| 协议类型 | 特点 | 典型应用 |
|---|---|---|
| SCPI | 标准化指令,可读性强 | 安捷伦、泰克等主流仪器 |
| Modbus | 二进制协议,效率高 | 工业传感器、PLC |
| 自定义协议 | 厂家自己定义,格式各异 | 国产仪器、老旧设备 |
SCPI协议最常用。它的指令格式很直观:
*IDN? # 查询仪器身份
MEAS:VOLT:DC? # 测量直流电压
SYST:ERR? # 查询错误信息
你想想看,这些指令是不是很像英语句子?这就是SCPI的设计初衷——让工程师不用翻手册也能猜个大概。
但Modbus就不一样了。它传输的是二进制数据,比如读取一个寄存器的值:
# Modbus RTU读取保持寄存器
# 请求帧: 01 03 00 00 00 01 84 0A
# 响应帧: 01 03 02 00 64 B9 AF
嗯,这里要注意:Modbus的校验码是CRC16,计算方式比较特殊。我建议直接用现成的库,比如pymodbus,别自己手写CRC算法。我曾经手写过一次,结果算出来的校验码总是不对,查了三天才发现是多项式顺序搞反了。
2.4 虚拟仪器模拟
没有真实仪器怎么办?我们可以用Python模拟一个。这在调试阶段特别有用——你总不想每次测试都搬一台几十公斤的仪器到办公桌上吧?
下面是一个简单的虚拟信号发生器:
import serial
import threading
import time
import numpy as np
class VirtualSignalGenerator:
def __init__(self, port='COM3', baudrate=9600):
self.ser = serial.Serial(port, baudrate, timeout=1)
self.running = True
self.frequency = 1000 # 默认1kHz
self.amplitude = 1.0 # 默认1V
def generate_signal(self):
"""模拟生成正弦波数据"""
t = np.linspace(0, 1, 1000)
while self.running:
signal = self.amplitude * np.sin(2 * np.pi * self.frequency * t)
# 模拟发送数据
data = f"{signal[0]:.3f}\n".encode()
self.ser.write(data)
time.sleep(0.001)
def handle_command(self, cmd):
"""处理SCPI指令"""
cmd = cmd.strip().upper()
if cmd == '*IDN?':
return 'Virtual Signal Generator v1.0'
elif cmd.startswith('FREQ '):
self.frequency = float(cmd.split()[1])
return 'OK'
elif cmd.startswith('VOLT '):
self.amplitude = float(cmd.split()[1])
return 'OK'
else:
return 'ERROR: Unknown command'
def run(self):
"""启动虚拟仪器"""
thread = threading.Thread(target=self.generate_signal)
thread.start()
while self.running:
if self.ser.in_waiting:
cmd = self.ser.readline().decode().strip()
response = self.handle_command(cmd)
self.ser.write(response.encode() + b'\n')
def stop(self):
self.running = False
self.ser.close()
# 使用示例
if __name__ == '__main__':
vg = VirtualSignalGenerator()
try:
vg.run()
except KeyboardInterrupt:
vg.stop()
我在做项目时,经常先用虚拟仪器把上位机调通,再连真实仪器。这样做的好处是:
- 调试速度快,不用等仪器启动
- 不会因为误操作损坏真实仪器
- 可以模拟各种异常情况(比如数据丢包、指令超时)
最后说一句:虚拟仪器不是玩具,它是工程实践中非常实用的工具。我见过不少工程师,连真实仪器之前连个简单的模拟程序都不写,结果一上来就烧了仪器的通信接口。嗯,那场面,挺尴尬的。