Files
MiPi_Investigation/MIPI_FLICKER_SPEC.md
david rice 0edb95d7e1 Updates
2026-05-06 15:57:48 +01:00

32 KiB
Raw Blame History

MIPI D-PHY Display Flicker Investigation Suite

Specification for Claude Code — Build From Scratch


1. CONTEXT & BACKGROUND

This suite investigates infrequent vertical jitter/flicker and total display blackout on a custom PCB using a TI SN65DSI83 MIPI-DSI-to-LVDS bridge IC.

The LVDS output side has been validated with static test images and is considered healthy. The fault is upstream — on the MIPI D-PHY physical layer between the i.MX 8M Mini SoM and the bridge input.

Root Cause Hypothesis (from hardware engineer investigation)

The Linux mainline samsung-dsim driver uses "best-fit" (round-to-nearest) rounding when converting D-PHY timing parameters from picoseconds to byte-clock cycles. Several parameters consistently round below the MIPI D-PHY v1.1 specification minimums, causing the SN65DSI83 to occasionally fail to latch the Start-of-Transmission (SoT) sequence, producing a frame jump or blackout.

Key violating parameters at 72 MHz pixel clock (432 Mbps DSI, byte clock = 54 MHz):

Parameter Spec Min (ps) Mainline "Best Fit" (ps) Violation?
clk_prepare 38,000 37,037 YES
clk_zero 262,000* 259,259 YES
clk_trail 60,000 55,555 YES
hs_zero 118,890 111,111 YES
hs_trail 97,040 92,592 YES
hs_exit 100,000 92,592 YES

*clk_prepare + clk_zero combined minimum is 300,000 ps per MIPI D-PHY v1.1 Table 14.

A "round-up" patch was applied (u-boot dsi-tweak bit 2) and reduces flicker frequency but does not fully eliminate it. Additional per-parameter padding registers are available.

The goal of this suite is to automate stress testing across parameter combinations and correlate scope-captured D-PHY waveforms with bridge error registers to find the exact boundary condition that triggers a flicker event.

Prior Art: Falcon Board MIPI Analysis (S. Bouriot, May 2024)

A previous investigation on the Falcon board (different product, same SN65DSI83 bridge, lower pixel clock ~52 MHz) manually decoded MIPI DSI packets from scope captures during a spread-spectrum-induced shift fault. That analysis is directly applicable here. Key findings:

What was proven by direct packet decoding:

The fault manifests at the DSI packet payload layer, not just the PHY layer. Two distinct failure modes were identified by decoding Lane 0 data bytes:

Failure Mode A — Pixel Data Offset (Shifted Display):

  • In a healthy frame, the first long packet (0x3E = Packed Pixel Stream, 24-bit RGB) contains the correct pixel colour bytes (e.g. 0xD1, 0xB5, 0x90) starting from byte 0 of the payload.
  • In the fault state, the first long packet contains only 0x00 bytes. The correct pixel data (0xD1, 0xB5, 0x90) appears in the second long packet, offset into the line, not at the start.
  • This means the bridge is rendering pixel N's data at pixel N+offset position — causing the visible horizontal/vertical shift.
  • The packet header (0x21 H-Sync Start, 0x3E Data ID) is identical in both good and fault states — the fault is in the payload, not the framing.

Failure Mode B — Lane Stall (Flickering/Blackout):

  • The data lane enters LP-11 (Stop state) for ~20 ms at ~20 ms intervals.
  • No pixel data packets are transmitted during this period.
  • This is not observed in the healthy display state — it is definitively abnormal.
  • This corresponds to the "total blackout" failure mode seen on the Nexio board.

What the Falcon analysis could NOT determine:

  • Whether the fault originates at the DSI transmitter (i.MX), the bridge input, or is a timing/PLL issue. The Falcon fault was triggered by spread spectrum; the Nexio fault occurs without spread spectrum, suggesting a different (or additional) root cause.

Implication for this suite: The analysis must operate at two levels simultaneously:

  1. PHY level — scope capture of LP/HS transitions, measuring T_HS_PREPARE etc.
  2. Packet level — decoding Lane 0 differential data to verify pixel bytes are in the correct position within each long packet.

The packet-level decode is the ground truth for whether a flicker event has occurred, independent of what the SN65DSI83 error registers report.


2. SYSTEM ARCHITECTURE

┌─────────────────────────────────────────────────────────┐
│                   Host PC (Python 3.x)                  │
│                                                         │
│  master_loop.py                                         │
│    ├── TargetController  → HTTP REST  → 192.168.45.8   │
│    ├── ScopeController   → VXI-11     → 192.168.45.4   │
│    └── PSUController     → VXI-11     → 192.168.45.3   │
└─────────────────────────────────────────────────────────┘

