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

PhaseApproachResult
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

RunDurationPLL unlocks RateI²C read errors
Hold (no cycling)650.9 s 00.0/min 0.0%

3.2 Video on/off cycling

RunDurationCycles PLL unlocksUnlocks / cycle
May 11 — 30 unlock-recovery pairs ~9 min~54 300.56
May 11 — controlled (17 cycles) 177 s17 80.47

3.3 Unlock pulse-width distribution

MetricValueNotes
min 14.5 msunder 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

ScriptPurpose
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

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.