第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(),或者自己写一个字节读取循环。
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
resize操作虽然方便,但频繁调用会影响性能。如果你每秒采集上千个点,建议先攒一批(比如1000个点),然后一次性resize并写入。我在项目中用这个技巧,写入速度提升了近10倍。
4.4 知识体系总览
这一章的内容,说白了就是一条流水线:串口读数据 → 实时显示 → 存到文件。下面这张图帮你理清整体结构:
这张图里,步骤3和步骤4是并行执行的。什么意思?就是你在屏幕上看到波形在滚动,同时后台也在往文件里写数据。这两件事互不干扰。我见过有人把显示和存储写在一个循环里,结果显示一卡,存储也跟着卡——这就是没做好并行。
好了,这一章的内容就这些。串口读取、实时波形、数据存储——这三板斧你掌握了,量子光学实验的数据采集基础就算打牢了。下一章咱们会聊更高级的话题,但先把今天的内容练熟。记住:实验数据是科研的命根子,采集环节出问题,后面分析得再漂亮也是白搭。
公众号:蓝海资料掘金营,微信deep3321