Target (192.168.45.8:5000)
  └── i.MX 8M Mini (Digi ConnectCore 8M Mini SoM)
        ├── samsung-dsim driver  →  MIPI D-PHY 4-lane
        ├── TI SN65DSI83 bridge  →  I2C bus 2 @ 0x2C
        └── Flask REST server    →  /registers, /sn65_registers,
                                    /sn65_settling, /display, /video

Scope (192.168.45.4)
  └── Agilent/Keysight DSO80204B  (2 GHz, LXI)
        CH1: MIPI CLK+  (19.2× atten, DC 50Ω)
        CH2: MIPI CLK-  (19.2× atten, DC 50Ω)
        CH3: MIPI DAT0+ (19.2× atten, DC 50Ω)
        CH4: MIPI DAT0- (19.2× atten, DC 50Ω)

PSU (192.168.45.3)
  └── Siglent SPD3303X-E  (LXI, SCPI)
        CH1: Display 3.3V rail

3. PROJECT FILE STRUCTURE

mipi_flicker/
├── SPEC.md                  ← This document
├── requirements.txt
├── config.py                ← All constants, IPs, thresholds
├── hardware/
│   ├── __init__.py
│   ├── scope.py             ← ScopeController  (VXI-11, DSO80204B)
│   ├── psu.py               ← PSUController    (VXI-11, SPD3303X-E)
│   └── target.py            ← TargetController (HTTP REST)
├── analysis/
│   ├── __init__.py
│   ├── waveform.py          ← D-PHY timing extraction from CSV
│   ├── registers.py         ← SN65 / DSIM register parsing & flagging
│   └── report.py            ← Per-run HTML/JSON/CSV report generation
├── server/
│   ├── app.py               ← Flask REST server (deploy on i.MX8)
│   └── hw_interface.py      ← memtool, i2cget, display/video control
├── master_loop.py           ← Main entry point — the flicker hunt loop
└── captures/                ← Auto-created; one subfolder per run
    └── run_001_20260505_143022/
        ├── waveform_ch1.csv
        ├── waveform_ch2.csv
        ├── waveform_ch3.csv
        ├── waveform_ch4.csv
        ├── registers.json
        └── summary.txt

4. PYTHON DEPENDENCIES (requirements.txt)

python-vxi11>=0.9
requests>=2.28
flask>=3.0          # server side only
numpy>=1.24
pandas>=2.0
matplotlib>=3.7     # optional, for waveform plots

5. CONFIGURATION (config.py)

# Network
TARGET_IP   = "192.168.45.8"
TARGET_PORT = 5000
SCOPE_IP    = "192.168.45.4"
PSU_IP      = "192.168.45.3"

# Scope hardware
SCOPE_CHANNELS = {
    "CLK_P":  1,   # MIPI Clock Lane +
    "CLK_N":  2,   # MIPI Clock Lane -
    "DAT0_P": 3,   # MIPI Data Lane 0 +
    "DAT0_N": 4,   # MIPI Data Lane 0 -
}
PROBE_ATTENUATION = 19.2    # 910R + 50R divider, calibrated
SCOPE_TIMEBASE    = 5e-9    # 5 ns/div  — resolves ~430 MHz transitions
SCOPE_POINTS      = 500_000
TRIGGER_CHANNEL   = 3       # DAT0+ — catches LP-01→LP-00 SoT entry
TRIGGER_LEVEL_V   = 0.05    # 50 mV after 19.2× attenuation factor
                             # (represents ~960 mV on actual signal)
TRIGGER_SLOPE     = "NEGative"

# PSU
PSU_CHANNEL_DISPLAY = 1
PSU_DISPLAY_VOLTAGE = 3.3
PSU_DISPLAY_CURRENT = 1.0
PSU_POWER_CYCLE_DELAY_S = 2.0

# Pixel clock & DSI parameters
PIXEL_CLOCK_HZ    = 72_000_000
DSI_LANES         = 4
BITS_PER_PIXEL    = 24
# DSI clock = pixel_clock * bpp / lanes  (DDR, so /2 for freq)
DSI_CLK_HZ        = PIXEL_CLOCK_HZ * BITS_PER_PIXEL // DSI_LANES  # 432 MHz
BYTE_CLK_HZ       = DSI_CLK_HZ // 8                                # 54 MHz
UI_NS             = 1e9 / DSI_CLK_HZ                               # ~2.315 ns

