From b172cadbc5e602045177bf04917c36660f1aa777 Mon Sep 17 00:00:00 2001 From: David Montero Date: Fri, 12 Jun 2026 21:21:28 +0200 Subject: [PATCH] wip(picow): deterministic gSPI framing via per-transfer restart hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cyw43_spi_transfer calls pio_sm_restart before each transfer's count words, so hooking restart() to reset the sniffer makes framing deterministic across the firmware-stream fast-path (no phantom-transfer carryover). Verified: restarts fire 3625x (once per transfer), F1 phantom count drops, and the CLM IOCTL write now frames correctly (cmd decodes to F2, 'clmload' payload). Wired into RP2040Simulator + the harness. Remaining (next session): the CLM/IOCTL write doesn't complete its payload and wifi_on still fails (active()=False) — bus_init stalls at/around clm_load with only 2 STATUS reads and goes idle. Next: trace the CLM write's DMA/PIO drain and the SDPCM IOCTL response path. See findings.md F-13. --- ...cow-cyw43-boot-harness.investigate.test.ts | 36 +++++++++++++++---- frontend/src/simulation/RP2040Simulator.ts | 14 ++++++++ 2 files changed, 43 insertions(+), 7 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 a412d387..66f84fd9 100644 --- a/frontend/src/__tests__/picow-cyw43-boot-harness.investigate.test.ts +++ b/frontend/src/__tests__/picow-cyw43-boot-harness.investigate.test.ts @@ -48,19 +48,26 @@ const INJECT_CODE = [ 'import network, time', 'print("STEP_IMPORT_OK")', 'w=network.WLAN(network.STA_IF)', - 'print("STEP_WLAN_OBJ_OK pre_active=" + str(w.active()))', + 'print("STEP_WLAN_OBJ_OK")', 'try:', ' w.active(True)', - ' print("STEP_ACTIVE active=" + str(w.active()) + " status=" + str(w.status()))', 'except Exception as e:', ' print("ACTIVE_EXC " + repr(e))', + // Give the background driver task time to finish wifi_on (CLM load + IOCTLs). + 'for i in range(40):', + ' print("ACT", i, "active=" + str(w.active()) + " status=" + str(w.status()))', + ' if w.active():', + ' break', + ' time.sleep_ms(150)', 'try:', ' w.connect("Velxio-GUEST", "")', ' print("STEP_CONNECT_CALLED status=" + str(w.status()))', 'except Exception as e:', ' print("CONNECT_EXC " + repr(e))', - 'for i in range(4):', + 'for i in range(20):', ' print("POLL", i, "status", w.status(), "conn", w.isconnected())', + ' if w.isconnected():', + ' break', ' time.sleep_ms(200)', 'print("HARNESS_DONE")', ].join('\n'); @@ -163,12 +170,26 @@ describe.skipIf(!process.env.CYW43_HARNESS)('Pico W cyw43 boot harness (investig } let rxPulled = 0; + let restartCount = 0; for (const pio of (sim.rp2040 as any).pio) { for (const sm of pio.machines) { const tx = sm.txFIFO; if (!tx) continue; const orig = tx.push.bind(tx); - tx.push = (v: number) => { pushCount++; rawWords.push(v >>> 0); feedWord(v, sm); return orig(v); }; + tx.push = (v: number) => { + pushCount++; + rawWords.push(v >>> 0); + if (rawWords.length > 600) rawWords.splice(0, rawWords.length - 400); // ring + feedWord(v, sm); + return orig(v); + }; + // Reset the sniffer at each transfer boundary: cyw43_spi_transfer + // calls pio_sm_restart before pushing the count words, so this keeps + // framing deterministic even after the firmware-stream fast-path. + if (typeof sm.restart === 'function') { + const origRestart = sm.restart.bind(sm); + sm.restart = () => { restartCount++; sniffer.reset(); return origRestart(); }; + } const rx = sm.rxFIFO; if (rx) { const origPull = rx.pull.bind(rx); @@ -247,11 +268,12 @@ describe.skipIf(!process.env.CYW43_HARNESS)('Pico W cyw43 boot harness (investig '===== TOP COMMAND COUNTS (poll loops) =====\n' + polls.map(([k, n]) => ` ${String(n).padStart(6)} ${k}`).join('\n') + '\n\n' + `===== FUNCTION HISTOGRAM F0..F3 = ${funcHist.join(',')} =====\n` + - `===== initInbound=${initInbound} statusReads=${statusReads} statusReadsWithPkt=${statusReadsWithPkt} finalInbound=${chip.debugInboundCount()} =====\n\n` + + `===== initInbound=${initInbound} statusReads=${statusReads} statusReadsWithPkt=${statusReadsWithPkt} finalInbound=${chip.debugInboundCount()} restarts=${restartCount} =====\n\n` + '===== F2/IOCTL TRANSFERS (total seen, non-ring) =====\n' + `count=${f2Log.length}\n` + f2Log.join('\n') + '\n\n' + - '===== TRACE TAIL (post-firmware) =====\n' + trace.slice(-120).join('\n') + '\n'; - void rawWords; + '===== TRACE TAIL (post-firmware) =====\n' + trace.slice(-120).join('\n') + '\n\n' + + '===== LAST RAW TX WORDS (hex) — the end of the run =====\n' + + rawWords.slice(-70).map((w) => w.toString(16).padStart(8, '0')).join(' ') + '\n'; try { writeFileSync('/tmp/cyw43-trace.txt', report); } catch { /* noop */ } resolve({ serial, diff --git a/frontend/src/simulation/RP2040Simulator.ts b/frontend/src/simulation/RP2040Simulator.ts index e5d7dd17..f554e9d9 100644 --- a/frontend/src/simulation/RP2040Simulator.ts +++ b/frontend/src/simulation/RP2040Simulator.ts @@ -460,6 +460,20 @@ export class RP2040Simulator { this.feedCyw43Word(value); return origPush(value); }; + // 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. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (typeof (sm as any).restart === 'function') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const origRestart: () => void = (sm as any).restart.bind(sm); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (sm as any).restart = () => { this.cyw43Sniffer?.reset(); return origRestart(); }; + this.cyw43HookedFifos.push({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + restore: () => { (sm as any).restart = origRestart; }, + }); + } // Serve the chip's response when the driver's DMA actually reads the // RX FIFO. Pushing into the FIFO eagerly raced the async DMA/PIO and // the data arrived late or was lost; serving on pull keeps it in lock