Add
This commit is contained in:
Binary file not shown.
@@ -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
|
# LP-low plateau below this → SoT sequence too brief for receiver to detect → flicker risk
|
||||||
FLICKER_LP_LOW_MAX_NS = 50.0 # ns
|
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 lane LP-00 minimum for SN65DSI83 CLK lane lock (TCLK_PREPARE + TCLK_ZERO ≥ 300 ns)
|
||||||
CLK_LP_LOW_MIN_NS = 300.0
|
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
|
# 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
|
# 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.
|
# (~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).
|
# Static solid-colour content (e.g. FF 33 BB repeating) has fewer bit transitions, so healthy
|
||||||
# Partial or transient HS dropout sits between these bands → suspicious → send to Claude.
|
# 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_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
|
# 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 ""))
|
+ (f" avg {self.hs_burst_dur_ns:.0f} ns" if self.hs_burst_dur_ns else ""))
|
||||||
if self.hs_amplitude_mv is not None:
|
if self.hs_amplitude_mv is not None:
|
||||||
lines.append(f" HS amplitude : {self.hs_amplitude_mv:.0f} mV (single-ended p-p/2)")
|
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 self.flicker_suspect:
|
||||||
if not self.lp_transition_valid and not self.lp11_voltage_v:
|
if not self.lp_transition_valid and not self.lp11_voltage_v:
|
||||||
lines.append(
|
lines.append(
|
||||||
@@ -823,6 +835,20 @@ class LPMetrics:
|
|||||||
f"(amplitude {self.hs_amplitude_mv:.0f} mV < {HS_BURST_AMPLITUDE_MIN_MV:.0f} mV, "
|
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) ***"
|
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:
|
else:
|
||||||
lines.append(
|
lines.append(
|
||||||
f" *** FLICKER SUSPECT: LP-low plateau absent or < {FLICKER_LP_LOW_MAX_NS:.0f} ns ***"
|
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 is not None
|
||||||
and HS_OSC_FRACTION_SUSPICIOUS_LO < hs_osc_fraction < HS_OSC_FRACTION_SUSPICIOUS_HI
|
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 = (
|
flicker_suspect = (
|
||||||
channel == "dat"
|
channel == "dat"
|
||||||
and (
|
and (
|
||||||
@@ -1076,6 +1110,7 @@ def analyze_lp_file(path: Path) -> "LPMetrics":
|
|||||||
lp_low_duration_ns is None
|
lp_low_duration_ns is None
|
||||||
or hs_burst_absent
|
or hs_burst_absent
|
||||||
or mode_f_partial_hs
|
or mode_f_partial_hs
|
||||||
|
or mode_g_short_lp_low
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,6 +42,39 @@ REGISTER_COMMANDS = [
|
|||||||
SN65_I2C_BUS = 4 # i2c-4 on this board
|
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
|
||||||
|
|
||||||
|
# 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)
|
# Known Samsung DSIM register names (base 0x32E10000, i.MX 8M Mini)
|
||||||
_DSIM_NAMES = {
|
_DSIM_NAMES = {
|
||||||
0x32e10004: "DSIM_STATUS",
|
0x32e10004: "DSIM_STATUS",
|
||||||
@@ -107,6 +140,9 @@ def control_display():
|
|||||||
_video_proc.kill()
|
_video_proc.kill()
|
||||||
_video_proc.wait()
|
_video_proc.wait()
|
||||||
time.sleep(0.15) # let DSI reach LP-11
|
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:
|
try:
|
||||||
log = open("/tmp/kiosk.log", "w")
|
log = open("/tmp/kiosk.log", "w")
|
||||||
_video_proc = subprocess.Popen(
|
_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)
|
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"])
|
@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."""
|
||||||
|
|||||||
@@ -63,8 +63,9 @@ PROTO_SCALE = 4e-6 # 4 µs/div → 40 µs window (was 1 µs/div)
|
|||||||
PROTO_POINTS = 500_000
|
PROTO_POINTS = 500_000
|
||||||
|
|
||||||
# Pass 3 — LP state capture (single-ended, widens vertical range to show LP-11)
|
# 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_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_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_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
|
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}")
|
print(f" REGISTERS: SN65DSI83 error — {e}")
|
||||||
combined["sn65"] = None
|
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
|
# Save combined JSON
|
||||||
try:
|
try:
|
||||||
DATA_DIR.mkdir(exist_ok=True)
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
@@ -673,15 +707,19 @@ def dual_capture(iteration: int) -> str:
|
|||||||
print(f"CAPTURE #{iteration:04d} [{ts}]")
|
print(f"CAPTURE #{iteration:04d} [{ts}]")
|
||||||
|
|
||||||
# ── Pass 1: LP / SoT startup ───────────────────────────────────────────
|
# ── 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...")
|
print(" PASS 1: LP STARTUP TRANSITION...")
|
||||||
_configure_for_lp()
|
_configure_for_lp()
|
||||||
_set_timebase(LP_SCALE, LP_POINTS)
|
_set_timebase(LP_SCALE, LP_POINTS)
|
||||||
|
scope.write(f":TIMebase:POSition {LP_TRIG_OFFSET:.2E}")
|
||||||
if rigol_scope.is_connected():
|
if rigol_scope.is_connected():
|
||||||
rigol_scope.arm()
|
rigol_scope.arm()
|
||||||
if _arm_and_wait(timeout=30):
|
if _arm_and_wait(timeout=30):
|
||||||
_save_pass_channels("lp", iteration, ts)
|
_save_pass_channels("lp", iteration, ts)
|
||||||
else:
|
else:
|
||||||
print(" SKIPPING LP SAVE.")
|
print(" SKIPPING LP SAVE.")
|
||||||
|
scope.write(":TIMebase:POSition 0") # restore centred for subsequent passes
|
||||||
if rigol_scope.is_connected():
|
if rigol_scope.is_connected():
|
||||||
DATA_DIR.mkdir(exist_ok=True)
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
v18_path = DATA_DIR / f"{ts}_pwr_{iteration:04d}_1v8.csv"
|
v18_path = DATA_DIR / f"{ts}_pwr_{iteration:04d}_1v8.csv"
|
||||||
|
|||||||
Reference in New Issue
Block a user