From 2385fc68780ad8917f27c5962bd40101e423655b Mon Sep 17 00:00:00 2001 From: david rice Date: Thu, 9 Apr 2026 08:45:57 +0100 Subject: [PATCH] Updates --- __pycache__/analyze_captures.cpython-312.pyc | Bin 13447 -> 14516 bytes __pycache__/csv_preprocessor.cpython-312.pyc | Bin 28399 -> 32841 bytes __pycache__/rigol_scope.cpython-312.pyc | Bin 0 -> 7301 bytes analyze_captures.py | 39 +++-- csv_preprocessor.py | 102 +++++++++++- mipi_test.py | 36 ++++- rigol_scope.py | 158 +++++++++++++++++++ 7 files changed, 321 insertions(+), 14 deletions(-) create mode 100644 __pycache__/rigol_scope.cpython-312.pyc create mode 100644 rigol_scope.py diff --git a/__pycache__/analyze_captures.cpython-312.pyc b/__pycache__/analyze_captures.cpython-312.pyc index ce869eed21359a026c1793505de1163befb7caf6..41b95be3b2d754787dbb36b1fd557ff16019c204 100644 GIT binary patch delta 3021 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 l@oAlz4q_Z#vC0U>HzaQE-0y`={{tX4{j&f7 diff --git a/__pycache__/csv_preprocessor.cpython-312.pyc b/__pycache__/csv_preprocessor.cpython-312.pyc index fff6d4bfa83ab4f25d91552cacf2e1d90ea860cc..38ac48fe93c23f6a2ea6eb0f0cfe02e1b89c2334 100644 GIT binary patch delta 6059 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 delta 2732 zcmZ{leQXrR6~K4ayX%hw8{^pV2lgH3^T);ZVb`Bc0UNMmW3X{daPWaSj+;4iXZ!9v zyJv#~*p{5qe6$IfL?Iy{+Wt`qqE_i9O4KUqg(5DQ%UO9?E`d*nSJ2WW)nAd`SW#G^W#Gw4*Vr3% z!B*F@H7Q%>0y#UqF11>rq?>K8*@r7Ng+q8k=^Yy}t*uc*&xaQI@SV789cMT59>;-O_%%Wo>vx>CvSW4>-_ ztP|WeLB{J@Qnl&x2_BY3}K;yhu1LGFf9GwtnYuy@=a42#VXMUSU z;%N;<79J0#MmL}h>bp7v)aQtw$ZD;>%hm1c>z$vmRWWw`C# zECjp8ZQ+wdTIiZj3({6|eb$qiWSjY3UWE*a_)C5_={KuaUy#X0@!11wNWGa=_@zvG zUMQ|6y`rYLMI8lS(_(+|zqMT;@ey;hE}4c@tX%-)63Dd7Zc)1=-TZc|4iWIov;qVtp7imEAGaAKD2m#f zwNz5Xj`lazH(}+wW>&|5X1+c6lswNDL2+As$QROQEE0fxKaIXKL-DCZWRme8;z-lu z++fsF#wX(k_%op5&mw+=z>usQof(vcgULw)T)c&<%tX*I7>}i_9!JwxP%j(0rG|~e z3C3TQ#21e?dfx-}CMaM1GzbgQe{!X~{O6)iMSoW^aFu^>^vcoexnm2xH(J}x;o+Z= zm6n4hdC-W&V}Yb@ezj9pRWGqfNT{Q&s)2sQjZIZz_h?PkT#jqR<*L;aEac)o@Se;7 z{B=Qa?89@aYsBkvnI}h`gj^Eu`0EBPgPQ*aVC8Qg3T)r;c%XaNaA08TU|?`3zk-w3 z5WhvdkGP6(BEAcNLU{$14HknRRPPP6`3Ui` z_-cQT`Zo|?GW&wZ<%8(kE+3ACp!E`E>|t6<9S<3BxZEjWo%{4MH1ljQ?$W) zvb02yQbyAYbR)>&o+vCvb)H0{8Gu+UD-iv(RJ#R(pAs`lH?JtZQ@i~8Xm=doCE!zB zNK82HEakhz!_!^zH6o5ow~&7E#`HP|R&V4Va3lDV_-y((`Ms(0LNb35-Q*)KA?{p> z)cJqo=?WcV+C>t;#EX0Vg+IZ zaSHJo;-dJ4QQmwv{=0d^;`slS@R9i1C?p3&VX}d|Beo=eKz50%$!%I5-0athSw7RF zYW8h;TO54C*~!bdGoCsW z41{-zvq#Fdy@m65Do|8>dg7(9{|B3sr#=7x diff --git a/__pycache__/rigol_scope.cpython-312.pyc b/__pycache__/rigol_scope.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..955fce61129f98f432d6ef4f4316bd531b9f1587 GIT binary patch literal 7301 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 str: "Each capture has three passes per lane (CLK and DAT0):\n" " sig — high-res HS differential (rise/fall times)\n" " proto — long-window HS differential (jitter, clock freq, amplitude)\n" - " lp — single-ended LP state capture (LP-11 voltage, SoT sequence, HS bursts)\n\n" + " lp — single-ended LP state capture (LP-11 voltage, SoT sequence, HS bursts)\n" + " pwr — 1.8 V supply rail captured during LP→HS transition (droop, ripple, spec)\n\n" f"{body}\n\n" "Please:\n" "1. Identify any consistent spec concerns (HS voltage, LP-11 voltage, LP-low timing).\n" - "2. Highlight any trends over captures (amplitude drift, jitter, LP-11 voltage, etc.).\n" + "2. Highlight any trends over captures (amplitude drift, jitter, LP-11 voltage, 1.8 V droop, etc.).\n" "3. Flag anomalies — missing LP transitions, short LP-low, unexpected burst counts.\n" - "4. For any ERROR or WARNING lines in the summaries, explain the most likely cause " - " (e.g. missing file, bad trigger, signal absent, probe issue) and what to check.\n" - "5. Provide specific, actionable recommendations to address all identified issues and anomalies.\n" - "6. Summarise overall signal health in 2–3 sentences." + "4. Correlate 1.8 V supply droop/ripple with MIPI LP anomalies — does droop depth or ripple " + " correlate with SoT timing violations, short LP-low plateaux, or LP-11 voltage drops? " + " If pwr data is absent, note that supply correlation could not be assessed.\n" + "5. For any ERROR or WARNING lines in the summaries, explain the most likely cause " + " (e.g. missing file, bad trigger, signal absent, probe issue, supply marginal) and what to check.\n" + "6. Provide specific, actionable recommendations to address all identified issues and anomalies.\n" + "7. Summarise overall signal health in 2–3 sentences." ) diff --git a/csv_preprocessor.py b/csv_preprocessor.py index 3511769..60954d3 100644 --- a/csv_preprocessor.py +++ b/csv_preprocessor.py @@ -22,6 +22,13 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Optional +# 1.8 V supply rail spec (i.MX 8M Mini internal regulator, ±5 %) +V18_NOMINAL_V = 1.800 +V18_SPEC_MIN_V = 1.710 # −5 % +V18_SPEC_MAX_V = 1.890 # +5 % +V18_DROOP_WARN_MV = 50.0 # mV droop depth worth flagging +V18_RIPPLE_WARN_MV = 20.0 # mV RMS ripple worth flagging + # MIPI D-PHY HS-TX spec limits HS_VDIFF_MIN_MV = 140.0 # |Vdiff| minimum (mV) HS_VDIFF_MAX_MV = 270.0 # |Vdiff| maximum (mV) @@ -318,6 +325,97 @@ def analyze_file(path: Path) -> ChannelMetrics: ) +@dataclass +class V1V8Metrics: + timestamp: str + capture_num: int + + sample_rate_mhz: float + duration_us: float + n_samples: int + + mean_v: float # mean supply voltage + min_v: float # minimum (worst-case droop) + max_v: float # maximum + droop_mv: float # mean − min (droop depth) + ripple_mv_rms: float # AC ripple (std dev of voltage) + + spec_pass: bool # mean within ±5 % of 1.8 V + droop_pass: bool # minimum above V18_SPEC_MIN_V + + warnings: list = field(default_factory=list) + + def summary(self) -> str: + ok = lambda c: "✓" if c else "✗" + lines = [ + f"Capture {self.capture_num:04d} {self.timestamp} [pwr/1v8]", + f" Mean voltage : {self.mean_v:.4f} V " + f"(spec {V18_SPEC_MIN_V:.2f}–{V18_SPEC_MAX_V:.2f} V) {ok(self.spec_pass)}", + f" Min voltage : {self.min_v:.4f} V {ok(self.droop_pass)}", + f" Droop depth : {self.droop_mv:.1f} mV", + f" Ripple RMS : {self.ripple_mv_rms:.2f} mV", + ] + for w in self.warnings: + lines.append(f" WARNING: {w}") + return "\n".join(lines) + + +def analyze_1v8_file(path: Path) -> "V1V8Metrics": + """Analyse a 1.8 V supply rail CSV captured by the Rigol DS1202Z-E.""" + m = re.match(r"(\d{8}_\d{6})_pwr_(\d+)_1v8\.csv", path.name, re.IGNORECASE) + if not m: + raise ValueError(f"Filename does not match 1v8 pattern: {path.name}") + timestamp, cap_str = m.groups() + capture_num = int(cap_str) + + times, volts = _read_csv(path) + dt = float(np.diff(times).mean()) + sample_rate = 1.0 / dt + duration_us = (float(times[-1]) - float(times[0])) * 1e6 + + mean_v = float(volts.mean()) + min_v = float(volts.min()) + max_v = float(volts.max()) + droop_mv = (mean_v - min_v) * 1000.0 + ripple_mv_rms = float(volts.std()) * 1000.0 + + spec_pass = V18_SPEC_MIN_V <= mean_v <= V18_SPEC_MAX_V + droop_pass = min_v >= V18_SPEC_MIN_V + + warnings = [] + if not spec_pass: + warnings.append( + f"Mean supply {mean_v:.4f} V outside spec " + f"({V18_SPEC_MIN_V:.2f}–{V18_SPEC_MAX_V:.2f} V)" + ) + if not droop_pass: + warnings.append( + f"Supply droops to {min_v:.4f} V — below {V18_SPEC_MIN_V:.2f} V spec min" + ) + if droop_mv > V18_DROOP_WARN_MV: + warnings.append( + f"Droop depth {droop_mv:.1f} mV — possible insufficient decoupling near MIPI PHY" + ) + if ripple_mv_rms > V18_RIPPLE_WARN_MV: + warnings.append(f"Ripple {ripple_mv_rms:.1f} mV RMS is elevated") + + return V1V8Metrics( + timestamp = timestamp, + capture_num = capture_num, + sample_rate_mhz = round(sample_rate / 1e6, 1), + duration_us = round(duration_us, 2), + n_samples = len(times), + mean_v = round(mean_v, 4), + min_v = round(min_v, 4), + max_v = round(max_v, 4), + droop_mv = round(droop_mv, 1), + ripple_mv_rms = round(ripple_mv_rms, 2), + spec_pass = spec_pass, + droop_pass = droop_pass, + warnings = warnings, + ) + + def group_captures(data_dir: Path) -> dict[tuple[str, int], dict[str, Path]]: """ Scan data_dir and group CSV files by (timestamp, capture_number). @@ -325,7 +423,9 @@ def group_captures(data_dir: Path) -> dict[tuple[str, int], dict[str, Path]]: Example key: ("20260408_111448", 1) Example value: {"sig_clk": Path(...), "sig_dat": ..., "proto_clk": ..., "proto_dat": ...} """ - pattern = re.compile(r"(\d{8}_\d{6})_(sig|proto|lp)_(\d+)_(clk|dat)\.csv", re.IGNORECASE) + pattern = re.compile( + r"(\d{8}_\d{6})_(sig|proto|lp|pwr)_(\d+)_(clk|dat|1v8)\.csv", re.IGNORECASE + ) groups: dict[tuple[str, int], dict[str, Path]] = {} for f in sorted(data_dir.glob("*.csv")): m = pattern.match(f.name) diff --git a/mipi_test.py b/mipi_test.py index 0ac84e3..892f041 100644 --- a/mipi_test.py +++ b/mipi_test.py @@ -13,8 +13,10 @@ import sys import requests import threading from datetime import datetime +from pathlib import Path import ai_mgmt import analyze_captures +import rigol_scope # --- Configuration --- URL = "http://192.168.45.8:5000/display" @@ -42,6 +44,7 @@ LP_V_OFFSET = 0.6 # V — center display at 0.6 V (range −0.2 V to 1.4 LP_TRIG_LEVEL = 0.6 # V — midpoint of LP-11 (1.2 V) → LP-01 (0 V) fall DISPLAY_SETTLE_S = 1.0 # seconds to wait after display ON before arming scope +DATA_DIR = Path(__file__).parent / "data" test_running = False # controls both worker threads resume_event = threading.Event() # cleared to pause test_worker, set to resume @@ -57,6 +60,9 @@ except Exception as e: print(f"ERROR: CANNOT CONNECT TO INSTRUMENTS: {e}") sys.exit(1) +# Rigol DS1202Z-E for 1.8 V supply monitoring (optional — test continues if unavailable) +rigol_scope.connect() + # --------------------------------------------------------------------------- def setup_scope(): @@ -299,16 +305,37 @@ def dual_capture(iteration): else: print(" SKIPPING PASS 2 SAVE.") - # ── Pass 3: LP / SoT structure ──────────────────────────────────────── + # ── Pass 3: LP / SoT structure + 1.8 V supply monitoring ───────────── # Widens vertical range to capture LP-11 (1.2 V) and falls-edge triggers # on the LP-11 → LP-01 SoT transition. Saves Ch1 and Ch3 single-ended. + # Rigol is armed first (non-blocking) so the LP→HS current step droops + # the 1.8 V rail and triggers the Rigol while the Agilent captures. print(" PASS 3: LP TRANSITION...") _configure_for_lp() _set_timebase(LP_SCALE, LP_POINTS) + + if rigol_scope.is_connected(): + rigol_scope.arm() # arm Rigol before LP trigger so it catches the droop + if _arm_and_wait(timeout=30): _save_pass_channels("lp", iteration, ts) else: print(" SKIPPING PASS 3 SAVE.") + + # Collect Rigol 1.8 V waveform (Agilent save takes ~5 s, Rigol should be done) + if rigol_scope.is_connected(): + print(" PASS 3: WAITING FOR RIGOL 1.8 V CAPTURE...") + if rigol_scope.wait_captured(timeout_s=10.0): + DATA_DIR.mkdir(exist_ok=True) + v18_path = DATA_DIR / f"{ts}_pwr_{iteration:04d}_1v8.csv" + n = rigol_scope.read_waveform_csv(v18_path) + if n: + print(f" SAVED: {v18_path.name} ({n} samples)") + else: + print(" RIGOL: Waveform read returned 0 samples.") + else: + print(" RIGOL: Timed out waiting for capture.") + _restore_hs_config() # ── Restore original timebase ───────────────────────────────────────── @@ -396,9 +423,15 @@ def main_menu(): if choice == '1': print(f"PSU : {psu.ask('*IDN?').strip()}") print(f"SCOPE: {scope.ask('*IDN?').strip()}") + if rigol_scope.is_connected(): + print(f"RIGOL: {rigol_scope.rigol.ask('*IDN?').strip()}") + else: + print("RIGOL: NOT CONNECTED") elif choice == '2': setup_scope() + if rigol_scope.is_connected(): + rigol_scope.configure() elif choice == '3': psu.write('CH1:VOLT 24.0') @@ -431,6 +464,7 @@ def main_menu(): test_running = False psu.close() scope.close() + rigol_scope.disconnect() print("INSTRUMENTS CLOSED. BYE.") break diff --git a/rigol_scope.py b/rigol_scope.py new file mode 100644 index 0000000..3e02649 --- /dev/null +++ b/rigol_scope.py @@ -0,0 +1,158 @@ +""" +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 + +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 for 1.8 V supply monitoring. + 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) + + rigol.write(":CHANnel1:DISPlay 1") + rigol.write(":CHANnel2:DISPlay 0") + rigol.write(":CHANnel1:COUPling DC") + rigol.write(":CHANnel1:PROBe 1") + rigol.write(f":CHANnel1:SCALe {V18_SCALE:.3f}") + rigol.write(f":CHANnel1:OFFSet {V18_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) + + print(f"[RIGOL] Configured: 1.8 V rail, {int(V18_TIMEBASE*1e6)} µs/div, " + f"trigger <{V18_TRIG_LEVEL} V falling (AUTO sweep)") + + +# --------------------------------------------------------------------------- +# Acquisition +# --------------------------------------------------------------------------- + +def arm(): + """Arm for a single acquisition. Non-blocking — returns immediately.""" + rigol.write(":SINGle") + + +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_waveform_csv(path: Path) -> int: + """ + Read Ch1 waveform from Rigol over SCPI and write to CSV. + The Rigol returns ASCII voltage values; we reconstruct the time axis + from the waveform preamble. + + Returns the number of samples written, or 0 on error. + """ + try: + rigol.write(":WAVeform:SOURce CHANnel1") + rigol.write(":WAVeform:FORMat ASCII") + rigol.write(":WAVeform:MODE NORMal") + + 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]) + + raw = rigol.ask(":WAVeform:DATA?").strip() + + # Strip any 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()] + + 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] Waveform read error: {e}") + return 0