Skip to content

lp-ws281x: compute the RMT block plan at driver init from the manifest's declared channel count - #276

Merged
Yona-Appletree merged 8 commits into
mainfrom
claude/ws281x-runtime-block-plan
Aug 2, 2026
Merged

lp-ws281x: compute the RMT block plan at driver init from the manifest's declared channel count#276
Yona-Appletree merged 8 commits into
mainfrom
claude/ws281x-runtime-block-plan

Conversation

@Yona-Appletree

Copy link
Copy Markdown
Member

What

Replaces the compile-time BLOCKS_PER_CHANNEL constant in the lp-ws281x chip
backends with a block plan computed once at driver init from the board
manifest's declared /rmt/ws281xK count
. One build now serves every board
shape; the ws281x_2blocks cargo feature is deleted.

Rule: N declared channels on a chip with B usable blocks → each channel gets
floor(B/N) consecutive blocks; absorbed slots are never configured (same
construction as before); remainder blocks stay idle.

Chip Declared (shipped manifest) Plan Window / half vs. before
C6 (XIAO) 2 [1,1,0,0] 48 w / 24 w unchanged (G1 default)
C6 (DevKitC) 1 [4,0,0,0]RX blocks absorbed 192 w / 96 w (~120 µs) was 48 w; restores legacy-driver margin
classic (DOM-Z-102) 4 [2,0,2,0,2,0,2,0] 128 w / 64 w byte-identical plan; P4 baseline comparability preserved
classic 1 [4,0,…] (cap: tx_lim is 9-bit) 256 w / 128 w new
S3 (XIAO Plus) 4 [1,1,1,1] 48 w / 24 w unchanged
S3 (DevKitC) 1 [4,0,0,0] 192 w / 96 w was 48 w

Why

RMT refill misses tear frames, and margin (bigger halves) is the only software
lever left at Priority::max() (RMT-priority plan F1). Measured on the C6:
28 % of frames truncate under WiFi scan at 24-word halves vs 0.49 % at 48
(docs/debt/c6-scan-truncation-accepted.md). A single-strip board was paying
those deadlines while blocks idled; the deleted legacy C6 driver gave its lone
channel all four blocks. Since the manifest is already the sole authority on
channel count (ADR 2026-07-31), the split follows from it — per Yona: "if
users are using only 1 strip, we should be using the whole buffer."

How

  • lp-ws281x core: BlockPlan::for_channels(n, width) (+ nth_available
    / index_of_slot slot mapping) and SharedBlockPlan — an atomic,
    set-once-at-init, fail-closed slot each chip backend reads (windows can
    never change after init, honoring the RmtHw::ram_words stability
    contract). The driver core drops its duplicate plan copy:
    ram_words(ch) == 0 now means unavailable, making the backend the single
    source of truth (with_blocks/block_plan() removed).
  • C6: plan ranges over all 4 blocks; a lone TX channel absorbs the RX
    pair — verified against esp-hal 1.1.1 (reserve_channel_memory bounds
    memsize by NUM_CHANNELS - channel = 4 for ch0, absorbed channels marked
    Reserved) and by the legacy driver's years on this silicon. Harness
    LedChannel publishes the 1-channel plan, so every test_rmt-class run
    exercises the widened window on silicon.
  • classic: per-channel cap of 4 blocks — tx_lim is 9 bits, a 512-word
    window cannot be expressed.
  • S3: gains the classic/C6 manifest-index → RMT-slot indirection (needed
    once plans can widen); loopback harness publishes the 4-channel plan
    explicitly so its on-silicon expectations (TRUNC_EXPECT_BITS etc.) are
    unchanged. RX blocks left alone (loopback uses them).
  • Telemetry line format unchanged — half= simply reflects the computed plan.

Validation

  • cargo test -p lp-ws281x — all suites green, including new host tests for
    the plan arithmetic (shipped shapes, windows/stride, absorbed slots, odd
    remainder, SharedBlockPlan set-once/fail-closed).
  • Clippy -D warnings: fw-esp32c6 (+ all 14 harnesses + ws281x_telemetry),
    fw-esp32v3 (+ ws281x_telemetry), fw-esp32s3 (+ test_loopback).
  • Size checks: c6 2,881,856 B (263,872 B headroom), v3 1,727,840 B, s3 PASS.
  • just fw-manifest-check-esp32c6 — embedded manifest unchanged.
  • just check test — PASS.

Hardware

Desk boards were not connected during this session (no usbmodem* /
wchusbserial* devices present) — hardware smoke is pending: C6 jig 2-channel
default + a temporary 1-channel manifest for the 192-word window; classic
quad-strips unchanged-outcome check.

Notes

🤖 Generated with Claude Code

Yona-Appletree and others added 8 commits August 2, 2026 00:46
…kPlan; driver derives availability from the backend

The driver core no longer carries its own BlockPlan copy: ram_words(ch)==0
now means unavailable, making the backend (and the plan it reads from a
set-once SharedBlockPlan) the single source of truth. for_channels(n, w)
is the manifest-count -> plan arithmetic, with the odd remainder left idle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lete ws281x_2blocks

Two declared channels get one 48-word block each (the shipped G1 default,
unchanged). One declared channel now absorbs all four blocks including the
RX pair — esp-hal permits memsize 4 on TX ch0 and the legacy driver always
ran that way — restoring the legacy 192-word window and ~120us refill
deadlines for single-strip boards (e.g. the C6 DevkitC manifest). The
harness LedChannel publishes the 1-channel plan, so every harness run
exercises the widened window on silicon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…el count

The classic's four-channel manifest (DOM-Z-102) computes the same
[2,0,2,0,2,0,2,0] plan the old constant produced — windows, interrupt
demand and stress baselines unchanged — with a 4-block cap per channel
because tx_lim is 9 bits and cannot express a 512-word window. The S3's
four-channel manifest (XIAO Plus) likewise keeps [1,1,1,1]; its one-channel
DevKitC manifest now gets the whole 192-word TX group. The S3 driver gains
the classic/C6 manifest-index -> RMT-slot indirection, which slot-widening
plans require; the loopback harness publishes the four-channel plan
explicitly so its on-silicon expectations are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	lp-fw/fw-esp32c6/README.md
A 1-channel manifest now self-serves the wide 192-word config at driver
init, so the carried debt is only the 2-channel tradeoff; exit criteria
updated accordingly. The entry stays carried.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…outputs

Found by the runtime-block-plan hardware smoke: 1-channel manifest + the
2-output 'C6 two strip' project = load/compile retry after the failed
second open, heap driven from ~126 KB to 290 KB then to zero, 53 caught
panics in one boot. Driver behavior is correct; the load path is not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The C6 builds with -Zfmt-debug=none, so the plan={:?} field printed as
'plan=' on the desk — replaced with ch0_blocks, which formats via Display.
Defect entry gains the isolation result (memory_fs + basic renders clean
through the 192-word window) and an attribution caveat from the restored
board's ledger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Yona-Appletree
Yona-Appletree merged commit 0f021ea into main Aug 2, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant