Hardware Engineering Display interface investigation

# MIPI DSI Flicker — Root Cause Investigation

Display flicker on i.MX 8M Mini → SN65DSI83 → LVDS panel
Investigation period: May 7 – May 11, 2026 · Author: David Rice
TL;DR. The flicker is caused by the `PUT /video stop` path on the i.MX, not by signal-integrity on the MIPI bus, not by the SN65DSI83 itself, and not by the panel. Each video stop tears down the DSI HS-clock; the SN65's PLL loses lock for 15–45 ms (1–3 display frames); on the subsequent start, the PLL must re-acquire and the LVDS output is briefly garbled — the visible flicker. Fix: change "stop" semantics so the HS-clock keeps running (e.g. GStreamer `PAUSED` instead of `NULL`), or simply avoid stopping video in production.

## 1. The problem

An i.MX 8M Mini SoC drives a SN65DSI83 MIPI-DSI → LVDS bridge into an LCD panel. Under steady-state operation the display sometimes flickers. Initial hypothesis: a MIPI bit-error or signal-integrity issue on the CLK or data lanes.

## 2. Investigation summary

Phase | Approach | Result
--- | --- | ---
1 – proto decoder | Decode raw MIPI bursts from differential captures, look for byte-level anomalies | Inconclusive. Capture coverage was ~0.0004% of time (20 µs windows on a 10 s cycle). Flicker events not captured.
2 – segmented memory | Use scope's segmented-memory mode to capture 100 LP-trigger events per acquisition (~50× higher coverage), analyse per-segment | Negative result. Flicker and non-flicker captures statistically indistinguishable across all LP-state metrics.
3 – SN65 register monitor | High-rate HTTP polling of SN65DSI83's status registers (`csr_0a`, `csr_e5`) to catch transient PLL events the post-event snapshot missed | Smoking gun found. PLL unlocks during flicker sessions, never during good sessions.
4 – Pulse-width characterisation | 100 Hz polling (median 20 ms) to measure actual unlock pulse width | Pulse width 15–45 ms (1–3 display frames). Too short for a software-driven clock pause; too short for a brownout-and-restart. Consistent with a brief clock-lane disturbance.
5 – Cycling vs hold | Compare PLL behaviour under continuous video to behaviour under on/off cycling | Definitive. 0 unlocks in 10 min 51 s of continuous video. 30 unlocks in 9 min of cycling. The cycling is the trigger.
6 – Transition isolation | Time-correlate every unlock against each `PUT /video start` and `PUT /video stop` event | Conclusive. 100% of unlocks happen 225–259 ms after `stop`. 0% after `start`.

## 3. Key data

### 3.1 Continuous video (hold) — baseline

Run | Duration | PLL unlocks | Rate | I²C read errors
--- | --- | --- | --- | ---
Hold (no cycling) | 650.9 s | 0 | 0.0/min | 0.0%

### 3.2 Video on/off cycling

Run | Duration | Cycles | PLL unlocks | Unlocks / cycle
--- | --- | --- | --- | ---
May 11 — 30 unlock-recovery pairs | ~9 min | ~54 | 30 | 0.56
May 11 — controlled (17 cycles) | 177 s | 17 | 8 | 0.47

### 3.3 Unlock pulse-width distribution

Metric | Value | Notes
--- | --- | ---
min | 14.5 ms | under 1 frame at 60 Hz
median | 21.3 ms | ~1.3 frames
p90 | 40.0 ms | ~2.4 frames
max | 44.5 ms | ~2.7 frames

Transition-isolation verdict (n = 8)
Unlocks after STOP: 8 / 8 (100%) · median offset 230 ms (range 225–259 ms)
Unlocks after START: 0 / 8 (0%)
Unlocks unattributable to either: 0 / 8 (0%)

## 4. Mechanism

                      PUT /video stop arrives
                          │
                          │ ~5 ms     HTTP / Flask processing
                          │ ~50-150ms App + GStreamer pipeline tears down
                          │           (state goes to NULL)
                          ▼
                  DSIM driver disables HS_CLK_EN  ──────►  ~230 ms after stop
                          │
                          ▼
                  MIPI CLK lane goes to LP-11
                          │
                          ▼
                  SN65DSI83 sees no reference clock
                          │
                          ▼
                  PLL drops lock          ◄── csr_e5.pll_unlock = 1 caught here
                                                (pulse width 15-45 ms)
                          │
                          ▼
                  PLL re-acquires to "no-signal" idle state
                          │
                      (~500 ms OFF window)
                          │
                      PUT /video start
                          ▼
                  DSIM re-enables HS_CLK_EN; MIPI traffic resumes
                          │
                          ▼
                  SN65DSI83 PLL has to re-acquire to the new clock
                          │      (~10-30 ms, LVDS output is garbage during this)
                          ▼
                  ──── visible flicker on the panel ────

