Skip to content

labwired test: fast-boot ESP32-S3 from the manifest (unblocks S3 labs via MCP)#586

Merged
w1ne merged 1727 commits into
mainfrom
fix/s3-test-path-boot
Jul 18, 2026
Merged

labwired test: fast-boot ESP32-S3 from the manifest (unblocks S3 labs via MCP)#586
w1ne merged 1727 commits into
mainfrom
fix/s3-test-path-boot

Conversation

@w1ne

@w1ne w1ne commented Jul 18, 2026

Copy link
Copy Markdown
Owner

The bug (found dogfooding the deck through the MCP path)

labwired test's non-rom-boot path calls build_esp32_system_from_manifest, which builds a classic ESP32 (LX6) memory map regardless of chip. Loading an ESP32-S3 ELF there puts its XIP segments (0x3c00_0020 / 0x4200_0020) outside the map → Memory access violation at step 0. So no S3 lab could run via labwired test — and the MCP's run_lab uses exactly that path, so every S3 lab was blocked from the MCP. (labwired run --chip fast-boots S3 fine; only test was missing the branch.)

The fix

Add an is_esp32s3 && !rom_boot branch that mirrors the run command + wasm twin: configure_xtensa_esp32s3 + attach_esp32_external_devices (wires the manifest's SH1107) + fast_boot, routing USB_SERIAL_JTAG to the uart sink. Classic ESP32 and S3 --rom-boot paths are untouched.

Verified

labwired test on the OpenAI deck now boots and asserts openai-deck-s3: boot + OLED ready (exit 0). Previously: memory violation at step 0.

w1ne added 30 commits July 12, 2026 17:19
Migrate the shared I2C model's NXP Kinetis variant off the per-cycle walk.
Its tick() is a PURE level-IRQ re-assertion ((S.IICIF & C1.IICIE)); all
byte/device work is synchronous in read/write (Cell-based, works under the
&self read path), so the timer/systimer held-level re-pend event pattern
reproduces it cycle-exactly:

- uses_scheduler / needs_legacy_walk true only for the Kinetis variant with
  a clock attached; tick() inert in that mode.
- the level-check event arms when IICIE is set (a C1 write) and
  self-perpetuates at delay 1 while IICIE stays set, pending the own NVIC
  line whenever (IICIF & IICIE) -- so a &self D-read that latches IICIF is
  caught the next cycle, exactly like the walk.

The STM32 F1/L4 variants STAY on the legacy walk: their cycles_remaining
transaction countdown is driven by &self-read side effects (rxne_consumed /
read_dr_consumed) that arm subsequent IRQ-raising transitions, which cannot
be delivered through the write-armed event path under the &self read
constraint without dropping the IRQ or overrunning the read-gated byte
stream (a fidelity loss). Per the non-negotiable fidelity rule they remain
walk-driven; scheduler_mode() is false for F1/L4 regardless of clock.

Fidelity: per-cycle property harness (master write level IRQ, master read
D-latch level IRQ -- the read-set-IICIF crux, IICIE-off no-pend, F1/L4
stay-on-walk guard) comparing walk-tick vs on_event pend cycles + full
snapshot every cycle.

Board flip: i2c1 was the last walker on the FRDM-KW41Z bus (after #522's
mcg/rsim/dwt batch); with it migrated the walk-forcing set is EMPTY and the
bus derives walk-deletion with zero hand flags -- the campaign's first full-
board flip. kw41z_walk_free_differential flip test un-ignored + forcing-set
test rewritten to assert empty.

Perf (native release, FRDM-KW41Z, tight spin loop): walk-on interval-1
6.25 MIPS -> walk-deleted interval-64 15.97 MIPS = 2.56x.

CI: stm32_dma_walk_differential added to the scheduler-IRQ gate. manifest
drift_ack provenance comment for the B4 dma/i2c migration (keys already
2026-07-12).
perf: walk-free STM32 DMA + Kinetis I2C via event scheduler (cycle-exact); flip FRDM-KW41Z
get_ssd1306_framebuffer downcast the I2C peripheral only to the generic
STM32 I2c and the ESP32-C3 Esp32c3I2c (fixed in #455 / 2e47dd0). On an
ESP32-S3 the OLED sits on the command-list Esp32s3I2c, so the downcast
failed and the accessor returned 'not an I2C controller' — the display
poll swallowed it and an S3 OLED rendered blank in the playground/embed.

- esp32s3/i2c.rs: expose attached_slaves() (mirrors Esp32c3I2c::attached_slaves
  and I2c::attached_devices).
- inspect.rs: get_ssd1306_framebuffer now also tries Esp32s3I2c::attached_slaves()
  before erroring, downcasting the matched slave to Ssd1306.
- test: ssd1306_gddram_is_readable_through_esp32s3_i2c drives a real GDDRAM
  write through the S3 command-list engine and reads it back through the exact
  attached_slaves() path inspect.rs uses, asserting the real bytes (not blank).
fix(wasm): read SSD1306 framebuffer from the ESP32-S3 I2C controller
Migrate the ESP32-C3 I2C0 bit-level command-list engine off the per-cycle
legacy walk onto the event scheduler (mirrors the SYSTIMER #519 and the
generic STM32 SPI bit-engine).

Model (peripherals/esp32c3/i2c.rs):
- Split the old `tick_elapsed` acc-loop into `advance_engine(cycles)`, shared
  by BOTH the legacy walk and the scheduler paths, so the num/den module-tick
  cadence is byte-identical however cycles are batched.
- uses_scheduler() once the bus attaches its CycleClock (feature-gated);
  attach_cycle_clock/sync_to anchor the engine to `last_synced` and advance it
  before every MMIO write (fresh SR/FIFO_ST/INT_RAW at the write choke).
- matrix_irq_sources() exposes the I2C0 level (int_raw & int_ena) — the C3
  interrupt matrix re-derives it, replacing the walk's explicit_irqs push.
- take_scheduled_events()/on_event() keep exactly ONE module-tick event in
  flight (the `scheduled` gate): bootstrap arms it from TRANS_START (delay-1 to
  offset the bus write-path +1); on_event fires one tick and re-arms the
  successor at cycles_to_next_module_tick until the engine parks. The
  self-perpetuating per-tick event tracks timing_from_regs() exactly, so wire
  timing stays tick-interval-independent. force_legacy_walk() detaches the clock
  for the differential reference / direct-drive unit tests.
- Bus is unchanged: the existing sched-source union + matrix routing already
  handle any uses_scheduler peripheral.

Fidelity: new oled_lab_i2c0_walk_on_vs_scheduler_is_byte_identical asserts
byte-identical serial + total_cycles + SSD1306 framebuffer (walk vs scheduler)
at interval 1, and framebuffer identity at interval 64. I2C0 drops from the C3
walk-pinner ledger (EXPECTED_PINNERS 5->4); derive_walk_deletable stays false
(4 real workers remain). Direct-drive I2C0 unit tests pin force_legacy_walk
(no Machine scheduler loop).
…e migration

The i2c0 walk->scheduler migration touches esp32c3 model files, tripping the
silicon-validation drift gate. The change is byte-identical to the walk path
(oled_lab_i2c0_walk_on_vs_scheduler_is_byte_identical, interval 1 + 64), so the
reset oracle is unaffected; ack the model-file touch and regen VALIDATION_STATUS.md.
perf: walk-free ESP32-C3 I2C0 via self-perpetuating module-tick events
The level-only pair of the C3 walk-free campaign. Both spi2 and apb_saradc
are one-shot LEVEL re-emitters whose int_raw is write-armed by a transaction
(spi2 launch) / conversion (apb_saradc ONETIME_START) — no free-running
counter — so they migrate off the per-cycle walk with uses_scheduler +
attach_cycle_clock + a matrix_irq_sources level export and NO scheduled
events (simpler than the i2c0 #525 module-tick migration). The bus glue is
unchanged: refresh_esp32c3_sched_sources / poll_scheduler_matrix_sources
already union any uses_scheduler peripheral, so the walk-skipped level is
re-derived each aggregation and routed to the RISC-V line identically to the
legacy walk (which still runs — ledc + wifi_mac keep pinning).

Walk pinners now 4->2: {ledc, wifi_mac}.

Gates (all green, --features jit,event-scheduler):
- c3_level_peripheral_matrix_routing: direct IRQ-delivery identity — walk
  (force_legacy_walk, tick explicit_irqs) vs scheduler (matrix_irq_sources)
  produce the same riscv_irq_lines for spi2 src 19 / apb_saradc src 43, and
  both de-assert after INT_CLR.
- oled_lab_spi2_apb_saradc_walk_on_vs_scheduler_is_byte_identical: the pair
  is quiescent in the OLED demo, so serial + total_cycles + SSD1306
  framebuffer are byte-identical at interval 1 and 64.
- oled_lab_walk_pinners_after_rtc_migration: EXPECTED_PINNERS = {ledc,
  wifi_mac}; derive_walk_deletable() stays false, max_safe_tick_interval 1.
- validation/manifest.yaml: dated 2026-07-12 drift note (ack date unchanged;
  byte-identical, no live re-capture).
configure_xtensa_esp32s3 hard-attaches board I2C slaves the factory does
not model (TMP102 always, opt-in PCA9685). Add an SSD1306 128x64 @ 0x3C
the same way so an esp-hal S3 firmware that drives I2C0 paints a real
panel the browser twin reads back via get_ssd1306_framebuffer. Distinct
address (TMP102 0x48 / PCA9685 0x40), inert until firmware addresses it.

Also add configs/systems/esp32s3-oled-demo.yaml: the S3-Zero + SSD1306
lab manifest (oled external_device on i2c0 + the 'oled' board_io readback
binding) consumed by the playground esp32s3-oled-lab.
feat(esp32s3): attach SSD1306 OLED @ 0x3C to I2C0 for the S3 OLED lab
perf: walk-free ESP32-C3 spi2 + apb_saradc (level-only matrix export)
Shared, architecture-neutral scaffold for the universal dispatch JIT
behind the new `jit-framework` cargo feature. No per-ISA codegen: the
only frontend is a passthrough that side-exits every block to the
interpreter, proving the full loop (cache -> dispatch -> instantiate ->
run -> side-exit -> fallback -> chaining) compiles and runs end-to-end
with zero code generation.

Adds crates/core/src/cpu/jit_framework/: block_cache (flash-PC keyed,
hot-counter promotion, invalidate-all-on-flash-write), side_exit
protocol, IsaFrontend trait + PassthroughFrontend, JitRuntime abstraction
with the imported-WebAssembly.Memory binding scheme + InterpreterRuntime,
JitHost interpreter-fallback hook + SafetyGate correctness rails, the
chaining dispatch loop, and the lockstep differential harness. Design
doc at docs/engineering/universal-jit-framework.md.

Default build and the existing `jit` Xtensa pilot are untouched.
Move the ESP32-C3 LEDC controller onto the event scheduler, the MEDIUM
"timer port" in the C3 walk-free campaign. Its four low-speed timers are
genuine up-counters that latch LSTIMERx_OVF over time, so unlike the
level-only spi2/apb_saradc pair this needs a real timer migration
(SYSTIMER/STM32 TIMx pattern), not just a level export.

- Counters advance LAZILY in closed form off the bus-published CycleClock
  (fresh on every &self read / write-path sync_to / matrix level poll);
  each overflow rides a SCHEDULED event (take_scheduled_events arms the
  nearest wrap, on_event materialises the latch at its exact cycle and
  re-arms, an arm-token kills a stale chain on reconfig). Level exported
  via matrix_irq_sources; uses_scheduler/needs_legacy_walk/attach_cycle_
  clock/force_legacy_walk added. All quirks preserved (RST/PAUSE freeze,
  2^DUTY_RES wrap clamped to 14 bits, integer CLK_DIV divider, sticky W1C
  OVF latch); legacy walk path unchanged when no clock is attached.
- LEDC was the last chip-yaml walk pinner, so the C3 devkit from_config
  bus now derives walk-deletion. Re-derive legacy_walk_disabled at the end
  of the rom-boot assembly (after wifi_mac is appended) so the real walk
  worker keeps the walk enabled (interval 1) — byte-identical to before.
  The bus-level level-pair routing test re-derives the flag after
  force_legacy_walk for the same reason.
- Gates: in-module scheduler_mode walk-identity differential (all four
  counters + INT_RAW + OVF assert cycles at interval 1, interval-64
  batched, no-poll event delivery, non-vacuity OVF>0); bus-level
  c3_ledc_matrix_routing (walk vs scheduler route source 23 identically,
  force_legacy_walk re-emits, INT_CLR de-asserts); EXPECTED_PINNERS now
  {wifi_mac}. validation drift note added.
esp32c3: migrate LEDC off the per-cycle walk (timer port)
refactor(cli): extract artifact serialization
wifi_mac was the final peripheral pinning the per-cycle legacy walk on the
ESP32-C3 OLED rom-boot bus, on TWO axes. Both migrate with no new event
machinery:

* Interrupt LEVEL (matrix source 0, asserted while a MAC event is pending):
  move off the walk with uses_scheduler + a matrix_irq_sources export (the
  level-only pattern shared with spi2/apb_saradc). On a walk-DELETED bus the
  write-armed EVENT_CLR acknowledge is re-derived at the MMIO write choke
  (sync_esp32c3_irq_cache_write, gated on legacy_walk_disabled) so the level
  de-asserts without a walk tick — the one legitimate bus addition the last
  walker needs.

* Descriptor-ring PUMP (tick_with_bus): needs_bus_tick() was unconditionally
  true. Make it honest — true only while WiFi is actually up (rx_ring != 0 /
  a pending TX / medium mode) — so an idle MAC (WiFi off, as in the OLED demo)
  arms nothing and the bus is walk-DELETABLE. The pump rides the existing
  write-armed, self-perpetuating bus-tick path (refresh_bus_tick_index); the
  frame-moving model is byte-for-byte unchanged.

THE UNLOCK: with wifi_mac migrated the C3 OLED rom-boot bus derives
walk-DELETED, EXPECTED_PINNERS is empty, and max_safe_tick_interval rises to
64 — native OLED-lab throughput ~5.2 -> ~16.2 MIPS.

Gates (all green): the WiFi-exercising pump differential
c3_wifi_mac_walk_differential (TX+RX ring delivery byte-identical
walk-vs-scheduler at interval 1 AND 64, non-vacuity = frames moved); the
direct IRQ-delivery gate c3_wifi_mac_matrix_routing (walk vs scheduler route
MAC source 0 identically; force_legacy_walk re-emits the same source;
EVENT_CLR de-asserts at the write choke on a walk-deleted bus); the endgame
pinner ledger (EXPECTED_PINNERS = {}, walk-deletable, interval > 1); and the
OLED rom-boot lab byte-identical at interval 1 and the now-unlocked 64.

Adds SystemBus::recompute_walk_deletable() so out-of-crate harnesses that flip
a peripheral's drive mode after assembly (build_oled_lab's force_legacy_walk)
re-derive the flag — without it a peripheral pinned back onto a now-deletable
bus is silently starved.
esp32c3: migrate wifi_mac off the per-cycle walk (last C3 pinner → OLED lab ~5.2→16.2 MIPS)
…l gate

Foundation milestone of the RISC-V (RV32IMC) frontend for the universal
dispatch JIT. Zero codegen: the frontend walks a basic block over the flash
CodeView (reusing decoder::riscv::decode_rv32 + the length rule),
classifies every instruction (sequential / control-flow / unmodeled), and
emits a correct BlockPlan (entry_pc, end_pc, instr_count, side-exit map)
with an EMPTY code body, so every block side-exits to the interpreter.

- riscv/mod.rs: block-walker, classification, RiscVFrontend (isa_name
  rv32imc, translate_block), plus the cycle-CSR ignore-index hook.
- riscv/host.rs: JitHost adapter for Machine<RiscV> (pc/interpret_one/
  resume_at/code_view over bus.flash/safety/snapshot_state/take_flash_dirty)
  and the StateVec snapshot (x0..x31, pc, mstatus,mie,mip,mtvec,mscratch,
  mepc,mcause,mtval).
- tests/riscv_jit_lockstep.rs: differential harness proving the all-bail
  frontend is byte-identical to the interpreter over a hand-assembled hot
  loop (3000 compares, non-vacuous floor).

All new code gated behind the existing jit-framework feature.
jit(riscv): RV32IMC frontend foundation (all-bail walker + host adapter + differential gate)
* feat(peripherals): model SH1107 128x128 I2C OLED

Register-level SH1107 controller model for 1.5" 128x128 mono OLED modules
(e.g. GME128128-01-IIC), registered as the `oled-sh1107` peripheral kit and
surfaced through a new `get_sh1107_framebuffer` wasm accessor.

SH1107 vs SSD1306: 16 GDDRAM pages (128 rows), 7-bit column address, and
single-byte page/vertical addressing-mode selects (0x20/0x21) with all
multi-byte commands taking exactly one parameter. Reuses the generic I2C
attach path, so it works on the STM32 and both ESP32 I2C controllers.
Regenerated the vendored peripherals manifest fixture.

* fix(peripherals): satisfy clippy erasing_op/identity_op in SH1107 test
First RV32IMC block that executes instead of bailing. Adds the shared emit
infra chunks D/E build on:

- wasm_encode: minimal binary-wasm encoder (LEB128 + section framing) →
  real wasm bytes in BlockPlan.code.
- emit: ALU-emittable block walker + per-op codegen (LUI/AUIPC, reg-imm and
  reg-reg arith/logic/shift, RV32M mul/mulh*/div*/rem*, C.ADDI/C.LI/C.MV/
  C.ADDI16SP/C.ADDI4SPN/C.SLLI). Register-file-in-locals model: prologue
  loads touched regs from an imported "regs" memory into wasm locals, body
  operates on locals, epilogue stores back; x0 reads const 0, x0 writes
  dropped. div/rem reproduce RISC-V div-by-zero (→ -1 / dividend) and
  INT_MIN/-1 overflow exactly, trap-free via guarded if/else.
- exec (jit feature): wasmtime executor filling the scaffold's elided
  register handle RISC-V-locally; syncs cpu.x <-> register memory, maps
  wire 0 → SideExit::Chain{end_pc}. RiscvJitEngine ties BlockCache
  promotion to the executor with per-unit retired-instruction counts.

RiscVFrontend now emits ALU prefixes (all-bail fallback for non-ALU entry).

Gates: differential byte-identity over an ALU hot loop (incl. div/rem edge
cases) with compiled-block execution asserted; per-op fuzz vs interpreter
over 200+ operand pairs incl. divide edges; micro-bench 51x (12.3 → 630
MIPS) on the ALU loop. All-bail lockstep + framework tests stay green.
Emit real wasm for the RV32IMC control-flow terminators — the conditional
branches (Beq/Bne/Blt/Bge/Bltu/Bgeu), Jal/Jalr, and the compressed
C.J/C.JR/C.JALR/C.BEQZ/C.BNEZ (C.JAL arrives as Jal{rd:1}). A block is now
the maximal ALU prefix (chunk C) ended by at most one such terminator.

Exit protocol: introduce a second wire code WIRE_CHAIN_DYNAMIC=1 alongside
WIRE_FALL_THROUGH=0. A terminated block resolves its taken/not-taken/jump
target IN wasm and stores it to a fixed next-PC slot (word 32, byte 128,
just past x31) in the imported register memory, then returns the dynamic
wire; the runtime reads that slot and Chain{}s to it. One code covers every
terminator: conditional branches pick between two compile-time addresses
with an in-wasm `if`, and Jalr/C.JR compute a data-dependent one. The
register sync in CompiledBlock::run grows 128->132 bytes to carry the slot;
WIRE_FALL_THROUGH is unchanged and 2 is left free for chunk E (MEM_FAULT).

Semantics mirror RiscV::step byte-for-byte: sign-extended pc+imm targets,
the (rs1+imm)&!1 low-bit mask for Jalr/C.JR/C.JALR, pc+inst_len links
(so C.JAL links pc+2 and Jal links pc+4), C.JALR links x1=pc+2, rd=x0
drops the link, and the next PC is resolved before the link write so a
`jalr rd, rd` reads the pre-write rs1.

New gate tests/riscv_jit_branch_lockstep.rs (jit + jit-framework):
- branch_hot_loop_is_byte_identical_and_chains: a backward-branch hot loop
  whose body block chains back to itself; byte-identical to the interpreter
  at every aligned boundary and non-vacuous (compiled=6, block_runs=991,
  block_instrs=2975, interpreted=25).
- every_terminator_matches_interpreter: per-terminator differential, taken
  and not-taken, covering Jalr/C.JR masking, rd=x0, rd==rs1 Jalr, backward
  vs forward targets, and signed vs unsigned compares.
Widen the emittable prefix to RV32(I)C loads/stores (Lb,Lh,Lw,Lbu,Lhu,
Sb,Sh,Sw + compressed CLw,CSw,CLwsp,CSwsp). A load/store extends the
block rather than ending it.

Memory binding: the block range-checks the effective address against a
single contiguous guest-RAM window (bus.ram) bound at translate time and
carried in the same imported memory at byte offset 256. In-window accesses
use inline iN.load/iN.store (widths + sign/zero extension from the wasm
opcode); the runtime syncs the RAM window in/out only for blocks that
touch memory, so pure-ALU blocks stay byte-identical to chunk C.

Fault exit: an out-of-window (MMIO/unknown) access side-exits via the new
WIRE_MEM_FAULT = 2 (leaves 1 for chunk D). The faulting op publishes its
own resume PC + retired-so-far count to dedicated slots (words 33/34,
clear of D's word-32 next-PC slot), flushes only the registers written by
prior instructions, and returns; the runtime resumes the interpreter at
that PC so MMIO side effects stay interpreter-owned. A block that faults
on its entry instruction retires 0, so the dispatcher interprets one
instruction to guarantee forward progress.

Stores clear the LR/SC reservation precisely: an inline store sets a flag
slot and the runtime clears cpu.reservation iff set (a faulted store is
cleared by the interpreter that replays it) — mirrors RiscV::step with no
spurious over-clearing.

New riscv_jit_mem_lockstep.rs: hot loop byte-identity + non-vacuity + RAM
byte-identity, per-op differential (all widths, +/- offsets, sign/zero),
compressed forms, and the fault-resume path.

Full regression: jit,event-scheduler = 2034 passed/0 failed;
jit,event-scheduler,jit-framework = 2069 passed/0 failed. fmt+clippy clean.
jit(riscv): wasm emit skeleton + integer-ALU codegen (chunk C)
jit(riscv): branch/jump codegen (chunk D)
Reconcile the RV32IMC wasm-JIT frontend so integer-ALU (chunk C),
branches/jumps (chunk D), and loads/stores (chunk E) all coexist in one
compiled block. The three side-exit wire codes are now disjoint and
served by a single runtime match:

  0 WIRE_FALL_THROUGH  straight-line body ran to end_pc (C/E)
  1 WIRE_CHAIN_DYNAMIC branch/jump terminator resolved next PC in wasm (D)
  2 WIRE_MEM_FAULT     out-of-window load/store side-exit to interpreter (E)

Slot layout is the union of both designs, all non-overlapping:
  regs x0..x31   bytes   0..128
  next-PC (D)    word 32 byte 128
  fault-PC/retired/res-flag (E) words 33/34/35 bytes 132/140
  guest-RAM window (E)          byte 256

emit.rs: emit_block now walks the maximal straight-line prefix of
{ALU (C) union in-window load/store (E)} body ops, then optionally
appends one branch/jump terminator (D). A block can therefore contain a
load, ALU, a store, and end on a conditional branch. The mem-fault edge
is added whenever the body touches RAM.

exec.rs: CompiledBlock::run syncs the superset of live slots (regs +
word 32 + words 33/34/35) plus E's RAM window for memory-touching
blocks, and its match resolves all three wire codes.

Add crates/core/tests/riscv_jit_combined_lockstep.rs proving the three
chunks compose: a hot loop whose single block mixes ALU + load + store +
a backward conditional-branch terminator stays StateVec byte-identical
to the interpreter (non-vacuously), plus an MMIO-fault case that stays
identical via interpreter-resume. The alu/branch/mem lockstep gates
still pass (branch's run/compile call sites updated to chunk-E
signatures).
jit(riscv): load/store codegen (chunk E)
MERGE BAR #2 gate for the composed RV32IMC frontend (chunks C+D+E): a
realistic firmware-style hot loop mixing integer ALU, in-window loads/
stores, and a conditional-branch terminator, timed compiled-vs-interpreter.
Ignored by default; run with --ignored --nocapture.

Measured (native, arm64): interp 1.1 MIPS, jit 8.5 MIPS = 7.64x, with the
loop running almost entirely through compiled blocks (block_instrs=486002
vs interpreted=91). Clears the >=3x bar with headroom.
w1ne and others added 24 commits July 18, 2026 19:19
test: ESP32-S3 I2C/GDMA/SPI, nRF52 EasyDMA, and multi-board display fidelity oracles
# Conflicts:
#	crates/core/tests/board_coverage_ratchet.rs
- Potentiometer input component (linear divider → ADC counts) with kit + wasm setter
- openai-deck-s3 esp-hal firmware: SH1107 OLED render + 10 key switches over USB-serial-JTAG
- native boot test asserting framebuffer render + key-press serial protocol
- SH1107 LabRef entry linking the deck lab
test: executing walk-vs-scheduler fidelity gates for STM32 f401/l073/l476/h563 + F103 DMA
OpenAI Deck ESP32-S3 example + Potentiometer stimulus device
Add a 12-channel RP2040 DMA transfer engine: READ_ADDR/WRITE_ADDR/
TRANS_COUNT/CTRL_TRIG with the AL1/AL2/AL3 aliased register windows,
memory-to-memory byte movement over the chip bus (permanent TREQ),
address increment with optional RING wrap, per-beat TRANS_COUNT
decrement, CHAIN_TO handoff, MULTI_CHAN_TRIGGER, CHAN_ABORT, and
INTR/INTE0/INTF0/INTS0/INTE1/INTF1/INTS1 aggregation onto DMA_IRQ_0
(NVIC 11) and DMA_IRQ_1 (NVIC 12). Wire it into rp2040.yaml at
0x50000000 and the rp2040 factory path.

Refs #577
Drive the DMA over the real from_config RP2040 bus with the peripheral
walk live, executing Thumb machine code. Asserts byte-exact M2M movement
with address increment and TRANS_COUNT drain, a two-channel CHAIN_TO
handoff, and INTS0 completion IRQ delivery through NVIC (DMA_IRQ_0,
exception 27) with ISR acknowledge.

Refs #577
The F401 reused the F1 FLASH and L4 PWR peripheral models, whose
family-specific reset words are wrong for F4 silicon, and the shared
F4 RCC dropped the HSITRIM default from RCC_CR.

- RCC F4Rcc: CR reset 0x03 -> 0x0000_0083 (HSITRIM=0x10 default, bits
  7:3). Shared with F407; correct for both parts.
- FLASH: add isolated Stm32F4 layout (ACR=0 no F1 prefetch default,
  CR LOCK at bit 31, OPTCR present) selected by profile stm32f4.
- PWR: add PwrF4 (only PWR_CR/PWR_CSR exist, both reset 0; VOSRDY
  asserted on scale select) selected by profile stm32f4.

Refs #576
- flash/pwr profiles set to stm32f4 (were f1 / L4 default).
- gpioa/gpiob reset_moder/ospeedr/pupdr set to the SWD/JTAG pin
  defaults per the STM32F401 SVD (RM0368 section 8.4).

Refs #576
Pins RCC/FLASH/PWR/GPIO reset values to the STM32F401 SVD oracle
(cross-read against RM0368), mirroring the h563 conformance test.

Closes #576
Add a StreamDma peripheral for the STM32F4 stream-based DMA (RM0090 §10),
distinct from the F1/L4 channel-based Dma1: two controllers with 8 streams
each, split LISR/HISR + LIFCR/HIFCR flag registers, per-stream config
(SxCR/SxNDTR/SxPAR/SxM0AR/SxM1AR/SxFCR), CHSEL, circular and double-buffer
modes, and per-stream NVIC vectors.

The model reuses the F1 Dma1 scheduler-migration machinery (scheduler_mode
predicate, one service_stream_once body shared by both drive modes,
take_scheduled_events/on_event chain) but forks the register layout, since
the stream MMIO map and flag geometry share nothing with the channel model.

Modeled: stream enable latch, NDTR countdown, PINC/MINC increment,
HTIF/TCIF, per-stream IRQ via HTIE/TCIE, circular + double-buffer reload,
and the RM0090 §10.3.3 constraint that memory-to-memory is DMA2-only.
FIFO/burst pacing and priority arbitration are documented deferrals.

Wire DMA1 (0x40026000) and DMA2 (0x40026400) into stm32f407.yaml via the
generic factory (new stm32f4_dma type, dma2 + stream_irqs config), gated on
RCC_AHB1ENR DMA1EN/DMA2EN.

Tests: model-level walk-vs-scheduler byte-identical differential (full
snapshot + request stream + IRQ pend set, every cycle) under
event-scheduler, plus an end-to-end stm32f407_dma_walk_differential
integration test driving a real M2M transfer on the from_config bus and
asserting the RM0090 observables (enable, NDTR->0, PINC/MINC copy, TCIF
latch + LIFCR clear, per-stream NVIC routing, DMA1 M2M blocked).

Closes #578
The F401 CR reset correction is confined to F4Rcc; L0Rcc is byte-for-byte untouched. Mirrors the 2026-07-04 L4-only rcc.rs precedent.
Model STM32F407 stream-based DMA controllers (DMA1/DMA2)
RP2040 DMA controller model + fidelity oracle
Fix STM32F401 cold-reset register values (RCC/FLASH/PWR/GPIO)
…deck

The S3 fast-boot constructor never called attach_esp32_external_devices, so a
manifest's declared external_devices were silently dropped on S3 — the classic
ESP32 path wired them, S3 did not. The OpenAI-deck SH1107 (declared on i2c0 @
0x3D) therefore never attached; the panel only rendered in a test that attached
it out-of-band, masking the gap.

- attach_esp32_external_devices: build_i2c_external_device wires the I2C OLED
  type (oled-sh1107) to its declared connection at config.i2c_address; SPI
  e-paper path unchanged (returns None → falls through).
- wasm S3 constructor: call the factory after configure_xtensa_esp32s3, like the
  classic-ESP32 path, so declared devices are connected.
- deck boot test: attach the SH1107 through the factory from a declared manifest
  (no out-of-band attach) so a pass proves the real app/CLI path renders.

Existing S3 configs unaffected: esp32s3-zero/spice-dispenser declare no devices;
esp32s3-oled-demo's SSD1306 still renders via the pre-existing 0x3C hardcode.
Wire manifest external_devices on ESP32-S3 (render the SH1107 deck OLED)
The test command's non-rom-boot path called build_esp32_system_from_manifest,
which builds a CLASSIC ESP32 (LX6) memory map regardless of chip. Loading an
ESP32-S3 ELF there put its XIP segments (0x3c00_0020 / 0x4200_0020) outside the
map → 'Memory access violation' at step 0. So S3 labs couldn't run via
`labwired test` — and the MCP's run_lab uses that path, blocking every S3 lab
from the MCP.

Add an is_esp32s3 && !rom_boot branch that mirrors the run command + wasm twin:
configure_xtensa_esp32s3 + attach_esp32_external_devices (wires the manifest's
SH1107) + fast_boot, routing USB_SERIAL_JTAG to the uart sink. Classic ESP32 and
S3 --rom-boot paths are unchanged.

Verified: `labwired test` on the OpenAI deck now boots and asserts
'openai-deck-s3: boot' + 'OLED ready' (exit 0).
The test command's non-rom-boot path called build_esp32_system_from_manifest,
which builds a CLASSIC ESP32 (LX6) memory map regardless of chip. Loading an
ESP32-S3 ELF there put its XIP segments (0x3c00_0020 / 0x4200_0020) outside the
map → 'Memory access violation' at step 0. So S3 labs couldn't run via
`labwired test` — and the MCP's run_lab uses that path, blocking every S3 lab
from the MCP.

Add an is_esp32s3 && !rom_boot branch that mirrors the run command + wasm twin:
configure_xtensa_esp32s3 + attach_esp32_external_devices (wires the manifest's
SH1107) + fast_boot, routing USB_SERIAL_JTAG to the uart sink. Classic ESP32 and
S3 --rom-boot paths are unchanged.

Verified: `labwired test` on the OpenAI deck now boots and asserts
'openai-deck-s3: boot' + 'OLED ready' (exit 0).
w1ne added 2 commits July 18, 2026 22:59
# Conflicts:
#	configs/chips/stm32f401cdu6.yaml
#	crates/cli/src/commands/test.rs
#	crates/core/src/system/xtensa/esp32.rs
#	crates/core/src/system/xtensa/esp32s3.rs
#	crates/core/src/system/xtensa/mod.rs
#	crates/core/tests/e2e_i2c_tmp102.rs
#	crates/core/tests/esp32s3_oled_profile.rs
#	crates/core/tests/esp32s3_ssd1306_differential.rs
#	examples/spice-dispenser/VALIDATION.md
@w1ne
w1ne merged commit eb999d9 into main Jul 18, 2026
3 checks passed
@w1ne
w1ne deleted the fix/s3-test-path-boot branch July 18, 2026 21:07
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.

2 participants