From ad8f51916db816464331e1e0816d83ad69f7bf8d Mon Sep 17 00:00:00 2001 From: Uri Shaked Date: Sat, 22 Aug 2020 16:43:31 +0300 Subject: [PATCH] feat(spi): implement SPI master #33 close #33 --- src/index.ts | 1 + src/peripherals/spi.spec.ts | 222 ++++++++++++++++++++++++++++++++++++ src/peripherals/spi.ts | 129 +++++++++++++++++++++ src/utils/test-utils.ts | 17 ++- 4 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 src/peripherals/spi.spec.ts create mode 100644 src/peripherals/spi.ts 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< { + 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'); + } }