Updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -59,7 +59,13 @@ CLK_LP_LOW_MIN_NS = 300.0
|
||||
# On this hardware normal HS = 105–122 mV; confirmed flicker = 14–32 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 (~342–380 ns) → bridge misses SoT → returns to LP-11
|
||||
# Signature: lp11_to_hs fires at real LP-low end (~347 ns), hs_amplitude ≈ 15–30 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 (50–200 ns, vs nominal ~342–380 ns) → marginal SoT timing
|
||||
# → HS burst starts but is weak, hs_amplitude ≈ 40–60 mV (vs normal 100–122 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 ≈ 0–3 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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user