From 6bdb590b0a3b8c94e5891eeb405ef525bca8cd1c Mon Sep 17 00:00:00 2001 From: David Montero Date: Fri, 12 Jun 2026 23:01:00 +0200 Subject: [PATCH] perf(cyw43): fast-path firmware download in boot harness Add PioBusSniffer.inDiscardableWriteData(): true while framing a large non-F2 write (firmware/backplane bulk write the chip discards). The boot harness drops those data words (keeping ~4 so the PIO raises TXSTALL, which is all the driver's write path waits for) instead of bit-banging the full ~224 KB through the PIO. F2/SDPCM IOCTL writes and every count/command word are retained in full, so the bring-up still completes the 23-IOCTL wifi_on sequence (F1 framing 3613 -> 97, F2 unchanged). Also adds IPSR + PC-histogram sampling: confirmed the post-mcast_list stall is thread-mode (no GPIO IRQ storm) inside MicroPython's host-side cyw43_cb_tcpip_init (lwIP), above the chip emulation. --- ...cow-cyw43-boot-harness.investigate.test.ts | 21 +++++++++++++++++-- .../src/simulation/cyw43/PioBusSniffer.ts | 14 +++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/frontend/src/__tests__/picow-cyw43-boot-harness.investigate.test.ts b/frontend/src/__tests__/picow-cyw43-boot-harness.investigate.test.ts index a700b3b5..2a86c1d8 100644 --- a/frontend/src/__tests__/picow-cyw43-boot-harness.investigate.test.ts +++ b/frontend/src/__tests__/picow-cyw43-boot-harness.investigate.test.ts @@ -233,6 +233,16 @@ describe.skipIf(!process.env.CYW43_HARNESS)('Pico W cyw43 boot harness (investig pushCount++; rawWords.push(v >>> 0); if (rawWords.length > 600) rawWords.splice(0, rawWords.length - 400); // ring + // Firmware/backplane bulk-write data: discard most of it (keep up to + // 4 words so the PIO drains a little then raises TXSTALL, which is all + // the driver's write path waits for). This fast-paths the ~224 KB + // download instead of bit-banging every word through the PIO. F2 + // (IOCTL) writes and all count/command words go through feedWord and + // are retained in full. + if (sniffer.inDiscardableWriteData()) { + if (q.length - head < 4) q.push(v >>> 0); + return; + } feedWord(v, sm); q.push(v >>> 0); }; @@ -352,9 +362,13 @@ describe.skipIf(!process.env.CYW43_HARNESS)('Pico W cyw43 boot harness (investig // blocks in active(True) with no bus activity, so a PC histogram localizes // the stuck loop — DMA wait, TXSTALL wait, or a delay). const pcHist = new Map(); + const ipsrHist = new Map(); const pcSampler = setInterval(() => { - const pc = ((sim.rp2040 as any).core?.PC ?? 0) >>> 0; + const core = (sim.rp2040 as any).core; + const pc = (core?.PC ?? 0) >>> 0; pcHist.set(pc, (pcHist.get(pc) ?? 0) + 1); + const ipsr = (core?.IPSR ?? 0) & 0x3f; // 0=thread, else exception number + ipsrHist.set(ipsr, (ipsrHist.get(ipsr) ?? 0) + 1); }, 20); const deadline = setTimeout(finish, 70_000); function finish() { @@ -396,7 +410,10 @@ describe.skipIf(!process.env.CYW43_HARNESS)('Pico W cyw43 boot harness (investig `===== CPU FAULTS faultCount=${faultCount} =====\n` + faultLog.join('\n') + '\n' + '===== HOT PCs (busy-wait localization) =====\n' + [...pcHist.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12) - .map(([pc, n]) => ` ${String(n).padStart(6)} PC=0x${pc.toString(16)}`).join('\n') + '\n\n' + + .map(([pc, n]) => ` ${String(n).padStart(6)} PC=0x${pc.toString(16)}`).join('\n') + '\n' + + '===== IPSR (0=thread, else exception#) =====\n' + + [...ipsrHist.entries()].sort((a, b) => b[1] - a[1]) + .map(([ipsr, n]) => ` ${String(n).padStart(6)} IPSR=${ipsr}`).join('\n') + '\n\n' + '===== PIO/SM STATE AT DEADLINE (deadlock) =====\n' + pioState + '\n' + '===== F2/IOCTL TRANSFERS (total seen, non-ring) =====\n' + `count=${f2Log.length}\n` + f2Log.join('\n') + '\n\n' + diff --git a/frontend/src/simulation/cyw43/PioBusSniffer.ts b/frontend/src/simulation/cyw43/PioBusSniffer.ts index ec49f475..5748f465 100644 --- a/frontend/src/simulation/cyw43/PioBusSniffer.ts +++ b/frontend/src/simulation/cyw43/PioBusSniffer.ts @@ -146,6 +146,20 @@ export class PioBusSniffer { /** Debug hook (tests): fired with each count word's raw value + decode. */ onCount: ((kind: 'out' | 'in', raw: number, bytes: number) => void) | null = null; + /** + * True while the in-flight transfer is a large non-F2 write — i.e. a firmware + * download block (or any backplane bulk write the chip discards). The host + * pushes ~224 KB of these; their data words carry no information the emulator + * needs (firmware lands in chip SRAM we don't model), so a non-dropping FIFO + * forces the PIO to bit-bang every one and the emulation crawls. The harness + * uses this to drop those data words while keeping F2/SDPCM writes (IOCTLs) + * and every command/count word intact. 36 bytes = count(0) + cmd(4) + a few + * config words; firmware blocks are 64-byte payloads, well above it. + */ + inDiscardableWriteData(): boolean { + return this.state === 'writeData' && this.cmd?.function !== 2 && this.outBytes > 36; + } + *feedWord(rawWord: number): Generator { const raw = rawWord >>> 0;