From 3685e1a9b43f8c79d1e0a5554a1a15d4d0c77142 Mon Sep 17 00:00:00 2001 From: Uri Shaked Date: Thu, 16 Jul 2020 19:34:28 +0300 Subject: feat(eeprom): implement EEPROM peripheral close #15 --- src/index.ts | 7 ++ src/peripherals/eeprom.spec.ts | 195 +++++++++++++++++++++++++++++++++++++++++ src/peripherals/eeprom.ts | 147 +++++++++++++++++++++++++++++++ src/peripherals/twi.spec.ts | 68 ++++++-------- src/utils/assembler.ts | 2 +- src/utils/test-utils.ts | 31 +++++++ 6 files changed, 408 insertions(+), 42 deletions(-) create mode 100644 src/peripherals/eeprom.spec.ts create mode 100644 src/peripherals/eeprom.ts create mode 100644 src/utils/test-utils.ts diff --git a/src/index.ts b/src/index.ts index 8db88f9..0a9335e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,4 +26,11 @@ export { PinState, } from './peripherals/gpio'; export { AVRUSART, usart0Config } from './peripherals/usart'; +export { + AVREEPROM, + AVREEPROMConfig, + EEPROMBackend, + EEPROMMemoryBackend, + eepromConfig, +} from './peripherals/eeprom'; export * from './peripherals/twi'; diff --git a/src/peripherals/eeprom.spec.ts b/src/peripherals/eeprom.spec.ts new file mode 100644 index 0000000..ba66100 --- /dev/null +++ b/src/peripherals/eeprom.spec.ts @@ -0,0 +1,195 @@ +import { CPU } from '../cpu/cpu'; +import { AVREEPROM, EEPROMMemoryBackend } from './eeprom'; +import { asmProgram, TestProgramRunner } from '../utils/test-utils'; + +// EEPROM Registers +const EECR = 0x3f; +const EEDR = 0x40; +const EEARL = 0x41; +const EEARH = 0x42; +const SREG = 95; + +// Register bit names +/* eslint-disable @typescript-eslint/no-unused-vars */ +const EERE = 1; +const EEPE = 2; +const EEMPE = 4; +const EERIE = 8; +const EEPM0 = 16; +const EEPM1 = 32; +/* eslint-enable @typescript-eslint/no-unused-vars */ + +describe('EEPROM', () => { + describe('Reading the EEPROM', () => { + it('should return 0xff when reading from an empty location', () => { + const cpu = new CPU(new Uint16Array(0x1000)); + const eeprom = new AVREEPROM(cpu, new EEPROMMemoryBackend(1024)); + cpu.writeData(EEARL, 0); + cpu.writeData(EEARH, 0); + cpu.writeData(EECR, EERE); + eeprom.tick(); + expect(cpu.cycles).toEqual(4); + expect(cpu.data[EEDR]).toEqual(0xff); + }); + + it('should return the value stored at the given EEPROM address', () => { + const cpu = new CPU(new Uint16Array(0x1000)); + const eepromBackend = new EEPROMMemoryBackend(1024); + const eeprom = new AVREEPROM(cpu, eepromBackend); + eepromBackend.memory[0x250] = 0x42; + cpu.writeData(EEARL, 0x50); + cpu.writeData(EEARH, 0x2); + cpu.writeData(EECR, EERE); + eeprom.tick(); + expect(cpu.data[EEDR]).toEqual(0x42); + }); + }); + + describe('Writing to the EEPROM', () => { + it('should write a byte to the given EEPROM address', () => { + const cpu = new CPU(new Uint16Array(0x1000)); + const eepromBackend = new EEPROMMemoryBackend(1024); + const eeprom = new AVREEPROM(cpu, eepromBackend); + cpu.writeData(EEDR, 0x55); + cpu.writeData(EEARL, 15); + cpu.writeData(EEARH, 0); + cpu.writeData(EECR, EEMPE); + cpu.writeData(EECR, EEPE); + eeprom.tick(); + expect(cpu.cycles).toEqual(2); + expect(eepromBackend.memory[15]).toEqual(0x55); + expect(cpu.data[EECR] & EEPE).toEqual(EEPE); + }); + + it('should not erase the memory when writing if EEPM1 is high', () => { + // We subtract 0x20 to translate from RAM address space to I/O register space + const { program } = asmProgram(` + ; register addresses + _REPLACE TWSR, ${EECR - 0x20} + _REPLACE EEARL, ${EEARL - 0x20} + _REPLACE EEDR, ${EEDR - 0x20} + _REPLACE EECR, ${EECR - 0x20} + + LDI r16, 0x55 + OUT EEDR, r16 + LDI r16, 9 + OUT EEARL, r16 + SBI EECR, 5 ; EECR |= EEPM1 + SBI EECR, 2 ; EECR |= EEMPE + SBI EECR, 1 ; EECR |= EEPE + `); + + const cpu = new CPU(program); + const eepromBackend = new EEPROMMemoryBackend(1024); + const eeprom = new AVREEPROM(cpu, eepromBackend); + eepromBackend.memory[9] = 0x0f; // high four bits are cleared + + const runner = new TestProgramRunner(cpu, eeprom); + runner.runInstructions(program.length); + + // EEPROM was 0x0f, and our program wrote 0x55. + // Since write (without erase) only clears bits, we expect 0x05 now. + expect(eepromBackend.memory[9]).toEqual(0x05); + }); + + it('should clear the EEPE bit and fire an interrupt when write has been completed', () => { + const cpu = new CPU(new Uint16Array(0x1000)); + const eepromBackend = new EEPROMMemoryBackend(1024); + const eeprom = new AVREEPROM(cpu, eepromBackend); + cpu.writeData(EEDR, 0x55); + cpu.writeData(EEARL, 15); + cpu.writeData(EEARH, 0); + cpu.writeData(EECR, EEMPE | EERIE); + cpu.data[SREG] = 0x80; // SREG: I------- + cpu.writeData(EECR, EEPE); + cpu.cycles += 1000; + eeprom.tick(); + // At this point, write shouldn't be complete yet + expect(cpu.data[EECR] & EEPE).toEqual(EEPE); + expect(cpu.pc).toEqual(0); + cpu.cycles += 10000000; + // And now, 10 million cycles later, it should. + eeprom.tick(); + expect(eepromBackend.memory[15]).toEqual(0x55); + expect(cpu.data[EECR] & EEPE).toEqual(0); + expect(cpu.pc).toEqual(0x2c); // EEPROM Ready interrupt + }); + + it('should skip the write if EEMPE is clear', () => { + const cpu = new CPU(new Uint16Array(0x1000)); + const eepromBackend = new EEPROMMemoryBackend(1024); + const eeprom = new AVREEPROM(cpu, eepromBackend); + cpu.writeData(EEDR, 0x55); + cpu.writeData(EEARL, 15); + cpu.writeData(EEARH, 0); + cpu.writeData(EECR, EEMPE); + cpu.cycles = 8; // waiting for more than 4 cycles should clear EEMPE + eeprom.tick(); + cpu.writeData(EECR, EEPE); + eeprom.tick(); + // Ensure that nothing was written, and EEPE bit is clear + expect(cpu.cycles).toEqual(8); + expect(eepromBackend.memory[15]).toEqual(0xff); + expect(cpu.data[EECR] & EEPE).toEqual(0); + }); + + it('should skip the write if another write is already in progress', () => { + const cpu = new CPU(new Uint16Array(0x1000)); + const eepromBackend = new EEPROMMemoryBackend(1024); + const eeprom = new AVREEPROM(cpu, eepromBackend); + + // Write 0x55 to address 15 + cpu.writeData(EEDR, 0x55); + cpu.writeData(EEARL, 15); + cpu.writeData(EEARH, 0); + cpu.writeData(EECR, EEMPE); + cpu.writeData(EECR, EEPE); + eeprom.tick(); + expect(cpu.cycles).toEqual(2); + + // Write 0x66 to address 16 (first write is still in progress) + cpu.writeData(EEDR, 0x66); + cpu.writeData(EEARL, 16); + cpu.writeData(EEARH, 0); + cpu.writeData(EECR, EEMPE); + cpu.writeData(EECR, EEPE); + eeprom.tick(); + + // Ensure that second write didn't happen + expect(cpu.cycles).toEqual(2); + expect(eepromBackend.memory[15]).toEqual(0x55); + expect(eepromBackend.memory[16]).toEqual(0xff); + }); + }); + + describe('EEPROM erase', () => { + it('should only erase the memory when EEPM0 is high', () => { + // We subtract 0x20 to translate from RAM address space to I/O register space + const { program } = asmProgram(` + ; register addresses + _REPLACE TWSR, ${EECR - 0x20} + _REPLACE EEARL, ${EEARL - 0x20} + _REPLACE EEDR, ${EEDR - 0x20} + _REPLACE EECR, ${EECR - 0x20} + + LDI r16, 0x55 + OUT EEDR, r16 + LDI r16, 9 + OUT EEARL, r16 + SBI EECR, 4 ; EECR |= EEPM0 + SBI EECR, 2 ; EECR |= EEMPE + SBI EECR, 1 ; EECR |= EEPE + `); + + const cpu = new CPU(program); + const eepromBackend = new EEPROMMemoryBackend(1024); + const eeprom = new AVREEPROM(cpu, eepromBackend); + eepromBackend.memory[9] = 0x22; + + const runner = new TestProgramRunner(cpu, eeprom); + runner.runInstructions(program.length); + + expect(eepromBackend.memory[9]).toEqual(0xff); + }); + }); +}); diff --git a/src/peripherals/eeprom.ts b/src/peripherals/eeprom.ts new file mode 100644 index 0000000..97ca178 --- /dev/null +++ b/src/peripherals/eeprom.ts @@ -0,0 +1,147 @@ +import { CPU } from '../cpu/cpu'; +import { avrInterrupt } from '../cpu/interrupt'; +import { u8, u16, u32 } from '../types'; + +export interface EEPROMBackend { + readMemory(addr: u16): u8; + writeMemory(addr: u16, value: u8): void; + eraseMemory(addr: u16): void; +} + +export class EEPROMMemoryBackend implements EEPROMBackend { + readonly memory: Uint8Array; + + constructor(size: u16) { + this.memory = new Uint8Array(size); + this.memory.fill(0xff); + } + + readMemory(addr: u16) { + return this.memory[addr]; + } + + writeMemory(addr: u16, value: u8) { + this.memory[addr] &= value; + } + + eraseMemory(addr: u16) { + this.memory[addr] = 0xff; + } +} + +export interface AVREEPROMConfig { + eepromReadyInterrupt: u8; + + EECR: u8; + EEDR: u8; + EEARL: u8; + EEARH: u8; + + /** The amount of clock cycles erase takes */ + eraseCycles: u32; + /** The amount of clock cycles a write takes */ + writeCycles: u32; +} + +export const eepromConfig: AVREEPROMConfig = { + eepromReadyInterrupt: 0x2c, + EECR: 0x3f, + EEDR: 0x40, + EEARL: 0x41, + EEARH: 0x42, + eraseCycles: 28800, // 1.8ms at 16MHz + writeCycles: 28800, // 1.8ms at 16MHz +}; + +const EERE = 1 << 0; +const EEPE = 1 << 1; +const EEMPE = 1 << 2; +const EERIE = 1 << 3; +const EEPM0 = 1 << 4; +const EEPM1 = 1 << 5; + +export class AVREEPROM { + /** + * Used to keep track on the last write to EEMPE. From the datasheet: + * The EEMPE bit determines whether setting EEPE to one causes the EEPROM to be written. + * When EEMPE is set, setting EEPE within four clock cycles will write data to the EEPROM + * at the selected address If EEMPE is zero, setting EEPE will have no effect. + */ + private writeEnabledCycles = 0; + + private writeCompleteCycles = 0; + + constructor( + private cpu: CPU, + private backend: EEPROMBackend, + private config: AVREEPROMConfig = eepromConfig + ) { + this.cpu.writeHooks[this.config.EECR] = (eecr) => { + const { EEARH, EEARL, EECR, EEDR } = this.config; + + const addr = (this.cpu.data[EEARH] << 8) | this.cpu.data[EEARL]; + + if (eecr & EEMPE) { + this.writeEnabledCycles = this.cpu.cycles + 4; + } + + // Read + if (eecr & EERE) { + this.cpu.data[EEDR] = this.backend.readMemory(addr); + // When the EEPROM is read, the CPU is halted for four cycles before the + // next instruction is executed. + this.cpu.cycles += 4; + return true; + } + + // Write + if (eecr & EEPE) { + // If EEMPE is zero, setting EEPE will have no effect. + if (this.cpu.cycles >= this.writeEnabledCycles) { + return true; + } + // Check for write-in-progress + if (this.writeCompleteCycles) { + return true; + } + + const eedr = this.cpu.data[EEDR]; + + this.writeCompleteCycles = this.cpu.cycles; + + // Erase + if (!(eecr & EEPM1)) { + this.backend.eraseMemory(addr); + this.writeCompleteCycles += this.config.eraseCycles; + } + // Write + if (!(eecr & EEPM0)) { + this.backend.writeMemory(addr, eedr); + this.writeCompleteCycles += this.config.writeCycles; + } + + this.cpu.data[EECR] |= EEPE; + // When EEPE has been set, the CPU is halted for two cycles before the + // next instruction is executed. + this.cpu.cycles += 2; + return true; + } + + return false; + }; + } + + tick() { + const { EECR, eepromReadyInterrupt } = this.config; + + if (this.writeEnabledCycles && this.cpu.cycles > this.writeEnabledCycles) { + this.cpu.data[EECR] &= ~EEMPE; + } + if (this.writeCompleteCycles && this.cpu.cycles > this.writeCompleteCycles) { + this.cpu.data[EECR] &= ~EEPE; + if (this.cpu.interruptsEnabled && this.cpu.data[EECR] & EERIE) { + avrInterrupt(this.cpu, eepromReadyInterrupt); + } + } + } +} diff --git a/src/peripherals/twi.spec.ts b/src/peripherals/twi.spec.ts index f47449b..1a2b2ae 100644 --- a/src/peripherals/twi.spec.ts +++ b/src/peripherals/twi.spec.ts @@ -1,7 +1,6 @@ import { CPU } from '../cpu/cpu'; +import { asmProgram, TestProgramRunner } from '../utils/test-utils'; import { AVRTWI, twiConfig } from './twi'; -import { assemble } from '../utils/assembler'; -import { avrInstruction } from '../cpu/instruction'; const FREQ_16MHZ = 16e6; @@ -24,25 +23,10 @@ const TWSTA = 0x20; const TWEA = 0x40; const TWINT = 0x80; -function asmProgram(source: string) { - const { bytes, errors, lines } = assemble(source); - if (errors.length) { - throw new Error('Assembly failed: ' + errors); - } - return { program: new Uint16Array(bytes.buffer), lines }; -} - -function runInstructions(cpu: CPU, twi: AVRTWI, count: number) { - for (let i = 0; i < count; i++) { - if (cpu.progMem[cpu.pc] === 0x9598) { - console.log(cpu.data[TWCR].toString(16)); - console.log(cpu.data[R16]); - throw new Error('BREAK instruction encountered'); - } - avrInstruction(cpu); - twi.tick(); - } -} +const onTestBreak = (cpu: CPU) => { + console.log(cpu.data[TWCR].toString(16)); + console.log(cpu.data[R16]); +}; describe('TWI', () => { it('should correctly calculate the sclFrequency from TWBR', () => { @@ -186,6 +170,7 @@ describe('TWI', () => { `); const cpu = new CPU(program); const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); + const runner = new TestProgramRunner(cpu, twi, onTestBreak); twi.eventHandler = { start: jest.fn(), stop: jest.fn(), @@ -195,35 +180,35 @@ describe('TWI', () => { }; // Step 1: wait for start condition - runInstructions(cpu, twi, 4); + runner.runInstructions(4); expect(twi.eventHandler.start).toHaveBeenCalledWith(false); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeStart(); // Step 2: wait for slave connect in write mode - runInstructions(cpu, twi, 16); + runner.runInstructions(16); expect(twi.eventHandler.connectToSlave).toHaveBeenCalledWith(0x22, true); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeConnect(true); // Step 3: wait for first data byte - runInstructions(cpu, twi, 16); + runner.runInstructions(16); expect(twi.eventHandler.writeByte).toHaveBeenCalledWith(0x55); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeWrite(true); // Step 4: wait for stop condition - runInstructions(cpu, twi, 16); + runner.runInstructions(16); expect(twi.eventHandler.stop).toHaveBeenCalled(); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeStop(); // Step 5: wait for the assembly code to indicate success by settings r17 to 0x42 - runInstructions(cpu, twi, 16); + runner.runInstructions(16); expect(cpu.data[R17]).toEqual(0x42); }); @@ -354,6 +339,7 @@ describe('TWI', () => { `); const cpu = new CPU(program); const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ); + const runner = new TestProgramRunner(cpu, twi, onTestBreak); twi.eventHandler = { start: jest.fn(), stop: jest.fn(), @@ -363,42 +349,42 @@ describe('TWI', () => { }; // Step 1: wait for start condition - runInstructions(cpu, twi, 4); + runner.runInstructions(4); expect(twi.eventHandler.start).toHaveBeenCalledWith(false); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeStart(); // Step 2: wait for slave connect in read mode - runInstructions(cpu, twi, 16); + runner.runInstructions(16); expect(twi.eventHandler.connectToSlave).toHaveBeenCalledWith(0x50, false); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeConnect(true); // Step 3: send the first byte to the master, expect ack - runInstructions(cpu, twi, 16); + runner.runInstructions(16); expect(twi.eventHandler.readByte).toHaveBeenCalledWith(true); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeRead(0x66); // Step 4: send the first byte to the master, expect nack - runInstructions(cpu, twi, 16); + runner.runInstructions(16); expect(twi.eventHandler.readByte).toHaveBeenCalledWith(false); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeRead(0x77); // Step 5: wait for stop condition - runInstructions(cpu, twi, 24); + runner.runInstructions(24); expect(twi.eventHandler.stop).toHaveBeenCalled(); - runInstructions(cpu, twi, 16); + runner.runInstructions(16); twi.completeStop(); // Step 6: wait for the assembly code to indicate success by settings r17 to 0x42 - runInstructions(cpu, twi, 16); + runner.runInstructions(16); expect(cpu.data[R17]).toEqual(0x42); }); }); diff --git a/src/utils/assembler.ts b/src/utils/assembler.ts index 17b20b4..4823937 100644 --- a/src/utils/assembler.ts +++ b/src/utils/assembler.ts @@ -46,7 +46,7 @@ interface LineTablePass1 { byteOffset: number; } -interface LineTable extends LineTablePass1 { +export interface LineTable extends LineTablePass1 { bytes: string; } diff --git a/src/utils/test-utils.ts b/src/utils/test-utils.ts new file mode 100644 index 0000000..dfb6b32 --- /dev/null +++ b/src/utils/test-utils.ts @@ -0,0 +1,31 @@ +import { CPU } from '../cpu/cpu'; +import { assemble } from './assembler'; +import { avrInstruction } from '../cpu/instruction'; + +export function asmProgram(source: string) { + const { bytes, errors, lines } = assemble(source); + if (errors.length) { + throw new Error('Assembly failed: ' + errors); + } + return { program: new Uint16Array(bytes.buffer), lines }; +} + +export class TestProgramRunner { + constructor( + private readonly cpu: CPU, + private readonly peripheral: { tick: () => void }, + private readonly onBreak?: (cpu: CPU) => void + ) {} + + runInstructions(count: number) { + const { cpu, peripheral, onBreak } = this; + for (let i = 0; i < count; i++) { + if (cpu.progMem[cpu.pc] === 0x9598) { + onBreak?.(cpu); + throw new Error('BREAK instruction encountered'); + } + avrInstruction(cpu); + peripheral.tick(); + } + } +} -- cgit v1.2.3