# MIPI D-PHY v1.1 timing minimums (nanoseconds)
# Table 14, Section 9 — all are MINIMUM values; violations cause SoT errors
DPHY_SPEC = {
    "t_lpx":          50.0,
    "t_clk_prepare":  38.0,
    "t_clk_zero":     262.0,           # clk_prepare + clk_zero >= 300 ns
    "t_clk_prepare_plus_zero": 300.0,  # combined hard limit
    "t_clk_trail":    60.0,
    "t_clk_post":     60.0 + 52 * UI_NS,
    "t_hs_prepare":   40.0 + 4 * UI_NS,
    "t_hs_zero":      145.0 + 10 * UI_NS,
    "t_hs_trail":     max(8 * UI_NS, 60.0 + 4 * UI_NS),
    "t_hs_exit":      100.0,
}

# SN65DSI83 I2C
SN65_I2C_ADDR   = 0x2C
SN65_I2C_BUS    = 2          # /dev/i2c-2 on target
SN65_REG_IRQ    = 0xE5       # IRQ_STAT — volatile, must bypass cache
SN65_REG_PLL    = 0x0A       # RC_LVDS_PLL — bit 7 = PLL lock
SN65_REG_CLK    = 0x0B       # RC_LVDS_CLK — bit 0 = clock detect

# Error bit masks in SN65 REG_IRQ (0xE5)
SN65_ERR_SYNCH  = (1 << 3)   # CHA_SYNCH_ERR
SN65_ERR_SOT    = (1 << 4)   # CHA_SOT_ERR
SN65_ERR_UNC    = (1 << 6)   # CHA_UNC_ECC_ERR
SN65_FLICKER_MASK = SN65_ERR_SYNCH | SN65_ERR_SOT | SN65_ERR_UNC

# DSIM PHY timing registers (i.MX8M Mini, samsung-dsim)
DSIM_PHYTIMING_BASE = 0x32E100B4   # PHY_TIMING  offset 0
DSIM_PHYTIMING1     = 0x32E100B8   # PHY_TIMING1 offset 4
DSIM_PHYTIMING2     = 0x32E100BC   # PHY_TIMING2 offset 8

# Tweak parameters available via u-boot env (for reference/logging)
TWEAK_BIT_FIFO_FLUSH  = (1 << 0)
TWEAK_BIT_ROUND_UP    = (1 << 2)

# Output
CAPTURE_ROOT = "captures"

6. MODULE SPECIFICATIONS

6.1 hardware/psu.pyPSUController

Purpose: Control the Siglent SPD3303X-E to power-cycle the display 3.3 V rail.

Library: python-vxi11

SCPI command reference:

*IDN?                       → identity string
CH1:VOLTage <V>             → set voltage
CH1:CURRent <A>             → set current limit  
OUTPut CH1,ON / OFF         → enable/disable output
MEASure:VOLTage? CH1        → read back actual voltage
MEASure:CURRent? CH1        → read back actual current

Methods required:

Method Signature Description
__init__ (ip: str) Connect via vxi11, verify IDN, configure CH1 to 3.3 V / 1.0 A
output_on () OUTPut CH1,ON
output_off () OUTPut CH1,OFF
power_cycle (delay_s=2.0) Off → sleep → On
measure () → dict Returns {"voltage_v": float, "current_a": float}
close () Disconnect instrument

6.2 hardware/scope.pyScopeController

Purpose: Configure the DSO80204B, arm triggers, detect acquisitions, and download waveform data.

Library: python-vxi11

Key SCPI commands for DSO80204B:

*RST                                    → factory reset
:RUN / :STOP / :SINGle                  → run modes
:CHANnel<N>:DISPlay ON
:CHANnel<N>:INPut DC50                  → 50Ω termination (REQUIRED)
:CHANnel<N>:PROBe 19.2                  → attenuation ratio
:CHANnel<N>:SCALe 0.05                  → 50 mV/div
:CHANnel<N>:OFFSet 0.0
:CHANnel<N>:LABel '<name>'
:TIMebase:SCALe 5E-9                    → 5 ns/div
:TIMebase:POSition 0
:TIMebase:REFerence CENTer
:TRIGger:MODE EDGE
:TRIGger:EDGE:SOURce CHANnel<N>
:TRIGger:EDGE:SLOPe NEGative
:TRIGger:EDGE:LEVel <V>
:TRIGger:SWEep NORMal
:ACQuire:MODE RTIMe
:ACQuire:INTerpolate ON
:ACQuire:POINts 500000
:DISPlay:LAYout STACKed
:TER?                                   → trigger event register (1 = triggered)
:WAVeform:SOURce CHANnel<N>
:WAVeform:FORMat ASCii
:WAVeform:STReaming ON
:WAVeform:DATA?                         → returns CSV waveform data
:WAVeform:PREamble?                     → returns x-increment, x-origin etc.

Methods required:

Method Signature Description
__init__ (ip: str) Connect, verify IDN
setup () Full channel + timebase + trigger configuration per config.py
arm_single () :SINGle — waits for one trigger event then holds
wait_for_trigger (timeout_s=30) → bool Poll :TER? every 100 ms; return True on trigger
download_waveform (channel: int) → pd.DataFrame Fetch ASCII waveform, apply preamble scaling, return DataFrame with columns time_s, voltage_v
download_all () → dict[str, pd.DataFrame] Download all 4 channels; keys: CLK_P, CLK_N, DAT0_P, DAT0_N
close () Disconnect

Important: After downloading a waveform, apply the x_increment and x_origin from :WAVeform:PREamble? so timestamps are absolute seconds.


6.3 hardware/target.pyTargetController

Purpose: HTTP REST client for the i.MX 8M Mini target.

Library: requests

Endpoints (all at http://192.168.45.8:5000):

Method Endpoint Payload Returns
GET /registers JSON: DSIM PHY_TIMING registers
GET /sn65_registers JSON: Full SN65DSI83 register map
GET /sn65_settling JSON: Register poll over settling window
PUT /display {"state": "on"|"off"} {"ok": true}
PUT /video {"action": "start"|"stop", "mode": "static-pink"} {"ok": true}

Methods required:

Method Signature Description
__init__ (ip: str, port: int) Build base URL, verify connectivity with a GET /registers
get_dsim_registers () → dict GET /registers
get_sn65_registers () → dict GET /sn65_registers — server MUST bypass regmap cache before reading
get_sn65_settling () → dict GET /sn65_settling
display_on () PUT /display {"state": "on"}
display_off () PUT /display {"state": "off"}
video_start (mode="static-pink") PUT /video {"action": "start", "mode": mode}
video_stop () PUT /video {"action": "stop"}

7. REST SERVER SPECIFICATION (server/app.py)

Deploy on i.MX 8M Mini. Python 3.x, Flask.

7.1 GET /registers

Executes on the SoM:

memtool md -l 0x32e100b4+0x0c

Parse and return:

{
  "PHY_TIMING":  "0x00000305",
  "PHY_TIMING1": "0x020e0a03",
  "PHY_TIMING2": "0x00030605",
  "raw_hex": "00000305 020e0a03 00030605"
}

7.2 GET /sn65_registers

Critical: Before any I2C read, execute:

echo 1 > /sys/kernel/debug/regmap/2-002c/cache_bypass

Then read the SN65DSI83 register file:

cat /sys/kernel/debug/regmap/2-002c/registers

Parse the output and return a JSON dict of { "reg_hex": value_hex } pairs.

Always explicitly include and flag these registers:

{
  "registers": { "00": "35", ..., "0a": "85", "e5": "00" },
  "pll_locked":   true,      // reg 0x0A bit 7
  "clk_detected": true,      // reg 0x0B bit 0  
  "irq_stat_raw": "0x00",    // reg 0xE5 raw value
  "sot_err":    false,       // reg 0xE5 bit 4
  "synch_err":  false,       // reg 0xE5 bit 3
  "unc_ecc_err":false        // reg 0xE5 bit 6
}

7.3 GET /sn65_settling

Poll GET /sn65_registers every 100 ms for 2 seconds after power-up. Return array of snapshots with timestamps. This catches transient error spikes during the LP→HS initialization handshake.

7.4 PUT /display

{"state": "on"}     echo 0 > /sys/class/graphics/fb0/blank
{"state": "off"}    echo 4 > /sys/class/graphics/fb0/blank

7.5 PUT /video

{"action": "start", "mode": "static-pink"}

Start a GStreamer pipeline (or equivalent) that outputs a solid pink framebuffer image to drive continuous DSI traffic. Example pipeline:

gst-launch-1.0 videotestsrc pattern=solid-color foreground-color=0xFFFF69B4 \
  ! video/x-raw,width=1280,height=800,framerate=60/1 \
  ! fbdevsink device=/dev/fb0

{"action": "stop"} → kill the pipeline.


8. ANALYSIS MODULE SPECIFICATIONS

8.1 analysis/waveform.py

Input: Dict of DataFrames from ScopeController.download_all()

Purpose: Extract D-PHY timing parameters from raw voltage waveforms.

8.1.1 Signal Reconstruction

  • Compute differential signals: CLK_DIFF = CLK_P - CLK_N, DAT0_DIFF = DAT0_P - DAT0_N
  • Compute common-mode: CLK_CM = (CLK_P + CLK_N) / 2

8.1.2 Lane State Detection

The D-PHY lane state machine uses single-ended voltage levels for LP states and differential for HS. Thresholds (after 19.2× probe):

State CLK_P CLK_N Interpretation
LP-11 (Stop/Idle) ~62 mV ~62 mV Both high (~1.2 V on wire)
LP-01 (HS Request) ~0 mV ~62 mV Dp low, Dn high
LP-00 (Bridge) ~0 mV ~0 mV Both low (Prepare phase)
HS-0 (HS burst) diff ~±10 mV Low-swing differential

Threshold for LP "high": > 40 mV post-attenuation (~770 mV on wire). Threshold for LP "low": < 10 mV post-attenuation (~192 mV on wire).

8.1.3 Timing Measurements Required

measure_t_lpx(data_lane_p, data_lane_n) → float (ns)

  • Measure duration of LP-01 state on data lane (Dp low, Dn high).

measure_t_hs_prepare(data_lane_p, data_lane_n) → float (ns)

  • Measure duration of LP-00 state on data lane before HS-0 transition.

measure_t_clk_prepare(clk_p, clk_n) → float (ns)

  • Measure duration of LP-00 on clock lane before HS clock starts.

measure_t_clk_zero(clk_p, clk_n) → float (ns)

  • Measure duration of HS-0 (differential zero) on clock lane before first clock toggle. Start: exit LP-00. End: first differential edge.

measure_t_clk_prepare_plus_zero(clk_p, clk_n) → float (ns)

  • Combined t_clk_prepare + t_clk_zero. Must be >= 300 ns.

measure_t_hs_zero(data_lane_p, data_lane_n) → float (ns)

  • Measure HS-0 preamble duration on data lane before SoT sync byte (00011101).

8.1.4 Spec Validation

check_spec_compliance(measurements: dict) → dict

  • Compare each measured value against DPHY_SPEC from config.py.
  • Return dict with per-parameter: {"measured_ns": float, "min_ns": float, "pass": bool, "margin_ns": float}

8.2 analysis/registers.py

parse_sn65(reg_json: dict) → dict

  • Extract and interpret all flags from /sn65_registers response.
  • Return structured dict including flicker_detected: bool based on SN65_FLICKER_MASK.

parse_dsim(reg_json: dict) → dict

  • Decode PHY_TIMING, PHY_TIMING1, PHY_TIMING2 into individual cycle counts.
  • PHY_TIMING (0x32E100B4): bits [7:4]=lpx, bits[3:0]=hs_exit
  • PHY_TIMING1 (0x32E100B8): bits[31:24]=clk_zero, bits[23:16]=clk_post, bits[15:8]=clk_trail, bits[7:0]=clk_prepare
  • PHY_TIMING2 (0x32E100BC): bits[23:16]=hs_prepare, bits[15:8]=hs_zero, bits[7:0]=hs_trail
  • Convert cycles to nanoseconds: ns = cycles / BYTE_CLK_HZ * 1e9

8.3 analysis/report.py

Per-run outputs saved to captures/run_NNN_YYYYMMDD_HHMMSS/:

File Format Content
waveform_ch1.csv through ch4.csv CSV Raw scope data, columns: time_s, voltage_v
registers.json JSON DSIM + SN65 register snapshot at trigger time
timing_analysis.json JSON Measured D-PHY timings + spec compliance results
summary.txt Text Human-readable pass/fail + key measurements
flicker_log.csv CSV (appended) Master log — one row per run

flicker_log.csv columns:

run_id, timestamp, flicker_detected, sot_err, synch_err, pll_locked,
t_lpx_ns, t_hs_prepare_ns, t_hs_prepare_pass,
t_clk_prepare_ns, t_clk_zero_ns, t_clk_prep_plus_zero_ns, t_clk_prep_zero_pass,
phy_timing_raw, phy_timing1_raw, phy_timing2_raw,
notes

9. MASTER LOOP (master_loop.py)

9.1 Startup Sequence

1. Instantiate PSUController, ScopeController, TargetController
2. PSU: Configure CH1 to 3.3V / 1.0A, output OFF
3. Target: PUT /display off, PUT /video stop
4. Scope: setup() — full channel + trigger configuration
5. Log start time and initial DSIM register state

9.2 Main Loop (runs until KeyboardInterrupt or max_runs reached)

FOR each run:

  [RESET PHASE]
  1. target.display_off()
  2. target.video_stop()
  3. psu.output_off()
  4. sleep(PSU_POWER_CYCLE_DELAY_S)

  [ARM PHASE]
  5. scope.arm_single()
  6. Create timestamped run directory under captures/

  [STIMULUS PHASE]
  7. psu.output_on()
  8. sleep(0.5)                          # allow rails to stabilise
  9. target.display_on()
  10. target.video_start(mode="static-pink")

  [ACQUIRE PHASE]
  11. triggered = scope.wait_for_trigger(timeout_s=30)
  12. If NOT triggered: log "TIMEOUT", continue to next run

  [CAPTURE PHASE]
  13. waveforms = scope.download_all()   # all 4 channels
  14. sn65_data = target.get_sn65_registers()
  15. dsim_data = target.get_dsim_registers()
  16. settling  = target.get_sn65_settling()

  [ANALYSIS PHASE]
  17. timings   = waveform.measure_all(waveforms)
  18. spec_pass = waveform.check_spec_compliance(timings)
  19. sn65_parsed = registers.parse_sn65(sn65_data)
  20. dsim_parsed  = registers.parse_dsim(dsim_data)

  [DETECT PHASE]
  21. flicker_detected = sn65_parsed["flicker_detected"]
  22. If flicker_detected: print "*** FLICKER EVENT CAPTURED ***"

  [SAVE PHASE]
  23. Save all waveform CSVs
  24. Save registers.json, timing_analysis.json, summary.txt
  25. Append row to flicker_log.csv

  [REPORT PHASE]
  26. Print one-line status: run ID, flicker Y/N, key timing margins

END LOOP

9.3 Command-Line Arguments

python master_loop.py [options]

--max-runs N          Stop after N captures (default: unlimited)
--timeout S           Scope trigger timeout per run in seconds (default: 30)
--pixel-clock HZ      Override pixel clock (default: 72000000)
--note "string"       Append a note to every log row (e.g. "dsi-tweak=5")
--no-video            Skip PUT /video (test display blank/unblank only)
--output-dir PATH     Override captures root directory

10. DSIM REGISTER DECODE REFERENCE

At 72 MHz pixel clock (DSI = 432 MHz, byte clock = 54 MHz):

Expected "Round Up" values (dsi-tweak bit 2 set):

PHY_TIMING  = 0x00000306
  [7:4] hs_exit    = 0x0 = 0 cycles  ← NOTE: check kernel log for actual value
  [3:0] lpx        = 6   = 6 cycles  → 111 ns  (spec ≥50 ns ✓)

PHY_TIMING1 = 0x03120a04
  [31:24] clk_zero  = 0x03 = 3?      ← cross-check with kernel log
  [23:16] clk_post  = 0x12 = 18 cycles
  [15:8]  clk_trail = 0x0a = 10 cycles
  [7:0]   clk_prepare = 0x04 = 4 cycles → 74 ns (spec ≥38 ns ✓)

PHY_TIMING2 = 0x00040707
  [23:16] hs_prepare = 0x04 = 4 cycles → 74 ns
  [15:8]  hs_zero    = 0x07 = 7 cycles → 130 ns
  [7:0]   hs_trail   = 0x07 = 7 cycles → 130 ns

Note: The exact bit-field layout is not well-documented in the i.MX8M Mini reference manual (see the TODO comment in samsung-dsim.c). The register parser should log the raw hex values AND the decoded cycle counts so they can be cross-referenced against kernel log output for verification.


11. SN65DSI83 REGISTER REFERENCE

I2C address: 0x2C (ADDR pin = GND) Bus: i2c-2 on target (/dev/i2c-2)

Register Address Key Bits Description
RC_RESET 0x09 [0] = SOFT_RESET Write 1 to reset
RC_LVDS_PLL 0x0A [7] = PLL_EN, read-back = PLL_LOCK Volatile — reads hardware
RC_LVDS_CLK 0x0B [0] = CLK_DETECT Clock detected on DSI input
IRQ_STAT 0xE5 [6]=UNC_ECC, [4]=SOT_ERR, [3]=SYNCH_ERR, [1]=CRC_ERR Volatile — must bypass cache

Cache bypass (execute before reading IRQ_STAT):

echo 1 > /sys/kernel/debug/regmap/2-002c/cache_bypass

Manual I2C read (fallback if regmap unavailable):

i2cget -y -f 2 0x2c 0xe5

Test pattern commands (useful for isolating LVDS vs DSI fault):

# Enable test pattern (removes DSI as variable)
i2cset -y -f 4 0x2c 0x3c 0x10
# Disable test pattern
i2cset -y -f 4 0x2c 0x3c 0x00

12. U-BOOT TUNING PARAMETERS (Reference)

These are set on the SoM via U-Boot environment variables. The REST server can optionally expose a PUT /uboot_env endpoint to change them between test runs (requires reboot), or they can be set manually between test sessions.

# Pixel clock
setenv flb_dtovar/lvds-freq 72000000

# DSI clock override (should be >= 6 × lvds-freq for 24bpp/4-lane)
setenv flb_dtovar/dsi-freq 432000000

# DSI tweak bitmask
# Bit 0: FIFO flush on VSync (recommended ON)
# Bit 2: Round-up rounding (recommended ON)
setenv flb_dtovar/dsi-tweak 5

# Per-parameter extra cycle padding
setenv flb_dtovar/dsi-phy-extra-clk_zero    3
setenv flb_dtovar/dsi-phy-extra-hs_prepare  1
setenv flb_dtovar/dsi-phy-extra-hs_trail    1

# Other available padding parameters
# flb_dtovar/dsi-phy-extra-lpx
# flb_dtovar/dsi-phy-extra-hs_exit
# flb_dtovar/dsi-phy-extra-clk_prepare
# flb_dtovar/dsi-phy-extra-clk_post
# flb_dtovar/dsi-phy-extra-clk_trail
# flb_dtovar/dsi-phy-extra-hs_zero

saveenv
reset

Note: Changing lvds-freq breaks U-Boot splash (hardcoded PLL table). dsi-tweak / padding params do NOT require matching splash changes.


13. PACKET-LEVEL DECODE (Lane 0 Data Analysis)

This section defines how the analysis script should decode the DSI packet stream from the raw Lane 0 differential waveform. This is the ground truth fault detector, validated by the Falcon board manual decode (S. Bouriot, May 2024).

13.1 DSI Long Packet Structure (24-bit RGB, Data Type 0x3E)

 1 byte     2 bytes    1 byte    WORD COUNT bytes         2 bytes
[DATA TYPE][WORD COUNT][ ECC  ][ 24bpp Pixel Stream ... ][ CRC  ]
 <-------- Packet Header ------> <-- Packet Payload ----> Footer

With 4 data lanes, bytes are distributed across lanes in round-robin order. Lane 0 carries bytes 0, 4, 8, 12... of the serialised packet stream.

For a 1024-pixel-wide line (24bpp, 4 lanes):

  • Total payload bytes = 1024 x 3 = 3072
  • Bytes on Lane 0 = 768
  • Theoretical clock edges per line (header excluded) = 1024 x 3 x 8 / 4 = 6144 (Falcon measured ~6150 OK / ~6146 KO — within measurement error, line count consistent)

13.2 Short Packet Types Observed

  • 0x21 = Sync Event, H Sync Start (short packet preceding each long packet)
  • 0x3E = Packed Pixel Stream, 24-bit RGB (long packet, one per display line)
  • Each frame is preceded by a group of short packets (~39 LP11->LP0x transitions per frame, per Falcon measurement on a 768-line display)

13.3 Healthy Frame Signature

Lane 0 byte sequence after SoT:

0x21  → H-Sync short packet (repeated ~39 times as frame preamble)
0x3E  → Long packet Data Type (start of line 1 RGB payload)
[payload byte 0]: non-zero R/G/B value present IMMEDIATELY from start of payload

Example with test pixels (R=0xD1, G=0xB5, B=0x90):

Lane 0 payload bytes: D1 B5 90 00 D1 B5 90 00 ...  (pixel 0 RR, pixel 1 GG, pixel 2 BB...)

13.4 Fault Signature A — Pixel Data Offset (Shifted Display)

First long packet after frame preamble:

0x3E  → Data Type correct
payload bytes 0..N: 00 00 00 00 00 00 ...  ← ALL ZEROS, pixel data absent

Second long packet (~995 µs later per Falcon measurement):

payload at offset ~15 µs: D1 B5 90  ← pixel data arrives in WRONG packet/position

This is confirmed by Falcon analysis as the direct cause of visible horizontal shift. The packet headers are identical in OK and KO states — the fault is purely in payload position, not in framing or Data Type bytes.

13.5 Fault Signature B — Lane Stall (Flickering/Blackout)

  • Lane 0 differential collapses to ~0 V (LP-11 Stop state) for ~20 ms at ~20 ms intervals
  • Zero HS bursts transmitted during stall window
  • Abnormal — never observed in healthy display operation
  • Corresponds to the Nexio "total blackout" failure mode requiring power cycle to recover
  • May co-occur with Fault A (shifted display + flicker seen simultaneously on Falcon)

13.6 Packet Decode Implementation Notes

analysis/waveform.py should implement decode_lane0_packets():

  1. Compute DAT0_DIFF = DAT0_P - DAT0_N
  2. Identify HS burst boundaries: look for differential swing exceeding ±8 mV (post 19.2x attenuation) after a period of LP-00 (both lines near 0 V)
  3. Use recovered clock from CLK_DIFF edges to sample data bits (DDR — both edges)
  4. Deserialise bytes MSB-first; check for Lane 0 SoT sync word (0xB8 after the LP-11 -> LP-01 -> LP-00 -> HS-0 preamble)
  5. Extract Data Type byte, Word Count (2 bytes), ECC (1 byte), then payload
  6. Return structured list: [{burst_idx, timestamp_s, data_type, word_count, payload_hex}]

Scope window constraint: At 5 ns/div with 500 kpts, the capture window is ~2.5 µs — sufficient for SoT + packet header + first ~200 bytes of payload. For full line decode, set timebase to 500 ns/div or use the scope's segmented memory mode if available. The header check alone (first 4 payload bytes = 0x00 vs non-zero) is sufficient to classify Fault A without needing to decode the entire line.


14. KNOWN FAILURE MODES & EXPECTED SIGNATURES

14.1 Transient Flicker (Vertical Jitter)

  • Frequency: Every few seconds to minutes depending on pixel clock
  • Scope signature: Normal-looking SoT sequence but t_hs_prepare or t_clk_prepare + t_clk_zero just below spec minimum
  • Register signature: REG_IRQ 0xE5 bits 3 or 4 set transiently
  • Packet signature: First long packet payload starts with 0x00 bytes; pixel data found offset into second packet (confirmed by Falcon analysis)
  • Recovery: Automatic — next valid frame corrects sync

14.2 Total Display Blackout

  • Frequency: Rare; requires power cycle or driver unbind/rebind to recover
  • Cause: DSI controller enters hang state; display power rail remains up (shared with touchscreen, light sensor, LED)
  • Recovery steps:
    echo 3-0023 > /sys/bus/i2c/devices/i2c-3/3-0023/driver/unbind
    echo 3-002d > /sys/bus/i2c/devices/i2c-3/3-002d/driver/unbind
    echo 3-0041 > /sys/bus/i2c/devices/i2c-3/3-0041/driver/unbind
    echo 4 > /sys/class/graphics/fb0/blank
    echo 0 > /sys/class/graphics/fb0/blank
    
  • Scope signature: LP lines fail to return from LP-00 to LP-11 after burst; lane stays in LP-11 for ~20 ms intervals (confirmed by Falcon analysis)

14.3 Pixel Clock Sensitivity Pattern

Empirically observed — some clock frequencies are stable, others cause consistent jitter. The boundary appears non-monotonic:

Pixel Clock Behaviour
70 MHz Stable
71 MHz Jitter
72 MHz Stable (current setting)
72.4 MHz Jitter (datasheet "typical")
73 MHz Stable
74 MHz Jitter
7576 MHz Stable

This non-monotonic pattern suggests resonance interaction between the DSI PLL and the SN65DSI83 PLL, or undocumented constraints in the samsung-dsim PHY. The scope capture at multiple clock frequencies may reveal whether t_clk_prepare + t_clk_zero tracks the instability boundary.


15. IMPORTANT IMPLEMENTATION NOTES

  1. Always use DC 50Ω input on scope — 1 MΩ input with MIPI-speed signals will produce useless ringing. The 910R + 50R divider is only correct with 50Ω input.

  2. 19.2× attenuation: The probe factor is (910 + 50) / 50 = 19.2. LP-11 state (~1.2 V on wire) appears as ~62.5 mV at scope. HS swing (~200 mV on wire) appears as ~10.4 mV. Set scope vertical scale to 50 mV/div.

  3. Trigger level 50 mV (post-attenuation): This catches the falling edge of DAT0+ from LP-01 (~62 mV) to LP-00 (~0 mV) — the entry into T_HS_PREPARE.

  4. Cache bypass is mandatory before reading IRQ_STAT (0xE5). Without it you get the last-written value, not the current hardware state. The server must do this every time /sn65_registers is called.

  5. Unit Interval (UI): At 72 MHz pixel clock, DSI = 432 Mbps per lane, UI = 1/432MHz ≈ 2.315 ns. All spec minimums involving UI must be calculated dynamically if pixel clock is changed via --pixel-clock argument.

  6. The DSIM register layout is undocumented (see TODO in samsung-dsim.c). Log raw hex AND decoded values. Cross-reference against kernel dmesg output which prints the cycle counts explicitly when dsi-tweak logging is enabled.