2026-03-26 11:43:36 +00:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
MIPI TEST APPLICATION - MIPI_TEST.PY
|
|
|
|
|
|
- ENTRY POINT OF APPLICATION
|
|
|
|
|
|
|
2026-04-02 16:04:45 +01:00
|
|
|
|
VERSION: 0.3
|
2026-03-26 11:43:36 +00:00
|
|
|
|
AUTHOR: D. RICE 25/03/2026
|
|
|
|
|
|
© 2026 ARRIVE
|
|
|
|
|
|
"""
|
|
|
|
|
|
import vxi11
|
|
|
|
|
|
import time
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import requests
|
|
|
|
|
|
import threading
|
2026-04-02 16:04:45 +01:00
|
|
|
|
from datetime import datetime
|
2026-03-26 15:18:58 +00:00
|
|
|
|
|
|
|
|
|
|
# --- Configuration ---
|
2026-04-02 15:56:20 +01:00
|
|
|
|
URL = "http://192.168.45.8:5000/display"
|
|
|
|
|
|
SCOPE_IP = "192.168.45.4"
|
|
|
|
|
|
PSU_IP = "192.168.45.3"
|
2026-03-26 11:43:36 +00:00
|
|
|
|
|
2026-04-02 16:04:45 +01:00
|
|
|
|
# --- Capture settings ---
|
|
|
|
|
|
# Pass 1 — signal quality: resolves individual bits at 140 Mbit/s (7.1 ns/bit)
|
|
|
|
|
|
SIG_SCALE = 2e-9 # 2 ns/div → 20 ns window
|
|
|
|
|
|
SIG_POINTS = 500_000 # 500 k pts → ~25 GSa/s
|
|
|
|
|
|
|
|
|
|
|
|
# Pass 2 — protocol/frame structure: shows LP↔HS transitions and burst envelope
|
|
|
|
|
|
PROTO_SCALE = 1e-6 # 1 µs/div → 10 µs window
|
|
|
|
|
|
PROTO_POINTS = 500_000 # 500 k pts → 50 MSa/s (enough to see burst structure)
|
|
|
|
|
|
|
2026-04-02 16:08:50 +01:00
|
|
|
|
DISPLAY_SETTLE_S = 1.0 # seconds to wait after display ON before arming scope
|
|
|
|
|
|
|
2026-03-26 11:43:36 +00:00
|
|
|
|
test_running = False # Global flag to control the background thread
|
|
|
|
|
|
|
2026-03-26 15:18:58 +00:00
|
|
|
|
# --- Instrument Connection ---
|
2026-03-26 11:43:36 +00:00
|
|
|
|
try:
|
2026-04-02 15:56:20 +01:00
|
|
|
|
psu = vxi11.Instrument(PSU_IP)
|
|
|
|
|
|
scope = vxi11.Instrument(SCOPE_IP)
|
|
|
|
|
|
scope.timeout = 30 # seconds — needs to cover *RST and image saves
|
|
|
|
|
|
psu.timeout = 5
|
2026-03-26 11:43:36 +00:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"ERROR: CANNOT CONNECT TO INSTRUMENTS: {e}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
2026-04-02 15:56:20 +01:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-03-26 15:18:58 +00:00
|
|
|
|
def setup_scope():
|
2026-04-02 15:56:20 +01:00
|
|
|
|
"""Initialises DSO80204B for MIPI DSI signals (~210 MHz)."""
|
|
|
|
|
|
print("CONFIGURING SCOPE...")
|
2026-03-26 15:18:58 +00:00
|
|
|
|
|
2026-04-02 15:56:20 +01:00
|
|
|
|
cmds = [
|
|
|
|
|
|
# ── Reset & stop ──────────────────────────────────────────────────
|
|
|
|
|
|
"*RST",
|
|
|
|
|
|
":RUN",
|
|
|
|
|
|
":STOP",
|
|
|
|
|
|
|
|
|
|
|
|
# ── Channel 1 — Clock D+ ─────────────────────────────────────────
|
|
|
|
|
|
":CHANnel1:DISPlay ON",
|
|
|
|
|
|
":CHANnel1:INPut DC50",
|
|
|
|
|
|
":CHANnel1:PROBe 19.2", # 910Ω + 50Ω divider = 19.2:1
|
|
|
|
|
|
":CHANnel1:SCALe 0.1", # 100 mV/div
|
|
|
|
|
|
":CHANnel1:OFFSet 0.0",
|
|
|
|
|
|
":CHANnel1:LABel 'CLK+'",
|
|
|
|
|
|
|
|
|
|
|
|
# ── Channel 2 — Clock D- ─────────────────────────────────────────
|
|
|
|
|
|
":CHANnel2:DISPlay ON",
|
|
|
|
|
|
":CHANnel2:INPut DC50",
|
|
|
|
|
|
":CHANnel2:PROBe 19.2",
|
|
|
|
|
|
":CHANnel2:SCALe 0.1",
|
|
|
|
|
|
":CHANnel2:OFFSet 0.0",
|
|
|
|
|
|
":CHANnel2:LABel 'CLK-'",
|
|
|
|
|
|
|
|
|
|
|
|
# ── Channel 3 — Data Lane 0 D+ ───────────────────────────────────
|
|
|
|
|
|
":CHANnel3:DISPlay ON",
|
|
|
|
|
|
":CHANnel3:INPut DC50",
|
|
|
|
|
|
":CHANnel3:PROBe 19.2",
|
|
|
|
|
|
":CHANnel3:SCALe 0.1",
|
|
|
|
|
|
":CHANnel3:OFFSet 0.0",
|
|
|
|
|
|
":CHANnel3:LABel 'DAT0+'",
|
|
|
|
|
|
|
|
|
|
|
|
# ── Channel 4 — Data Lane 0 D- ───────────────────────────────────
|
|
|
|
|
|
":CHANnel4:DISPlay ON",
|
|
|
|
|
|
":CHANnel4:INPut DC50",
|
|
|
|
|
|
":CHANnel4:PROBe 19.2",
|
|
|
|
|
|
":CHANnel4:SCALe 0.1",
|
|
|
|
|
|
":CHANnel4:OFFSet 0.0",
|
|
|
|
|
|
":CHANnel4:LABel 'DAT0-'",
|
|
|
|
|
|
|
|
|
|
|
|
# ── Timebase — 5 ns/div shows ~10 cycles of 200 MHz clock ────────
|
|
|
|
|
|
":TIMebase:SCALe 5E-9",
|
|
|
|
|
|
":TIMebase:POSition 0",
|
|
|
|
|
|
":TIMebase:REFerence CENTer",
|
|
|
|
|
|
|
|
|
|
|
|
# ── Trigger — rising edge on Ch1 (Clock D+) ──────────────────────
|
|
|
|
|
|
":TRIGger:MODE EDGE",
|
|
|
|
|
|
":TRIGger:EDGE:SOURce CHANnel1",
|
|
|
|
|
|
":TRIGger:EDGE:SLOPe POSitive",
|
|
|
|
|
|
":TRIGger:EDGE:LEVel 0.05", # 50 mV post-attenuation
|
|
|
|
|
|
":TRIGger:SWEep NORMal",
|
|
|
|
|
|
|
|
|
|
|
|
# ── Acquisition ───────────────────────────────────────────────────
|
|
|
|
|
|
":ACQuire:MODE RTIMe",
|
|
|
|
|
|
":ACQuire:INTerpolate ON",
|
|
|
|
|
|
":ACQuire:POINts 500000",
|
|
|
|
|
|
|
|
|
|
|
|
# ── Display ───────────────────────────────────────────────────────
|
|
|
|
|
|
":DISPlay:LAYout STACKED",
|
|
|
|
|
|
|
|
|
|
|
|
":RUN",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for cmd in cmds:
|
2026-03-26 15:18:58 +00:00
|
|
|
|
scope.write(cmd)
|
2026-04-02 15:56:20 +01:00
|
|
|
|
time.sleep(0.05)
|
|
|
|
|
|
|
|
|
|
|
|
print("CHANNEL SETUP COMPLETE.")
|
|
|
|
|
|
setup_math_channels()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup_math_channels():
|
|
|
|
|
|
"""
|
|
|
|
|
|
F1 = Ch1 - Ch2 (clock differential)
|
|
|
|
|
|
F2 = Ch3 - Ch4 (lane 0 differential)
|
|
|
|
|
|
|
|
|
|
|
|
DSO80204B firmware 05.30.0005 confirmed syntax:
|
|
|
|
|
|
:FUNCtion<n>:DISPlay ON
|
|
|
|
|
|
:FUNCtion<n>:SUBTract CHANnel<a>,CHANnel<b>
|
|
|
|
|
|
"""
|
|
|
|
|
|
print("SETTING UP MATH CHANNELS...")
|
|
|
|
|
|
|
|
|
|
|
|
scope.write("*CLS")
|
|
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
|
|
|
|
|
|
math_cmds = [
|
|
|
|
|
|
# ── F1 = Ch1 - Ch2 (clock differential) ─────────────────────────
|
|
|
|
|
|
":FUNCtion1:DISPlay ON",
|
|
|
|
|
|
":FUNCtion1:SUBTract CHANnel1,CHANnel2",
|
|
|
|
|
|
":FUNCtion1:RANGe 0.8", # 0.8V range = 100mV/div (range = 8 × scale)
|
|
|
|
|
|
":FUNCtion1:OFFSet 0.0",
|
|
|
|
|
|
|
|
|
|
|
|
# ── F2 = Ch3 - Ch4 (lane 0 differential) ─────────────────────────
|
|
|
|
|
|
":FUNCtion2:DISPlay ON",
|
|
|
|
|
|
":FUNCtion2:SUBTract CHANnel3,CHANnel4",
|
|
|
|
|
|
":FUNCtion2:RANGe 0.8", # 0.8V range = 100mV/div
|
|
|
|
|
|
":FUNCtion2:OFFSet 0.0",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for cmd in math_cmds:
|
|
|
|
|
|
scope.write(cmd)
|
|
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
time.sleep(1.0)
|
|
|
|
|
|
opc = scope.ask("*OPC?")
|
|
|
|
|
|
print(f" SCOPE SYNC OK (OPC={opc.strip()})")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" WARNING: OPC SYNC FAILED ({e})")
|
2026-03-26 15:18:58 +00:00
|
|
|
|
|
2026-04-02 15:56:20 +01:00
|
|
|
|
try:
|
|
|
|
|
|
err = scope.ask(":SYSTem:ERRor?")
|
|
|
|
|
|
if err.strip().startswith("0"):
|
|
|
|
|
|
print(" MATH COMMANDS ACCEPTED — NO SCPI ERRORS.")
|
|
|
|
|
|
print(" F1 = CLK DIFF (CH1-CH2), F2 = DAT DIFF (CH3-CH4)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" SCPI ERROR: {err.strip()}")
|
2026-03-26 15:18:58 +00:00
|
|
|
|
except Exception as e:
|
2026-04-02 15:56:20 +01:00
|
|
|
|
print(f" COULD NOT READ ERROR QUEUE ({e})")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-02 16:04:45 +01:00
|
|
|
|
def _set_timebase(scale, points):
|
|
|
|
|
|
"""Apply timebase scale and record length, then let the scope settle."""
|
|
|
|
|
|
scope.write(f":TIMebase:SCALe {scale:.3E}")
|
|
|
|
|
|
scope.write(f":ACQuire:POINts {points}")
|
|
|
|
|
|
time.sleep(0.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _arm_and_wait(timeout=20):
|
|
|
|
|
|
"""
|
2026-04-02 16:17:47 +01:00
|
|
|
|
Fire a single acquisition using :DIGitize (blocking on Infiniium) and
|
|
|
|
|
|
confirm completion with *OPC?. Temporarily extends scope.timeout to
|
|
|
|
|
|
cover the full wait period.
|
|
|
|
|
|
Returns True on success, False on error/timeout.
|
2026-04-02 16:04:45 +01:00
|
|
|
|
"""
|
2026-04-02 16:17:47 +01:00
|
|
|
|
prev_timeout = scope.timeout
|
|
|
|
|
|
try:
|
|
|
|
|
|
scope.timeout = timeout + 5 # headroom over the DIGitize wait
|
|
|
|
|
|
scope.write(":DIGitize")
|
|
|
|
|
|
resp = scope.ask("*OPC?").strip()
|
|
|
|
|
|
return resp == "1"
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" ACQUIRE ERROR: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
finally:
|
|
|
|
|
|
scope.timeout = prev_timeout
|
2026-04-02 16:04:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _save_pass(tag, iteration, ts):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Save F1 (CLK diff) and F2 (DAT diff) as CSV, plus a PNG screenshot.
|
|
|
|
|
|
Files land in C:\\TEMP on the scope's local disk.
|
|
|
|
|
|
Naming: <YYYYMMDD_HHMMSS>_<tag>_<iter>_clk.csv (date-first for easy sorting/deletion)
|
|
|
|
|
|
"""
|
|
|
|
|
|
base = f"C:\\TEMP\\{ts}_{tag}_{iteration:04d}"
|
2026-04-02 15:56:20 +01:00
|
|
|
|
try:
|
2026-04-02 16:04:45 +01:00
|
|
|
|
scope.write(f':DISK:SAVE:WAVeform FUNCtion1,"{base}_clk.csv",CSV')
|
|
|
|
|
|
time.sleep(2.5)
|
|
|
|
|
|
scope.write(f':DISK:SAVE:WAVeform FUNCtion2,"{base}_dat.csv",CSV')
|
|
|
|
|
|
time.sleep(2.5)
|
|
|
|
|
|
print(f" SAVED: {base}_clk.csv {base}_dat.csv")
|
2026-04-02 15:56:20 +01:00
|
|
|
|
except Exception as e:
|
2026-04-02 16:04:45 +01:00
|
|
|
|
print(f" SAVE ERROR ({tag}): {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def dual_capture(iteration):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Two-pass capture per test iteration:
|
|
|
|
|
|
Pass 1 — signal quality (SIG_SCALE / SIG_POINTS)
|
|
|
|
|
|
Pass 2 — frame structure (PROTO_SCALE / PROTO_POINTS)
|
|
|
|
|
|
Restores the original 5 ns/div timebase when done.
|
|
|
|
|
|
"""
|
|
|
|
|
|
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
print(f"DUAL CAPTURE #{iteration:04d} [{ts}]")
|
|
|
|
|
|
|
|
|
|
|
|
# ── Pass 1: signal quality ─────────────────────────────────────────────
|
|
|
|
|
|
print(" PASS 1: SIGNAL QUALITY...")
|
|
|
|
|
|
_set_timebase(SIG_SCALE, SIG_POINTS)
|
|
|
|
|
|
if _arm_and_wait():
|
|
|
|
|
|
_save_pass("sig", iteration, ts)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(" SKIPPING PASS 1 SAVE.")
|
|
|
|
|
|
|
|
|
|
|
|
# ── Pass 2: frame/protocol structure ──────────────────────────────────
|
|
|
|
|
|
print(" PASS 2: FRAME STRUCTURE...")
|
|
|
|
|
|
_set_timebase(PROTO_SCALE, PROTO_POINTS)
|
|
|
|
|
|
if _arm_and_wait():
|
|
|
|
|
|
_save_pass("proto", iteration, ts)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(" SKIPPING PASS 2 SAVE.")
|
|
|
|
|
|
|
|
|
|
|
|
# ── Restore original timebase ─────────────────────────────────────────
|
|
|
|
|
|
_set_timebase(5e-9, 500_000)
|
|
|
|
|
|
scope.write(":RUN")
|
2026-04-02 15:56:20 +01:00
|
|
|
|
|
2026-03-26 15:18:58 +00:00
|
|
|
|
|
2026-04-02 16:08:50 +01:00
|
|
|
|
def test_worker():
|
2026-04-02 15:56:20 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Background loop:
|
2026-04-02 16:08:50 +01:00
|
|
|
|
- Turns display ON, waits DISPLAY_SETTLE_S for it to stabilise
|
2026-04-02 16:04:45 +01:00
|
|
|
|
- Runs dual_capture (signal quality + frame structure)
|
2026-04-02 15:56:20 +01:00
|
|
|
|
- Turns display OFF for 1 second
|
|
|
|
|
|
- Repeats until test_running = False
|
|
|
|
|
|
"""
|
2026-03-26 11:43:36 +00:00
|
|
|
|
global test_running
|
2026-03-26 15:18:58 +00:00
|
|
|
|
count = 1
|
2026-04-02 15:56:20 +01:00
|
|
|
|
|
2026-03-26 11:43:36 +00:00
|
|
|
|
while test_running:
|
2026-03-26 15:18:58 +00:00
|
|
|
|
requests.put(URL, json={"state": "on"}, timeout=2)
|
2026-04-02 16:08:50 +01:00
|
|
|
|
time.sleep(DISPLAY_SETTLE_S)
|
2026-04-02 16:04:45 +01:00
|
|
|
|
dual_capture(count)
|
2026-03-26 15:18:58 +00:00
|
|
|
|
count += 1
|
|
|
|
|
|
requests.put(URL, json={"state": "off"}, timeout=2)
|
|
|
|
|
|
time.sleep(1.0)
|
2026-03-26 11:43:36 +00:00
|
|
|
|
|
2026-04-02 15:56:20 +01:00
|
|
|
|
|
2026-03-26 11:43:36 +00:00
|
|
|
|
def main_menu():
|
|
|
|
|
|
global test_running
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
2026-03-26 15:18:58 +00:00
|
|
|
|
print("\n===== MIPI TEST CONTROL =====")
|
2026-03-26 11:43:36 +00:00
|
|
|
|
print("1. RUN IDN CHECK (PSU & SCOPE)")
|
2026-03-26 15:18:58 +00:00
|
|
|
|
print("2. SETUP SCOPE (RUN FIRST)")
|
2026-04-02 15:56:20 +01:00
|
|
|
|
print("3. CONFIGURE PSU (DEFAULT 24V / 1.5A)")
|
2026-03-26 15:18:58 +00:00
|
|
|
|
print("4. PSU OUTPUT ON/OFF (CH1)")
|
|
|
|
|
|
print("5. START TEST & CAPTURE")
|
|
|
|
|
|
print("6. STOP TEST")
|
|
|
|
|
|
print("7. EXIT")
|
2026-04-02 15:56:20 +01:00
|
|
|
|
|
|
|
|
|
|
choice = input("\nSELECT OPTION (1-7): ").strip()
|
2026-03-26 11:43:36 +00:00
|
|
|
|
|
|
|
|
|
|
if choice == '1':
|
2026-04-02 15:56:20 +01:00
|
|
|
|
print(f"PSU : {psu.ask('*IDN?').strip()}")
|
|
|
|
|
|
print(f"SCOPE: {scope.ask('*IDN?').strip()}")
|
|
|
|
|
|
|
2026-03-26 11:43:36 +00:00
|
|
|
|
elif choice == '2':
|
2026-03-26 15:18:58 +00:00
|
|
|
|
setup_scope()
|
2026-04-02 15:56:20 +01:00
|
|
|
|
|
2026-03-26 15:18:58 +00:00
|
|
|
|
elif choice == '3':
|
2026-03-26 11:43:36 +00:00
|
|
|
|
psu.write('CH1:VOLT 24.0')
|
|
|
|
|
|
psu.write('CH1:CURR 1.5')
|
2026-04-02 15:56:20 +01:00
|
|
|
|
print("PSU CONFIGURED: 24V / 1.5A")
|
|
|
|
|
|
|
2026-03-26 15:18:58 +00:00
|
|
|
|
elif choice == '4':
|
2026-04-02 15:56:20 +01:00
|
|
|
|
state = input("TYPE 'ON' OR 'OFF': ").strip().upper()
|
|
|
|
|
|
if state in ('ON', 'OFF'):
|
|
|
|
|
|
psu.write(f'OUTP CH1,{state}')
|
|
|
|
|
|
print(f"PSU OUTPUT {state}.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("INVALID — TYPE 'ON' OR 'OFF'.")
|
|
|
|
|
|
|
2026-03-26 15:18:58 +00:00
|
|
|
|
elif choice == '5':
|
2026-03-26 11:43:36 +00:00
|
|
|
|
if not test_running:
|
2026-04-02 16:08:50 +01:00
|
|
|
|
test_running = True
|
|
|
|
|
|
t = threading.Thread(target=test_worker, daemon=True)
|
|
|
|
|
|
t.start()
|
|
|
|
|
|
print("TEST STARTED.")
|
2026-03-26 11:43:36 +00:00
|
|
|
|
else:
|
|
|
|
|
|
print("TEST IS ALREADY RUNNING!")
|
2026-04-02 15:56:20 +01:00
|
|
|
|
|
2026-03-26 15:18:58 +00:00
|
|
|
|
elif choice == '6':
|
2026-03-26 11:43:36 +00:00
|
|
|
|
print("STOPPING TEST...")
|
|
|
|
|
|
test_running = False
|
2026-04-02 15:56:20 +01:00
|
|
|
|
|
|
|
|
|
|
elif choice == '7':
|
|
|
|
|
|
test_running = False
|
2026-03-26 11:43:36 +00:00
|
|
|
|
psu.close()
|
|
|
|
|
|
scope.close()
|
2026-04-02 15:56:20 +01:00
|
|
|
|
print("INSTRUMENTS CLOSED. BYE.")
|
2026-03-26 11:43:36 +00:00
|
|
|
|
break
|
2026-04-02 15:56:20 +01:00
|
|
|
|
|
2026-03-26 11:43:36 +00:00
|
|
|
|
else:
|
2026-04-02 15:56:20 +01:00
|
|
|
|
print("INVALID ENTRY. PLEASE CHOOSE 1-7.")
|
|
|
|
|
|
|
2026-03-26 11:43:36 +00:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main_menu()
|