第4章:数据采集基础——串口读取SPAD计数、实时波形显示、数据存储

好,咱们进入实战环节了。

前面几章讲了量子光学的基本原理和SPAD探测器的工作特性。但说实话,理论再漂亮,最终还是要落到数据上。我见过不少同学,实验装置搭得漂漂亮亮,一到数据采集就抓瞎——要么串口读不到数,要么波形卡成PPT,要么存下来的数据打不开。

这一章,咱们就把这些坑一个个填平。

4.1 串口通信:SPAD计数怎么读?

SPAD探测器通常通过USB转串口(虚拟COM口)和电脑通信。你插上探测器,设备管理器里会多出一个COM口,比如COM3或COM5。嗯,这里要注意:不同操作系统下,串口名称不一样。Windows是COMx,Linux是/dev/ttyUSB0或/dev/ttyACM0。

我个人习惯用Python的pyserial库来操作串口。为什么不用别的?因为pyserial跨平台支持好,而且API设计得干净利落。

先看一个最简单的读取示例:

import serial
import time

# 打开串口,参数根据你的探测器手册调整
ser = serial.Serial(
    port='COM3',        # Windows下
    baudrate=115200,    # 常见波特率
    bytesize=8,
    parity='N',
    stopbits=1,
    timeout=1           # 读取超时1秒
)

# 读取一行数据
line = ser.readline().decode('utf-8').strip()
print(f"原始数据: {line}")

# 解析计数(假设格式是 "COUNT:12345")
if line.startswith("COUNT:"):
    count = int(line.split(":")[1])
    print(f"SPAD计数: {count}")

ser.close()

我在项目中遇到过一个问题:有些探测器发送的数据末尾不是换行符,而是回车符。这时候readline()会一直等,直到超时。解决办法是改用read_until(),或者自己写一个字节读取循环。

避坑指南: 我曾经因为忘记设置串口超时,程序卡死在读取阶段,整个采集流程停了半小时才发现。记住:永远给串口设置一个合理的timeout,哪怕只有0.1秒。

4.2 实时波形显示:让数据“动”起来

光能读到计数还不够。做实验时,你需要实时看到计数随时间的变化——比如调个激光功率,波形立刻要有反应。这就是实时波形显示。

我推荐用matplotlib.animation来实现。为什么不用pyqtgraph?pyqtgraph确实更快,但matplotlib的生态更成熟,调试起来方便。等你需要处理每秒上万帧的数据时,再换pyqtgraph不迟。

下面是一个滚动波形显示的代码框架:

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import serial
from collections import deque

# 数据缓冲区,保存最近100个点
buffer = deque(maxlen=100)

# 初始化串口
ser = serial.Serial('COM3', 115200, timeout=0.1)

def update(frame):
    """动画更新函数,每帧调用一次"""
    try:
        line = ser.readline().decode().strip()
        if line.startswith("COUNT:"):
            count = int(line.split(":")[1])
            buffer.append(count)
    except:
        pass
    
    # 清空并重绘
    ax.clear()
    ax.plot(list(buffer), color='#2196F3', linewidth=1.5)
    ax.set_ylim(0, max(buffer) * 1.2 if buffer else 100)
    ax.set_xlabel("采样点")
    ax.set_ylabel("SPAD计数 (cps)")
    ax.set_title("实时SPAD计数波形")
    ax.grid(True, alpha=0.3)

# 创建画布
fig, ax = plt.subplots(figsize=(10, 4))
ani = animation.FuncAnimation(fig, update, interval=50)  # 50ms更新一次
plt.tight_layout()
plt.show()

你想想看,这个代码的核心思路是什么?其实就是三个步骤:读数据→存缓冲区→画图。缓冲区用deque,因为它支持固定长度,新数据进来,旧数据自动丢弃,正好实现滚动效果。

小技巧: 如果波形闪烁严重,可以尝试把interval调大到100ms,或者改用blit=True开启局部刷新。我个人习惯先调interval,简单粗暴有效。

4.3 数据存储:CSV还是HDF5?

数据采完了,得存下来。存成什么格式?这取决于你的数据量和后续分析需求。

CSV格式:简单、通用、Excel能直接打开。适合数据量不大(几万行以内)的场景。

HDF5格式:高效、支持大数据、能存多维数组。适合长时间采集(几小时甚至几天)的场景。

我一般这样选:

场景 推荐格式 原因
快速验证、调试 CSV 随手就能用Excel打开看
正式实验、长时间采集 HDF5 读写快,压缩率高
需要保存原始波形+元数据 HDF5 一个文件搞定所有

来看CSV的存储代码:

import csv
import time

filename = f"spad_data_{time.strftime('%Y%m%d_%H%M%S')}.csv"
with open(filename, 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(["timestamp", "count"])  # 写表头
    
    # 假设ser已经打开
    for _ in range(1000):
        line = ser.readline().decode().strip()
        if line.startswith("COUNT:"):
            count = int(line.split(":")[1])
            writer.writerow([time.time(), count])

再来看HDF5的存储代码。这里要用到h5py库:

import h5py
import numpy as np
import time

filename = f"spad_data_{time.strftime('%Y%m%d_%H%M%S')}.h5"
with h5py.File(filename, 'w') as f:
    # 创建可扩展的数据集
    dset = f.create_dataset(
        "counts",
        shape=(0,),
        maxshape=(None,),
        dtype='i4',
        compression="gzip"
    )
    ts_dset = f.create_dataset(
        "timestamps",
        shape=(0,),
        maxshape=(None,),
        dtype='f8'
    )
    
    # 存储元数据
    f.attrs['experiment'] = "SPAD计数采集"
    f.attrs['baudrate'] = 115200
    
    # 实时写入
    for _ in range(10000):
        line = ser.readline().decode().strip()
        if line.startswith("COUNT:"):
            count = int(line.split(":")[1])
            now = time.time()
            
            # 扩展数据集并写入
            dset.resize((dset.shape[0] + 1,))
            dset[-1] = count
            ts_dset.resize((ts_dset.shape[0] + 1,))
            ts_dset[-1] = now
核心要点: HDF5的resize操作虽然方便,但频繁调用会影响性能。如果你每秒采集上千个点,建议先攒一批(比如1000个点),然后一次性resize并写入。我在项目中用这个技巧,写入速度提升了近10倍。

4.4 知识体系总览

这一章的内容,说白了就是一条流水线:串口读数据 → 实时显示 → 存到文件。下面这张图帮你理清整体结构:

SPAD数据采集流水线 步骤1:串口读取 pyserial → readline() 步骤2:数据解析 字符串 → 整数计数 步骤3:实时显示 matplotlib动画 步骤4:数据存储 CSV / HDF5 数据流方向:串口 → 解析 → 显示(同时)→ 存储 注意:显示和存储是并行进行的,互不阻塞

这张图里,步骤3和步骤4是并行执行的。什么意思?就是你在屏幕上看到波形在滚动,同时后台也在往文件里写数据。这两件事互不干扰。我见过有人把显示和存储写在一个循环里,结果显示一卡,存储也跟着卡——这就是没做好并行。

我的建议: 如果数据量不大(每秒几十个点),单线程就够了。如果数据量大(每秒上千个点),一定要用多线程:一个线程负责读串口和存数据,另一个线程只负责画图。这样即使画图卡了,数据也不会丢。

好了,这一章的内容就这些。串口读取、实时波形、数据存储——这三板斧你掌握了,量子光学实验的数据采集基础就算打牢了。下一章咱们会聊更高级的话题,但先把今天的内容练熟。记住:实验数据是科研的命根子,采集环节出问题,后面分析得再漂亮也是白搭


公众号:蓝海资料掘金营,微信deep3321