This commit is contained in:
david rice
2026-04-20 16:06:01 +01:00
parent 712a42ecb7
commit ac65270cef
6 changed files with 103 additions and 70 deletions

View File

@@ -59,7 +59,13 @@ CLK_LP_LOW_MIN_NS = 300.0
# On this hardware normal HS = 105122 mV; confirmed flicker = 1432 mV (DC / LP-11 recovery). # On this hardware normal HS = 105122 mV; confirmed flicker = 1432 mV (DC / LP-11 recovery).
# Captures where LP-01/LP-00 completed normally but the bridge never entered HS mode show # Captures where LP-01/LP-00 completed normally but the bridge never entered HS mode show
# essentially zero amplitude (the burst window is DC LP-11), so lp_low alone cannot detect this. # essentially zero amplitude (the burst window is DC LP-11), so lp_low alone cannot detect this.
HS_BURST_AMPLITUDE_MIN_MV = 50.0 # mV — below this, no real HS burst is present HS_BURST_AMPLITUDE_MIN_MV = 40.0 # mV — below this, no real HS burst is present
# Lowered from 50 mV: 48 mV capture (0001) was a false alarm; true flicker (0008) at 34 mV.
# Mode A minimum amplitude: LP-11-return edge artifacts produce near-zero amplitude in the
# burst window (burst is pure LP-low DC between two LP-11 regions). Require ≥ this to
# distinguish a genuine weak-HS attempt from a false rolling-std trigger on LP-11 return.
HS_MODE_A_MIN_MV = 10.0 # mV
@dataclass @dataclass
@@ -969,13 +975,20 @@ def analyze_lp_file(path: Path) -> "LPMetrics":
f"(TCLK_PREPARE+TCLK_ZERO minimum) — SN65DSI83 may fail to lock CLK lane" f"(TCLK_PREPARE+TCLK_ZERO minimum) — SN65DSI83 may fail to lock CLK lane"
) )
# Flicker suspect: three confirmed failure modes on this hardware: # Flicker suspect: four confirmed failure modes on this hardware:
# #
# A) Normal LP-low (~342380 ns) → bridge misses SoT → returns to LP-11 # A) Normal LP-low (~342380 ns) → bridge misses SoT → returns to LP-11
# Signature: lp11_to_hs fires at real LP-low end (~347 ns), hs_amplitude ≈ 1530 mV. # Signature: lp11_to_hs fires at real LP-low end (~347 ns), hs_amplitude ≈ 1530 mV.
# Guard: lp11_to_hs >= LP_LOW_DUR_MIN_NS prevents DC-content false positives # Guard: lp11_to_hs >= LP_LOW_DUR_MIN_NS prevents DC-content false positives
# where the ~3 ns noise spike fires the gate but HS IS present. # where the ~3 ns noise spike fires the gate but HS IS present.
# #
# A2) LP-11 present, HS attempt made but amplitude too weak for rolling-std to fire
# Signature: lp11_to_hs is None (rolling-std < HS_OSC_STD_V throughout 500 ns
# lookahead), hs_amplitude < 50 mV, LP-11 returns ~500 ns later.
# Root cause: marginal VDD_DSI (LP-11 sags to ~1.0 V vs 1.2 V nominal), reducing
# HS drive strength below the detection threshold.
# Confirmed: capture 0010 (lp11_to_hs=None, amp≈32 mV, LP-11 returned at +513 ns).
#
# B) Short LP-low (50200 ns, vs nominal ~342380 ns) → marginal SoT timing # B) Short LP-low (50200 ns, vs nominal ~342380 ns) → marginal SoT timing
# → HS burst starts but is weak, hs_amplitude ≈ 4060 mV (vs normal 100122 mV). # → HS burst starts but is weak, hs_amplitude ≈ 4060 mV (vs normal 100122 mV).
# Signature: lp_low anomalously short, lp11_to_hs fires at noise spike (~3 ns). # Signature: lp_low anomalously short, lp11_to_hs fires at noise spike (~3 ns).
@@ -997,8 +1010,16 @@ def analyze_lp_file(path: Path) -> "LPMetrics":
hs_amplitude_mv is not None hs_amplitude_mv is not None
and hs_amplitude_mv < HS_BURST_AMPLITUDE_MIN_MV and hs_amplitude_mv < HS_BURST_AMPLITUDE_MIN_MV
and ( and (
# Mode A: LP-low normal, HS never started (rolling-std confirms it) # Mode A: LP-low normal, rolling-std fired but HS amplitude is sub-threshold.
(lp11_to_hs_ns is not None and lp11_to_hs_ns >= LP_LOW_DUR_MIN_NS) # Require amp ≥ HS_MODE_A_MIN_MV to exclude LP-11-return artifacts: when LP-11
# returns after LP-low without any HS attempt the burst window is pure DC ~0 V
# (two LP-11 regions straddling a clean LP-low), giving amp ≈ 03 mV. A genuine
# weak HS attempt leaves measurable oscillations well above this floor.
(lp11_to_hs_ns is not None and lp11_to_hs_ns >= LP_LOW_DUR_MIN_NS
and hs_amplitude_mv >= HS_MODE_A_MIN_MV)
# Mode A2: rolling-std never fired — HS absent or amplitude below HS_OSC_STD_V;
# weak oscillations are misclassified as LP-low, masking the true HS failure
or lp11_to_hs_ns is None
# Mode B: LP-low anomalously short + low amplitude = marginal HS launch # Mode B: LP-low anomalously short + low amplitude = marginal HS launch
or _lp_low_short or _lp_low_short
) )

