Changes
This commit is contained in:
234
mipi_test.py
234
mipi_test.py
@@ -3,7 +3,7 @@
|
|||||||
MIPI TEST APPLICATION - MIPI_TEST.PY
|
MIPI TEST APPLICATION - MIPI_TEST.PY
|
||||||
- ENTRY POINT OF APPLICATION
|
- ENTRY POINT OF APPLICATION
|
||||||
|
|
||||||
VERSION: 0.1
|
VERSION: 0.2
|
||||||
AUTHOR: D. RICE 25/03/2026
|
AUTHOR: D. RICE 25/03/2026
|
||||||
© 2026 ARRIVE
|
© 2026 ARRIVE
|
||||||
"""
|
"""
|
||||||
@@ -12,7 +12,6 @@ import time
|
|||||||
import sys
|
import sys
|
||||||
import requests
|
import requests
|
||||||
import threading
|
import threading
|
||||||
import os
|
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
URL = "http://192.168.45.8:5000/display"
|
URL = "http://192.168.45.8:5000/display"
|
||||||
@@ -23,92 +22,176 @@ test_running = False # Global flag to control the background thread
|
|||||||
|
|
||||||
# --- Instrument Connection ---
|
# --- Instrument Connection ---
|
||||||
try:
|
try:
|
||||||
psu = vxi11.Instrument("192.168.45.3")
|
psu = vxi11.Instrument(PSU_IP)
|
||||||
scope = vxi11.Instrument("192.168.45.4")
|
scope = vxi11.Instrument(SCOPE_IP)
|
||||||
# Increase timeout for large image data transfers
|
scope.timeout = 30 # seconds — needs to cover *RST and image saves
|
||||||
scope.timeout = 5
|
psu.timeout = 5
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: CANNOT CONNECT TO INSTRUMENTS: {e}")
|
print(f"ERROR: CANNOT CONNECT TO INSTRUMENTS: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def setup_scope():
|
def setup_scope():
|
||||||
"""Initializes DSO80204B for 210MHz MIPI Signals"""
|
"""Initialises DSO80204B for MIPI DSI signals (~210 MHz)."""
|
||||||
print("Configuring Scope...")
|
print("CONFIGURING SCOPE...")
|
||||||
scope.write("*RST")
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Setup Channels (1&2=Clock, 3&4=Data)
|
cmds = [
|
||||||
for ch in range(1, 5):
|
# ── Reset & stop ──────────────────────────────────────────────────
|
||||||
scope.write(f":CHANnel{ch}:DISPlay ON")
|
"*RST",
|
||||||
scope.write(f":CHANnel{ch}:PROBe 19.2") # 910R/50R Divider
|
":RUN",
|
||||||
scope.write(f":CHANnel{ch}:INPut DC50")
|
":STOP",
|
||||||
scope.write(f":CHANnel{ch}:SCALe 0.1") # 100mV/div (200mV HS swing)
|
|
||||||
scope.write(f":CHANnel{ch}:OFFSet 0.0")
|
# ── 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})")
|
||||||
|
|
||||||
# Timebase & Trigger
|
|
||||||
scope.write(":TIMebase:SCALe 10e-6") # 10us/div
|
|
||||||
scope.write(":TRIGger:EDGE:SOURce CHANnel3")
|
|
||||||
scope.write(":TRIGger:EDGE:SLOPe FALLing") # Catch LP-11 to HS transition
|
|
||||||
scope.write(":TRIGger:LEVel CHANnel3, 0.15") # 150mV
|
|
||||||
scope.write(":TRIGger:SWEep SINGle")
|
|
||||||
print("Scope Configured.")
|
|
||||||
|
|
||||||
def save_screenshot(iteration):
|
def save_screenshot(iteration):
|
||||||
|
"""Save a PNG screenshot to C:\\TEMP on the scope's local disk."""
|
||||||
try:
|
try:
|
||||||
scope.clear()
|
filename = f"C:\\TEMP\\cap{iteration:04d}.png"
|
||||||
# Use a very short, simple path. No subdirectories if possible.
|
print(f"SAVING SCREENSHOT: {filename}")
|
||||||
# Ensure C:\TEMP exists!
|
scope.write(f':DISK:SAVE:IMAGe "{filename}",PNG')
|
||||||
filename = f"C:\\TEMP\\cap{iteration}.png"
|
time.sleep(5.0) # wait for scope to write file — do not query during this
|
||||||
|
print("SCREENSHOT SAVED.")
|
||||||
print(f"ATTEMPTING SIMPLEST XP SYNTAX: {filename}")
|
|
||||||
|
|
||||||
# Try ONLY the filename and the format.
|
|
||||||
# Many XP versions hate the SCR,COL extra parameters via SCPI.
|
|
||||||
cmd = f':DISK:SAVE:IMAGe "{filename}",PNG'
|
|
||||||
|
|
||||||
scope.write(cmd)
|
|
||||||
|
|
||||||
# DO NOT use scope.ask() or any queries yet.
|
|
||||||
# Just sleep and check the folder manually.
|
|
||||||
time.sleep(5.0)
|
|
||||||
print("COMMAND SENT. CHECK C:\\TEMP")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"XP COMM ERROR: {e}")
|
print(f"SCREENSHOT ERROR: {e}")
|
||||||
|
|
||||||
|
|
||||||
def test_worker(on_time):
|
def test_worker(on_time):
|
||||||
""" Background loop:
|
"""
|
||||||
- Sets display ON for user-defined duration.
|
Background loop:
|
||||||
- Sets display OFF for exactly 1 second.
|
- Arms scope in single trigger mode
|
||||||
|
- Turns display ON for on_time seconds
|
||||||
|
- Saves screenshot
|
||||||
|
- Turns display OFF for 1 second
|
||||||
|
- Repeats until test_running = False
|
||||||
"""
|
"""
|
||||||
global test_running
|
global test_running
|
||||||
count = 1
|
count = 1
|
||||||
|
|
||||||
while test_running:
|
while test_running:
|
||||||
# Arm scope
|
|
||||||
scope.write(":SINGle")
|
scope.write(":SINGle")
|
||||||
|
|
||||||
# Start display
|
|
||||||
requests.put(URL, json={"state": "on"}, timeout=2)
|
requests.put(URL, json={"state": "on"}, timeout=2)
|
||||||
|
time.sleep(on_time + 0.5) # +0.5s to ensure acquisition completes
|
||||||
# Wait for the scope to trigger and fill memory
|
|
||||||
# We wait slightly longer than the 'on_time' to ensure capture is done
|
|
||||||
time.sleep(on_time + 0.5)
|
|
||||||
|
|
||||||
# Pull the image
|
|
||||||
save_screenshot(count)
|
save_screenshot(count)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
# Set Display OFF
|
|
||||||
requests.put(URL, json={"state": "off"}, timeout=2)
|
requests.put(URL, json={"state": "off"}, timeout=2)
|
||||||
|
|
||||||
# Fixed 1s OFF period
|
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
|
||||||
def main_menu():
|
def main_menu():
|
||||||
global test_running
|
global test_running
|
||||||
|
|
||||||
"""Main Application Loop."""
|
|
||||||
while True:
|
while True:
|
||||||
print("\n===== MIPI TEST CONTROL =====")
|
print("\n===== MIPI TEST CONTROL =====")
|
||||||
print("1. RUN IDN CHECK (PSU & SCOPE)")
|
print("1. RUN IDN CHECK (PSU & SCOPE)")
|
||||||
@@ -119,42 +202,55 @@ def main_menu():
|
|||||||
print("6. STOP TEST")
|
print("6. STOP TEST")
|
||||||
print("7. EXIT")
|
print("7. EXIT")
|
||||||
|
|
||||||
choice = input("\nSELECT OPTION (1-7): ")
|
choice = input("\nSELECT OPTION (1-7): ").strip()
|
||||||
|
|
||||||
if choice == '1':
|
if choice == '1':
|
||||||
print(f"PSU: {psu.ask('*IDN?')}")
|
print(f"PSU : {psu.ask('*IDN?').strip()}")
|
||||||
print(f"SCOPE: {scope.ask('*IDN?')}")
|
print(f"SCOPE: {scope.ask('*IDN?').strip()}")
|
||||||
|
|
||||||
elif choice == '2':
|
elif choice == '2':
|
||||||
setup_scope()
|
setup_scope()
|
||||||
|
|
||||||
elif choice == '3':
|
elif choice == '3':
|
||||||
psu.write('CH1:VOLT 24.0')
|
psu.write('CH1:VOLT 24.0')
|
||||||
psu.write('CH1:CURR 1.5')
|
psu.write('CH1:CURR 1.5')
|
||||||
print("PSU CONFIGURED...")
|
print("PSU CONFIGURED: 24V / 1.5A")
|
||||||
|
|
||||||
elif choice == '4':
|
elif choice == '4':
|
||||||
state = input("TYPE 'ON' OR 'OFF': ").upper()
|
state = input("TYPE 'ON' OR 'OFF': ").strip().upper()
|
||||||
|
if state in ('ON', 'OFF'):
|
||||||
psu.write(f'OUTP CH1,{state}')
|
psu.write(f'OUTP CH1,{state}')
|
||||||
|
print(f"PSU OUTPUT {state}.")
|
||||||
|
else:
|
||||||
|
print("INVALID — TYPE 'ON' OR 'OFF'.")
|
||||||
|
|
||||||
elif choice == '5':
|
elif choice == '5':
|
||||||
if not test_running:
|
if not test_running:
|
||||||
try:
|
try:
|
||||||
sec = float(input("ON DURATION (sec): "))
|
sec = float(input("ON DURATION (seconds): "))
|
||||||
test_running = True
|
test_running = True
|
||||||
t = threading.Thread(target=test_worker, args=(sec,), daemon=True)
|
t = threading.Thread(target=test_worker, args=(sec,), daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
print(f"TEST STARTED — ON TIME: {sec}s")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("INVALID TIME ENTERED.")
|
print("INVALID TIME ENTERED.")
|
||||||
else:
|
else:
|
||||||
print("TEST IS ALREADY RUNNING!")
|
print("TEST IS ALREADY RUNNING!")
|
||||||
|
|
||||||
elif choice == '6':
|
elif choice == '6':
|
||||||
print("STOPPING TEST...")
|
print("STOPPING TEST...")
|
||||||
test_running = False
|
test_running = False
|
||||||
|
|
||||||
elif choice == '7':
|
elif choice == '7':
|
||||||
test_running = False # Ensure thread stops
|
test_running = False
|
||||||
psu.close()
|
psu.close()
|
||||||
scope.close()
|
scope.close()
|
||||||
|
print("INSTRUMENTS CLOSED. BYE.")
|
||||||
break
|
break
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("INVALID ENTRY. PLEASE CHOOSE 1-6")
|
print("INVALID ENTRY. PLEASE CHOOSE 1-7.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Optional: Run auto-config on startup
|
|
||||||
main_menu()
|
main_menu()
|
||||||
Reference in New Issue
Block a user