aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/index.ts1
-rw-r--r--src/peripherals/spi.spec.ts222
-rw-r--r--src/peripherals/spi.ts129
-rw-r--r--src/utils/test-utils.ts17
4 files changed, 368 insertions, 1 deletions
diff --git a/src/index.ts b/src/index.ts
index 0a9335e..0795ab3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -34,3 +34,4 @@ export {
eepromConfig,
} from './peripherals/eeprom';
export * from './peripherals/twi';
+export { spiConfig, SPIConfig, SPITransferCallback, AVRSPI } from './peripherals/spi';
diff --git a/src/peripherals/spi.spec.ts b/src/peripherals/spi.spec.ts
new file mode 100644
index 0000000..1bb099f
--- /dev/null
+++ b/src/peripherals/spi.spec.ts
@@ -0,0 +1,222 @@
+import { CPU } from '../cpu/cpu';
+import { AVRSPI, spiConfig } from './spi';
+import { asmProgram, TestProgramRunner } from '../utils/test-utils';
+
+const FREQ_16MHZ = 16e6;
+
+// CPU registers
+const R17 = 17;
+const SREG = 95;
+
+// SPI Registers
+const SPCR = 0x4c;
+const SPSR = 0x4d;
+const SPDR = 0x4e;
+
+// Register bit names
+const SPR0 = 1;
+const SPR1 = 2;
+const CPOL = 4;
+const CPHA = 8;
+const MSTR = 0x10;
+const DORD = 0x20;
+const SPE = 0x40;
+const SPIE = 0x80;
+const WCOL = 0x40;
+const SPIF = 0x80;
+const SPI2X = 1;
+
+describe('SPI', () => {
+ it('should correctly calculate the frequency based on SPCR/SPST values', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+
+ // Values in this test are based on Table 19-5 in the datasheet, page 177:
+ // http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
+
+ // Standard SPI speed:
+ cpu.writeData(SPSR, 0);
+ cpu.writeData(SPCR, 0);
+ expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 4);
+ cpu.writeData(SPCR, SPR0);
+ expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 16);
+ cpu.writeData(SPCR, SPR1);
+ expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 64);
+ cpu.writeData(SPCR, SPR1 | SPR0);
+ expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 128);
+
+ // Double SPI speed:
+ cpu.writeData(SPSR, SPI2X);
+ cpu.writeData(SPCR, 0);
+ expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 2);
+ cpu.writeData(SPCR, SPR0);
+ expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 8);
+ cpu.writeData(SPCR, SPR1);
+ expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 32);
+ cpu.writeData(SPCR, SPR1 | SPR0);
+ expect(spi.spiFrequency).toEqual(FREQ_16MHZ / 64);
+ });
+
+ it('should correctly report the data order (MSB/LSB first), based on SPCR value', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+
+ cpu.writeData(SPCR, 0);
+ expect(spi.dataOrder).toBe('msbFirst');
+
+ cpu.writeData(SPCR, DORD);
+ expect(spi.dataOrder).toBe('lsbFirst');
+ });
+
+ it('should correctly report the SPI mode, based on SPCR value', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+
+ // Values in this test are based on Table 2 in the datasheet, page 174.
+ cpu.writeData(SPCR, 0);
+ expect(spi.spiMode).toBe(0);
+
+ cpu.writeData(SPCR, CPHA);
+ expect(spi.spiMode).toBe(1);
+
+ cpu.writeData(SPCR, CPOL);
+ expect(spi.spiMode).toBe(2);
+
+ cpu.writeData(SPCR, CPOL | CPHA);
+ expect(spi.spiMode).toBe(3);
+ });
+
+ it('should indicate slave/master operation, based on SPCR value', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+
+ expect(spi.isMaster).toBe(false);
+
+ cpu.writeData(SPCR, MSTR);
+ expect(spi.isMaster).toBe(true);
+ });
+
+ it('should call the `onTransfer` callback when initiating an SPI trasfer by writing to SPDR', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+ spi.onTransfer = jest.fn();
+
+ cpu.writeData(SPCR, SPE | MSTR);
+ cpu.writeData(SPDR, 0x8f);
+
+ expect(spi.onTransfer).toHaveBeenCalledWith(0x8f);
+ });
+
+ it('should ignore SPDR writes when the SPE bit in SPCR is clear', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+ spi.onTransfer = jest.fn();
+
+ cpu.writeData(SPCR, MSTR);
+ cpu.writeData(SPDR, 0x8f);
+
+ expect(spi.onTransfer).not.toHaveBeenCalled();
+ });
+
+ it('should transmit a byte successfully (integration)', () => {
+ // Based on code example from section 19.2 of the datasheet, page 172
+ const { program } = asmProgram(`
+ ; register addresses
+ _REPLACE SPCR, ${SPCR - 0x20}
+ _REPLACE SPDR, ${SPDR - 0x20}
+ _REPLACE SPSR, ${SPSR - 0x20}
+ _REPLACE DDR_SPI, 0x4 ; PORTB
+
+ SPI_MasterInit:
+ ; Set MOSI and SCK output, all others input
+ LDI r17, 0x28
+ OUT DDR_SPI, r17
+
+ ; Enable SPI, Master, set clock rate fck/16
+ LDI r17, 0x51 ; (1<<SPE)|(1<<MSTR)|(1<<SPR0)
+ OUT SPCR, r17
+
+ SPI_MasterTransmit:
+ LDI r16, 0xb8 ; byte to transmit
+ OUT SPDR, r16
+
+ Wait_Transmit:
+ IN r16, SPSR
+ SBRS r16, 7
+ RJMP Wait_Transmit
+
+ ; Now read the result into r17
+ IN r17, SPDR
+ BREAK
+ `);
+
+ const cpu = new CPU(program);
+ const spi = new AVRSPI(cpu, spiConfig, 16e6);
+
+ let byteReceivedFromAsmCode: number | null = null;
+
+ spi.onTransfer = (value) => {
+ byteReceivedFromAsmCode = value;
+ return 0x5b; // we copy this byte to
+ };
+
+ const runner = new TestProgramRunner(cpu, spi);
+ runner.runToBreak();
+
+ // 16 cycles per clock * 8 bits = 128
+ expect(cpu.cycles).toBeGreaterThanOrEqual(128);
+
+ expect(byteReceivedFromAsmCode).toEqual(0xb8);
+ expect(cpu.data[R17]).toEqual(0x5b);
+ });
+
+ it('should set the WCOL bit in SPSR if writing to SPDR while SPI is already transmitting', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+
+ cpu.writeData(SPCR, SPE | MSTR);
+ cpu.writeData(SPDR, 0x50);
+ spi.tick();
+ expect(cpu.readData(SPSR) & WCOL).toEqual(0);
+
+ cpu.writeData(SPDR, 0x51);
+ expect(cpu.readData(SPSR) & WCOL).toEqual(WCOL);
+ });
+
+ it('should clear the SPIF bit and fire an interrupt when SPI transfer completes', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+
+ cpu.writeData(SPCR, SPE | SPIE | MSTR);
+ cpu.writeData(SPDR, 0x50);
+ cpu.data[SREG] = 0x80; // SREG: I-------
+
+ // At this point, write shouldn't be complete yet
+ cpu.cycles += 10;
+ spi.tick();
+ expect(cpu.pc).toEqual(0);
+
+ // 100 cycles later, it should (8 bits * 8 cycles per bit = 64).
+ cpu.cycles += 100;
+ spi.tick();
+ expect(cpu.data[SPSR] & SPIF).toEqual(0);
+ expect(cpu.pc).toEqual(0x22); // SPI Ready interrupt
+ });
+
+ it('should should only update SPDR when tranfer finishes (double buffering)', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
+ spi.onTransfer = jest.fn(() => 0x88);
+
+ cpu.writeData(SPCR, SPE | MSTR);
+ cpu.writeData(SPDR, 0x8f);
+
+ cpu.cycles = 10;
+ spi.tick();
+ expect(cpu.readData(SPDR)).toEqual(0);
+
+ cpu.cycles = 32; // 4 cycles per bit * 8 bits = 32
+ spi.tick();
+ expect(cpu.readData(SPDR)).toEqual(0x88);
+ });
+});
diff --git a/src/peripherals/spi.ts b/src/peripherals/spi.ts
new file mode 100644
index 0000000..0c02b95
--- /dev/null
+++ b/src/peripherals/spi.ts
@@ -0,0 +1,129 @@
+import { CPU } from '../cpu/cpu';
+import { u8 } from '../types';
+import { avrInterrupt } from '../cpu/interrupt';
+
+export interface SPIConfig {
+ spiInterrupt: u8;
+
+ SPCR: u8;
+ SPSR: u8;
+ SPDR: u8;
+}
+
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// Register bits:
+const SPCR_SPIE = 0x80; // SPI Interrupt Enable
+const SPCR_SPE = 0x40; // SPI Enable
+const SPCR_DORD = 0x20; // Data Order
+const SPCR_MSTR = 0x10; // Master/Slave Select
+const SPCR_CPOL = 0x8; // Clock Polarity
+const SPCR_CPHA = 0x4; // Clock Phase
+const SPCR_SPR1 = 0x2; // SPI Clock Rate Select 1
+const SPCR_SPR0 = 0x1; // SPI Clock Rate Select 0
+const SPSR_SPR_MASK = SPCR_SPR1 | SPCR_SPR0;
+
+const SPSR_SPIF = 0x80; // SPI Interrupt Flag
+const SPSR_WCOL = 0x40; // Write COLlision Flag
+const SPSR_SPI2X = 0x1; // Double SPI Speed Bit
+/* eslint-enable @typescript-eslint/no-unused-vars */
+
+export const spiConfig: SPIConfig = {
+ spiInterrupt: 0x22,
+ SPCR: 0x4c,
+ SPSR: 0x4d,
+ SPDR: 0x4e,
+};
+
+export type SPITransferCallback = (value: u8) => u8;
+
+const bitsPerByte = 8;
+
+export class AVRSPI {
+ public onTransfer: SPITransferCallback | null = null;
+
+ private transmissionCompleteCycles = 0;
+ private receivedByte: u8 = 0;
+
+ constructor(private cpu: CPU, private config: SPIConfig, private freqMHz: number) {
+ const { SPCR, SPSR, SPDR } = config;
+ cpu.writeHooks[SPDR] = (value: u8) => {
+ if (!(cpu.data[SPCR] & SPCR_SPE)) {
+ // SPI not enabled, ignore write
+ return;
+ }
+
+ // Write collision
+ if (this.transmissionCompleteCycles > this.cpu.cycles) {
+ cpu.data[SPSR] |= SPSR_WCOL;
+ return true;
+ }
+
+ // Clear write collision / interrupt flags
+ cpu.data[SPSR] &= ~SPSR_WCOL & ~SPSR_SPIF;
+
+ this.receivedByte = this.onTransfer?.(value) ?? 0;
+ this.transmissionCompleteCycles = this.cpu.cycles + this.clockDivider * bitsPerByte;
+ return true;
+ };
+ }
+
+ tick() {
+ if (this.transmissionCompleteCycles && this.cpu.cycles >= this.transmissionCompleteCycles) {
+ const { SPSR, SPDR } = this.config;
+ this.cpu.data[SPSR] |= SPSR_SPIF;
+ this.cpu.data[SPDR] = this.receivedByte;
+ this.transmissionCompleteCycles = 0;
+ }
+ if (this.cpu.interruptsEnabled) {
+ const { SPSR, SPCR, spiInterrupt } = this.config;
+ if (this.cpu.data[SPCR] & SPCR_SPIE && this.cpu.data[SPSR] & SPSR_SPIF) {
+ avrInterrupt(this.cpu, spiInterrupt);
+ this.cpu.data[SPSR] &= ~SPSR_SPIF;
+ }
+ }
+ }
+
+ get isMaster() {
+ return this.cpu.data[this.config.SPCR] & SPCR_MSTR ? true : false;
+ }
+
+ get dataOrder() {
+ return this.cpu.data[this.config.SPCR] & SPCR_DORD ? 'lsbFirst' : 'msbFirst';
+ }
+
+ get spiMode() {
+ const CPHA = this.cpu.data[this.config.SPCR] & SPCR_CPHA;
+ const CPOL = this.cpu.data[this.config.SPCR] & SPCR_CPOL;
+ return ((CPHA ? 2 : 0) | (CPOL ? 1 : 0)) as 0 | 1 | 2 | 3;
+ }
+
+ /**
+ * The clock divider is only relevant for Master mode
+ */
+ get clockDivider() {
+ const base = this.cpu.data[this.config.SPSR] & SPSR_SPI2X ? 2 : 4;
+ switch (this.cpu.data[this.config.SPCR] & SPSR_SPR_MASK) {
+ case 0b00:
+ return base;
+
+ case 0b01:
+ return base * 4;
+
+ case 0b10:
+ return base * 16;
+
+ case 0b11:
+ return base * 32;
+ }
+ // We should never get here:
+ throw new Error('Invalid divider value!');
+ }
+
+ /**
+ * The SPI freqeuncy is only relevant to Master mode.
+ * In slave mode, the frequency can be as high as F(osc) / 4.
+ */
+ get spiFrequency() {
+ return this.freqMHz / this.clockDivider;
+ }
+}
diff --git a/src/utils/test-utils.ts b/src/utils/test-utils.ts
index ca483f3..2bec178 100644
--- a/src/utils/test-utils.ts
+++ b/src/utils/test-utils.ts
@@ -2,6 +2,8 @@ import { CPU } from '../cpu/cpu';
import { assemble } from './assembler';
import { avrInstruction } from '../cpu/instruction';
+const BREAK_OPCODE = 0x9598;
+
export function asmProgram(source: string) {
const { bytes, errors, lines } = assemble(source);
if (errors.length) {
@@ -20,7 +22,7 @@ export class TestProgramRunner {
runInstructions(count: number) {
const { cpu, peripheral, onBreak } = this;
for (let i = 0; i < count; i++) {
- if (cpu.progMem[cpu.pc] === 0x9598) {
+ if (cpu.progMem[cpu.pc] === BREAK_OPCODE) {
onBreak?.(cpu);
throw new Error('BREAK instruction encountered');
}
@@ -28,4 +30,17 @@ export class TestProgramRunner {
peripheral.tick();
}
}
+
+ runToBreak(maxIterations = 5000) {
+ const { cpu, peripheral, onBreak } = this;
+ for (let i = 0; i < maxIterations; i++) {
+ if (cpu.progMem[cpu.pc] === BREAK_OPCODE) {
+ onBreak?.(cpu);
+ return;
+ }
+ avrInstruction(cpu);
+ peripheral.tick();
+ }
+ throw new Error('Program ran for too long without a BREAK instruction');
+ }
}