From c10e63764cc8a0c65bb9c377dcca0543f077cda6 Mon Sep 17 00:00:00 2001 From: David Montero Date: Fri, 12 Jun 2026 23:48:04 +0200 Subject: [PATCH] feat(cyw43): wire WiFi bring-up into production RP2040Simulator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the gSPI wiring proven in the boot harness into the real simulator so WiFi works in the browser, not just the test: - Non-dropping TX FIFO (head-pointer queue) in installCyw43PioHooks, so the 260-word F2 IOCTL writes aren't truncated, with the firmware/ backplane bulk-write fast-path (inDiscardableWriteData) keeping the ~224 KB download cheap. Fully restorable on detach. - Drive WL_HOST_WAKE (GPIO24) from emu.onHostWake, and re-sync the pin level after installCyw43PioHooks (loadMicroPython resets GPIO while the chip's queue persists). - With a backend bridge attached, disable the built-in DHCP/ARP net so the bridge owns the network. Production steps the PIO in lockstep with the CPU (pioStepAccum), so no PIO-rate crank is needed (that was harness-only). The emulator-side fixes (host-wake, F2 byte order, join events, BDC header, virtual DHCP/ARP) are already shared. Not yet exercised in a browser e2e — the headless harness is the verification today. --- frontend/src/simulation/RP2040Simulator.ts | 68 +++++++++++++++++-- .../src/simulation/cyw43/Cyw43Emulator.ts | 8 +++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/frontend/src/simulation/RP2040Simulator.ts b/frontend/src/simulation/RP2040Simulator.ts index f554e9d9..2f065834 100644 --- a/frontend/src/simulation/RP2040Simulator.ts +++ b/frontend/src/simulation/RP2040Simulator.ts @@ -416,9 +416,19 @@ export class RP2040Simulator { emu.onPacketOut((ev: PacketOutEvent) => { this.cyw43Bridge?.sendPacket(ev.ether); }); + // Drive WL_HOST_WAKE (GPIO24, active-high). The driver gates poll_device on + // this pin until it has received its first packet, so without it the first + // IOCTL response is never read and wifi_on stalls. + emu.onHostWake((active: boolean) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + try { (this.rp2040 as any)?.gpio?.[24]?.setInputValue(active); } catch { /* noop */ } + }); if (bridge) { bridge.onPacketIn = (p) => emu.injectPacket(p.ether); + // A real backend bridge owns the network, so disable the built-in + // DHCP/ARP responder to avoid answering on its behalf. + emu.setVirtualNet(null); } this.installCyw43PioHooks(); @@ -453,13 +463,55 @@ export class RP2040Simulator { const tx = sm.txFIFO; const rx = sm.rxFIFO; if (!tx || !rx) continue; + const sniffer = this.cyw43Sniffer; + // Make the TX FIFO NON-DROPPING (head-pointer queue). rp2040js's 4-deep + // FIFO silently drops words once full, which truncates the 260-word F2 + // IOCTL writes (clm_load, the connect ioctls) so the chip never sees a + // complete frame. Real hardware paces the DMA with DREQ and never drops. + // To keep the ~224 KB firmware download cheap we still discard the bulk + // of each firmware/backplane write (inDiscardableWriteData): the PIO + // drains the few kept words, raises TXSTALL, and the driver moves on. + const q: number[] = []; + let head = 0; + const origFull = Object.getOwnPropertyDescriptor(tx, 'full'); + const origEmpty = Object.getOwnPropertyDescriptor(tx, 'empty'); + const origItem = Object.getOwnPropertyDescriptor(tx, 'itemCount'); const origPush: (v: number) => void = tx.push.bind(tx); + const origPull: () => number = tx.pull.bind(tx); + const origPeek = tx.peek?.bind(tx); + const origReset = tx.reset?.bind(tx); + Object.defineProperty(tx, 'full', { get: () => false, configurable: true }); + Object.defineProperty(tx, 'empty', { get: () => head >= q.length, configurable: true }); + Object.defineProperty(tx, 'itemCount', { get: () => q.length - head, configurable: true }); + tx.peek = () => (head < q.length ? q[head] : 0); + tx.reset = () => { q.length = 0; head = 0; }; tx.push = (value: number) => { + if (sniffer.inDiscardableWriteData()) { + if (q.length - head < 4) q.push(value >>> 0); // keep a few so the PIO TXSTALLs + return; + } // Feed the gSPI sniffer; commands that produce a response queue it // for on-demand delivery (see the rxFIFO.pull hook below). this.feedCyw43Word(value); - return origPush(value); + q.push(value >>> 0); }; + tx.pull = () => { + if (head >= q.length) return 0; + const v = q[head++]; + if (head > 8192 && head * 2 > q.length) { q.splice(0, head); head = 0; } // compact + return v; + }; + this.cyw43HookedFifos.push({ + restore: () => { + if (origFull) Object.defineProperty(tx, 'full', origFull); else delete tx.full; + if (origEmpty) Object.defineProperty(tx, 'empty', origEmpty); else delete tx.empty; + if (origItem) Object.defineProperty(tx, 'itemCount', origItem); else delete tx.itemCount; + tx.push = origPush; + tx.pull = origPull; + if (origPeek) tx.peek = origPeek; + if (origReset) tx.reset = origReset; + }, + }); // Reset the gSPI framing at each transfer boundary. cyw43_spi_transfer // does pio_sm_restart before pushing the count words, so this keeps the // sniffer deterministic even across the firmware-stream fast-path. @@ -479,19 +531,25 @@ export class RP2040Simulator { // the data arrived late or was lost; serving on pull keeps it in lock // step with the driver. // eslint-disable-next-line @typescript-eslint/no-explicit-any - const origPull: () => number = (rx as any).pull.bind(rx); + const origRxPull: () => number = (rx as any).pull.bind(rx); // eslint-disable-next-line @typescript-eslint/no-explicit-any (rx as any).pull = () => - this.cyw43RxQueue.length > 0 ? (this.cyw43RxQueue.shift() as number) : origPull(); + this.cyw43RxQueue.length > 0 ? (this.cyw43RxQueue.shift() as number) : origRxPull(); this.cyw43HookedFifos.push({ restore: () => { - tx.push = origPush; // eslint-disable-next-line @typescript-eslint/no-explicit-any - (rx as any).pull = origPull; + (rx as any).pull = origRxPull; }, }); } } + // Re-sync WL_HOST_WAKE: loadMicroPython swaps in a fresh RP2040 (GPIO reset + // to low) while the chip's frame queue — and thus its host-wake level — + // persists. onHostWake only fires on changes, so push the current level now. + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.rp2040 as any)?.gpio?.[24]?.setInputValue(this.cyw43.hostWakeLevel()); + } catch { /* noop */ } } private cyw43RxQueue: number[] = []; diff --git a/frontend/src/simulation/cyw43/Cyw43Emulator.ts b/frontend/src/simulation/cyw43/Cyw43Emulator.ts index 62b8405e..7fe2c9f1 100644 --- a/frontend/src/simulation/cyw43/Cyw43Emulator.ts +++ b/frontend/src/simulation/cyw43/Cyw43Emulator.ts @@ -325,6 +325,14 @@ export class Cyw43Emulator { /** True once the driver has switched the bus to 32-bit big-endian. */ isBigEndian(): boolean { return this.bigEndian; } + /** Current WL_HOST_WAKE level (true=high). Lets the host re-sync GPIO24 + * after the RP2040 (and its GPIO state) is recreated by a firmware reload. */ + hostWakeLevel(): boolean { return this.hostWakeAsserted; } + + /** Enable/replace (or disable with null) the self-contained DHCP/ARP net. + * Disable it when an external packet bridge owns the network. */ + setVirtualNet(cfg: VirtualNetConfig | null): void { this.virtualNet = cfg; } + /** Debug: queued chip→host frame count (for harness instrumentation). */ debugInboundCount(): number { return this.inboundEvents.length; }