The bridge is behaving correctly: a PLL is expected to lose lock when its reference clock disappears. The defect is upstream — the act of stopping the video drops the MIPI HS-clock, which puts the bridge through an unlock-relock cycle every time, and the next start has to re-acquire from cold. That re-acquisition window is the visible flicker.

## 5. Recommended fix

Two orthogonal options. Either should eliminate the flicker.

### 5.1 Don't tear the pipeline down

In the device-side video stack, change the "stop" path from a full teardown to a soft pause that keeps the DSI HS-clock running. For GStreamer:

    // Today  (causes flicker):
    gst_element_set_state(pipeline, GST_STATE_NULL);

    // Proposed:
    gst_element_set_state(pipeline, GST_STATE_PAUSED);

`PAUSED` retains the pipeline graph and buffers — and, importantly, doesn't trigger the bridge-disable path in the i.MX DSIM driver, so HS-CLK stays on and the SN65 PLL stays locked through the transition. Resume is near-instant and visually clean.

### 5.2 Don't stop video in production

If the only reason `/video stop` is called in real deployments is the flicker test harness itself, the flicker mode is purely an artefact of the test. Production code that starts the stream once at boot and leaves it running will see zero PLL unlocks (confirmed empirically — 0 unlocks in 10 min 51 s of continuous video).

### 5.3 Verify the fix

Once the device server gains a soft-stop action (e.g. `{"action": "pause"}`), `compare_stops.py` in this repo runs an A/B test automatically:

    STYLES = [
        ("stop_full",  {"action": "start", "mode": "static-pink"}, {"action": "stop"}),
        ("stop_pause", {"action": "start", "mode": "static-pink"}, {"action": "pause"}),
    ]
    $ python3 compare_stops.py --cycles 30

A successful fix will show `~0.5` unlocks/cycle for `stop_full` and `0.00` for `stop_pause`.

## 6. Tools developed

Script | Purpose
--- | ---
`sn65_monitor.py` | Polls SN65 status registers at 50–100 Hz, logs every PLL transition with millisecond timestamps. Rolling 30 s buffer dumped to JSON on `f`/`g` keypress.
`video_cycler.py` | Toggles `/video` start/stop on the device on a configurable cadence. Logs every transition to a CSV. Has a `--hold` mode for the no-cycling baseline.
`analyze_session.py` | Cross-references the latest SN65 log with the latest cycle log, classifies each unlock as "after_START / after_STOP / neither", prints a verdict.
`compare_stops.py` | Runs a controlled A/B/… test across multiple stop-style payloads. Polls SN65 inline, attributes unlocks to the active style, prints a comparison table. Use this to verify the eventual fix.

## 7. Open questions

- The rare catastrophic black-screen mode. Most unlocks recover cleanly within ~40 ms. Twice during the investigation a flicker resulted in a persistent black screen. That is presumably the same root cause but where PLL re-acquisition fails outright; eliminating the trigger should eliminate the rare variant too, but worth confirming with extended runs.
- The 8 % of "good" sessions that contained early unlock activity. One `f` press on 11 May (08:33:28) had zero PLL activity in the preceding minute, suggesting either an observation false-positive or a separate, sub-poll-rate fault path. Worth keeping the `sn65_monitor` running in any future regression testing to catch.
- Device-side endpoint additions. Three small additions would close out the diagnostic kit:
  - `POST /video` support for `action=pause` (GStreamer `GST_STATE_PAUSED`)
  - `/registers` to expose the runtime DSIM registers `DSIM_STATUS`, `DSIM_CLKCTRL`, `DSIM_INTSRC`, `DSIM_FIFOCTRL` alongside the existing PHY-timing config registers
  - `GET /video/state` exposing the GStreamer pipeline state (NULL / READY / PAUSED / PLAYING), which would let us time-correlate the actual pipeline state transitions with the PLL unlock window

Investigation traces, raw register snapshots, and the analysis scripts are in this repository under `data/sn65_log/`, `data/cycle_logs/`, and `data/compare_logs/`. Each timestamped run is independently reproducible.