Files
MiPi_TEST/mipi_test.py
2026-04-02 16:17:47 +01:00

325 lines
11 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.
#!/usr/bin/env python3
"""
MIPI TEST APPLICATION - MIPI_TEST.PY
- ENTRY POINT OF APPLICATION
VERSION: 0.3
AUTHOR: D. RICE 25/03/2026
© 2026 ARRIVE
"""
import vxi11
import time
import sys
import requests
import threading
from datetime import datetime
# --- Configuration ---
URL = "http://192.168.45.8:5000/display"
SCOPE_IP = "192.168.45.4"
PSU_IP = "192.168.45.3"
# --- 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)
DISPLAY_SETTLE_S = 1.0 # seconds to wait after display ON before arming scope
test_running = False # Global flag to control the background thread
# --- Instrument Connection ---
try:
psu = vxi11.Instrument(PSU_IP)
scope = vxi11.Instrument(SCOPE_IP)
scope.timeout = 30 # seconds — needs to cover *RST and image saves
psu.timeout = 5
except Exception as e:
print(f"ERROR: CANNOT CONNECT TO INSTRUMENTS: {e}")
sys.exit(1)
# ---------------------------------------------------------------------------
def setup_scope():
"""Initialises DSO80204B for MIPI DSI signals (~210 MHz)."""
print("CONFIGURING SCOPE...")
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:
scope.write(cmd)
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})")
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()}")
except Exception as e:
print(f" COULD NOT READ ERROR QUEUE ({e})")
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):
"""
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.
"""
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
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}"
try:
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")
except Exception as e:
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")
def test_worker():
"""
Background loop:
- Turns display ON, waits DISPLAY_SETTLE_S for it to stabilise
- Runs dual_capture (signal quality + frame structure)
- Turns display OFF for 1 second
- Repeats until test_running = False
"""
global test_running
count = 1
while test_running:
requests.put(URL, json={"state": "on"}, timeout=2)
time.sleep(DISPLAY_SETTLE_S)
dual_capture(count)
count += 1
requests.put(URL, json={"state": "off"}, timeout=2)
time.sleep(1.0)
def main_menu():
global test_running
while True:
print("\n===== MIPI TEST CONTROL =====")
print("1. RUN IDN CHECK (PSU & SCOPE)")
print("2. SETUP SCOPE (RUN FIRST)")
print("3. CONFIGURE PSU (DEFAULT 24V / 1.5A)")
print("4. PSU OUTPUT ON/OFF (CH1)")
print("5. START TEST & CAPTURE")
print("6. STOP TEST")
print("7. EXIT")
choice = input("\nSELECT OPTION (1-7): ").strip()
if choice == '1':
print(f"PSU : {psu.ask('*IDN?').strip()}")
print(f"SCOPE: {scope.ask('*IDN?').strip()}")
elif choice == '2':
setup_scope()
elif choice == '3':
psu.write('CH1:VOLT 24.0')
psu.write('CH1:CURR 1.5')
print("PSU CONFIGURED: 24V / 1.5A")
elif choice == '4':
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'.")
elif choice == '5':
if not test_running:
test_running = True
t = threading.Thread(target=test_worker, daemon=True)
t.start()
print("TEST STARTED.")
else:
print("TEST IS ALREADY RUNNING!")
elif choice == '6':
print("STOPPING TEST...")
test_running = False
elif choice == '7':
test_running = False
psu.close()
scope.close()
print("INSTRUMENTS CLOSED. BYE.")
break
else:
print("INVALID ENTRY. PLEASE CHOOSE 1-7.")
if __name__ == "__main__":
main_menu()