868 lines
32 KiB
Markdown
868 lines
32 KiB
Markdown
# 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 |
|
||
| 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.
|