""" 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 (#...) 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)