View File

@@ -27,7 +27,7 @@ REGISTER_COMMANDS = [
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# SN65DSI83 I2C configuration # SN65DSI83 I2C configuration
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
SN65_I2C_BUS = 2 # i2c-2 on i.MX 8M Mini — change if bridge is on a different bus SN65_I2C_BUS = 4 # i2c-4 on this board
SN65_I2C_ADDR = 0x2C # SN65DSI83 fixed 7-bit I2C address SN65_I2C_ADDR = 0x2C # SN65DSI83 fixed 7-bit I2C address
# Known Samsung DSIM register names (base 0x32E10000, i.MX 8M Mini) # Known Samsung DSIM register names (base 0x32E10000, i.MX 8M Mini)
@@ -124,25 +124,28 @@ def get_registers():
}), 200 }), 200
def _i2c_read_byte(bus: int, addr: int, reg: int) -> int | None: def _i2c_read_byte(bus: int, addr: int, reg: int) -> tuple[int | None, str]:
"""Read one byte from an I2C device using i2cget. Returns None on failure.""" """Read one byte via i2cget. Returns (value, "") on success or (None, error_str) on failure."""
try: try:
result = subprocess.run( result = subprocess.run(
["i2cget", "-y", str(bus), f"0x{addr:02x}", f"0x{reg:02x}"], ["i2cget", "-y", "-f", str(bus), f"0x{addr:02x}", f"0x{reg:02x}"],
capture_output=True, text=True, timeout=3 capture_output=True, text=True, timeout=3
) )
if result.returncode == 0: if result.returncode == 0:
return int(result.stdout.strip(), 16) return int(result.stdout.strip(), 16), ""
except Exception: stderr = result.stderr.strip() or f"exit code {result.returncode}"
pass return None, stderr
return None except FileNotFoundError:
return None, "i2cget not found in PATH"
except Exception as e:
return None, str(e)
@app.route("/sn65_registers", methods=["GET"]) @app.route("/sn65_registers", methods=["GET"])
def get_sn65_registers(): def get_sn65_registers():
"""Read SN65DSI83 CSR 0x0A (PLL/CLK status) and 0xE5 (error flags) via I2C.""" """Read SN65DSI83 CSR 0x0A (PLL/CLK status) and 0xE5 (error flags) via I2C."""
csr_0a = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0x0A) csr_0a, err_0a = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0x0A)
csr_e5 = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0xE5) csr_e5, err_e5 = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0xE5)
regs = {} regs = {}
errors = [] errors = []
@@ -151,10 +154,10 @@ def get_sn65_registers():
regs["csr_0a"] = { regs["csr_0a"] = {
"value": f"0x{csr_0a:02x}", "value": f"0x{csr_0a:02x}",
"pll_lock": bool(csr_0a & 0x80), # bit 7: PLL_EN_STAT (powered + locked) "pll_lock": bool(csr_0a & 0x80), # bit 7: PLL_EN_STAT (powered + locked)
"clk_det": bool(csr_0a & 0x40), # bit 6: high-speed CLK lane detected "clk_det": bool(csr_0a & 0x08), # bit 3: CHA_CLK_DET (HS clock detected)
} }
else: else:
errors.append("CSR 0x0A: i2cget failed") errors.append(f"CSR 0x0A: {err_0a}")
if csr_e5 is not None: if csr_e5 is not None:
regs["csr_e5"] = { regs["csr_e5"] = {
@@ -167,7 +170,7 @@ def get_sn65_registers():
"cha_crc_err": bool(csr_e5 & 0x40), "cha_crc_err": bool(csr_e5 & 0x40),
} }
else: else:
errors.append("CSR 0xE5: i2cget failed") errors.append(f"CSR 0xE5: {err_e5}")
return jsonify({ return jsonify({
"i2c_bus": SN65_I2C_BUS, "i2c_bus": SN65_I2C_BUS,

View File

@@ -29,6 +29,10 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
import math import math
import os
import struct
import tempfile
import wave
import anthropic import anthropic
import vxi11 import vxi11
@@ -37,8 +41,7 @@ from dotenv import load_dotenv
import ai_mgmt import ai_mgmt
import rigol_scope import rigol_scope
from csv_preprocessor import (analyze_lp_file, LPMetrics, from csv_preprocessor import (analyze_lp_file, LPMetrics,
HS_BURST_AMPLITUDE_MIN_MV, FLICKER_LP_LOW_MAX_NS, HS_BURST_AMPLITUDE_MIN_MV, FLICKER_LP_LOW_MAX_NS)
analyze_int_file)
load_dotenv(Path(__file__).parent / ".env") load_dotenv(Path(__file__).parent / ".env")
@@ -565,6 +568,7 @@ def _configure_for_lp():
scope.write(":TRIGger:EDGE:SOURce CHANnel3") scope.write(":TRIGger:EDGE:SOURce CHANnel3")
scope.write(":TRIGger:EDGE:SLOPe NEGative") scope.write(":TRIGger:EDGE:SLOPe NEGative")
scope.write(f":TRIGger:EDGE:LEVel {LP_TRIG_LEVEL:.3f}") scope.write(f":TRIGger:EDGE:LEVel {LP_TRIG_LEVEL:.3f}")
scope.write(":TRIGger:SWEep NORMal") # must wait for real LP-11→LP-01 edge, not auto-fire on HS
time.sleep(0.1) time.sleep(0.1)
@@ -653,19 +657,6 @@ def _fetch_registers(ts: str, iteration: int) -> None:
def _analyze_int_file(ts: str, iteration: int) -> None:
"""Print IRQ pin summary and alert if the SN65DSI83 asserted the IRQ line."""
path = DATA_DIR / f"{ts}_int_{iteration:04d}.csv"
if not path.exists():
return
try:
m = analyze_int_file(path)
print(m.summary())
if m.int_asserted:
print(f"\n *** IRQ ASSERTED: SN65DSI83 flagged a bridge error at "
f"capture {iteration:04d} — check CSR 0xE5 for error bits ***\n")
except Exception as e:
print(f" INT ANALYSIS ERROR: {e}")
def dual_capture(iteration: int) -> str: def dual_capture(iteration: int) -> str:
@@ -699,12 +690,6 @@ def dual_capture(iteration: int) -> str:
print(f" SAVED: {v18_path.name} ({n} samples)") print(f" SAVED: {v18_path.name} ({n} samples)")
else: else:
print(" RIGOL CH1: waveform read failed — check connection and probe.") print(" RIGOL CH1: waveform read failed — check connection and probe.")
int_path = DATA_DIR / f"{ts}_int_{iteration:04d}.csv"
n_int = rigol_scope.read_int_csv(int_path)
if n_int:
print(f" SAVED: {int_path.name} ({n_int} samples)")
else:
print(" RIGOL CH2: IRQ read failed.")
_restore_hs_config() _restore_hs_config()
try: try:
requests.put(URL, json={"state": "on"}, timeout=1) requests.put(URL, json={"state": "on"}, timeout=1)
@@ -856,6 +841,56 @@ def _append_flicker_log(ts: str, iteration: int, m: LPMetrics) -> None:
]) ])
def _play_alarm() -> None:
"""Play three short beeps using a generated WAV tone."""
sample_rate = 44100
freq = 880
duration = 0.35
n_samples = int(sample_rate * duration)
samples = [int(32767 * math.sin(2 * math.pi * freq * i / sample_rate))
for i in range(n_samples)]
packed = struct.pack(f"<{n_samples}h", *samples)
tmp = None
try:
fd, tmp = tempfile.mkstemp(suffix=".wav")
os.close(fd)
with wave.open(tmp, "w") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(sample_rate)
w.writeframes(packed)
# os.system inherits the full shell environment (XDG_RUNTIME_DIR, PULSE_SERVER, etc.)
played = False
for cmd in (f"aplay -q {tmp}", f"pw-play {tmp}", f"paplay {tmp}"):
if os.system(f"{cmd} 2>/dev/null") == 0:
played = True
for _ in range(2):
time.sleep(0.2)
os.system(f"{cmd} 2>/dev/null")
break
if not played:
try:
with open("/dev/tty", "w") as tty:
for _ in range(5):
tty.write("\a")
tty.flush()
time.sleep(0.3)
except Exception:
print("\a" * 5, end="", flush=True)
except Exception:
print("\a" * 5, end="", flush=True)
finally:
if tmp:
try:
os.unlink(tmp)
except Exception:
pass
def _analyze_lp_files( def _analyze_lp_files(
ts: str, iteration: int ts: str, iteration: int
) -> tuple[list[str], list[LPMetrics]]: ) -> tuple[list[str], list[LPMetrics]]:
@@ -1340,9 +1375,6 @@ def run_interactive_test() -> None:
except Exception as e: except Exception as e:
print(f" TRANSFER ERROR: {e}") print(f" TRANSFER ERROR: {e}")
# ── IRQ pin analysis ───────────────────────────────────────────
_analyze_int_file(ts, iteration)
# ── Rule-based LP analysis ───────────────────────────────────── # ── Rule-based LP analysis ─────────────────────────────────────
lp_summaries, suspects = _analyze_lp_files(ts, iteration) lp_summaries, suspects = _analyze_lp_files(ts, iteration)
@@ -1365,11 +1397,7 @@ def run_interactive_test() -> None:
if claude_flicker: if claude_flicker:
# ── Keep display ON — ask operator ───────────────────────── # ── Keep display ON — ask operator ─────────────────────────
# Play alarm sound once to alert the operator _play_alarm()
subprocess.run(
["pw-play", "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"],
check=False,
)
print("\n" + "=" * 64) print("\n" + "=" * 64)
print(" CLAUDE SUSPECTS FLICKER — OBSERVE THE DISPLAY NOW") print(" CLAUDE SUSPECTS FLICKER — OBSERVE THE DISPLAY NOW")
print("=" * 64) print("=" * 64)

