Files
MiPi_TEST/rigol_scope.py
david rice e718a93667 Updates
2026-04-20 10:34:42 +01:00

206 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
rigol_scope.py
Controls the Rigol DS1202Z-E at 192.168.45.5 for 1.8 V supply rail monitoring.
Called from dual_capture() in mipi_test.py during the LP pass.
The scope is armed (single trigger) just before the Agilent LP capture.
The LP→HS current step droops the 1.8 V rail, triggering the Rigol.
The waveform is then read over SCPI and written directly to the local data/ folder.
"""
import csv
import time
import vxi11
from pathlib import Path
RIGOL_HOST = "192.168.45.5"
V18_SCALE = 0.1 # V/div — 100 mV/div; 10 divs = ±500 mV around 1.8 V
V18_OFFSET = -1.8 # V — shifts zero reference so 1.8 V sits at screen centre
V18_TIMEBASE = 1e-6 # s/div — 1 µs/div = 10 µs total window
V18_TRIG_LEVEL = 1.76 # V — falling-edge trigger on supply droop > 40 mV
TRIG_TIMEOUT_S = 15.0 # s — wait this long for Rigol to capture after arming
# CH2 — SN65DSI83 IRQ pin (CMOS output, active HIGH, high-impedance when IRQ_EN=0)
# CSR 0xE0.0 IRQ_EN=0 (default): pin is high-impedance → reads ~0 V (no pull on PCB, normal)
# IRQ_EN=1, no error: driven LOW (~0 V)
# IRQ_EN=1, error asserted: driven HIGH (~1.25 V min per VOH spec)
# No pull-up required — CMOS output drives both high and low.
INT_V_SCALE = 0.2 # V/div — shows 0~1.8 V range clearly
INT_V_OFFSET = -0.9 # V — centres display on 0.9 V midpoint
rigol: vxi11.Instrument | None = None
# ---------------------------------------------------------------------------
# Connection
# ---------------------------------------------------------------------------
def connect() -> bool:
global rigol
try:
rigol = vxi11.Instrument(RIGOL_HOST)
rigol.timeout = 10
idn = rigol.ask("*IDN?").strip()
print(f"[RIGOL] Connected: {idn}")
return True
except Exception as e:
print(f"[RIGOL] Connection failed — 1.8 V monitoring disabled: {e}")
rigol = None
return False
def disconnect():
global rigol
if rigol:
try:
rigol.close()
except Exception:
pass
rigol = None
def is_connected() -> bool:
return rigol is not None
# ---------------------------------------------------------------------------
# Setup
# ---------------------------------------------------------------------------
def configure():
"""
Configure Rigol CH1 for 1.8 V supply monitoring and CH2 for SN65DSI83 INTB pin.
AUTO trigger sweep: if no droop occurs, scope still captures on timeout
so we always get a supply snapshot even when the rail is healthy.
"""
rigol.write(":STOP")
time.sleep(0.2)
# CH1 — 1.8 V supply rail
rigol.write(":CHANnel1:DISPlay 1")
rigol.write(":CHANnel1:COUPling DC")
rigol.write(":CHANnel1:PROBe 10")
rigol.write(f":CHANnel1:SCALe {V18_SCALE:.3f}")
rigol.write(f":CHANnel1:OFFSet {V18_OFFSET:.3f}")
# CH2 — SN65DSI83 INTB pin (active-low open-drain, external 10 kΩ pull-up to 1.8 V required)
rigol.write(":CHANnel2:DISPlay 1")
rigol.write(":CHANnel2:COUPling DC")
rigol.write(":CHANnel2:PROBe 1") # direct probe, no attenuation
rigol.write(f":CHANnel2:SCALe {INT_V_SCALE:.3f}")
rigol.write(f":CHANnel2:OFFSet {INT_V_OFFSET:.3f}")
rigol.write(f":TIMebase:MAIN:SCALe {V18_TIMEBASE:.2E}")
rigol.write(":TRIGger:MODE EDGE")
rigol.write(":TRIGger:EDGe:SOURce CHANnel1")
rigol.write(":TRIGger:EDGe:SLOPe NEGative")
rigol.write(f":TRIGger:EDGe:LEVel {V18_TRIG_LEVEL:.3f}")
rigol.write(":TRIGger:SWEep AUTO") # auto: captures even without a droop trigger
time.sleep(0.3)
rigol.write(":RUN") # start acquiring immediately after configure
time.sleep(0.2)
print(f"[RIGOL] Configured: CH1=1.8 V rail, CH2=INTB pin, {int(V18_TIMEBASE*1e6)} µs/div, "
f"trigger <{V18_TRIG_LEVEL} V falling (AUTO sweep, running)")
# ---------------------------------------------------------------------------
# Acquisition
# ---------------------------------------------------------------------------
def arm():
"""Ensure scope is running so it is actively acquiring when the LP event occurs.
The waveform is frozen with :STOP inside read_waveform_csv() at collection time."""
rigol.write(":RUN")
def wait_captured(timeout_s: float = TRIG_TIMEOUT_S) -> bool:
"""
Poll until the scope has completed its single acquisition.
DS1000Z reports STOP when done (triggered or auto-timed-out).
Returns True when ready, False if timeout exceeded.
"""
deadline = time.time() + timeout_s
while time.time() < deadline:
try:
status = rigol.ask(":TRIGger:STATus?").strip().upper()
if status in ("STOP", "TD"):
return True
except Exception:
pass
time.sleep(0.1)
return False
def _read_channel_csv(channel: str, path: Path, stop_first: bool = True) -> int:
"""
Read one Rigol channel waveform over SCPI and write to CSV.
stop_first=False skips :STOP when the scope was already stopped by a prior read.
Returns the number of samples written, or 0 on error.
"""
try:
if stop_first:
rigol.write(":STOP")
time.sleep(0.3)
rigol.write(f":WAVeform:SOURce {channel}")
rigol.write(":WAVeform:FORMat ASC") # Rigol DS1000Z uses ASC not ASCII
time.sleep(0.1)
except Exception as e:
print(f"[RIGOL] {channel} waveform setup error: {e}")
return 0
try:
preamble = rigol.ask(":WAVeform:PREamble?").strip().split(",")
# [0]=fmt [1]=type [2]=points [3]=count [4]=x_incr [5]=x_orig [6]=x_ref
# [7]=y_incr [8]=y_orig [9]=y_ref
x_incr = float(preamble[4])
x_orig = float(preamble[5])
x_ref = float(preamble[6])
except Exception as e:
print(f"[RIGOL] {channel} preamble error: {e}")
return 0
try:
raw = rigol.ask(":WAVeform:DATA?").strip()
# Strip TMC binary header (#<n_digits><byte_count>...) if present
if raw.startswith("#"):
n_digits = int(raw[1])
raw = raw[2 + n_digits:]
vals = [float(v) for v in raw.split(",") if v.strip()]
except Exception as e:
print(f"[RIGOL] {channel} data read error: {e}")
return 0
if not vals:
print(f"[RIGOL] {channel}: no samples parsed — check channel and format settings")
return 0
try:
path.parent.mkdir(exist_ok=True)
with open(path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Time (s)", "Voltage (V)"])
for i, v in enumerate(vals):
t = x_orig + (i - x_ref) * x_incr
writer.writerow([f"{t:.9f}", f"{v:.6f}"])
return len(vals)
except Exception as e:
print(f"[RIGOL] {channel} CSV write error: {e}")
return 0
def read_waveform_csv(path: Path) -> int:
"""Read CH1 (1.8 V supply) waveform from Rigol and write to CSV."""
return _read_channel_csv("CHANnel1", path, stop_first=True)
def read_int_csv(path: Path) -> int:
"""
Read CH2 (SN65DSI83 INTB pin) waveform from Rigol and write to CSV.
Must be called after read_waveform_csv() — scope is already stopped.
"""
return _read_channel_csv("CHANnel2", path, stop_first=False)