aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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');
+ }
}
If you find this content useful please consider a small donation via bitcoin: bitcoin:1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2?amount=0.001&message=Donation Scraping git contents via cgit is very resource intensive. The continued hosting of these git repositories is entirely financed by your bitcoin contributions. For donations of a least 1 mBTC you include your IP address in the message of your bitcoin donation. This will remove this message in the future for requests from your IP, which will significantly reduce token usage. In any case, any contributions to bitcoin address 1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2 are very welcome. Thanks in advance for you contribution to a self-sustaining ecosystem.