wip(picow): CYW43 emulation — chip bring-up works, active(True) returns
Brings the Pico W CYW43439 gSPI emulation from "fails at the first register read" to "the chip boots fully and MicroPython's network.WLAN().active(True) returns" — validated end-to-end against the real RPI_PICO_W firmware via a headless boot harness. What now works (Phases 1-2): - PioBusSniffer rewritten to the real cyw43_bus_pio_spi framing [out_bits][in_bits][cmd][write_data], skipping the two PIO loop-counter words. Self-healing: validates count1 (= tx_length*8-1, 4-aligned, <=2052) and skips non-conforming words — re-syncs after the extra word rp2040js pushes on large writes AND fast-paths the ~224 KB firmware stream. - Dual word-order regime: boot 16-bit-LE (swap16x2 / swap16) flips to 32-bit big-endian (bswap32) at the SPI_BUS_CONTROL write. Calibrated empirically against the firmware. Sniffer reads the mode via setModeProvider(). - Cyw43Emulator: encodeReadWord (per-regime), readBytes-sized backplane reads with the value in the last word (response-delay pad), ALP+HT clocks and F2 always ready, AI core registers (IOCTRL/RESETCTRL), interrupt register reports no errors, f1Mem echo store, SDPCM bus-credit granting + initial frame. - RP2040Simulator: serves chip responses on rxFIFO.pull (on-demand) instead of racing the async DMA/PIO; passes readBytes through. Not done yet (Phase 3+): connect() runs but stalls in the power-management / save-restore phase before any F2/IOCTL traffic; packet transport (Tier 2) and firmware-clocking perf are open. See project/picow-wifi-emulation/ for the full research, phases, and findings. The boot harness (picow-cyw43-boot-harness.investigate.test.ts) is gated behind CYW43_HARNESS=1 so it stays out of the normal test run.
This commit is contained in:
parent
478fd75a7e
commit
9197fcaaf6
|
|
@ -0,0 +1,259 @@
|
|||
/**
|
||||
* picow-cyw43-boot-harness.investigate.test.ts
|
||||
*
|
||||
* INVESTIGATION HARNESS (not a CI test — run explicitly).
|
||||
*
|
||||
* Boots the REAL Pico W MicroPython firmware on rp2040js, attaches the
|
||||
* production Cyw43Emulator to the PIO TX FIFOs exactly the way
|
||||
* RP2040Simulator.installCyw43PioHooks() does, injects a WiFi-bringup
|
||||
* snippet via the raw REPL, and captures the gSPI command trace so we can
|
||||
* see EXACTLY where the real cyw43 driver diverges from the emulator
|
||||
* (i.e. where it stalls).
|
||||
*
|
||||
* Run:
|
||||
* npx vitest run src/__tests__/picow-cyw43-boot-harness.investigate.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { Simulator, USBCDC, ConsoleLogger, LogLevel } from 'rp2040js';
|
||||
import { Cyw43Emulator } from '../simulation/cyw43/Cyw43Emulator';
|
||||
import { PioBusSniffer, formatCmd, type Cyw43Cmd } from '../simulation/cyw43/PioBusSniffer';
|
||||
import { bootromB1 } from '../simulation/rp2040-bootrom';
|
||||
|
||||
const FW_PATH =
|
||||
'/home/dave/velxio-prod/velxio/frontend/public/firmware/micropython-rp2040w.uf2';
|
||||
|
||||
const UF2_MAGIC0 = 0x0a324655;
|
||||
const UF2_MAGIC1 = 0x9e5d5157;
|
||||
const FLASH_START = 0x10000000;
|
||||
|
||||
function loadUF2(uf2: Uint8Array, flash: Uint8Array): number {
|
||||
const view = new DataView(uf2.buffer, uf2.byteOffset, uf2.byteLength);
|
||||
let blocks = 0;
|
||||
for (let off = 0; off + 512 <= uf2.length; off += 512) {
|
||||
if (view.getUint32(off, true) !== UF2_MAGIC0) continue;
|
||||
if (view.getUint32(off + 4, true) !== UF2_MAGIC1) continue;
|
||||
const addr = view.getUint32(off + 12, true);
|
||||
const payload = uf2.subarray(off + 32, off + 32 + 256);
|
||||
const o = addr - FLASH_START;
|
||||
if (o >= 0 && o + 256 <= flash.length) { flash.set(payload, o); blocks++; }
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// The WiFi bringup snippet. Prints between each step so the serial log
|
||||
// pinpoints how far MicroPython gets before the cyw43 driver stalls.
|
||||
const INJECT_CODE = [
|
||||
'import network, time',
|
||||
'print("STEP_IMPORT_OK")',
|
||||
'w=network.WLAN(network.STA_IF)',
|
||||
'print("STEP_WLAN_OBJ_OK")',
|
||||
'w.active(True)',
|
||||
'print("STEP_ACTIVE active=" + str(w.active()))',
|
||||
'w.connect("Velxio-GUEST", "")',
|
||||
'print("STEP_CONNECT_CALLED")',
|
||||
'for i in range(15):',
|
||||
' print("POLL", i, "status", w.status(), "conn", w.isconnected())',
|
||||
' time.sleep_ms(300)',
|
||||
'print("STEP_IFCONFIG " + str(w.ifconfig()))',
|
||||
'print("HARNESS_DONE")',
|
||||
].join('\n');
|
||||
|
||||
// Skipped in the normal gate (boots a real firmware for ~60s). Run with:
|
||||
// CYW43_HARNESS=1 npx vitest run src/__tests__/picow-cyw43-boot-harness.investigate.test.ts
|
||||
describe.skipIf(!process.env.CYW43_HARNESS)('Pico W cyw43 boot harness (investigation)', () => {
|
||||
it('captures the gSPI trace up to the stall', async () => {
|
||||
const result = await new Promise<{
|
||||
serial: string;
|
||||
trace: string[];
|
||||
reachedImport: boolean;
|
||||
reachedActive: boolean;
|
||||
lastCmds: string[];
|
||||
pollSummary: Array<[string, number]>;
|
||||
}>((resolve) => {
|
||||
const sim = new Simulator();
|
||||
sim.rp2040.loadBootrom(bootromB1);
|
||||
sim.rp2040.logger = new ConsoleLogger(LogLevel.Error);
|
||||
|
||||
const fwBlocks = loadUF2(new Uint8Array(readFileSync(FW_PATH)), sim.rp2040.flash);
|
||||
let usbConnected = false;
|
||||
|
||||
// ── attach production Cyw43Emulator the way RP2040Simulator does ──
|
||||
const chip = new Cyw43Emulator();
|
||||
const sniffer = new PioBusSniffer();
|
||||
sniffer.setModeProvider(() => chip.isBigEndian());
|
||||
let pushCount = 0;
|
||||
void pushCount;
|
||||
const rxQueue: number[] = [];
|
||||
const rawWords: number[] = [];
|
||||
const trace: string[] = [];
|
||||
const f2Log: string[] = []; // F2/IOCTL transfers, NOT subject to the ring
|
||||
const cmdCounts = new Map<string, number>();
|
||||
let ledOn = false;
|
||||
chip.onLed((e) => { ledOn = e.on; trace.push(`** LED ${e.on ? 'ON' : 'OFF'} **`); });
|
||||
chip.onConnect((e) => trace.push(`** WIFI CONNECT ssid=${e.ssid} **`));
|
||||
|
||||
const key = (c: Cyw43Cmd) =>
|
||||
`${c.write ? 'WR' : 'RD'} F${c.function} 0x${c.address.toString(16)} len=${c.length}`;
|
||||
|
||||
function queueReply(reply: Uint8Array) {
|
||||
for (let i = 0; i + 4 <= reply.length; i += 4) {
|
||||
rxQueue.push(
|
||||
((reply[i + 3] << 24) | (reply[i + 2] << 16) | (reply[i + 1] << 8) | reply[i]) >>> 0,
|
||||
);
|
||||
}
|
||||
if (reply.length % 4 !== 0) {
|
||||
const tail = reply.subarray(reply.length - (reply.length % 4));
|
||||
let w = 0;
|
||||
for (let i = 0; i < tail.length; i++) w |= tail[i] << (i * 8);
|
||||
rxQueue.push(w >>> 0);
|
||||
}
|
||||
}
|
||||
function feedWord(word: number, sm: any) {
|
||||
for (const ev of sniffer.feedWord(word)) {
|
||||
if (ev.kind === 'header') {
|
||||
const k = key(ev.cmd);
|
||||
cmdCounts.set(k, (cmdCounts.get(k) ?? 0) + 1);
|
||||
} else if (ev.kind === 'payload') {
|
||||
// Capture EVERY F2 transfer (IOCTLs/SDPCM) in a non-ring log.
|
||||
if (ev.cmd.function === 2 && f2Log.length < 60) {
|
||||
f2Log.push(`${ev.cmd.write ? 'WR' : 'RD'} F2 len=${ev.payload.length || ev.readBytes}`);
|
||||
}
|
||||
// Skip logging the ~3500 firmware-block writes (len>=64) — keep the
|
||||
// trace focused on the handshake + post-download sequence.
|
||||
const isFwBlock = ev.cmd.write && ev.payload.length >= 64;
|
||||
if (!isFwBlock) {
|
||||
trace.push(`-> ${formatCmd(ev.cmd)} rxBytes=${ev.readBytes}`);
|
||||
if (ev.cmd.write && ev.payload.length > 0) {
|
||||
const v = (ev.payload[0] | (ev.payload[1] << 8) | (ev.payload[2] << 16) | (ev.payload[3] << 24)) >>> 0;
|
||||
trace.push(` WR data u32=0x${v.toString(16).padStart(8, '0')} (${ev.payload.length}B)`);
|
||||
}
|
||||
}
|
||||
if (trace.length > 6000) trace.splice(0, trace.length - 4000); // ring
|
||||
const reply = chip.onCommand(ev.cmd, ev.payload, ev.readBytes);
|
||||
if (reply && reply.length > 0) {
|
||||
queueReply(reply);
|
||||
if (trace.length < 4000) {
|
||||
const hex = Array.from(reply.slice(0, 8))
|
||||
.map((b) => b.toString(16).padStart(2, '0')).join(' ');
|
||||
trace.push(`<- (${reply.length}B) ${hex}${reply.length > 8 ? ' ...' : ''}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rxPulled = 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); };
|
||||
const rx = sm.rxFIFO;
|
||||
if (rx) {
|
||||
const origPull = rx.pull.bind(rx);
|
||||
// Serve the chip's response on-demand: when the driver's DMA pulls,
|
||||
// hand back the next queued response word (bypasses the async
|
||||
// PIO/FIFO timing that otherwise lags/loses the data).
|
||||
rx.pull = () => {
|
||||
const v = rxQueue.length > 0 ? (rxQueue.shift() as number) : origPull();
|
||||
rxPulled++;
|
||||
return v;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── raw REPL injection (mirrors test_micropython_pico.mjs) ──
|
||||
const cdc = new USBCDC(sim.rp2040.usbCtrl);
|
||||
let serial = '';
|
||||
let buf = '';
|
||||
let state: 'idle' | 'prompt' | 'raw' | 'done' = 'idle';
|
||||
let reachedImport = false;
|
||||
let reachedActive = false;
|
||||
|
||||
cdc.onDeviceConnected = () => {
|
||||
usbConnected = true;
|
||||
cdc.sendSerialByte('\r'.charCodeAt(0));
|
||||
cdc.sendSerialByte('\n'.charCodeAt(0));
|
||||
};
|
||||
|
||||
function sendCode() {
|
||||
const bytes = Array.from(new TextEncoder().encode(INJECT_CODE));
|
||||
let off = 0;
|
||||
const chunk = () => {
|
||||
if (off >= bytes.length) {
|
||||
setTimeout(() => { cdc.sendSerialByte(0x04); state = 'done'; }, 200);
|
||||
return;
|
||||
}
|
||||
for (const b of bytes.slice(off, off + 64)) cdc.sendSerialByte(b);
|
||||
off += 64;
|
||||
setTimeout(chunk, 120);
|
||||
};
|
||||
chunk();
|
||||
}
|
||||
|
||||
cdc.onSerialData = (bytes: Uint8Array) => {
|
||||
for (const b of bytes) { const ch = String.fromCharCode(b); serial += ch; buf += ch; }
|
||||
if (state === 'idle' && buf.includes('>>>')) {
|
||||
state = 'prompt'; buf = '';
|
||||
setTimeout(() => cdc.sendSerialByte(0x01), 150); // Ctrl+A raw REPL
|
||||
}
|
||||
if (state === 'prompt' && buf.includes('raw REPL')) {
|
||||
state = 'raw'; buf = '';
|
||||
setTimeout(sendCode, 150);
|
||||
}
|
||||
if (serial.includes('STEP_IMPORT_OK')) reachedImport = true;
|
||||
if (serial.includes('STEP_ACTIVE')) reachedActive = true;
|
||||
if (serial.length > 60000) serial = serial.slice(-20000);
|
||||
if (serial.includes('HARNESS_DONE') || serial.includes('Traceback')) {
|
||||
setTimeout(finish, 300); // let the last line flush
|
||||
}
|
||||
};
|
||||
|
||||
let finished = false;
|
||||
const deadline = setTimeout(finish, 60_000);
|
||||
function finish() {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
clearTimeout(deadline);
|
||||
try { sim.stop(); } catch { /* noop */ }
|
||||
const polls = [...cmdCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12);
|
||||
const report =
|
||||
`fwBlocks=${fwBlocks} usbConnected=${usbConnected} ` +
|
||||
`state=${state} serialLen=${serial.length} traceLen=${trace.length} ` +
|
||||
`reachedImport=${reachedImport} reachedActive=${reachedActive} ledOn=${ledOn}\n\n` +
|
||||
'===== SERIAL OUTPUT (tail 2500) =====\n' + serial.slice(-2500) + '\n\n' +
|
||||
'===== TOP COMMAND COUNTS (poll loops) =====\n' +
|
||||
polls.map(([k, n]) => ` ${String(n).padStart(6)} ${k}`).join('\n') + '\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;
|
||||
try { writeFileSync('/tmp/cyw43-trace.txt', report); } catch { /* noop */ }
|
||||
resolve({
|
||||
serial,
|
||||
trace,
|
||||
reachedImport,
|
||||
reachedActive,
|
||||
lastCmds: trace.slice(-60),
|
||||
pollSummary: polls,
|
||||
});
|
||||
}
|
||||
|
||||
sim.rp2040.core.PC = 0x10000000;
|
||||
sim.execute();
|
||||
});
|
||||
|
||||
// ── report ──
|
||||
console.log('\n===== SERIAL OUTPUT (tail) =====\n' + result.serial.slice(-1500));
|
||||
console.log('\n===== TOP COMMAND COUNTS (poll loops) =====');
|
||||
for (const [k, n] of result.pollSummary) console.log(` ${n.toString().padStart(6)} ${k}`);
|
||||
console.log('\n===== LAST 60 TRACE LINES =====\n' + result.lastCmds.join('\n'));
|
||||
console.log('\n===== reachedImport=' + result.reachedImport + ' reachedActive=' + result.reachedActive + ' =====');
|
||||
|
||||
expect(result.trace.length).toBeGreaterThan(0);
|
||||
}, 120_000);
|
||||
});
|
||||
|
|
@ -34,7 +34,7 @@ import {
|
|||
decodeCdc,
|
||||
decodeEventBody,
|
||||
} from '../simulation/cyw43';
|
||||
import { decodeHeader } from '../simulation/cyw43/PioBusSniffer';
|
||||
import { decodeHeader, swap16x2 } from '../simulation/cyw43/PioBusSniffer';
|
||||
|
||||
function makeHdr(opts: {
|
||||
write: boolean;
|
||||
|
|
@ -74,14 +74,18 @@ describe('cyw43 barrel exports', () => {
|
|||
|
||||
describe('Cyw43Emulator bus handshake', () => {
|
||||
it('returns 0 then 0xFEEDBEAD on F0:0x14 reads', () => {
|
||||
// Responses are now wire-encoded for the chosen word-order regime (boot =
|
||||
// halfword-swapped), so decode with swap16x2 to recover the logical value.
|
||||
const chip = new Cyw43Emulator();
|
||||
const r1 = chip.onCommand(makeHdr({ write: false, func: 0, addr: F0.READ_TEST, length: 4 }), new Uint8Array(0))!;
|
||||
const r2 = chip.onCommand(makeHdr({ write: false, func: 0, addr: F0.READ_TEST, length: 4 }), new Uint8Array(0))!;
|
||||
expect(readU32(r1)).toBe(0);
|
||||
expect(readU32(r2)).toBe(TEST_PATTERN);
|
||||
expect(swap16x2(readU32(r1))).toBe(0);
|
||||
expect(swap16x2(readU32(r2))).toBe(TEST_PATTERN);
|
||||
});
|
||||
|
||||
it('flips HT_AVAIL after a HT_AVAIL_REQ write', () => {
|
||||
it('reports HT_AVAIL on the clock CSR', () => {
|
||||
// The chip reports ALP+HT available; backplane reads place the value in the
|
||||
// last word, wire-encoded — decode it before checking the bit.
|
||||
const chip = new Cyw43Emulator();
|
||||
chip.onCommand(
|
||||
makeHdr({ write: true, func: 1, addr: F1.SDIO_CHIP_CLOCK_CSR, length: 1 }),
|
||||
|
|
@ -91,7 +95,7 @@ describe('Cyw43Emulator bus handshake', () => {
|
|||
makeHdr({ write: false, func: 1, addr: F1.SDIO_CHIP_CLOCK_CSR, length: 1 }),
|
||||
new Uint8Array(0),
|
||||
)!;
|
||||
expect(r[0] & ClockCsr.HT_AVAIL).toBeGreaterThan(0);
|
||||
expect(swap16x2(readU32(r.subarray(r.length - 4))) & ClockCsr.HT_AVAIL).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -400,6 +400,9 @@ export class RP2040Simulator {
|
|||
if (this.cyw43) return this.cyw43;
|
||||
const emu = new Cyw43Emulator();
|
||||
const sniffer = new PioBusSniffer();
|
||||
// The sniffer de-swaps commands/data per the chip's word-order regime,
|
||||
// which the emulator owns (flips at the SPI_BUS_CONTROL write).
|
||||
sniffer.setModeProvider(() => emu.isBigEndian());
|
||||
this.cyw43 = emu;
|
||||
this.cyw43Sniffer = sniffer;
|
||||
this.cyw43Bridge = bridge;
|
||||
|
|
@ -452,14 +455,26 @@ export class RP2040Simulator {
|
|||
if (!tx || !rx) continue;
|
||||
const origPush: (v: number) => void = tx.push.bind(tx);
|
||||
tx.push = (value: number) => {
|
||||
// Feed the gSPI sniffer; if the command produces a response,
|
||||
// surface it word-by-word into the rxFIFO so the driver's
|
||||
// `pull` instruction reads it back.
|
||||
// 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);
|
||||
};
|
||||
// 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
|
||||
// step with the driver.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const origPull: () => 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.cyw43HookedFifos.push({
|
||||
restore: () => { tx.push = origPush; },
|
||||
restore: () => {
|
||||
tx.push = origPush;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(rx as any).pull = origPull;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -470,12 +485,10 @@ export class RP2040Simulator {
|
|||
if (!this.cyw43Sniffer || !this.cyw43) return;
|
||||
for (const ev of this.cyw43Sniffer.feedWord(word)) {
|
||||
if (ev.kind === 'payload') {
|
||||
const reply = this.cyw43.onCommand(ev.cmd, ev.payload);
|
||||
const reply = this.cyw43.onCommand(ev.cmd, ev.payload, ev.readBytes);
|
||||
if (reply && reply.length > 0) this.queueCyw43Reply(reply);
|
||||
}
|
||||
}
|
||||
// Drain queued reply words into any state machine that has space.
|
||||
this.drainCyw43RxIntoSomeSM();
|
||||
}
|
||||
|
||||
private queueCyw43Reply(reply: Uint8Array): void {
|
||||
|
|
@ -497,23 +510,6 @@ export class RP2040Simulator {
|
|||
}
|
||||
}
|
||||
|
||||
private drainCyw43RxIntoSomeSM(): void {
|
||||
if (!this.rp2040 || this.cyw43RxQueue.length === 0) return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pios: any[] = (this.rp2040 as any).pio;
|
||||
for (const pio of pios) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
for (const sm of pio.machines as any[]) {
|
||||
const rx = sm.rxFIFO;
|
||||
if (!rx) continue;
|
||||
while (this.cyw43RxQueue.length > 0 && !rx.full) {
|
||||
rx.push(this.cyw43RxQueue.shift());
|
||||
}
|
||||
if (this.cyw43RxQueue.length === 0) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getADC(): any {
|
||||
return this.rp2040?.adc ?? null;
|
||||
|
|
|
|||
|
|
@ -88,15 +88,39 @@ export class Cyw43Emulator {
|
|||
|
||||
// ── Bus state ────────────────────────────────────────────────────
|
||||
private f0Regs = new Uint32Array(16);
|
||||
private clockCsr = ClockCsr.ALP_AVAIL;
|
||||
// Report both ALP and HT clocks available from the start. The driver polls
|
||||
// ALP early (after requesting it) and HT after releasing the WLAN core
|
||||
// (without re-requesting) — a behavioural chip can just have both ready.
|
||||
private clockCsr = ClockCsr.ALP_AVAIL | ClockCsr.HT_AVAIL;
|
||||
private readTestPrimed = false;
|
||||
private f1Window = 0;
|
||||
/**
|
||||
* gSPI word-order regime. The chip powers up in 16-bit little-endian: the
|
||||
* driver drives all register access through read_reg_u32_swap /
|
||||
* write_reg_u32_swap (which apply SWAP32) until it writes SPI_BUS_CONTROL
|
||||
* (F0 0x00) with WORD_LENGTH_32 | ENDIAN_BIG, after which it switches to
|
||||
* 32-bit big-endian and stops swapping. We must mirror both regimes when we
|
||||
* encode read responses (and, later, when decoding commands/write-data).
|
||||
* See project/picow-wifi-emulation/research/01-gspi-bus-protocol.md §3.
|
||||
*/
|
||||
private bigEndian = false;
|
||||
private busControl = 0;
|
||||
private _rtN = 0;
|
||||
/** F2 channel usable. We have no real firmware to boot, so it's always ready. */
|
||||
private f2Ready = true;
|
||||
/** Echo store for direct F1 registers the driver writes-then-reads. */
|
||||
private f1Mem = new Map<number, number>();
|
||||
/** Flips true on the first read of F0:0x14. Available via isChipReady() for diagnostics. */
|
||||
private chipReady = false;
|
||||
|
||||
// ── Sequence counters (host expects monotonic) ──────────────────
|
||||
private hostToChipSeq = 0;
|
||||
private chipToHostSeq = 0;
|
||||
// SDPCM bus flow-control: the host may only transmit F2 data while the
|
||||
// chip-advertised credit is ahead of its transmit sequence number. We keep
|
||||
// the credit a window beyond the number of host frames we've consumed.
|
||||
private hostTxFrames = 0;
|
||||
private static readonly CREDIT_WINDOW = 4;
|
||||
|
||||
// ── WiFi state ──────────────────────────────────────────────────
|
||||
private linkState: LinkState = 'down';
|
||||
|
|
@ -121,6 +145,27 @@ export class Cyw43Emulator {
|
|||
this.staMac = opts.staMac ?? DEFAULT_STA_MAC;
|
||||
// F2 ready at boot — driver tolerates this being true early.
|
||||
this.f0Regs[F0.F2_INFO >> 2] = 0x01;
|
||||
// Send an initial SDPCM control frame so the host's first F2 poll grants it
|
||||
// bus credits (otherwise its very first IOCTL stalls forever and wifi_on
|
||||
// times out). Content is a benign empty control frame; only the credit
|
||||
// byte matters here.
|
||||
this.pushFrame(encodeSdpcm({
|
||||
channel: SdpcmChannel.CONTROL,
|
||||
sequence: this.chipToHostSeq++ & 0xff,
|
||||
payload: new Uint8Array(CDC_HEADER_LEN),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a chip→host SDPCM frame, stamping the bus-credit byte so the host
|
||||
* stays un-stalled, and raise the F2 packet-available interrupt.
|
||||
*/
|
||||
private pushFrame(sdpcm: Uint8Array): void {
|
||||
if (sdpcm.length > 9) {
|
||||
sdpcm[9] = (this.hostTxFrames + Cyw43Emulator.CREDIT_WINDOW) & 0xff;
|
||||
}
|
||||
this.inboundEvents.push(sdpcm);
|
||||
this.f0Regs[F0.INTERRUPT >> 2] |= 0x40;
|
||||
}
|
||||
|
||||
// ── Listener registration ───────────────────────────────────────
|
||||
|
|
@ -154,11 +199,15 @@ export class Cyw43Emulator {
|
|||
* payload carries data the driver wrote; for RD commands the chip
|
||||
* returns a Uint8Array of length ``cmd.length``.
|
||||
*/
|
||||
onCommand(cmd: Cyw43Cmd, payload: Uint8Array): Uint8Array | null {
|
||||
if (cmd.function === 0) return this.handleF0(cmd, payload);
|
||||
if (cmd.function === 1) return this.handleF1(cmd, payload);
|
||||
if (cmd.function === 2) return this.handleF2(cmd, payload);
|
||||
return cmd.write ? null : new Uint8Array(cmd.length);
|
||||
onCommand(cmd: Cyw43Cmd, payload: Uint8Array, readBytes?: number): Uint8Array | null {
|
||||
// readBytes = bytes the host will clock back (from the PIO in-count word).
|
||||
// It can exceed cmd.length for backplane reads (response-delay padding), so
|
||||
// the response must be sized by it, not by the command's length field.
|
||||
const rx = readBytes ?? cmd.length;
|
||||
if (cmd.function === 0) return this.handleF0(cmd, payload, rx);
|
||||
if (cmd.function === 1) return this.handleF1(cmd, payload, rx);
|
||||
if (cmd.function === 2) return this.handleF2(cmd, payload, rx);
|
||||
return cmd.write ? null : new Uint8Array(rx);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,16 +221,37 @@ export class Cyw43Emulator {
|
|||
sequence: this.chipToHostSeq++ & 0xff,
|
||||
payload: ether,
|
||||
});
|
||||
this.inboundEvents.push(sdpcm);
|
||||
// Light up the F2 packet-available bit so the driver polls.
|
||||
this.f0Regs[F0.INTERRUPT >> 2] |= 0x40;
|
||||
this.pushFrame(sdpcm);
|
||||
}
|
||||
|
||||
// ── F0: gSPI bus control ────────────────────────────────────────
|
||||
|
||||
private handleF0(cmd: Cyw43Cmd, payload: Uint8Array): Uint8Array | null {
|
||||
/**
|
||||
* Encode a 32-bit register value into the RX-FIFO word so that, after the
|
||||
* host's DMA byte-swap (+ SWAP32 in boot mode), the driver recovers `value`.
|
||||
* Calibrated against the real READ_TEST handshake (boot mode must yield
|
||||
* 0xFEEDBEAD). queueReply packs these bytes little-endian back into the word.
|
||||
*/
|
||||
private encodeReadWord(value: number): number {
|
||||
// Boot (16-bit-LE, driver SWAP32s): chip drives the halfword-swapped value.
|
||||
// 32-BE (after SPI_BUS_CONTROL): big-endian, so the value is byte-reversed
|
||||
// (symmetric with the bswap32 command decode). Validated: the clock-CSR
|
||||
// ALP/HT bits land in the byte the driver checks.
|
||||
return this.bigEndian ? bswap32(value >>> 0) : swap16(value >>> 0);
|
||||
}
|
||||
|
||||
/** True once the driver has switched the bus to 32-bit big-endian. */
|
||||
isBigEndian(): boolean { return this.bigEndian; }
|
||||
|
||||
private handleF0(cmd: Cyw43Cmd, payload: Uint8Array, rxBytes: number): Uint8Array | null {
|
||||
if (cmd.write) {
|
||||
const word = readU32LE(payload, 0);
|
||||
// SPI_BUS_CONTROL (F0 0x00) write flips the chip out of the 16-bit-LE
|
||||
// boot regime into 32-bit big-endian. After this, no more SWAP32.
|
||||
if (cmd.address === F0.BUS_CTRL) {
|
||||
this.busControl = word;
|
||||
this.bigEndian = true;
|
||||
}
|
||||
const idx = cmd.address >>> 2;
|
||||
if (idx >= 0 && idx < this.f0Regs.length) this.f0Regs[idx] = word;
|
||||
if (cmd.address === F0.RESET_BP) this.f1Window = 0;
|
||||
|
|
@ -191,24 +261,53 @@ export class Cyw43Emulator {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
const out = new Uint8Array(cmd.length);
|
||||
// F0 (bus) reads have no response-delay padding: the value is the first
|
||||
// word the host clocks back.
|
||||
const out = new Uint8Array(Math.max(4, rxBytes));
|
||||
let value = 0;
|
||||
if (cmd.address === F0.READ_TEST) {
|
||||
const value = this.readTestPrimed ? TEST_PATTERN : 0;
|
||||
// READ_TEST is only read during the boot (16-bit-LE) resync. If the
|
||||
// driver's bus_init do/while retried, the chip is back in boot mode, so
|
||||
// reset the regime flag (otherwise the retry's swapped commands decode
|
||||
// as 32-BE and turn to garbage).
|
||||
this.bigEndian = false;
|
||||
// First read returns 0 (chip not ready yet), then the magic — models the
|
||||
// real chip and exercises the driver's retry loop.
|
||||
value = this.readTestPrimed ? TEST_PATTERN : 0;
|
||||
this.readTestPrimed = true;
|
||||
writeU32LE(out, 0, value);
|
||||
this.chipReady = true;
|
||||
} else if (cmd.address === F0.BUS_CTRL) {
|
||||
value = this.busControl;
|
||||
} else if (cmd.address === F0.STATUS) {
|
||||
value = this.spiStatus();
|
||||
} else if (cmd.address === F0.INTR_STATUS) {
|
||||
// SPI_INTERRUPT_REGISTER (16-bit): report ONLY F2_PACKET_AVAILABLE when a
|
||||
// frame is queued — never the error bits (COMMAND_ERROR/DATA_ERROR/
|
||||
// F1_OVERFLOW), which would send the driver into an error-recovery loop.
|
||||
value = this.inboundEvents.length > 0 ? 0x20 : 0;
|
||||
} else {
|
||||
const idx = cmd.address >>> 2;
|
||||
if (idx >= 0 && idx < this.f0Regs.length) {
|
||||
writeU32LE(out, 0, this.f0Regs[idx]);
|
||||
}
|
||||
if (idx >= 0 && idx < this.f0Regs.length) value = this.f0Regs[idx];
|
||||
}
|
||||
writeU32LE(out, 0, this.encodeReadWord(value));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** SPI_STATUS_REGISTER (F0 0x08): advertise F2 ready / packet-available. */
|
||||
private spiStatus(): number {
|
||||
let s = 0;
|
||||
if (this.f2Ready) s |= 0x20; // STATUS_F2_RX_READY
|
||||
if (this.inboundEvents.length > 0) {
|
||||
// F2 packet available + its length in STATUS_F2_PKT_LEN (shift 9).
|
||||
const len = this.inboundEvents[0].length & 0x7ff;
|
||||
s |= 0x100 | (len << 9);
|
||||
}
|
||||
return s >>> 0;
|
||||
}
|
||||
|
||||
// ── F1: backplane window ────────────────────────────────────────
|
||||
|
||||
private handleF1(cmd: Cyw43Cmd, payload: Uint8Array): Uint8Array | null {
|
||||
private handleF1(cmd: Cyw43Cmd, payload: Uint8Array, rxBytes: number): Uint8Array | null {
|
||||
if (cmd.write) {
|
||||
if (cmd.address === F1.SDIO_BACKPLANE_ADDRESS_LOW) {
|
||||
this.f1Window = (this.f1Window & 0xffff00) | (payload[0] ?? 0);
|
||||
|
|
@ -220,28 +319,61 @@ export class Cyw43Emulator {
|
|||
const requested = payload[0] ?? 0;
|
||||
if (requested & ClockCsr.ALP_AVAIL_REQ) this.clockCsr |= ClockCsr.ALP_AVAIL;
|
||||
if (requested & ClockCsr.HT_AVAIL_REQ) this.clockCsr |= ClockCsr.HT_AVAIL;
|
||||
} else if (cmd.address >= 0x10000 && cmd.address <= 0x1ffff) {
|
||||
// Direct F1 register (sleep/wakeup CSR, watermark, frame control, …):
|
||||
// remember the written byte so the driver's write-then-poll succeeds.
|
||||
this.f1Mem.set(cmd.address, payload[0] ?? 0);
|
||||
}
|
||||
// Auto-increment window pointer for sequential streaming (firmware,
|
||||
// NVRAM, CLM blob). We don't store any of those bytes — the host
|
||||
// driver never reads them back.
|
||||
if (cmd.increment) this.f1Window += cmd.length;
|
||||
if (cmd.increment) this.f1Window += payload.length;
|
||||
return null;
|
||||
}
|
||||
|
||||
const out = new Uint8Array(cmd.length);
|
||||
// Backplane (F1) reads carry a response-delay pad: the host reads the data
|
||||
// word LAST (buf32[index-1]), so the value goes in the final 4 bytes.
|
||||
let value = 0;
|
||||
if (cmd.address === F1.SDIO_CHIP_CLOCK_CSR) {
|
||||
out[0] = this.clockCsr & 0xff;
|
||||
// ALP requested on first poll, HT once the WLAN core is released. Both
|
||||
// are reported available so the poll loops (x10 / x1000) pass fast.
|
||||
value = this.clockCsr & 0xff;
|
||||
} else if (cmd.address === F1.SDIO_INT_STATUS) {
|
||||
// Signal "F2 packet available" if we have queued frames for the host.
|
||||
if (this.inboundEvents.length > 0) writeU32LE(out, 0, 0x40);
|
||||
if (this.inboundEvents.length > 0) value = 0x40;
|
||||
} else if (cmd.address === 0x1001f) {
|
||||
// SDIO_SLEEP_CSR: echo back with DeviceOn set so the wake poll passes.
|
||||
value = (this.f1Mem.get(0x1001f) ?? 0) | 0x02;
|
||||
} else if (cmd.address >= 0x10000 && cmd.address <= 0x1ffff) {
|
||||
value = this.f1Mem.get(cmd.address) ?? 0;
|
||||
} else if (this.f1ReadValue) {
|
||||
value = this.f1ReadValue(this.f1Window);
|
||||
} else {
|
||||
// Windowed backplane read (core wrappers / chipcommon / RAM). The driver
|
||||
// checks each core is clocked + out of reset via the AI wrapper:
|
||||
// +0x408 AI_IOCTRL -> must read SICF_CLOCK_EN(0x1), FGC(0x2) clear
|
||||
// +0x800 AI_RESETCTRL -> must read AIRC_RESET(0x1) clear
|
||||
const off = cmd.address & 0xfff;
|
||||
if (off === 0x408) value = 0x01;
|
||||
else if (off === 0x800) value = 0x00;
|
||||
else value = 0;
|
||||
}
|
||||
if (cmd.increment) this.f1Window += cmd.length;
|
||||
return this.backplaneRead(value, rxBytes);
|
||||
}
|
||||
|
||||
/** Optional hook for chip-ID / backplane-memory reads (set by tests). */
|
||||
private f1ReadValue: ((addr: number) => number) | null = null;
|
||||
|
||||
/** Build an F1 read response of rxBytes with the value word at the end. */
|
||||
private backplaneRead(value: number, rxBytes: number): Uint8Array {
|
||||
const n = Math.max(4, rxBytes);
|
||||
const out = new Uint8Array(n);
|
||||
writeU32LE(out, n - 4, this.encodeReadWord(value >>> 0));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── F2: SDPCM frame channel ─────────────────────────────────────
|
||||
|
||||
private handleF2(cmd: Cyw43Cmd, _payload: Uint8Array): Uint8Array | null {
|
||||
private handleF2(cmd: Cyw43Cmd, _payload: Uint8Array, _rxBytes: number): Uint8Array | null {
|
||||
if (cmd.write) {
|
||||
const frame = decodeSdpcm(_payload);
|
||||
if (frame) this.handleHostFrame(frame.channel, frame.payload);
|
||||
|
|
@ -266,6 +398,8 @@ export class Cyw43Emulator {
|
|||
// ── Host → chip frame dispatch ──────────────────────────────────
|
||||
|
||||
private handleHostFrame(channel: number, payload: Uint8Array): void {
|
||||
// Count every host F2 frame so the credit we advertise stays ahead.
|
||||
this.hostTxFrames++;
|
||||
if (channel === SdpcmChannel.CONTROL) {
|
||||
this.handleIoctl(payload);
|
||||
} else if (channel === SdpcmChannel.DATA) {
|
||||
|
|
@ -382,8 +516,7 @@ export class Cyw43Emulator {
|
|||
sequence: this.chipToHostSeq++ & 0xff,
|
||||
payload: replyCdc,
|
||||
});
|
||||
this.inboundEvents.push(sdpcm);
|
||||
this.f0Regs[F0.INTERRUPT >> 2] |= 0x40;
|
||||
this.pushFrame(sdpcm);
|
||||
}
|
||||
|
||||
// ── Concrete IOCTL handlers ─────────────────────────────────────
|
||||
|
|
@ -480,8 +613,7 @@ export class Cyw43Emulator {
|
|||
payload,
|
||||
this.staMac,
|
||||
);
|
||||
this.inboundEvents.push(frame);
|
||||
this.f0Regs[F0.INTERRUPT >> 2] |= 0x40;
|
||||
this.pushFrame(frame);
|
||||
}
|
||||
|
||||
private hasAnyMaskBitsSet(): boolean {
|
||||
|
|
@ -520,6 +652,17 @@ function safe<T>(cb: (ev: T) => void, ev: T): void {
|
|||
try { cb(ev); } catch { /* harness — never throw across listener */ }
|
||||
}
|
||||
|
||||
/** Reverse the 4 bytes of a 32-bit word (mirrors the host DMA's bswap). */
|
||||
function bswap32(x: number): number {
|
||||
const v = x >>> 0;
|
||||
return (((v & 0xff) << 24) | ((v & 0xff00) << 8) | ((v >>> 8) & 0xff00) | (v >>> 24)) >>> 0;
|
||||
}
|
||||
/** Swap the two 16-bit halfwords (mirrors the driver's SWAP32/__swap16x2). */
|
||||
function swap16(x: number): number {
|
||||
const v = x >>> 0;
|
||||
return (((v & 0xffff) << 16) | (v >>> 16)) >>> 0;
|
||||
}
|
||||
|
||||
function readU32LE(buf: Uint8Array, off: number): number {
|
||||
return ((buf[off] | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24)) >>> 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ export function swap16x2(word: number): number {
|
|||
return (((w & 0xffff) << 16) | ((w >>> 16) & 0xffff)) >>> 0;
|
||||
}
|
||||
|
||||
/** Reverse the 4 bytes of a word (32-bit big-endian regime). */
|
||||
export function bswap32be(word: number): number {
|
||||
const w = word >>> 0;
|
||||
return (((w & 0xff) << 24) | ((w & 0xff00) << 8) | ((w >>> 8) & 0xff00) | (w >>> 24)) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The driver shifts data as 32-bit words via PIO `out pins, 1` over
|
||||
* 32 cycles per word. This streamer accepts whole 32-bit words pulled
|
||||
|
|
@ -76,48 +82,129 @@ export function swap16x2(word: number): number {
|
|||
*/
|
||||
export type SnifferEvent =
|
||||
| { kind: 'header'; cmd: Cyw43Cmd }
|
||||
| { kind: 'payload'; cmd: Cyw43Cmd; payload: Uint8Array };
|
||||
| { kind: 'payload'; cmd: Cyw43Cmd; payload: Uint8Array; readBytes: number };
|
||||
|
||||
/**
|
||||
* Transfer-aware decoder for the real cyw43_bus_pio_spi protocol.
|
||||
*
|
||||
* Per cyw43_spi_transfer() (pico-sdk), every transfer pushes this exact
|
||||
* sequence of 32-bit words into the PIO TX FIFO:
|
||||
*
|
||||
* [ tx_length*8 - 1 ] // OUT X,32 — bits to shift OUT
|
||||
* [ (rx_length - tx_length)*8 - 1 ] // OUT Y,32 — bits to shift IN (0 for write)
|
||||
* [ command word ] // DMA, byte-swapped
|
||||
* [ write_data... ] // DMA, only for writes (tx_length > 4)
|
||||
*
|
||||
* The first two words are PIO loop counters consumed by manual OUT X/Y
|
||||
* execs — they are NOT bus traffic. Read responses come back on the RX
|
||||
* FIFO, not TX, so a READ transfer puts only [count][count][cmd] on TX.
|
||||
*
|
||||
* The old decoder treated every first word as a command header, which
|
||||
* mis-decoded the count word (e.g. 31 -> "RD F0 0x3e0") and desynced the
|
||||
* whole stream, so the driver's bus-control writes never landed and
|
||||
* cyw43_ll_bus_init() failed with EPERM.
|
||||
*
|
||||
* NOTE on swaps: the count words arrive raw (pio_sm_put, no DMA bswap);
|
||||
* the command/data words arrive byte-swapped by the DMA, which combined
|
||||
* with the PIO halfword order is undone by swap16x2() (verified against a
|
||||
* real boot: the READ_TEST 0x14 header decodes correctly this way).
|
||||
*/
|
||||
type DecodeState = 'outCount' | 'inCount' | 'cmd' | 'writeData';
|
||||
|
||||
export class PioBusSniffer {
|
||||
private pendingCmd: Cyw43Cmd | null = null;
|
||||
private pendingPayload: number[] = [];
|
||||
private state: DecodeState = 'outCount';
|
||||
private outBytes = 0;
|
||||
private readBytes = 0;
|
||||
private wordsLeft = 0;
|
||||
private cmd: Cyw43Cmd | null = null;
|
||||
private writeBuf: number[] = [];
|
||||
|
||||
/**
|
||||
* Word-order regime provider. Boot (16-bit-LE) commands+data are SWAP32'd by
|
||||
* the driver and recovered with swap16(); after SPI_BUS_CONTROL the chip is
|
||||
* 32-bit big-endian and the words arrive un-swapped. The emulator owns this
|
||||
* state (it sees the SPI_BUS_CONTROL write); we read it at decode time.
|
||||
*/
|
||||
private bigEndian: () => boolean = () => false;
|
||||
setModeProvider(fn: () => boolean): void { this.bigEndian = fn; }
|
||||
|
||||
/** Apply the regime-appropriate de-swap to a command/data word. */
|
||||
private deswap(raw: number): number {
|
||||
// Boot (16-bit-LE, driver SWAP32s the command) → swap16x2 recovers it.
|
||||
// 32-BE (after SPI_BUS_CONTROL) → the bus is big-endian, so the word is
|
||||
// byte-reversed → bswap32 recovers it.
|
||||
return this.bigEndian() ? bswap32be(raw) : swap16x2(raw);
|
||||
}
|
||||
|
||||
/** Reset framing — call when the SM/transfer machinery is restarted. */
|
||||
reset(): void {
|
||||
this.state = 'outCount';
|
||||
this.cmd = null;
|
||||
this.writeBuf = [];
|
||||
}
|
||||
|
||||
/** Debug hook (tests): fired with each count word's raw value + decode. */
|
||||
onCount: ((kind: 'out' | 'in', raw: number, bytes: number) => void) | null = null;
|
||||
|
||||
*feedWord(rawWord: number): Generator<SnifferEvent> {
|
||||
const word = swap16x2(rawWord);
|
||||
const raw = rawWord >>> 0;
|
||||
|
||||
if (this.pendingCmd === null) {
|
||||
// First word of a transaction is always the header.
|
||||
const cmd = decodeHeader(word);
|
||||
this.pendingCmd = cmd;
|
||||
this.pendingPayload = [];
|
||||
yield { kind: 'header', cmd };
|
||||
|
||||
// Zero-length transactions exist (some F0 reads) — emit and reset.
|
||||
if (cmd.length === 0) {
|
||||
yield {
|
||||
kind: 'payload',
|
||||
cmd,
|
||||
payload: new Uint8Array(0),
|
||||
};
|
||||
this.pendingCmd = null;
|
||||
switch (this.state) {
|
||||
case 'outCount': {
|
||||
// count1 = tx_length*8 - 1, with tx_length a multiple of 4 (cmd + data,
|
||||
// all 4-aligned) and tx_length <= 2048+4. Any word that doesn't fit that
|
||||
// is NOT a transfer start — it's a stray word (rp2040js pushes one extra
|
||||
// after a large write) or firmware-download data (which we discard). Skip
|
||||
// it and stay in outCount, so the framing self-heals at the next real
|
||||
// transfer. This also fast-paths the ~224 KB firmware stream: every data
|
||||
// word fails validation and is skipped until the post-download HT-clock
|
||||
// poll re-syncs us.
|
||||
const ob = (raw + 1) / 8;
|
||||
if (raw === 0 || ((raw + 1) & 31) !== 0 || ob > 2052) return;
|
||||
this.outBytes = ob;
|
||||
this.onCount?.('out', raw, this.outBytes);
|
||||
this.state = 'inCount';
|
||||
return;
|
||||
}
|
||||
case 'inCount': {
|
||||
// (rx_length - tx_length)*8 - 1 -> bytes the chip drives back.
|
||||
// A write pushes literal 0 here (no read phase).
|
||||
this.readBytes = raw === 0 ? 0 : Math.round((raw + 1) / 8);
|
||||
this.onCount?.('in', raw, this.readBytes);
|
||||
// outBytes/4 words follow: word 0 = command, rest = write data.
|
||||
this.wordsLeft = Math.max(1, Math.round(this.outBytes / 4));
|
||||
this.writeBuf = [];
|
||||
this.cmd = null;
|
||||
this.state = 'cmd';
|
||||
return;
|
||||
}
|
||||
case 'cmd': {
|
||||
const cmd = decodeHeader(this.deswap(raw));
|
||||
this.cmd = cmd;
|
||||
this.wordsLeft -= 1;
|
||||
yield { kind: 'header', cmd };
|
||||
if (this.wordsLeft <= 0) {
|
||||
// Command-only transfer: a read, or a write with no data word.
|
||||
yield { kind: 'payload', cmd, payload: new Uint8Array(0), readBytes: this.readBytes };
|
||||
this.state = 'outCount';
|
||||
} else {
|
||||
this.state = 'writeData';
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'writeData': {
|
||||
const w = this.deswap(raw);
|
||||
// Write data is little-endian within the de-swapped word.
|
||||
for (let i = 0; i < 4; i++) this.writeBuf.push((w >>> (i * 8)) & 0xff);
|
||||
this.wordsLeft -= 1;
|
||||
if (this.wordsLeft <= 0) {
|
||||
const dataLen = Math.max(0, this.outBytes - 4);
|
||||
const payload = new Uint8Array(this.writeBuf.slice(0, dataLen));
|
||||
yield { kind: 'payload', cmd: this.cmd!, payload, readBytes: this.readBytes };
|
||||
this.state = 'outCount';
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Payload follows. Each 32-bit word carries 4 payload bytes,
|
||||
// little-endian on the wire (the PIO halfword-swap above already
|
||||
// converted to "host byte order"; payload bytes are then byte-LSB
|
||||
// first per pico-sdk's WORD swap macro on writes).
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.pendingPayload.push((word >>> (i * 8)) & 0xff);
|
||||
if (this.pendingPayload.length >= this.pendingCmd.length) break;
|
||||
}
|
||||
|
||||
if (this.pendingPayload.length >= this.pendingCmd.length) {
|
||||
const payload = new Uint8Array(this.pendingPayload.slice(0, this.pendingCmd.length));
|
||||
yield { kind: 'payload', cmd: this.pendingCmd, payload };
|
||||
this.pendingCmd = null;
|
||||
this.pendingPayload = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue