第2章:Python环境与仪器通信

说实话,很多做仪器分析的朋友,一听到「编程」两个字就头大。我当年也是这样。但后来我发现,真正难的不是Python本身,而是不知道怎么把Python和仪器连起来。这一章,我们就来解决这个问题。

2.1 Python科学计算环境搭建(Anaconda)

我个人习惯用Anaconda来管理Python环境。为什么?因为它把Python解释器、常用科学计算库、还有包管理器都打包好了。你不需要一个个去装,省心。

小提示: 我建议你下载Anaconda的64位版本。32位版本在处理大数组时会受内存限制,做数据分析时容易卡死。

安装完成后,打开Anaconda Prompt,输入以下命令验证:

conda --version
python --version

看到版本号,说明环境搭好了。接下来,我们需要安装几个核心库:

conda install numpy scipy matplotlib
pip install pyserial

这里要注意一点:numpyscipy用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

避坑指南: 不同仪器对换行符的要求不一样。Agilent/Keysight的设备通常认\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()
核心思路: 虚拟仪器的本质就是用软件模拟硬件的行为。你只需要实现两个功能:一是生成模拟数据,二是响应SCPI指令。这样,你的上位机程序就可以在完全不知道对面是真实仪器还是虚拟仪器的情况下正常工作。

我在做项目时,经常先用虚拟仪器把上位机调通,再连真实仪器。这样做的好处是:

  • 调试速度快,不用等仪器启动
  • 不会因为误操作损坏真实仪器
  • 可以模拟各种异常情况(比如数据丢包、指令超时)

最后说一句:虚拟仪器不是玩具,它是工程实践中非常实用的工具。我见过不少工程师,连真实仪器之前连个简单的模拟程序都不写,结果一上来就烧了仪器的通信接口。嗯,那场面,挺尴尬的。