一、项目背景

在光学实验(如激光测量、角度扫描、光强分布测试等)中,经常需要实现:

👉 “角度控制 + 光强采集”同步进行

因此设计了一个自动化系统,实现:

✅ 步进电机控制角度
✅ 光功率计实时采集光强
✅ 自动记录 CSV 数据
✅ 多次采样提高精度

二、系统架构

整个系统由三部分组成:

1️⃣ 电机控制模块(HDR50)

  • 基于 Thorlabs Kinesis SDK
  • 控制角度旋转

2️⃣ 光强采集模块(PM100USB)

  • 使用 Thorlabs 光功率计
  • DLL 调用(ctypes)

3️⃣ 主控制程序

  • 控制扫描流程
  • 数据采集 + 存储

三、系统流程

整体代码逻辑如下:

四、主程序代码解析

📌 核心控制代码

import csv
import time
from hdr50_controller import HDR50Controller
from power_meter import ThorlabsPowerMeter

📌 参数配置

SAMPLES_PER_ANGLE = 3 # 每个角度采样次数
CSV_FILE = "scan_data.csv"

👉 说明:

  • 多次采样可以降低噪声
  • 建议 ≥3 次

📌 初始化 CSV

with open(CSV_FILE, mode='w', newline='') as f:
writer = csv.writer(f)
header = ["Index", "Angle"]
for i in range(SAMPLES_PER_ANGLE):
header.append(f"Power{i+1}_uW")
writer.writerow(header)

生成类似:

Index Angle Power1 Power2 Power3

📌 初始化设备

motor = HDR50Controller("40344804")
pm = ThorlabsPowerMeter(wavelength_nm=633.0)


📌 设置电机参数

motor.set_velocity(5, 5)

👉 含义:

  • 最大速度:5 deg/s
  • 加速度:5 deg/s²

📌 扫描逻辑(核心)

for angle in range(30, 361, 5):
motor.move_to(angle)
time.sleep(0.7) # 等待稳定

👉 说明:

  • 从 30° 扫到 360°
  • 步长 5°

📌 光强采样

powers = []
for _ in range(SAMPLES_PER_ANGLE):
power = pm.read_power()
powers.append(power)
time.sleep(0.1)

👉 关键点:

  • 防止连续读取导致数据抖动
  • 加入采样间隔

📌 数据写入

with open(CSV_FILE, mode='a', newline='') as f:
writer = csv.writer(f)
writer.writerow([index, angle] + powers)


五、电机控制类解析(HDR50Controller)

📌 初始化设备

DeviceManagerCLI.BuildDeviceList()
self.device = BenchtopStepperMotor.CreateBenchtopStepperMotor(self.serial)

👉 使用 Thorlabs 官方 SDK


📌 启动设备

self.channel.StartPolling(250)
self.channel.EnableDevice()


📌 移动控制

def move_to(self, angle):
self.channel.MoveTo(Decimal(angle), 60000)

👉 精确角度控制(单位:度)


📌 扫描封装(高级用法)

def step_scan(self, start, end, step, delay=0.5, callback=None):

👉 支持回调函数(非常关键!)

例如:

def collect(angle):
print("采集数据")

motor.step_scan(0, 360, 5, callback=collect)


六、光强采集类解析(重点!)

📌 DLL加载

self.tlpm = ctypes.cdll.LoadLibrary("TLPM_64.dll")

👉 注意:

  • 必须安装 Thorlabs 驱动
  • 或指定路径

📌 自动设备查找

self.tlpm.TLPM_findRsrc(0, ctypes.byref(count))


📌 初始化设备

self.tlpm.TLPM_init(device_address, ...)


⭐ 核心优化:稳定处理

for _ in range(5):
self.tlpm.TLPM_measPower(...)
time.sleep(0.3)

👉 作用:

🚨 丢弃前几次不稳定数据(非常关键)


📌 光强读取

return power_W.value * 1e6

👉 单位转换:

  • W → μW

七、系统整体代码

使用VSCode编辑器打开:文件整体架构如下

main.py如下

import csv
import time
from hdr50_controller import HDR50Controller
from power_meter import ThorlabsPowerMeter

