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).
# 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.
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
@@ -969,13 +975,20 @@ def analyze_lp_file(path: Path) -> "LPMetrics":
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
# 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
# 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
# → 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).
@@ -997,8 +1010,16 @@ def analyze_lp_file(path: Path) -> "LPMetrics":
hs_amplitude_mv is not None
and hs_amplitude_mv < HS_BURST_AMPLITUDE_MIN_MV
and (
# Mode A: LP-low normal, HS never started (rolling-std confirms it)
(lp11_to_hs_ns is not None and lp11_to_hs_ns >= LP_LOW_DUR_MIN_NS)
# Mode A: LP-low normal, rolling-std fired but HS amplitude is sub-threshold.
# 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
or _lp_low_short
)

View File

@@ -27,7 +27,7 @@ REGISTER_COMMANDS = [
# ---------------------------------------------------------------------------
# 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
# Known Samsung DSIM register names (base 0x32E10000, i.MX 8M Mini)
@@ -124,25 +124,28 @@ def get_registers():
}), 200
def _i2c_read_byte(bus: int, addr: int, reg: int) -> int | None:
"""Read one byte from an I2C device using i2cget. Returns None on failure."""
def _i2c_read_byte(bus: int, addr: int, reg: int) -> tuple[int | None, str]:
"""Read one byte via i2cget. Returns (value, "") on success or (None, error_str) on failure."""
try:
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
)
if result.returncode == 0:
return int(result.stdout.strip(), 16)
except Exception:
pass
return None
return int(result.stdout.strip(), 16), ""
stderr = result.stderr.strip() or f"exit code {result.returncode}"
return None, stderr
except FileNotFoundError:
return None, "i2cget not found in PATH"
except Exception as e:
return None, str(e)
@app.route("/sn65_registers", methods=["GET"])
def get_sn65_registers():
"""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_e5 = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0xE5)
csr_0a, err_0a = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0x0A)
csr_e5, err_e5 = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0xE5)
regs = {}
errors = []
@@ -151,10 +154,10 @@ def get_sn65_registers():
regs["csr_0a"] = {
"value": f"0x{csr_0a:02x}",
"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:
errors.append("CSR 0x0A: i2cget failed")
errors.append(f"CSR 0x0A: {err_0a}")
if csr_e5 is not None:
regs["csr_e5"] = {
@@ -167,7 +170,7 @@ def get_sn65_registers():
"cha_crc_err": bool(csr_e5 & 0x40),
}
else:
errors.append("CSR 0xE5: i2cget failed")
errors.append(f"CSR 0xE5: {err_e5}")
return jsonify({
"i2c_bus": SN65_I2C_BUS,

View File

@@ -29,6 +29,10 @@ from datetime import datetime
from pathlib import Path
import math
import os
import struct
import tempfile
import wave
import anthropic
import vxi11
@@ -37,8 +41,7 @@ from dotenv import load_dotenv
import ai_mgmt
import rigol_scope
from csv_preprocessor import (analyze_lp_file, LPMetrics,
HS_BURST_AMPLITUDE_MIN_MV, FLICKER_LP_LOW_MAX_NS,
analyze_int_file)
HS_BURST_AMPLITUDE_MIN_MV, FLICKER_LP_LOW_MAX_NS)
load_dotenv(Path(__file__).parent / ".env")
@@ -565,6 +568,7 @@ def _configure_for_lp():
scope.write(":TRIGger:EDGE:SOURce CHANnel3")
scope.write(":TRIGger:EDGE:SLOPe NEGative")
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)
@@ -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:
@@ -699,12 +690,6 @@ def dual_capture(iteration: int) -> str:
print(f" SAVED: {v18_path.name} ({n} samples)")
else:
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()
try:
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(
ts: str, iteration: int
) -> tuple[list[str], list[LPMetrics]]:
@@ -1340,9 +1375,6 @@ def run_interactive_test() -> None:
except Exception as e:
print(f" TRANSFER ERROR: {e}")
# ── IRQ pin analysis ───────────────────────────────────────────
_analyze_int_file(ts, iteration)
# ── Rule-based LP analysis ─────────────────────────────────────
lp_summaries, suspects = _analyze_lp_files(ts, iteration)
@@ -1365,11 +1397,7 @@ def run_interactive_test() -> None:
if claude_flicker:
# ── Keep display ON — ask operator ─────────────────────────
# Play alarm sound once to alert the operator
subprocess.run(
["pw-play", "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"],
check=False,
)
_play_alarm()
print("\n" + "=" * 64)
print(" CLAUDE SUSPECTS FLICKER — OBSERVE THE DISPLAY NOW")
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.
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 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
@@ -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
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
@@ -70,7 +62,7 @@ def is_connected() -> bool:
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
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: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 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(":CHANnel2:DISPlay 0")
rigol.write(f":TIMebase:MAIN:SCALe {V18_TIMEBASE:.2E}")
rigol.write(":TRIGger:MODE EDGE")
@@ -101,7 +88,7 @@ def configure():
rigol.write(":RUN") # start acquiring immediately after configure
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)")
@@ -197,9 +184,3 @@ def read_waveform_csv(path: Path) -> int:
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)