fix(cyw43): WiFi only worked on the first Run after a page load

The CYW43439 emulator is stateful — bus handshake, backplane window, SDPCM
sequence and credit, event mask, link state — and attachPioPeripheral is
idempotent, called once at board-add. So the chip's state SURVIVED every later
Run while the guest driver started over from scratch. The second Run had a
fresh driver talking to a chip mid-conversation: the handshake "passed" against
stale registers, IOCTL replies came back zeroed (WiFi.macAddress() read all
00s), and the first call that actually waited on the chip — WiFi.begin(), or
the second IOCTL of any kind — blocked forever.

A rebuilt MCU means a rebooted board, so the chip is power-cycled with it: the
peripheral is replaced wholesale (it owns its network bridge and its whole
register model, so there is no partial-reset path to get wrong) right before
the FIFO hooks are re-installed on the new PIO.

Also: overlay board simulators now get onSimulationStart. The Pimoroni Pico
Plus 2 W and Badger 2350 carry the same CYW43439 die on an RP2350 simulator,
which is not an RP2040Simulator, so they never had their network bridge opened
— WiFi that reached an IP and could then reach nothing.

Verified on the RP2350 side with a harness that boots a real arduino-pico
binary twice in one simulator (pro overlay); before the fix run 2 hung with a
zeroed MAC, after it both runs associate.
This commit is contained in:
David Montero Crespo 2026-08-21 21:03:47 +02:00
parent 664c3581a3
commit 24ea1a73f8
2 changed files with 59 additions and 2 deletions

View File

@ -183,6 +183,9 @@ export class RP2040Simulator {
// ── Generic PIO/gSPI bus peripheral (e.g. the pro WiFi co-processor). Null
// in OSS (no factory installed); attached for boards a factory supports.
private pioPeripheral: PioPeripheral | null = null;
/** Board id the PIO peripheral was created for, so a rebooting guest can be
* handed a freshly powered chip (see powerCyclePioPeripheral). */
private pioBoardId: string | null = null;
private pioHookedFifos: Array<{ restore: () => void }> = [];
// The board kind this simulator runs (set by attachPioPeripheral). A
// 'pi-pico-w' boots the RPI_PICO_W firmware (with the `network` module)
@ -386,6 +389,7 @@ export class RP2040Simulator {
// RP2040, so those hooks now point at the discarded instance. Re-install
// them on the new PIO FIFOs or the peripheral never sees the bus traffic.
if (this.pioPeripheral) {
this.powerCyclePioPeripheral();
this.pioHookedFifos = [];
this.installPioPeripheralHooks();
}
@ -418,6 +422,7 @@ export class RP2040Simulator {
// factory-not-installed-yet) so loadMicroPython still picks the W firmware
// for a pi-pico-w board.
this.boardKind = boardKind;
this.pioBoardId = boardId;
if (this.pioPeripheral) return this.pioPeripheral;
const peripheral = createPioPeripheral(boardKind, boardId);
if (!peripheral) return null;
@ -435,6 +440,48 @@ export class RP2040Simulator {
return peripheral;
}
/**
* Give a rebooting guest a chip that has also rebooted.
*
* The CYW43439 emulator is stateful: the bus handshake result, the backplane
* window, the SDPCM sequence counters and credit, the event mask, the link
* state. attachPioPeripheral is idempotent and the store only calls it at
* board-add, so that state SURVIVED every later Run while the guest driver
* started over from scratch. The second Run then had a fresh driver talking
* to a chip mid-conversation: the handshake "passed" against stale
* registers, IOCTL replies came back zeroed, and the first call that waited
* on the chip blocked forever. Only the first Run after a page load worked.
*
* Called from the paths that rebuild the MCU, before the FIFO hooks are
* re-installed, so the new chip is what gets wired to the new PIO.
*/
private powerCyclePioPeripheral(): void {
if (!this.pioPeripheral || !this.pioBoardId) return;
const kind = this.boardKind;
const boardId = this.pioBoardId;
try {
this.pioPeripheral.detach?.();
} catch {
/* the old chip is being thrown away either way */
}
for (const h of this.pioHookedFifos) {
try {
h.restore();
} catch {
/* noop */
}
}
this.pioHookedFifos = [];
this.pioPeripheral = null;
const fresh = createPioPeripheral(kind, boardId);
if (!fresh) return;
this.pioPeripheral = fresh;
fresh.onHostWake((active: boolean) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
try { (this.rp2040 as any)?.gpio?.[24]?.setInputValue(active); } catch { /* noop */ }
});
}
/** Detach the PIO peripheral (called from teardown). */
detachPioPeripheral(): void {
for (const h of this.pioHookedFifos) h.restore();

View File

@ -2571,12 +2571,22 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
// Notify an attached PIO peripheral (the pro CYW43 WiFi co-processor)
// that the simulation started, with the board's source files so it can
// detect WiFi usage and open its network bridge. No-op in OSS.
if (rpSim instanceof RP2040Simulator) {
//
// Overlay boards carry the same peripheral: the Pimoroni Pico Plus 2 W
// and Badger 2350 have the RM2 (the same CYW43439 die) on an RP2350
// simulator, which is NOT an RP2040Simulator. Without this branch they
// associated to the local virtual AP but never got the network bridge
// opened, so real internet and the Pro custom-AP config never applied
// — WiFi that reached an IP and could reach nothing.
const pioSim = rpSim as
| (typeof rpSim & { getPioPeripheral?: () => { onSimulationStart?: (f: unknown[]) => void } | null })
| null;
if (typeof pioSim?.getPioPeripheral === 'function') {
const editorState = useEditorStore.getState();
const rawFiles = editorState.fileGroups[board.activeFileGroupId];
const boardFiles =
rawFiles && rawFiles.length > 0 ? rawFiles : editorState.files;
rpSim.getPioPeripheral()?.onSimulationStart?.(boardFiles);
pioSim.getPioPeripheral()?.onSimulationStart?.(boardFiles);
}
}