# ===== 参数 =====
SAMPLES_PER_ANGLE = 3   # 每个角度采样次数
CSV_FILE = "scan_data.csv"

# ===== 初始化 CSV(写表头)=====
with open(CSV_FILE, mode='w', newline='') as f:
    writer = csv.writer(f)
    
    # 表头
    header = ["Index", "Angle"]
    for i in range(SAMPLES_PER_ANGLE):
        header.append(f"Power{i+1}_uW")
    
    writer.writerow(header)

print("📄 CSV 文件已创建")

# ===== 初始化设备 =====
motor = None
pm = None

try:
    motor = HDR50Controller("40344804")
    pm = ThorlabsPowerMeter(wavelength_nm=633.0)

    motor.set_velocity(5, 5)#加速度和最大速度
    # motor.home()

    index = 0

    # ===== 扫描 =====
    for angle in range(30, 361, 5):  # 0~360;1步
        motor.move_to(angle)

        # 等待稳定(建议保留)
        time.sleep(0.7)#700ms

        # ===== 多次采样 =====
        powers = []
        for _ in range(SAMPLES_PER_ANGLE):
            power = pm.read_power()
            powers.append(power)
            time.sleep(0.1)  # 防止连续读取太快

        # ===== 打印 =====
        print(f"👉 角度: {angle:>3}° | 数据: {powers}")

        # ===== 实时写入 CSV =====
        with open(CSV_FILE, mode='a', newline='') as f:
            writer = csv.writer(f)
            writer.writerow([index, angle] + powers)

        index += 1

except Exception as e:
    print(f"\n❌ 实验出错: {e}")

finally:
    if motor:
        motor.close()
    if pm:
        pm.close()

    print("🔌 设备已安全关闭")

hdr50_controller.py

import sys
import time
import clr

sys.path.append(r"C:\Program Files\Thorlabs\Kinesis")
# type: ignore
clr.AddReference("Thorlabs.MotionControl.DeviceManagerCLI")
clr.AddReference("Thorlabs.MotionControl.GenericMotorCLI")
clr.AddReference("Thorlabs.MotionControl.Benchtop.StepperMotorCLI")

from Thorlabs.MotionControl.DeviceManagerCLI import DeviceManagerCLI
from Thorlabs.MotionControl.Benchtop.StepperMotorCLI import BenchtopStepperMotor
from System import Decimal


class HDR50Controller:
    def __init__(self, serial_number, channel_id=1):
        self.serial = serial_number

        DeviceManagerCLI.BuildDeviceList()

        self.device = BenchtopStepperMotor.CreateBenchtopStepperMotor(self.serial)
        self.device.Connect(self.serial)

        time.sleep(1)

        self.channel = self.device.GetChannel(channel_id)

        self.channel.StartPolling(250)
        time.sleep(1)

        self.channel.EnableDevice()
        time.sleep(1)

        # ⭐ 初始化参数(必须)
        self.channel.WaitForSettingsInitialized(10000)
        if not self.channel.IsSettingsInitialized():
            raise Exception("Settings not initialized")

        self.channel.LoadMotorConfiguration(self.serial)
        self.channel.SetSettings(self.channel.MotorDeviceSettings, True, False)

        print("✅ 设备初始化完成")

    # ===== 设置速度 =====
    def set_velocity(self, max_vel, acc):
        self.channel.SetVelocityParams(Decimal(max_vel), Decimal(acc))
        print(f"⚙️ 速度设置: {max_vel} deg/s, 加速度: {acc} deg/s²")

    # ===== 回零 =====
    def home(self):
        print("🏠 回零中...")
        self.channel.Home(60000)
        print("✅ 回零完成")

    # ===== 移动到指定角度 =====
    def move_to(self, angle):
        print(f"➡️ 移动到 {angle} deg")
        self.channel.MoveTo(Decimal(angle), 60000)

    # ===== 获取当前位置 =====
    def get_position(self):
        return float(self.channel.Position)

    # ===== 步进扫描(核心函数)=====
    def step_scan(self, start, end, step, delay=0.5, callback=None):
        """
        start: 起始角度
        end: 终止角度
        step: 步长(比如1度)
        delay: 每步等待时间
        callback: 采集函数(你自己的数据采集)
        """

        angle = start
        while angle <= end:
            self.move_to(angle)
            time.sleep(delay)

            print(f"📡 当前角度: {angle}")

            # 👉 调用你的采集函数
            if callback:
                callback(angle)

            angle += step

    # ===== 关闭设备 =====
    def close(self):
        self.channel.StopPolling()
        self.device.Disconnect()
        print("🔌 设备已断开")

