From 6a51228d5fc2831598c8b7015bc8c6aaf1f041db Mon Sep 17 00:00:00 2001 From: david rice Date: Mon, 27 Apr 2026 10:35:56 +0100 Subject: [PATCH] Add --- __pycache__/csv_preprocessor.cpython-312.pyc | Bin 47833 -> 48924 bytes csv_preprocessor.py | 41 +++++++++++++-- device_server.py | 51 +++++++++++++++++++ mipi_test_interactive.py | 40 ++++++++++++++- 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/__pycache__/csv_preprocessor.cpython-312.pyc b/__pycache__/csv_preprocessor.cpython-312.pyc index 9237a21fac49331271ee14f9c566bdfc9c03895c..7cee6467ecf4949d39aadab2bb9dfb22850db1fc 100644 GIT binary patch delta 7932 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 delta 7198 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 At^fc4 diff --git a/csv_preprocessor.py b/csv_preprocessor.py index bf3d254..3b3bac4 100644 --- a/csv_preprocessor.py +++ b/csv_preprocessor.py @@ -55,6 +55,12 @@ LP_LOW_HS_ONSET_MARGIN_NS = 20.0 # ns # LP-low plateau below this → SoT sequence too brief for receiver to detect → flicker risk FLICKER_LP_LOW_MAX_NS = 50.0 # ns +# Mode G: LP-low significantly shorter than the baseline seen on this hardware. +# Normal LP-low (THS_PREPARE + THS_ZERO + preamble HS-0 symbols) measures ~379–380 ns. +# Flicker-associated short LP-low values cluster at 34–194 ns (confirmed: cap 27 at 108 ns). +# The gap between 194 and 379 ns is unambiguous — 250 ns splits the populations cleanly. +LP_LOW_FLICKER_THRESHOLD_NS = 250.0 # ns — below this, LP-low is suspiciously short + # CLK lane LP-00 minimum for SN65DSI83 CLK lane lock (TCLK_PREPARE + TCLK_ZERO ≥ 300 ns) CLK_LP_LOW_MIN_NS = 300.0 @@ -69,10 +75,13 @@ HS_BURST_AMPLITUDE_MIN_MV = 40.0 # mV — below this, no real HS burst is prese # Measures fraction of post-LP-low window (100 ns margin, 3 µs look-ahead) where rolling_std # exceeds HS_OSC_STD_V. With dynamic video at 432 Mbps DDR each bit spans ~4.6 ns; transitions # (~1 ns) fire the 1 ns rolling window ~20% of the time → healthy HS → osc_frac ≈ 0.14–0.22. -# Blanking/control packets carry uniform data → osc_frac ≈ 0.00–0.02 (confirmed NOT flicker). -# Partial or transient HS dropout sits between these bands → suspicious → send to Claude. +# Static solid-colour content (e.g. FF 33 BB repeating) has fewer bit transitions, so healthy +# osc_frac is lower: ~0.11–0.17. Blanking/control packets → osc_frac ≈ 0.00–0.02 (normal). +# Confirmed partial/transient HS dropout (Apr-23, cap 0105): osc_frac = 0.079. +# Suspicious zone must sit below the lowest healthy static-pink value (~0.11) and above true +# dropout values (~0.04–0.08). Threshold set to 0.10 to give clear separation. HS_OSC_FRACTION_SUSPICIOUS_LO = 0.04 # below this: dead HS — blanking / control (normal) -HS_OSC_FRACTION_SUSPICIOUS_HI = 0.13 # above this: healthy HS; between bands → flag +HS_OSC_FRACTION_SUSPICIOUS_HI = 0.10 # above this: healthy HS; between bands → flag # Mode A minimum amplitude: LP-11-return edge artifacts produce near-zero amplitude in the @@ -809,6 +818,9 @@ class LPMetrics: + (f" avg {self.hs_burst_dur_ns:.0f} ns" if self.hs_burst_dur_ns else "")) if self.hs_amplitude_mv is not None: lines.append(f" HS amplitude : {self.hs_amplitude_mv:.0f} mV (single-ended p-p/2)") + if self.hs_osc_fraction is not None: + lines.append(f" HS osc fraction : {self.hs_osc_fraction:.4f} " + f"(suspicious {HS_OSC_FRACTION_SUSPICIOUS_LO:.2f}–{HS_OSC_FRACTION_SUSPICIOUS_HI:.2f})") if self.flicker_suspect: if not self.lp_transition_valid and not self.lp11_voltage_v: lines.append( @@ -823,6 +835,20 @@ class LPMetrics: f"(amplitude {self.hs_amplitude_mv:.0f} mV < {HS_BURST_AMPLITUDE_MIN_MV:.0f} mV, " f"lp11_to_hs={self.lp11_to_hs_ns:.0f} ns) ***" ) + elif (self.hs_osc_fraction is not None + and HS_OSC_FRACTION_SUSPICIOUS_LO < self.hs_osc_fraction < HS_OSC_FRACTION_SUSPICIOUS_HI): + lines.append( + f" *** FLICKER SUSPECT: partial HS dropout " + f"(osc_frac={self.hs_osc_fraction:.4f} in suspicious " + f"{HS_OSC_FRACTION_SUSPICIOUS_LO:.2f}–{HS_OSC_FRACTION_SUSPICIOUS_HI:.2f} zone) ***" + ) + elif (self.lp_low_duration_ns is not None + and self.lp_low_duration_ns < LP_LOW_FLICKER_THRESHOLD_NS): + lines.append( + f" *** FLICKER SUSPECT: short LP-low " + f"({self.lp_low_duration_ns:.0f} ns vs ~380 ns normal — " + f"bridge may miss SoT) ***" + ) else: lines.append( f" *** FLICKER SUSPECT: LP-low plateau absent or < {FLICKER_LP_LOW_MAX_NS:.0f} ns ***" @@ -1066,6 +1092,14 @@ def analyze_lp_file(path: Path) -> "LPMetrics": and hs_osc_fraction is not None and HS_OSC_FRACTION_SUSPICIOUS_LO < hs_osc_fraction < HS_OSC_FRACTION_SUSPICIOUS_HI ) + # Mode G: LP-low plateau present but much shorter than the ~380 ns baseline. + # Indicates insufficient THS_PREPARE+THS_ZERO (or preamble) for the bridge to lock + # the data lane SoT — the bridge likely misses the first few pixels of the line. + mode_g_short_lp_low = ( + lp_transition_valid + and lp_low_duration_ns is not None + and lp_low_duration_ns < LP_LOW_FLICKER_THRESHOLD_NS + ) flicker_suspect = ( channel == "dat" and ( @@ -1076,6 +1110,7 @@ def analyze_lp_file(path: Path) -> "LPMetrics": lp_low_duration_ns is None or hs_burst_absent or mode_f_partial_hs + or mode_g_short_lp_low ) ) ) diff --git a/device_server.py b/device_server.py index d59bed1..f4e6b53 100644 --- a/device_server.py +++ b/device_server.py @@ -42,6 +42,39 @@ REGISTER_COMMANDS = [ SN65_I2C_BUS = 4 # i2c-4 on this board SN65_I2C_ADDR = 0x2C # SN65DSI83 fixed 7-bit I2C address +# Settling-period poll — started in a background thread immediately after each +# kiosk kill+restart. Samples csr_0a and csr_e5 every SETTLING_INTERVAL_S for +# SETTLING_DURATION_S seconds. Results stored in _settling_log and returned by +# GET /sn65_settling so the host can correlate DSI errors with LP captures. +SETTLING_DURATION_S = 1.5 # seconds to poll after restart +SETTLING_INTERVAL_S = 0.010 # 10 ms between I2C reads + +_settling_log: list = [] +_settling_lock: threading.Lock = threading.Lock() + + +def _run_settling_poll() -> None: + """Poll SN65DSI83 csr_0a + csr_e5 at 10 ms intervals for 1.5 s after restart.""" + t_start = time.time() + t_end = t_start + SETTLING_DURATION_S + readings: list = [] + while time.time() < t_end: + t_ms = round((time.time() - t_start) * 1000, 1) + val_0a, _ = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0x0A) + val_e5, _ = _i2c_read_byte(SN65_I2C_BUS, SN65_I2C_ADDR, 0xE5) + readings.append({ + "t_ms": t_ms, + "csr_0a": f"0x{val_0a:02x}" if val_0a is not None else None, + "csr_e5": f"0x{val_e5:02x}" if val_e5 is not None else None, + "pll_lock": bool(val_0a & 0x80) if val_0a is not None else None, + "clk_det": bool(val_0a & 0x08) if val_0a is not None else None, + "any_error": bool(val_e5) if val_e5 is not None else None, + }) + time.sleep(SETTLING_INTERVAL_S) + with _settling_lock: + _settling_log.clear() + _settling_log.extend(readings) + # Known Samsung DSIM register names (base 0x32E10000, i.MX 8M Mini) _DSIM_NAMES = { 0x32e10004: "DSIM_STATUS", @@ -107,6 +140,9 @@ def control_display(): _video_proc.kill() _video_proc.wait() time.sleep(0.15) # let DSI reach LP-11 + # Start settling poll immediately — captures csr_e5 error flags + # during DSI startup so the host can determine root cause of flicker. + threading.Thread(target=_run_settling_poll, daemon=True).start() try: log = open("/tmp/kiosk.log", "w") _video_proc = subprocess.Popen( @@ -182,6 +218,21 @@ def _i2c_read_byte(bus: int, addr: int, reg: int) -> tuple[int | None, str]: return None, str(e) +@app.route("/sn65_settling", methods=["GET"]) +def get_sn65_settling(): + """Return the most recent post-restart settling poll (csr_0a + csr_e5 over 1.5 s).""" + with _settling_lock: + readings = list(_settling_log) + error_readings = [r for r in readings if r.get("any_error")] + return jsonify({ + "n_readings": len(readings), + "n_error": len(error_readings), + "duration_s": SETTLING_DURATION_S, + "interval_ms": int(SETTLING_INTERVAL_S * 1000), + "readings": readings, + }), 200 + + @app.route("/sn65_registers", methods=["GET"]) def get_sn65_registers(): """Read SN65DSI83 CSR 0x0A (PLL/CLK status) and 0xE5 (error flags) via I2C.""" diff --git a/mipi_test_interactive.py b/mipi_test_interactive.py index 42b91c3..e1479f7 100644 --- a/mipi_test_interactive.py +++ b/mipi_test_interactive.py @@ -63,8 +63,9 @@ PROTO_SCALE = 4e-6 # 4 µs/div → 40 µs window (was 1 µs/div) PROTO_POINTS = 500_000 # Pass 3 — LP state capture (single-ended, widens vertical range to show LP-11) -LP_SCALE = 500e-9 # 500 ns/div → 5 µs window +LP_SCALE = 1e-6 # 1 µs/div → 20 µs actual window (was 500 ns/div) LP_POINTS = 200_000 +LP_TRIG_OFFSET = 9e-6 # shift centre 9 µs after trigger → 1 µs pre / 19 µs post LP_V_SCALE = 0.2 # V/div LP_V_OFFSET = 0.6 # V — centres display between LP-00 (0 V) and LP-11 (1.2 V) LP_TRIG_LEVEL = 0.6 # V — catches LP-11 → LP-01 falling edge @@ -645,6 +646,39 @@ def _fetch_registers(ts: str, iteration: int) -> None: print(f" REGISTERS: SN65DSI83 error — {e}") combined["sn65"] = None + # SN65DSI83 post-restart settling poll + try: + resp = requests.get(f"{DEVICE_BASE}/sn65_settling", timeout=10) + resp.raise_for_status() + settling = resp.json() + combined["sn65_settling"] = settling + + n = settling.get("n_readings", 0) + n_err = settling.get("n_error", 0) + dur = settling.get("duration_s", 0) + if n_err: + # Print the first and last error readings for quick diagnosis + err_readings = [r for r in settling.get("readings", []) if r.get("any_error")] + times = [r["t_ms"] for r in err_readings] + print(f" SN65 SETTLING: *** {n_err}/{n} readings had csr_e5 errors " + f"over {dur:.1f} s (t={times[0]:.0f}–{times[-1]:.0f} ms) ***") + for r in err_readings[:3]: # show up to first 3 error readings + print(f" t={r['t_ms']:6.1f} ms csr_0a={r['csr_0a']} " + f"csr_e5={r['csr_e5']} " + f"pll={'Y' if r['pll_lock'] else 'N'} " + f"clk={'Y' if r['clk_det'] else 'N'}") + else: + clk_false = sum(1 for r in settling.get("readings", []) + if r.get("clk_det") is False) + print(f" SN65 SETTLING: no csr_e5 errors in {n} readings over {dur:.1f} s" + + (f" ({clk_false} readings with clk_det=False)" if clk_false else "")) + except requests.exceptions.RequestException as e: + print(f" REGISTERS: settling poll fetch failed — {e}") + combined["sn65_settling"] = None + except Exception as e: + print(f" REGISTERS: settling poll error — {e}") + combined["sn65_settling"] = None + # Save combined JSON try: DATA_DIR.mkdir(exist_ok=True) @@ -673,15 +707,19 @@ def dual_capture(iteration: int) -> str: print(f"CAPTURE #{iteration:04d} [{ts}]") # ── Pass 1: LP / SoT startup ─────────────────────────────────────────── + # Trigger position shifted so only 1 µs of LP-11 is pre-trigger; the + # remaining 19 µs shows the full HS burst so truncated bursts are visible. print(" PASS 1: LP STARTUP TRANSITION...") _configure_for_lp() _set_timebase(LP_SCALE, LP_POINTS) + scope.write(f":TIMebase:POSition {LP_TRIG_OFFSET:.2E}") if rigol_scope.is_connected(): rigol_scope.arm() if _arm_and_wait(timeout=30): _save_pass_channels("lp", iteration, ts) else: print(" SKIPPING LP SAVE.") + scope.write(":TIMebase:POSition 0") # restore centred for subsequent passes if rigol_scope.is_connected(): DATA_DIR.mkdir(exist_ok=True) v18_path = DATA_DIR / f"{ts}_pwr_{iteration:04d}_1v8.csv"