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

868 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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.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<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.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 |
| 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.