aboutsummaryrefslogtreecommitdiff
path: root/src/peripherals
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/peripherals/eeprom.spec.ts195
-rw-r--r--src/peripherals/eeprom.ts147
-rw-r--r--src/peripherals/twi.spec.ts68
3 files changed, 369 insertions, 41 deletions
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);
});
});