This commit is contained in:
david rice
2026-04-20 10:34:42 +01:00
parent 118d8ad380
commit e718a93667
8 changed files with 735 additions and 77 deletions

View File

@@ -52,6 +52,9 @@ HS_OSC_STD_V = 0.045 # V — rolling-std threshold above which a region i
# LP-low plateau below this → SoT sequence too brief for receiver to detect → flicker risk
FLICKER_LP_LOW_MAX_NS = 50.0 # ns
# CLK lane LP-00 minimum for SN65DSI83 CLK lane lock (TCLK_PREPARE + TCLK_ZERO ≥ 300 ns)
CLK_LP_LOW_MIN_NS = 300.0
# HS burst amplitude below this (single-ended p-p / 2, mV) → HS burst absent after LP transition.
# 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
@@ -576,6 +579,98 @@ def analyze_reg_file(path: Path) -> "RegDump":
)
# ---------------------------------------------------------------------------
# SN65DSI83 IRQ pin analysis (Rigol CH2 — CMOS output, active HIGH)
# ---------------------------------------------------------------------------
# IRQ is a CMOS output (Table 5-1). Default state (IRQ_EN=0): high-impedance → reads ~0 V.
# When IRQ_EN=1 (CSR 0xE0.0): driven LOW (~0 V) when no error, HIGH (≥1.25 V) on error.
# No pull-up required. 0 V is normal. Assertion requires IRQ_EN=1 + error bits in CSR 0xE1.
INT_ASSERTED_HIGH_V = 1.0 # V — IRQ considered asserted (error) above this
@dataclass
class INTMetrics:
timestamp: str
capture_num: int
sample_rate_mhz: float
duration_us: float
n_samples: int
mean_v: float
min_v: float
max_v: float
int_asserted: bool # True if IRQ went above INT_ASSERTED_HIGH_V
asserted_duration_us: Optional[float] # total assertion time, or None if not asserted
warnings: list = field(default_factory=list)
def summary(self) -> str:
ok = lambda c: "" if c else ""
lines = [
f"Capture {self.capture_num:04d} {self.timestamp} [int/irq]",
f" IRQ mean/min/max : {self.mean_v:.3f} V / {self.min_v:.3f} V / {self.max_v:.3f} V",
]
if self.int_asserted:
dur_str = (f" ({self.asserted_duration_us:.2f} µs)"
if self.asserted_duration_us else "")
lines.append(
f" IRQ status : *** ASSERTED HIGH — bridge flagged error{dur_str} *** ✗"
)
else:
lines.append(f" IRQ status : not asserted (no bridge error) ✓")
for w in self.warnings:
lines.append(f" WARNING: {w}")
return "\n".join(lines)
def analyze_int_file(path: Path) -> "INTMetrics":
"""Analyse a Rigol CH2 IRQ pin CSV file."""
m = re.match(r"(\d{8}_\d{6})_int_(\d+)\.csv", path.name, re.IGNORECASE)
if not m:
raise ValueError(f"Filename does not match int pattern: {path.name}")
timestamp, cap_str = m.groups()
capture_num = int(cap_str)
times, volts = _read_csv(path)
dt = float(np.diff(times).mean())
sample_rate = 1.0 / dt
duration_us = (float(times[-1]) - float(times[0])) * 1e6
mean_v = float(volts.mean())
min_v = float(volts.min())
max_v = float(volts.max())
asserted_mask = volts > INT_ASSERTED_HIGH_V
int_asserted = bool(asserted_mask.any())
asserted_duration_us = None
if int_asserted:
asserted_duration_us = round(float(asserted_mask.sum()) * dt * 1e6, 3)
warnings = []
if max_v < 0.1 and mean_v < 0.1:
warnings.append(
f"IRQ pin reads ~0 V throughout — likely high-impedance (IRQ_EN=0, default). "
f"Set CSR 0xE0.0=1 and enable error bits in CSR 0xE1 to activate IRQ output."
)
return INTMetrics(
timestamp = timestamp,
capture_num = capture_num,
sample_rate_mhz = round(sample_rate / 1e6, 1),
duration_us = round(duration_us, 2),
n_samples = len(times),
mean_v = round(mean_v, 3),
min_v = round(min_v, 3),
max_v = round(max_v, 3),
int_asserted = int_asserted,
asserted_duration_us = asserted_duration_us,
warnings = warnings,
)
def group_captures(data_dir: Path) -> dict[tuple[str, int], dict[str, Path]]:
"""
Scan data_dir and group CSV files by (timestamp, capture_number).
@@ -639,6 +734,12 @@ class LPMetrics:
lp_transition_valid: bool # LP-11 → LP-low → HS sequence present
# CLK lane startup check (only set when CLK LP-11 is captured — i.e. startup was caught)
# None = CLK was in continuous HS when triggered (startup not visible in this capture)
# True = CLK LP-00 duration ≥ 300 ns (SN65DSI83 CLK lock spec met)
# False = CLK LP-00 too short → bridge may fail to lock CLK lane
clk_lp_startup_ok: Optional[bool] = None
# Flicker detection
# A capture is flagged when the LP-low plateau is absent or shorter than
# FLICKER_LP_LOW_MAX_NS. Normal captures show ~340 ns; flicker shows 050 ns.
@@ -666,7 +767,19 @@ class LPMetrics:
f"(spec ≥{LP_LOW_DUR_MIN_NS:.0f} ns) {ok(ok_exit)}"
)
if self.lp_low_duration_ns is not None:
lines.append(f" LP-low plateau : {self.lp_low_duration_ns:.0f} ns")
if self.channel == "clk":
ok_clk = self.lp_low_duration_ns >= CLK_LP_LOW_MIN_NS
lines.append(
f" LP-00 (CLK) : {self.lp_low_duration_ns:.0f} ns "
f"(spec ≥{CLK_LP_LOW_MIN_NS:.0f} ns for bridge CLK lock) "
f"{'' if ok_clk else ''}"
)
else:
lines.append(f" LP-low plateau : {self.lp_low_duration_ns:.0f} ns")
if self.clk_lp_startup_ok is not None:
lines.append(
f" CLK startup : {'ok ✓' if self.clk_lp_startup_ok else '*** SHORT — bridge may not lock CLK ✗'}"
)
lines.append(
f" LP→HS sequence : {'valid ✓' if self.lp_transition_valid else 'NOT DETECTED ✗'}"
)
@@ -676,7 +789,11 @@ class LPMetrics:
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.flicker_suspect:
if (self.hs_amplitude_mv is not None
if not self.lp_transition_valid and not self.lp11_voltage_v:
lines.append(
f" *** FLICKER SUSPECT: MIPI link silent — no LP-11, LP-low, or HS detected ***"
)
elif (self.hs_amplitude_mv is not None
and self.hs_amplitude_mv < HS_BURST_AMPLITUDE_MIN_MV
and self.lp11_to_hs_ns is not None
and self.lp11_to_hs_ns >= LP_LOW_DUR_MIN_NS):
@@ -842,8 +959,17 @@ def analyze_lp_file(path: Path) -> "LPMetrics":
if n_hs_bursts == 0:
warnings.append("No HS bursts detected after LP transition")
# Flicker suspect: either the LP-low plateau is absent/short, OR the HS burst
# amplitude is too low. Two confirmed failure modes on this hardware:
# CLK lane startup check — only relevant when CLK LP-11 was captured (startup visible)
clk_lp_startup_ok: Optional[bool] = None
if channel == "clk" and lp11_regions and lp_low_duration_ns is not None:
clk_lp_startup_ok = lp_low_duration_ns >= CLK_LP_LOW_MIN_NS
if not clk_lp_startup_ok:
warnings.append(
f"CLK LP-00 {lp_low_duration_ns:.0f} ns < {CLK_LP_LOW_MIN_NS:.0f} ns "
f"(TCLK_PREPARE+TCLK_ZERO minimum) — SN65DSI83 may fail to lock CLK lane"
)
# Flicker suspect: three 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.
@@ -853,10 +979,15 @@ def analyze_lp_file(path: Path) -> "LPMetrics":
# 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).
# The lp11_to_hs guard cannot be used here (noise spike looks the same as mode A
# false positives), so LP-low duration itself gates the amplitude check.
# The lp11_to_hs guard cannot be used here, so LP-low duration gates the check.
# Confirmed example: capture 0120 (lp_low=108 ns, lp11_to_hs=1.7 ns, amp=49 mV).
#
# C) No LP-11 detected at all → MIPI link silent or stuck
# Signature: no LP-11 region found, lp_transition_valid=False, no LP or HS seen.
# This is the most severe failure: the bridge or DSI IP has stopped outputting.
# Confirmed: follow-up of capture 13 (no LP-11, no HS) while display was flickering.
# Guard: only flag DAT lane (not CLK which is normally in continuous HS mode).
#
# Only flag DAT lane (CLK is continuous HS — LP states not expected).
_lp_low_short = (
lp_low_duration_ns is not None
@@ -872,32 +1003,44 @@ def analyze_lp_file(path: Path) -> "LPMetrics":
or _lp_low_short
)
)
# Mode C: no LP-11 at all → link silent (but exclude CLK which is always HS)
link_silent = (
channel == "dat"
and not continuous_hs_clk
and not lp11_regions
)
flicker_suspect = (
channel == "dat"
and lp_transition_valid
and (
(lp_low_duration_ns is None or lp_low_duration_ns < FLICKER_LP_LOW_MAX_NS)
or hs_burst_absent
link_silent
or (
lp_transition_valid
and (
(lp_low_duration_ns is None or lp_low_duration_ns < FLICKER_LP_LOW_MAX_NS)
or hs_burst_absent
)
)
)
)
return LPMetrics(
timestamp = timestamp,
capture_num = capture_num,
channel = channel,
sample_rate_gsps = round(sample_rate / 1e9, 1),
duration_us = round(duration_us, 2),
n_samples = len(times),
lp11_voltage_v = lp11_voltage_v,
lp11_duration_us = lp11_duration_us,
lp11_to_hs_ns = lp11_to_hs_ns,
lp_low_duration_ns = lp_low_duration_ns,
n_hs_bursts = n_hs_bursts,
hs_burst_dur_ns = hs_burst_dur_ns,
hs_amplitude_mv = hs_amplitude_mv,
lp_transition_valid = lp_transition_valid,
flicker_suspect = flicker_suspect,
warnings = warnings,
timestamp = timestamp,
capture_num = capture_num,
channel = channel,
sample_rate_gsps = round(sample_rate / 1e9, 1),
duration_us = round(duration_us, 2),
n_samples = len(times),
lp11_voltage_v = lp11_voltage_v,
lp11_duration_us = lp11_duration_us,
lp11_to_hs_ns = lp11_to_hs_ns,
lp_low_duration_ns = lp_low_duration_ns,
n_hs_bursts = n_hs_bursts,
hs_burst_dur_ns = hs_burst_dur_ns,
hs_amplitude_mv = hs_amplitude_mv,
lp_transition_valid = lp_transition_valid,
clk_lp_startup_ok = clk_lp_startup_ok,
flicker_suspect = flicker_suspect,
warnings = warnings,
)