157 lines
5.4 KiB
Python
Executable File
157 lines
5.4 KiB
Python
Executable File
"""Oscilloscope SCPI policy for SRAS acquisition.
|
|
|
|
All the Tektronix-specific instrument setup the scan depends on, in one
|
|
Qt-free place: per-channel display/coupling config, trigger programming,
|
|
the background average, and per-row FastFrame transfer.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SAMPLE_RATE_HZ = 6.25e9 # 6.25 GS/s → 160 ps/sample
|
|
TRIG_LEVEL_V = 0.500
|
|
BACKGROUND_AVERAGES = 1024
|
|
BACKGROUND_TIMEOUT_S = 60.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ChannelProfile:
|
|
"""Display/input configuration for one scope channel."""
|
|
label: str
|
|
scale_v_div: float
|
|
position_div: float
|
|
termination_ohm: int
|
|
coupling: str
|
|
bandwidth_hz: float
|
|
|
|
|
|
# Standard SRAS front-end configuration.
|
|
SRAS_CHANNELS = {
|
|
1: ChannelProfile("RF Acoustic Packet", 0.07, 0.0, 50, "DC", 250e6),
|
|
2: ChannelProfile("Trigger Signal", 0.5, -2.72, 1_000_000, "DC", 20e6),
|
|
3: ChannelProfile("Max Vel Gate", 1.0, -2.72, 1_000_000, "DC", 20e6),
|
|
4: ChannelProfile("Bias - B", 0.125, -2.72, 1_000_000, "DC", 20e6),
|
|
}
|
|
|
|
|
|
def configure_channels(scope, profiles=None) -> None:
|
|
"""Apply the standard SRAS channel configuration."""
|
|
profiles = profiles if profiles is not None else SRAS_CHANNELS
|
|
for ch, p in profiles.items():
|
|
scope.write(f"SELect:CH{ch} ON")
|
|
scope.set_channel_label_name(ch, p.label)
|
|
scope.set_channel_scale(ch, p.scale_v_div)
|
|
scope.set_channel_position(ch, p.position_div)
|
|
scope.set_channel_termination(ch, p.termination_ohm)
|
|
scope.set_channel_coupling(ch, p.coupling)
|
|
scope.set_channel_bandwidth(ch, p.bandwidth_hz)
|
|
|
|
|
|
def configure_background_trigger(scope) -> None:
|
|
"""Program the edge trigger used for a background capture.
|
|
|
|
Edge trigger on the rising edge of CH2 (laser pulse), FastFrame off so
|
|
the capture runs as a single record. Every angle starts with a fresh
|
|
background, so the scan comes back here between angles from the
|
|
logic-AND trigger configure_scan_trigger leaves behind.
|
|
"""
|
|
scope.write("TRIGger:A:TYPe EDGE")
|
|
scope.set_trigger_source(2)
|
|
scope.set_trigger_slope("RISE")
|
|
scope.set_trigger_level(2, TRIG_LEVEL_V)
|
|
scope.set_trigger_mode("NORMAL") # wait for trigger (don't auto-sweep)
|
|
scope.set_acquire_mode("SAMPLE")
|
|
scope.set_fastframe_state(False)
|
|
|
|
|
|
def configure_acquisition(scope) -> int:
|
|
"""Program the edge trigger and timebase; returns samples per frame."""
|
|
configure_background_trigger(scope)
|
|
# Pin the transfer format instead of inheriting front-panel state — the
|
|
# file header hardcodes bytes_per_sample=1, and a scope left on 2 bytes
|
|
# would corrupt every frame written.
|
|
scope.set_data_encoding("RIBinary")
|
|
scope.set_data_width(1)
|
|
scope.set_sample_rate(SAMPLE_RATE_HZ)
|
|
scope.write("HORizontal:POSition 30") # 10 % trigger offset
|
|
time.sleep(0.3) # let the timebase settle before reading back
|
|
return scope.get_record_length()
|
|
|
|
|
|
def read_preambles(scope, channels) -> list[str]:
|
|
"""Snapshot WFMOutpre per channel (captures YMULT/YOFF/YZERO)."""
|
|
preambles = []
|
|
for ch in channels:
|
|
scope.set_data_source(ch)
|
|
preambles.append(scope.query_wfmoutpre())
|
|
return preambles
|
|
|
|
|
|
def capture_background(scope, should_abort=lambda: False,
|
|
on_status=lambda msg: None) -> bytes:
|
|
"""Capture one CH1 waveform averaged over BACKGROUND_AVERAGES shots.
|
|
|
|
The scope auto-stops after the sequence; poll ACQuire:STATE until it
|
|
does rather than assuming a duration.
|
|
"""
|
|
on_status(f"Capturing background waveform ({BACKGROUND_AVERAGES}-average) …")
|
|
scope.set_acquire_mode("AVERAGE")
|
|
scope.write(f"ACQuire:NUMAVg {BACKGROUND_AVERAGES}")
|
|
scope.write("ACQuire:STOPAfter SEQuence")
|
|
scope.set_data_source(1)
|
|
scope.write("ACQuire:STATE RUN")
|
|
|
|
deadline = time.time() + BACKGROUND_TIMEOUT_S
|
|
while time.time() < deadline:
|
|
if should_abort():
|
|
break
|
|
if scope.query("ACQuire:STATE?").strip() == "0":
|
|
break
|
|
time.sleep(0.25)
|
|
else:
|
|
scope.write("ACQuire:STATE STOP")
|
|
on_status("Warning: background average timed out; stopping early.")
|
|
time.sleep(0.1)
|
|
return scope.transfer_curve()
|
|
|
|
|
|
def configure_scan_trigger(scope) -> None:
|
|
"""Switch to the scan-time logic-AND trigger (CH2 HIGH AND CH3 HIGH).
|
|
|
|
CH3 is the BBD202 TRIGOUT_MAXV gate, so frames only accumulate while the
|
|
stage is at full scan velocity.
|
|
"""
|
|
scope.write("ACQuire:STOPAfter RUNSTop")
|
|
scope.set_acquire_mode("SAMPLE")
|
|
scope.set_fastframe_state(True)
|
|
|
|
scope.write("TRIGger:A:TYPe LOGIc")
|
|
scope.write("TRIGger:A:LOGIc:FUNCtion AND")
|
|
scope.set_trigger_level(2, TRIG_LEVEL_V)
|
|
scope.set_trigger_level(3, TRIG_LEVEL_V)
|
|
scope.write("TRIGger:A:LOGICPattern:CH2 HIGH")
|
|
scope.write("TRIGger:A:LOGICPattern:CH3 HIGH")
|
|
time.sleep(0.2)
|
|
|
|
|
|
def arm_row(scope) -> None:
|
|
"""Start acquisition for one scan row."""
|
|
scope.write("ACQuire:STATE RUN")
|
|
time.sleep(0.05)
|
|
|
|
|
|
def finish_row(scope) -> None:
|
|
"""Wait for trailing frames, then stop acquisition."""
|
|
time.sleep(0.2)
|
|
scope.write("ACQuire:STATE STOP")
|
|
|
|
|
|
def transfer_channel(scope, ch: int) -> list[bytes]:
|
|
"""Fetch one channel's FastFrame block as raw int8 frames."""
|
|
scope.set_data_source(ch)
|
|
return scope.transfer_fastframe(parse=False)
|