fix(cyw43): word-align SDPCM frames so the F2 byte-swap preserves the tail
The CYW43439 F2 (radio frame) channel is word-oriented: the real chip always drives frames padded up to a 4-byte boundary and the host reads that word-aligned length, byte-swapping every 32-bit word on the way in. encodeSdpcm built buffers of exactly 12 + payload bytes, so any frame whose total length was not a multiple of 4 ended with a partial word. The emulator's F2 read path (encodeFrameWords) byte-swaps whole words and copies the leftover tail raw; the host's symmetric per-word swap then mangles that final word, corrupting the last 1-3 bytes of the frame. This was invisible for DHCP/ARP (UDP checksum 0 -> lwIP skips the check, and the damage lands in trailing option padding) but silently dropped every DNS answer and TCP segment (real checksum -> lwIP discards the frame), so getaddrinfo()/connect() retried forever. Pad the backing buffer to a 4-byte boundary while keeping the size header at the true length, so the driver still parses exactly the real frame and ignores the pad. Matches real hardware framing.
This commit is contained in:
parent
6345ba5fab
commit
2639f80a22
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* cyw43-sdpcm-align.test.ts
|
||||
*
|
||||
* Regression test for the gSPI word-alignment bug that silently dropped
|
||||
* DNS/TCP replies on the emulated Pico W.
|
||||
*
|
||||
* The CYW43439 F2 (radio frame) channel is word-oriented: the chip always
|
||||
* drives frames padded up to a 4-byte boundary and the host reads that
|
||||
* word-aligned length, byte-swapping every 32-bit word on the way in. If the
|
||||
* emulator emits an SDPCM frame whose backing buffer length is NOT a multiple
|
||||
* of 4, the host's symmetric per-word swap mangles the final partial word —
|
||||
* corrupting the last 1-3 bytes of the Ethernet frame.
|
||||
*
|
||||
* That went unnoticed for DHCP/ARP (UDP checksum 0 -> lwIP skips the check,
|
||||
* and the damage lands in trailing option padding) but quietly killed every
|
||||
* DNS answer and TCP segment (real checksum -> lwIP discards the frame ->
|
||||
* getaddrinfo()/connect() retry forever). `encodeSdpcm` now pads the buffer to
|
||||
* a word boundary while keeping the `size` header at the true length.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { encodeSdpcm, decodeSdpcm, SDPCM_HEADER_LEN } from '../simulation/cyw43/sdpcm';
|
||||
import { SdpcmChannel } from '../simulation/cyw43/constants';
|
||||
|
||||
describe('SDPCM word alignment', () => {
|
||||
// Payload lengths that drive every total-length residue mod 4 once the
|
||||
// 12-byte header is added (12 is itself a multiple of 4).
|
||||
for (const payloadLen of [1, 2, 3, 4, 113, 125, 129, 314]) {
|
||||
it(`pads a ${payloadLen}-byte payload to a 4-byte boundary`, () => {
|
||||
const payload = new Uint8Array(payloadLen);
|
||||
for (let i = 0; i < payloadLen; i++) payload[i] = (i * 7 + 1) & 0xff;
|
||||
const frame = encodeSdpcm({ channel: SdpcmChannel.DATA, sequence: 5, payload });
|
||||
|
||||
// The backing buffer the chip drives MUST be word-aligned.
|
||||
expect(frame.length % 4).toBe(0);
|
||||
|
||||
// The `size` header stays the TRUE (unpadded) length so the driver
|
||||
// parses exactly the real frame and ignores the pad bytes.
|
||||
const trueSize = SDPCM_HEADER_LEN + payloadLen;
|
||||
const size = frame[0] | (frame[1] << 8);
|
||||
expect(size).toBe(trueSize);
|
||||
expect(frame[2] | (frame[3] << 8)).toBe(~trueSize & 0xffff);
|
||||
|
||||
// decode recovers the exact payload, last byte intact.
|
||||
const decoded = decodeSdpcm(frame);
|
||||
expect(decoded).not.toBeNull();
|
||||
expect(Array.from(decoded!.payload)).toEqual(Array.from(payload));
|
||||
});
|
||||
}
|
||||
|
||||
it('survives a full per-word byte-swap round-trip with the last byte intact', () => {
|
||||
// The real DNS reply payload length the bug bit on: 4-byte BDC + 125-byte
|
||||
// Ethernet -> total 141, residue 1 mod 4. The last byte (0xf3, tail of the
|
||||
// second A-record IP) used to be lost.
|
||||
const payload = new Uint8Array(129);
|
||||
payload[128] = 0xf3;
|
||||
const frame = encodeSdpcm({ channel: SdpcmChannel.DATA, sequence: 0x2d, payload });
|
||||
expect(frame.length).toBe(144); // 141 padded up to 144
|
||||
|
||||
// Model the gSPI path: the chip byte-swaps every 32-bit word, the host
|
||||
// byte-swaps them back. With a word-aligned buffer this is lossless.
|
||||
const swap = (b: Uint8Array) => {
|
||||
const out = new Uint8Array(b.length);
|
||||
for (let i = 0; i < b.length; i += 4) {
|
||||
out[i] = b[i + 3]; out[i + 1] = b[i + 2]; out[i + 2] = b[i + 1]; out[i + 3] = b[i];
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const roundTripped = swap(swap(frame));
|
||||
const decoded = decodeSdpcm(roundTripped);
|
||||
expect(decoded).not.toBeNull();
|
||||
expect(decoded!.payload[128]).toBe(0xf3);
|
||||
expect(Array.from(decoded!.payload)).toEqual(Array.from(payload));
|
||||
});
|
||||
});
|
||||
|
|
@ -42,11 +42,21 @@ export interface SdpcmFrame {
|
|||
|
||||
/** Build an SDPCM frame for a given channel. */
|
||||
export function encodeSdpcm(opts: SdpcmFrame): Uint8Array {
|
||||
const total = SDPCM_HEADER_LEN + opts.payload.length;
|
||||
const size = SDPCM_HEADER_LEN + opts.payload.length;
|
||||
// gSPI / F2 is word-oriented: the real CYW43439 always drives frames padded
|
||||
// up to a 4-byte boundary, and the host reads that word-aligned length. The
|
||||
// emulator's F2 read path byte-swaps every 32-bit word (encodeFrameWords);
|
||||
// if the buffer length is NOT a multiple of 4 the final partial word gets
|
||||
// mangled by the host's symmetric swap, corrupting the last 1-3 bytes of the
|
||||
// frame. That goes unnoticed for DHCP/ARP (UDP checksum 0, trailing pad) but
|
||||
// silently drops DNS/TCP replies (real checksum -> lwIP discards). Pad the
|
||||
// backing buffer to a word boundary; the `size` field stays the true length
|
||||
// so the driver still parses exactly the real frame and ignores the pad.
|
||||
const total = (size + 3) & ~3;
|
||||
const buf = new Uint8Array(total);
|
||||
const dv = new DataView(buf.buffer);
|
||||
dv.setUint16(0, total, true);
|
||||
dv.setUint16(2, ~total & 0xffff, true);
|
||||
dv.setUint16(0, size, true);
|
||||
dv.setUint16(2, ~size & 0xffff, true);
|
||||
buf[4] = opts.sequence & 0xff;
|
||||
buf[5] = opts.channel & 0xff;
|
||||
buf[6] = 0; // next_length
|
||||
|
|
|
|||
Loading…
Reference in New Issue