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.
This commit is contained in:
David Montero 2026-06-12 23:01:00 +02:00
parent c4cbb17591
commit 6bdb590b0a
2 changed files with 33 additions and 2 deletions

View File

@ -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<number, number>();
const ipsrHist = new Map<number, number>();
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' +

View File

@ -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<SnifferEvent> {
const raw = rawWord >>> 0;