diff --git a/docs/tec1g-emulation-review.md b/docs/tec1g-emulation-review.md index c986f13..c360f76 100644 --- a/docs/tec1g-emulation-review.md +++ b/docs/tec1g-emulation-review.md @@ -755,6 +755,7 @@ External add-on emulation. Fully independent of other stages. - [x] ACMD41 (SD_SEND_OP_COND): respond with R1 = 0x00 (ready) after N retries - [x] CMD58 (READ_OCR): respond with OCR register - [x] CMD17 (READ_SINGLE_BLOCK): respond with data token + 512 bytes from virtual image +- [x] CMD24 (WRITE_SINGLE_BLOCK): accept data token + 512 bytes and write into virtual image - [x] Unit tests: full initialization sequence (CMD0 -> CMD8 -> ACMD41 -> CMD58) #### 6C — SD card integration with runtime diff --git a/src/platforms/tec1g/README.md b/src/platforms/tec1g/README.md index 4f326d6..2a64b39 100644 --- a/src/platforms/tec1g/README.md +++ b/src/platforms/tec1g/README.md @@ -72,7 +72,7 @@ The TEC-1G panel can switch speed modes; the serial timing assumes FAST mode. - Bit 5: CAPS (caps lock — not yet decoded). - Bits 6-7: FF-D5/FF-D6 (reserved — not yet decoded). - `OUT 0xFC`: RTC DS1302 (bit-banged emulation). -- `OUT 0xFD`: SD card SPI (bit-banged emulation, SPI mode). +- `OUT 0xFD`: SD card SPI (bit-banged emulation, SPI mode; read + write single block). ## Serial (bitbang) - TX uses bit 6 on `OUT 0x01`; RX uses bit 7 on `IN 0x00` (mirrored on `IN 0x03`). diff --git a/src/platforms/tec1g/sd-spi.ts b/src/platforms/tec1g/sd-spi.ts index f5cb111..7fb7591 100644 --- a/src/platforms/tec1g/sd-spi.ts +++ b/src/platforms/tec1g/sd-spi.ts @@ -42,6 +42,15 @@ export class SdSpi { private appCommand = false; private initTries = 0; private ready = false; + private writeState: + | { + start: number; + awaitingToken: boolean; + buffer: Uint8Array; + index: number; + crcRemaining: number; + } + | null = null; /** * Creates a new SD SPI bit-bang helper. @@ -111,6 +120,7 @@ export class SdSpi { this.appCommand = false; this.pendingResponse = null; this.delayBytes = 0; + this.writeState = null; this.csActive = true; } @@ -126,6 +136,7 @@ export class SdSpi { this.appCommand = false; this.pendingResponse = null; this.delayBytes = 0; + this.writeState = null; } private shiftIn(bit: number): void { @@ -133,6 +144,12 @@ export class SdSpi { this.inBitIndex += 1; if (this.inBitIndex >= 8) { const byte = this.inShift & 0xff; + if (this.writeState) { + this.consumeWriteByte(byte); + this.inShift = 0; + this.inBitIndex = 0; + return; + } if (this.commandBytes.length === 0 && (byte & 0xc0) !== 0x40) { this.inShift = 0; this.inBitIndex = 0; @@ -248,6 +265,24 @@ export class SdSpi { this.delayBytes = 1; break; } + case 24: { + if (!this.ready) { + this.pendingResponse = [0x01]; + this.delayBytes = 1; + break; + } + const start = this.resolveAddress(command.arg); + this.writeState = { + start, + awaitingToken: true, + buffer: new Uint8Array(512), + index: 0, + crcRemaining: 0, + }; + this.pendingResponse = [0x00]; + this.delayBytes = 1; + break; + } default: { this.pendingResponse = [this.ready ? 0x00 : 0x01]; this.delayBytes = 1; @@ -256,9 +291,13 @@ export class SdSpi { } } + private resolveAddress(arg: number): number { + return this.highCapacity ? ((arg >>> 0) << 9) >>> 0 : (arg >>> 0); + } + private readBlock(arg: number): number[] { // SDHC uses block (LBA) addressing; SDSC uses byte addressing. - const start = this.highCapacity ? ((arg >>> 0) << 9) >>> 0 : (arg >>> 0); + const start = this.resolveAddress(arg); const payload = new Array(512).fill(0x00); if (!this.image || this.image.length === 0) { return payload; @@ -271,4 +310,48 @@ export class SdSpi { } return payload; } + + private consumeWriteByte(byte: number): void { + if (!this.writeState) { + return; + } + if (this.writeState.awaitingToken) { + if (byte === 0xfe) { + this.writeState.awaitingToken = false; + } + return; + } + if (this.writeState.index < 512) { + this.writeState.buffer[this.writeState.index] = byte & 0xff; + this.writeState.index += 1; + if (this.writeState.index >= 512) { + this.writeState.crcRemaining = 2; + } + return; + } + if (this.writeState.crcRemaining > 0) { + this.writeState.crcRemaining -= 1; + if (this.writeState.crcRemaining === 0) { + this.commitWrite(); + } + } + } + + private commitWrite(): void { + if (!this.writeState) { + return; + } + if (this.image && this.image.length > 0) { + const end = this.writeState.start + this.writeState.buffer.length; + for (let i = 0; i < this.writeState.buffer.length; i += 1) { + const idx = this.writeState.start + i; + if (idx >= 0 && idx < this.image.length && idx < end) { + this.image[idx] = this.writeState.buffer[i] ?? 0x00; + } + } + } + // Data response token: 0bxxx00101 = 0x05 (accepted). + this.enqueueResponse([0x05, 0xff]); + this.writeState = null; + } } diff --git a/tests/platforms/tec1g/sd-spi.test.ts b/tests/platforms/tec1g/sd-spi.test.ts index 087edcf..975081f 100644 --- a/tests/platforms/tec1g/sd-spi.test.ts +++ b/tests/platforms/tec1g/sd-spi.test.ts @@ -48,6 +48,15 @@ function sendCommand(spi: SdSpi, bytes: number[]): void { bytes.forEach((byte) => writeByte(spi, byte)); } +function writeDataBlock(spi: SdSpi, payload: Uint8Array): void { + writeByte(spi, 0xfe); + for (let i = 0; i < payload.length; i += 1) { + writeByte(spi, payload[i] ?? 0x00); + } + writeByte(spi, 0xff); + writeByte(spi, 0xff); +} + describe('SdSpi', () => { it('returns 0xff when chip-select is inactive', () => { const spi = new SdSpi({ csMask: CS_BIT }); @@ -144,4 +153,29 @@ describe('SdSpi', () => { expect(readByte(spi)).toBe(0x00); expect(readByte(spi)).toBe(0xa5); }); + + it('accepts CMD24 and writes a single data block', () => { + const image = new Uint8Array(1024); + const payload = new Uint8Array(512); + payload[0] = 0x12; + payload[1] = 0x34; + payload[2] = 0x56; + const spi = new SdSpi({ csMask: CS_BIT, image }); + writeSpi(spi, 0x00); + sendCommand(spi, [0x77, 0x00, 0x00, 0x00, 0x00, 0x65]); + readResponseByte(spi); + sendCommand(spi, [0x69, 0x40, 0x00, 0x00, 0x00, 0x77]); + readResponseByte(spi); + sendCommand(spi, [0x77, 0x00, 0x00, 0x00, 0x00, 0x65]); + readResponseByte(spi); + sendCommand(spi, [0x69, 0x40, 0x00, 0x00, 0x00, 0x77]); + readResponseByte(spi); + sendCommand(spi, [0x58, 0x00, 0x00, 0x00, 0x00, 0xff]); + expect(readResponseByte(spi)).toBe(0x00); + writeDataBlock(spi, payload); + expect(readResponseByte(spi)).toBe(0x05); + expect(image[0]).toBe(0x12); + expect(image[1]).toBe(0x34); + expect(image[2]).toBe(0x56); + }); });