Files
MiPi_Investigation/MIPI_FLICKER_SPEC.md

868 lines
32 KiB
Markdown
Raw Permalink Normal View History

2026-05-06 15:57:48 +01:00
# 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.