Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/tec1g-emulation-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/tec1g/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
85 changes: 84 additions & 1 deletion src/platforms/tec1g/sd-spi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -111,6 +120,7 @@ export class SdSpi {
this.appCommand = false;
this.pendingResponse = null;
this.delayBytes = 0;
this.writeState = null;
this.csActive = true;
}

Expand All @@ -126,13 +136,20 @@ export class SdSpi {
this.appCommand = false;
this.pendingResponse = null;
this.delayBytes = 0;
this.writeState = null;
}

private shiftIn(bit: number): void {
this.inShift = ((this.inShift << 1) | (bit & 0x01)) & 0xff;
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;
Expand Down Expand Up @@ -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;
Expand All @@ -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<number>(512).fill(0x00);
if (!this.image || this.image.length === 0) {
return payload;
Expand All @@ -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;
}
}
34 changes: 34 additions & 0 deletions tests/platforms/tec1g/sd-spi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
});
});
Loading