PoleFX Signal Forensics
PoleFX · Bench controller · 6 × 480px WS2813 · ESP32 I2S parallel

LED Signal Forensics — 60fps validation soak

Firmware a3c0109 (led-determinism → main) · captured 2026-07-09 with an fx2lafw logic analyzer @ 8 MHz · engine streaming 240×12 @ 60fps over multicast UDP
Output frame rate
60.08 fps median
was 42.5 fps this morning
Sparkle pixels
0.00 /frame
was up to 2.5/frame
Late frames
1 / 477
intervals > 1.5× median (was 41%)
Dense soak
0 flips / 392k lane-frames
complete · 23 valid iterations · 5 lanes

Soak monitor — per lane

Complete: 24 back-to-back ~11s captures over ~18 minutes, ~70% wire-time coverage. Every frame decoded, every pulse width checked. A flip is a single-bit pixel corruption (the RGB sparkle signature). Totals per lane below; iteration 22 excluded — its millisecond-scale “pulses” with lanes disagreeing are probe-handling artifacts (a shared-engine fault would hit all lanes identically, and the device heartbeat never blinked).

LaneStatusFrames (23 valid its)Sparkle flipsOut-of-spec pulsesMedian intervalLate
D1CLEAN~15,7000benign only*16.65 ms~0.5%
D2CLEAN~15,7000benign only*16.65 ms~0.5%
D3CLEAN~15,7000benign only*16.65 ms~0.5%
D5CLEAN~15,7000benign only*16.65 ms~0.5%
D6CLEAN~15,7000benign only*16.65 ms~0.5%
NO SIGNAL6th strip — probe never made contact during the soak (D0/D4/D7 idle); shared-engine result makes it representative, but probe it for completeness
*benign = a handful of 500 ns edge-roundings on ‘0’ pulses per capture (decode correctly) and ~1 stray 60–80 µs high per capture in the post-frame dead zone past pixel 480 — zero decoded-pixel corruption in ~392,000 lane-frames. Residual late frames trace to the engine (0.4% of its send intervals >25 ms), not the firmware.

Captured waveform — frame start, all lanes

A real 80 µs window from the multi-lane capture. All six outputs are shifted out by one I2S peripheral, one time-slice per DMA word — lanes start within 125 ns of each other. Short high = ‘0’ bit (250 ns), long high = ‘1’ bit (750 ns).

Pulse widths — the sparkle mechanism

Before: I2S FIFO underruns (DMA losing memory-bus arbitration to Ethernet bursts) held the line high out of spec — 0.5–2.5 µs pulses the LEDs read as flipped ‘1’ bits. After halving DMA bandwidth (250 ns slices): clean. Log scale.

before — WS2812 timing, 10 slices/bit (8s) after — 250/500/500 ns, 5 slices/bit (8s)

Frame intervals — morning vs now

This morning the render loop overran its 16.67 ms budget and skipped in bursts — the second mode at ~33 ms is a missed frame. Now the distribution collapses onto the frame period.

baseline — 42.5 fps, 41% skipped final — 60.08 fps, 0.2% late

The day’s builds

Each fix measured on the wire before moving on. Sparkle rate wandered over time with all builds before the granularity fix — it was a longstanding bus-contention bug, not introduced today.

BuildChangeFPSSkippedSparkles/frame
07:xxbaseline — stock WS2813 timing, 2 DMA buffers42.5
41%2.22
#1WS2812 timing + 4 DMA buffers52.0
14.5%0.76
#2+ end-of-frame stop countdown (fixes tail truncation)49.5
21%0.82
#3+ lwIP pinned to core 0  (commit 0e527da)56.0
7%1.14
#4pixel map + gamma moved into UDP task (render = memcpy, 1.19 ms → 24 µs)60.0
2–4%0.03–2.5
#5250 ns pulse granularity — I2S DMA bandwidth halved  (commit a3c0109)60.08
0.2%0.00

Upstream — engine packet stream

Measured on the Pi’s eth0 with a raw-socket sniffer (multicast loopback is disabled, so you can’t just join the group).

Send rate60.2 fps · median 16.67 ms
Frame payload8,640 B = 6 pkts × 1,440 B
Groups239.192.0.1 + .63 :6868
Framenum skips0
Hiccups (13 min watch)188 / 46,800 intervals >25 ms (0.4%) watch
Device heartbeat0 gaps >1.5 s in 13 min STABLE

Method

AnalyzerFX2 clone (Saleae-compatible) · sigrok fx2lafw
Sampling8 MHz × 8 ch · 125 ns resolution
Bit decodehigh > 500 ns ⇒ ‘1’ · low > 50 µs ⇒ frame gap
Sparkle detectorsingle-channel jump ≥32 vs both neighbors, ≤2 bits set
Soak coverage22 × ~11 s captures, ~70% duty, 5–6 lanes
LED timing250/500/500 ns · 1.25 µs/bit · T0H 250 · T1H 750

Reading the pulse-width chart: in-spec ‘0’ bits sample as 250/375 ns and ‘1’ bits as 750/875–1000 ns; anything between or beyond is a generation glitch. The before-series bars from 1.1–2.6 µs are the FIFO underruns — the output holding its last level while the DMA starved. Validation details live in VALIDATION_PLAN.md (status log) in the pfx-firmware repo; captures and analysis scripts in the session scratchpad.