power_meter.py

import time
import ctypes

# ==========================================
# 终极稳定版:光强探测器类 (带自动丢弃不稳定数据功能)
# ==========================================
class ThorlabsPowerMeter:
    def __init__(self, wavelength_nm=633.0):
        print("🔌 正在连接光强探测器...")
        
        # 1. 加载 DLL
        try:
            self.tlpm = ctypes.cdll.LoadLibrary("TLPM_64.dll")
        except OSError:
            try:
                self.tlpm = ctypes.cdll.LoadLibrary(r"C:\Program Files\IVI Foundation\VISA\Win64\Bin\TLPM_64.dll")
            except OSError:
                raise Exception("❌ 找不到 TLPM_64.dll!")

        # 2. 查找并初始化设备
        count = ctypes.c_uint32(0)
        self.tlpm.TLPM_findRsrc(0, ctypes.byref(count))
        if count.value == 0:
            raise Exception("❌ 没有找到光强探测器。")

        buffer = ctypes.create_string_buffer(256)
        self.tlpm.TLPM_getRsrcName(0, 0, buffer)
        device_address = buffer.value
        
        self.handle = ctypes.c_uint32(0)
        status = self.tlpm.TLPM_init(device_address, ctypes.c_uint16(1), ctypes.c_uint16(1), ctypes.byref(self.handle))
        if status != 0:
            raise Exception(f"❌ 初始化失败,错误码: {status}")

        # 3. 设置波长
        self.set_wavelength(wavelength_nm)

        # ==================================================
        # ⭐ 核心修复:添加硬件稳定期(抛弃初期的垃圾数据)
        # ==================================================
        print("⏳ 正在等待硬件自动量程匹配与数据稳定 (约 1.5 秒)...")
        # 连续读取并丢弃 5 次数据,每次间隔 0.3 秒
        for _ in range(5):
            power_W = ctypes.c_double(0.0)
            self.tlpm.TLPM_measPower(self.handle, ctypes.byref(power_W))
            time.sleep(0.3)
            
        print("✅ 探测器已完全稳定就绪!\n")

    def set_wavelength(self, wl_nm):
        """设置探测器的校准波长"""
        status = self.tlpm.TLPM_setWavelength(self.handle, ctypes.c_double(wl_nm))
        if status == 0:
            print(f"🌈 波长已设置为: {wl_nm} nm")
        else:
            print(f"⚠️ 设置波长失败,错误码: {status}")

    def read_power(self):
        """
        读取当前光强
        :return: 光强值,单位 微瓦 (uW)
        """
        power_W = ctypes.c_double(0.0)
        self.tlpm.TLPM_measPower(self.handle, ctypes.byref(power_W))
        
        # 转换为微瓦
        return power_W.value * 1e6

    def close(self):
        """关闭设备"""
        if hasattr(self, 'handle') and self.handle.value != 0:
            self.tlpm.TLPM_close(self.handle)
            print("🔌 光强计连接已安全关闭。")


# ==========================================
# 单独测试代码
# ==========================================
# if __name__ == "__main__":
#     pm = None
#     try:
#         # 当执行这行代码时,系统会自动把前几组跳动的数据吃掉
#         pm = ThorlabsPowerMeter(wavelength_nm=633.0)
        
#         print("🚀 开始正式读取光强数据 (连续读取 5 次)...")
#         print("-" * 45)
        
#         for i in range(1, 20):
#             power_val_uW = pm.read_power()
#             print(f"👉 第 {i:2d} 次正式读取: 当前光强 = {power_val_uW:.3f} uW")
#             time.sleep(0.1)
            
#         print("-" * 45)
#         print("🎉 测试完毕!")
        
#     except Exception as e:
#         print(f"\n⚠️ 异常:\n{e}")
        
#     finally:
#         if pm is not None:
#             pm.close()

更多推荐