-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
close #33
- Loading branch information
Showing
4 changed files
with
368 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters