feat(cyw43): wire WiFi bring-up into production RP2040Simulator

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.
This commit is contained in:
David Montero 2026-06-12 23:48:04 +02:00
parent 4d631b3a98
commit c10e63764c
2 changed files with 71 additions and 5 deletions

View File

@ -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[] = [];

View File

@ -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; }