View File

@@ -4,9 +4,9 @@ rigol_scope.py
Controls the Rigol DS1202Z-E at 192.168.45.5 for 1.8 V supply rail monitoring. 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. 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 scope is armed just before the Agilent LP capture.
The LP→HS current step droops the 1.8 V rail, triggering the Rigol. 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. The CH1 waveform is read over SCPI and written to the local data/ folder.
""" """
import csv import csv
@@ -21,14 +21,6 @@ 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 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 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 rigol: vxi11.Instrument | None = None
@@ -70,7 +62,7 @@ def is_connected() -> bool:
def configure(): def configure():
""" """
Configure Rigol CH1 for 1.8 V supply monitoring and CH2 for SN65DSI83 INTB pin. Configure Rigol CH1 for 1.8 V supply monitoring.
AUTO trigger sweep: if no droop occurs, scope still captures on timeout 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. so we always get a supply snapshot even when the rail is healthy.
""" """
@@ -84,12 +76,7 @@ def configure():
rigol.write(f":CHANnel1:SCALe {V18_SCALE:.3f}") rigol.write(f":CHANnel1:SCALe {V18_SCALE:.3f}")
rigol.write(f":CHANnel1:OFFSet {V18_OFFSET:.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 0")
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(f":TIMebase:MAIN:SCALe {V18_TIMEBASE:.2E}")
rigol.write(":TRIGger:MODE EDGE") rigol.write(":TRIGger:MODE EDGE")
@@ -101,7 +88,7 @@ def configure():
rigol.write(":RUN") # start acquiring immediately after configure rigol.write(":RUN") # start acquiring immediately after configure
time.sleep(0.2) time.sleep(0.2)
print(f"[RIGOL] Configured: CH1=1.8 V rail, CH2=INTB pin, {int(V18_TIMEBASE*1e6)} µs/div, " print(f"[RIGOL] Configured: CH1=1.8 V rail, {int(V18_TIMEBASE*1e6)} µs/div, "
f"trigger <{V18_TRIG_LEVEL} V falling (AUTO sweep, running)") f"trigger <{V18_TRIG_LEVEL} V falling (AUTO sweep, running)")
@@ -197,9 +184,3 @@ def read_waveform_csv(path: Path) -> int:
return _read_channel_csv("CHANnel1", path, stop_first=True) 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)