Skip to content

Commit

Permalink
feat(spi): implement SPI master #33
Browse files Browse the repository at this point in the history
close #33
  • Loading branch information
urish committed Aug 22, 2020
1 parent 13ad0f5 commit ad8f519
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ export {
eepromConfig,
} from './peripherals/eeprom';
export * from './peripherals/twi';
export { spiConfig, SPIConfig, SPITransferCallback, AVRSPI } from './peripherals/spi';
222 changes: 222 additions & 0 deletions src/peripherals/spi.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
129 changes: 129 additions & 0 deletions src/peripherals/spi.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
17 changes: 16 additions & 1 deletion src/utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -20,12 +22,25 @@ 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');
}
avrInstruction(cpu);
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');
}
}

0 comments on commit ad8f519

Please sign in to comment.