feat: Classic ESP32 (LX6) bring-up — the 4-output WLED-class deployment target - #239
Draft
Yona-Appletree wants to merge 28 commits into
Draft
feat: Classic ESP32 (LX6) bring-up — the 4-output WLED-class deployment target#239Yona-Appletree wants to merge 28 commits into
Yona-Appletree wants to merge 28 commits into
Conversation
Classic (LX6) heap has no I-bus view — no address offset makes a heap JIT buffer fetchable, so placement becomes a per-chip strategy instead of a per-arch address rule: - codemem_esp32: fixed SRAM1 code region written through the word-mirrored D-bus walk (iram = 0x400B_FFFC - (dram - 0x3FFE_0000)), span arena with a real TooLarge capacity edge, host-testable math pinned against lp-xt-emu's BoardProfile::esp32(). - link::link_jit_at: link an image against the execute base it will be installed at (in-place link_jit byte-identical, untouched). - rt_jit::JitBuffer::Placed + compile_module_jit_placed: reserve span, link at base, install via mirrored walk, fence, wrap. - tests/xt_classic_profile.rs: real pipeline output executed under the classic profile, including an install THROUGH the mirrored D-bus view of the silicon-measured alias model. Q6 deviation, recorded in the plan: the xtn.q32 corpus lane needs a classic-linked builtins base image (the old roadmap's 'corpus for free' trap), so the classic guard is these emitted-code + install-walk tests instead. Plan: 2026-07-31-1444-classic-esp32-bringup (M1) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…M-Z-102 New per-SOC firmware crate for the classic ESP32 (WROOM-32E v3 class), per ADR 2026-07-29-per-chip-fw-toolchains: own workspace root, esp toolchain, abort-tier panic posture, C6-shaped 4 MB partition table (factory 3 MB + lpfs 960 KB), release-esp32v3 profile at opt-level=s. Hardware-verified on the desk DOM-Z-102 (esp32 rev v3.1, 4 MB, /dev/cu.wchusbserial1140): [INIT] fw-esp32v3 boot / chip=esp32 arch=xtensa heap_free=102400 / ready, steady 1 Hz heartbeat, image 100,656 B of 3,145,728 B. Carries the experiment repo's UART baud-divisor fix (esp-println uart never programs the divisor; a kept Uart binding does). Bench note: espflash monitor alone halts the app in the flash stub — use flash --monitor under a pty on this board, same flow as the S3. Plan: 2026-07-31-1444-classic-esp32-bringup (M2 P1+P2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runtime manifest for the classic bring-up roadmap's desk board and WLED-class exemplar (Q5 as amended): LED1-4 on GPIO 18/16/14/2 per the board silkscreen, four /rmt/ws281xK resources (channel count lives in the manifest per ADR 2026-07-31-lp-ws281x-multi-channel-driver-adoption), UART0 GPIO1/3 reserved as the CH340K host link, GPIO0 = OPT button. default_esp32v3_hardware_manifest() mirrors the C6/S3 pattern; schemas/hardware.schema.json regenerated for the new target variant (drift gate green). C6 size-check green after the shared-crate edit (headroom 284,528 B). Plan: 2026-07-31-1444-classic-esp32-bringup (M2 P5, partial) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…DOM-Z-102 Radio-linked (esp-rtos 0.3 + esp-radio 0.18, the experiment-proven cohort): wifi::new + STA config costs 44,244 B of heap; steady-state free = 29,528 B of a 72 KB arena. Static cost ~+20 KB bss / +8 KB data; flash +~390 KB (image 490,368 B). Radio-off baseline: 100 KB arena fully free, ~87 KB static bss, image 100,656 B. G-M3's remaining input is the measured heap cost of a real on-device compile (M3). Plan: 2026-07-31-1444-classic-esp32-bringup (M2 P3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan: 2026-07-31-1444-classic-esp32-bringup (M2 P5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lippy recipes build-fw-esp32v3 / clippy-fw-esp32v3 / fw-esp32v3-size-check (3 MB factory limit, 64 KB margin; prints 3,045,072 B headroom today). _xt-gcc-dir generalized to probe per-chip gcc binaries. Workflow job cloned from firmware-xtensa: same pinned fork (1.95.0.0, buildtargets esp32), same broad firmware path filter, rust-cache pointed at the crate's own workspace target dir. Plan: 2026-07-31-1444-classic-esp32-bringup (M2 P4) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ds elsewhere domraem/dom-z-102's runtime manifest arrives with the classic bring-up roadmap while its display sidecar + catalog entry are authored on PR #231 (board catalog work) — the first runtime-first board, so the drift gate gains the mirror of DISPLAY_ONLY, strict the same way: once #231 merges, the entry must be removed or the gate fails. Also refreshed the two stale DISPLAY_ONLY reasons ('no HardwareTarget yet' — HardwareTarget::Esp32 landed with this branch). Plan: 2026-07-31-1444-classic-esp32-bringup (M2 P5 follow-through) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Yona-Appletree
added a commit
that referenced
this pull request
Aug 1, 2026
The runtime manifest (PR #239) deliberately omits /gpio/13 — its function is unverified — so the display pin must be non-output-eligible or the drift gate's Rule 2 fails the moment both files share a branch. rsvd keeps the terminal drawn (it exists on the silkscreen) but muted and never claimable; both files upgrade together if IO13 is verified usable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…2, not LED1-4) Cross-checked with the board-catalog session's silkscreen read (PR #231): terminals are printed IO18/IO16/IO14/IO2, and an unverified IO13 terminal exists (omitted here, drawn non-claimable in their sidecar). Description now states which facts are silkscreen-verified vs standard-wiring inference. Plan: 2026-07-31-1444-classic-esp32-bringup (M2 P5 follow-through) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… classic ESP32 M3-P1: fold the crate into the root workspace (fw-esp32s3 shape; release-esp32v3 profile moves to the root manifest), port flash_storage + board init, and add the genuinely new piece: a UART0 transport (src/serial/) — this chip has no USB-Serial-JTAG. TX chunks are sized in line time (64 B ≈ 5.6 ms at 115200) with RX drained between chunks (128-byte FIFO ≈ 11 ms); RX is polled read_buffered() because the classic cannot clear the RX-timeout interrupt while the FIFO is non-empty. Hardware-verified on the DOM-Z-102: boots to the server loop, littlefs mounted, manifest domraem/dom-z-102 loaded, 819 fps idle, and a raw hello round-trip over the wire answers with proto=4. Image 1,675,120 B of 3 MB (headroom 1,470,608 B). Heap 110 KB / stack 47,136 B — dram_seg is a hard 1:1 trade, ceiling ≈156 KB measured via stack.x. Shader compile works; EXECUTION faults until M3-P2 wires the placed path (marked⚠️ ⚠️ at the TargetLpvmGraphics construction site). lp-cli connect is blocked host-side: transport_serial's reset dance never asserts DTR for a CH340-bridged classic (FrameBeforeHello) — fixed next. Plan: 2026-07-31-1444-classic-esp32-bringup (M3 P1) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… in the fixed region The xt-placed-code feature (enabled only by fw-esp32v3) redirects link_compiled_module_jit — the chokepoint shared by compile, compile_with_config, and the async compile job — onto the placed path: a span from the boot-installed codemem_esp32::global arena, link_jit_at against its I-bus base, install via the mirrored D-bus walk, and a PlacedGlobal buffer that frees its span on drop (Studio recompiles must not leak the 92 KiB region). Region-full is a clean NativeError. Hosts, the C6 and the S3 never enable it: S3 image byte-identical (1,699,072 B), C6 unchanged (2,861,200 B). Plan: 2026-07-31-1444-classic-esp32-bringup (M3 P2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-status TIOCMSET Two hardware-found fixes for the first UART-bridged device (DOM-Z-102): - The WCH CH34x macOS driver silently ignores the single-bit TIOCMBIS/ TIOCMBIC ioctls behind serialport's per-line DTR/RTS calls, while honoring whole-status TIOCMSET (the same reason espflash carries UnixTightReset). set_signals now issues ONE TIOCMSET when both lines are written; single-line writes keep the per-line calls the USB-JTAG dance depends on. - reset_after_open gains a UartBridge style, selected by USB VID (non-Espressif => external bridge): the classic EN pulse with IO0 held high, passing through both-asserted so it can never select the ROM bootloader. Espressif-VID ports keep the exact JTAG dance byte-for-byte. Verified on the DOM-Z-102: connect resets the chip, readiness gates on the fresh hello, upload round-trips — twice. Plan: 2026-07-31-1444-classic-esp32-bringup (M3 P2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tes on the classic ESP32 Boot installs CodeRegion::ESP32_DEFAULT into the global arena and the engine's link step lands every module in SRAM1's I-bus image (0x400A_1000..0x400B_8000). On the DOM-Z-102: quad-strips auto-loads from lpfs, its shader compiles on-device in 65 ms (1,267 B GLSL -> 2,032 B Xtensa code) and renders continuously — the same project that crash-looped the board (EXCCAUSE=2 at a D-bus heap PC) before this wiring. Unload frees the span (heap 90k->14k used), recompile reuses it. UART0 now runs at 921600 = lpc_model::DEFAULT_SERIAL_BAUD_RATE — the first chip where that constant meets a real wire; at 115200 every lp-cli connect opened a deaf port (NoSerialOutput). Write chunks stay 64 B: line TIME is what protects the 128-byte RX FIFO, so the chunk does not grow with the baud. Plan: 2026-07-31-1444-classic-esp32-bringup (M3 P2+P3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflict was justfile only: this branch added clippy-fw-esp32v3 directly
above build-fw-esp32s3, while main gave build-fw-esp32s3 a features=""
argument. Kept both — the new clippy recipe and main's parameterized
build-fw-esp32s3 header (its {{ features }} body had already merged in).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rift exemptions Semantic cross-PR drift the textual merge couldn't see: PR #231 (main) landed the display sidecar with a DISPLAY_ONLY exemption waiting on this branch's runtime manifest, while this branch carried the RUNTIME_ONLY exemption waiting on #231. Merging main satisfied both conditions at once, and the drift test correctly demanded both entries be removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… each, from the board manifest
M4-P1: the experiment repo's hardware-validated classic RmtHw backend
(esp32_rmt.rs @ b1dff8f) ported behind the lp-ws281x driver core per ADR
2026-07-31-lp-ws281x-multi-channel-driver-adoption. 64-word halves
(BLOCKS_PER_CHANNEL=2, findings §12: the ~48k refills/s ISR ceiling makes
32-word halves a hard 2-channel limit); absorbed slots skipped BY
CONSTRUCTION (slots 1/3/5/7 never reach configure_tx — the experiment
harness's failure). Channel count from the manifest's /rmt/ws281xK
resources; endpoints ws281x:rmt:{board label} exactly like the S3; classic
pin guards (AnyPin::steal panics on nonexistent GPIOs, 34-39 are
input-only). init_tx()'s global mem_tx_wrap_en and set_tx_threshold's
reverted-restart negative results carried over verbatim.
ws281x_telemetry feature (off by default, clippy-linted on): per-channel
[WS281X] counters every 10 s from the frame path — P3's capacity sweep
reads these. Test projects: quad-strips-v3 (G-M4 walk) and
quad-equal100-v3 (the §12 equal-length stress shape through the real app).
Image 1,691,728 B (+16,608). ⚠️ .stack 47,136→43,400 — the driver static
was paid from the stack; recorded in the README DRAM table. Also fixes the
unreachable-statement warning M3-P2 left in lpvm-native's chokepoint on
device builds.
lp-ws281x untouched (byte-identical to the proven experiment copy).
Plan: 2026-07-31-1444-classic-esp32-bringup (M4 P1)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght-player/lightplayer into claude/infallible-bose-84a52e
…ing it cannot here Panic handler now masks interrupts, drains the TX FIFO before and between prints (esp-println DROPS what does not fit rather than blocking), and prints location and message separately so a truncated report still names a file. All three are correct for panics outside exception context. They do NOT rescue a fault inside the RMT open path, and the attempt is worth more than the fix: docs/defects/2026-08-01-classic-rmt-open-fault.md records that opening a WS281x channel reset-loops the DOM-Z-102 in under a millisecond, that bisection puts the fault inside open() ABOVE the RAM clear and clock configure, and that stack exhaustion / RAM base / double panic are all ruled out by measurement. It also records why the channel cannot report: a second exception inside exception context vectors to reset. The instrument that would name it is lp-recovery's RTC ledger, deferred to M7 — this is the argument for pulling it forward. Also adds the classic desk projects: quad-strips-v3 (silkscreen endpoints, the G-M4 walk) and quad-equal100-v3 (the findings §12 equal-length stress shape). Plan: 2026-07-31-1444-classic-esp32-bringup (M4 P2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t IO13 Corrects a photo misread, per Yona's sketch of the physical board and the product note "Relay configuration (to GPIO12)". Still omitted from the runtime manifest, now for a stronger reason than 'unverified': GPIO12 is the classic ESP32's MTDI strap pin (flash voltage select at reset), so driving a relay from it has boot implications that need hardware verification first. Buttons relabeled BOOT/RESET per the silkscreen. Plan: 2026-07-31-1444-classic-esp32-bringup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30707561648
Contributor
CI refreshed the story baselines on this branchThe Review every PNG in the PR's Files changed view (swipe / onion-skin).
|
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30708915489
`xtensa_lx::interrupt::disable()` is a safe fn in esp-hal 1.1.1, so the `unsafe` block around it is `unused_unsafe` — and `-D warnings` makes that fatal for the firmware job. The `link_jit` import is genuinely dead under `xt-placed-code`, where every module routes through `link_jit_at` instead; gate the import on the same cfg as its only use rather than deleting it, since the non-placed path still needs it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o M4 The classic cannot report a fault in the WS281x open path. Measured 2026-08-01: it resets after ~5 characters of panic output, and interrupt masking, FIFO drains, chunked writes and two heap sizes all produced the same five. A faster print is not the answer; not needing to print is. Three pieces, each with a classic-specific wrinkle the S3 does not have: * `src/recovery/` mirrors fw-esp32s3's split minus the watchdog. The RTC fast-RAM region lands at 0x3FF8_0000 (976 B of the chip's 8 KiB, and zero bytes of the 192 KB dram_seg that .stack fights over — .stack pays only 504 B, for the code). The reset-cause map is NOT the S3's: the classic has no USB peripheral, so espflash's EN-line reset reports as ChipPowerOn and there is no UserReset arm at all. * The panic path inverts the S3's order — ledger first, serial second — and commits the record rather than leaving it staged. That needed a new `lp_recovery::commit_staged_crash`: `Recovery::init` throws a tentative record away unless the reset was a watchdog, which this fault is not, so staging alone would have bought nothing. It is sound on the abort tier only, where entering the handler already means a reset is certain. * `lpc_shared::backtrace`'s Xtensa walker is LX6/LX7-generic in mechanism but bounds-checks frames against the S3's IRAM/flash windows. On classic silicon it would reject every candidate and report zero frames, which the panic path renders as "the stack was unreadable" — confidently wrong. The windows are now chip-selected by `xt-map-esp32-classic`. Both maps compile on host so the classic constants are testable; the tests record that the two maps genuinely OVERLAP (the classic's 11 MB IROM window contains the S3's IRAM), so the guarantee runs one way only. Safe mode is the immediate practical win: quad-strips-v3's reset loop was unbreakable from the host before this, and clearing it meant erasing lpfs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30709576589
Added a `#[alloc_error_handler]` so an allocation failure reaches the RTC
ledger as an `Oom` carrying requested/align/free/used, and so the stack
walk starts at the allocation site instead of eight frames deep in
`panic_nounwind_fmt` machinery (where the chain broke before naming any
caller). Both changes paid off on the first boot after flashing:
allocation failed: requested=12864 align=8 free=11228 used=101408
...RawVec<(i32, ChannelState)>::grow_one
...Esp32OutputProvider::open
...LpServer::advance_frame
`Esp32OutputProvider`'s `VecMap<i32, ChannelState>` grows to capacity 4 when
the third channel opens; 4 x size_of::<(i32, ChannelState)>() = 4 x 3,216 =
12,864 B, against 11,228 free on a heap already 90% resident. 96% of each
element is `DisplayPipeline::lut: [[u32; 257]; 3]` — 3,084 bytes INLINE,
copied on every growth, and built unconditionally even though every output
node in quad-strips-v3 sets `lut_enabled: false`.
This exonerates the prime suspect the defect had carried since P2: the
registry/lease layer this port added is not involved, and `led-lab-esp32`
survives the same code only because its resident heap is smaller. It also
finally attributes `L553`, the one character-fragment that escaped the
blind bisection — it was alloc.rs:553, `handle_alloc_error`. The line
number was pointing at the answer all along and could not be read.
Fix not applied: the effective one moves the LUT out of line in
`lpc-shared`, which changes the S3 and C6 too (both latent, not immune).
That is a design call, recorded in the defect rather than taken here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30709993351
rustfmt sorts the two cfg'd `use ... as xt_map` lines alphabetically, which moved the doc comment onto whichever arm sorted first. Demoted it to a plain comment above both and formatted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D7 resolved as option A. `DisplayPipeline` carried
`lut: [[u32; 257]; 3]` — 3,084 bytes INLINE per channel, copied on every
`VecMap` growth — to evaluate what measurement shows is a straight line:
lut[i] = clamp(round((i/256) * white_point * 65535))
Linearly interpolating between samples of a line reproduces the line, so
the table computed `value * white_point` and nothing else. Replaced with
a Q16.16 multiply precomputed once from the (immutable) options.
Verified rather than asserted. `legacy_lut` in the test module keeps the
deleted implementation executable as a migration oracle:
`white_point_matches_the_lut_it_replaced` sweeps all 65,536 inputs at
eight white points and holds the delta to <= 2 counts of 65,535 — a
quarter ULP at the 8-bit wire the pipeline drives, so no LED resolves it.
Where they differ the multiply is the more correct of the two: the table
was a piecewise approximation of a line, this is the line.
`unit_white_point_is_the_identity` pins the overwhelmingly common case as
EXACTLY identity, since a systematic off-by-one there would dim every
strip in the product forever.
`lut.rs` is deleted rather than kept as an extension point: the module was
private, so keeping it unused would need `#[allow(dead_code)]`. The
interpolation shape it pioneered is wanted by the 16-bit gamma work
(separate branch) as a shared const table in flash — which is what settled
D7 toward A, since the one plausible future user of the per-channel heap
machinery turned out to want a different, cheaper mechanism.
Also reserves `RESERVED_CHANNELS` in the provider so opening the Nth
channel never reallocs the live ones. That guard is now cheap insurance
rather than the fix, and its comment says so.
ON SILICON (DOM-Z-102, quad-strips-v3): all four channels open on boot 1,
no safe mode, stable. Heap 18,128 free / 94,508 used, against 11,228 free
at the OOM — with two MORE channels live. Closes the M4 blocker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The intermediate point between quad-strips-v3 (120 LEDs, runs) and quad-equal100-v3 (400 LEDs, OOMs). 240 LEDs runs with 7,384 B free, which brackets the classic's real ceiling at ~300 total LEDs. P3's finding, recorded in the plan: the constraint is HEAP, not RMT. Refill lag peaked at 20 of 64 words with zero trips/skips/errors at both 120 and 240 LEDs, so `BLOCKS_PER_CHANNEL=2` carries four outputs with 3x margin — the §12 arithmetic was marginal, the measurement is not. Heap meanwhile runs out at ~89.5 B/LED, only ~21 of which is DisplayPipeline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Plan: lp2025/2026-07-31-1444-classic-esp32-bringup
Path: ~/.photomancer/planning/lp2025/2026-07-31-1444-classic-esp32-bringup/plan.md
Classic ESP32 (WROOM-32E "v3", Xtensa LX6) as a first-class target — the chip
behind 4-output WLED-class controllers.
What works
GLSL compiles and executes on the classic ESP32. A shader compiles
on-device in 65 ms (1,267 B GLSL → 2,032 B Xtensa) into the fixed SRAM1 code
region and renders continuously on the desk DOM-Z-102.
codemem_esp32(word-mirrored installwalk, span arena, real
TooLarge),link_jit_at,JitBuffer::Placed.Classic-profile host tests execute real emitted code through the mirrored
D-bus install. rv32 and S3 images byte-identical.
fw-esp32v3crate: esp toolchain, abort tier, 4 MB partitions,gated CI job,
domraem/dom-z-102manifest, RAM ledger measured (radio costs44,244 B heap + ~28 KB static + ~390 KB flash).
USB-Serial-JTAG), plus the
xt-placed-codeengine wiring. G-M3 gate passed:radio-off for v1, WiFi deferred with its price known.
4 of 4 usable RMT TX channels (blocks/channel=2, half_words=64).What does not work
Diagnosed 2026-08-01; not RMT. Opening the third WS281x channel grows
Esp32OutputProvider'sVecMap<i32, ChannelState>to capacity 4, asking for12,864 contiguous bytes against 11,228 free on a heap already 90 % resident:
96 % of each 3,216-byte element is
DisplayPipeline::lut: [[u32; 257]; 3]—3,084 bytes carried inline, copied on every
Vecgrowth, and builtunconditionally even though every output node in
quad-strips-v3sets"lut_enabled": false.This exonerates the prime suspect the defect had carried (the registry/lease
layer this port added);
led-lab-esp32survives the same code only because itsresident heap is smaller. It also finally attributes
L553, the one fragmentthat escaped the blind bisection —
alloc.rs:553,handle_alloc_error. Theline number was pointing at the answer and could not be read.
Evidence and fix options:
docs/defects/2026-08-01-classic-rmt-open-fault.md.How it was found (M4-P2)
The ledger came forward from M7 into M4 and named the fault on the first
boot after flashing. Three supporting pieces, each of which was load-bearing:
fw-esp32v3/src/recovery/— the RTC-RAM ledger (976 B in RTC fast RAM at0x3FF8_0000; 504 B of.stackfor the code, zero DRAM for the region). NoRWDT — arming one on a board under investigation for reset-looping would add
a second reset source to the diagnosis. That stays M7.
lp_recovery::commit_staged_crash()— commit without resetting.Recovery::initdiscards a tentative record unless the reset was awatchdog, and this fault is not one, so staging alone bought nothing. Abort
tier only, where entering the panic handler already means a reset is certain.
lpc-sharedfeaturext-map-esp32-classic— the Xtensa stack walker's boundschecks were hard-wired to the S3's IRAM/flash windows. Every frame in this
backtrace (
0x400d…–0x4013…) would have been rejected and the walk wouldhave reported zero frames as "the stack was unreadable". Both chip maps now
compile on host so the classic constants are unit-tested; the tests also
record that the two maps genuinely overlap (the classic's 11 MB IROM
window contains the S3's IRAM), so the guarantee runs one way only.
Plus a
#[alloc_error_handler], so an OOM recordsrequested/align/free/usedand the walk starts at the allocation site instead of eight frames deep in
panic_nounwind_fmtmachinery — where the chain broke and named no caller.Bonus: safe mode now breaks the reset loop. The board stays up, reachable,
at ~830 fps with the crash record on every heartbeat. It used to be
unrecoverable from the host — clearing it meant
espflash erase-regionon lpfs.Next
Yona owes one decision (D7): how to fix the LUT. The effective fix is in
lpc-shared'sDisplayPipeline, not in fw-esp32v3 —Option<Box<[[u32; 257]; 3]>>built only whenlut_enabledtakesChannelStatefrom 3,216 B to ~132 B.That changes the S3 and C6 too; both carry the same waste today and merely have
the arena to absorb it, so it is latent there rather than absent. Not taken
unilaterally.
M4-P3 (capacity sweep) and M4-P4 (full pipeline + G-M4 visual gate) stay blocked
behind it — the sweep cannot measure channels the board cannot open.
🤖 Generated with Claude Code