# 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`) ```python # 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.py` — `PSUController` **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 → set voltage CH1:CURRent → 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.py` — `ScopeController` **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:DISPlay ON :CHANnel:INPut DC50 → 50Ω termination (REQUIRED) :CHANnel:PROBe 19.2 → attenuation ratio :CHANnel:SCALe 0.05 → 50 mV/div :CHANnel:OFFSet 0.0 :CHANnel:LABel '' :TIMebase:SCALe 5E-9 → 5 ns/div :TIMebase:POSition 0 :TIMebase:REFerence CENTer :TRIGger:MODE EDGE :TRIGger:EDGE:SOURce CHANnel :TRIGger:EDGE:SLOPe NEGative :TRIGger:EDGE:LEVel :TRIGger:SWEep NORMal :ACQuire:MODE RTIMe :ACQuire:INTerpolate ON :ACQuire:POINts 500000 :DISPlay:LAYout STACKed :TER? → trigger event register (1 = triggered) :WAVeform:SOURce CHANnel :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.py` — `TargetController` **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: ```bash memtool md -l 0x32e100b4+0x0c ``` Parse and return: ```json { "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: ```bash echo 1 > /sys/kernel/debug/regmap/2-002c/cache_bypass ``` Then read the SN65DSI83 register file: ```bash 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:** ```json { "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` ```json {"state": "on"} → echo 0 > /sys/class/graphics/fb0/blank {"state": "off"} → echo 4 > /sys/class/graphics/fb0/blank ``` ### 7.5 PUT `/video` ```json {"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: ```bash 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):** ```bash echo 1 > /sys/kernel/debug/regmap/2-002c/cache_bypass ``` **Manual I2C read (fallback if regmap unavailable):** ```bash i2cget -y -f 2 0x2c 0xe5 ``` **Test pattern commands (useful for isolating LVDS vs DSI fault):** ```bash # 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. ```bash # 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:** ```bash 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 | | 75–76 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.