aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUri Shaked2020-07-16 19:34:28 +0300
committerUri Shaked2020-07-16 19:51:59 +0300
commit3685e1a9b43f8c79d1e0a5554a1a15d4d0c77142 (patch)
tree365bfa8634f75c1ef4fc84ecbc4ab290f4d2734a
parenttest(timer): remove stray console.log (diff)
downloadavr8js-3685e1a9b43f8c79d1e0a5554a1a15d4d0c77142.tar.gz
avr8js-3685e1a9b43f8c79d1e0a5554a1a15d4d0c77142.tar.bz2
avr8js-3685e1a9b43f8c79d1e0a5554a1a15d4d0c77142.zip
feat(eeprom): implement EEPROM peripheral
close #15
Diffstat (limited to '')
-rw-r--r--src/index.ts7
-rw-r--r--src/peripherals/eeprom.spec.ts195
-rw-r--r--src/peripherals/eeprom.ts147
-rw-r--r--src/peripherals/twi.spec.ts68
-rw-r--r--src/utils/assembler.ts2
-rw-r--r--src/utils/test-utils.ts31
6 files changed, 408 insertions, 42 deletions
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();
+ }
+ }
+}
If you find this content useful please consider a small donation via bitcoin: bitcoin:1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2?amount=0.001&message=Donation Scraping git contents via cgit is very resource intensive. The continued hosting of these git repositories is entirely financed by your bitcoin contributions. For donations of a least 1 mBTC you include your IP address in the message of your bitcoin donation. This will remove this message in the future for requests from your IP, which will significantly reduce token usage. In any case, any contributions to bitcoin address 1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2 are very welcome. Thanks in advance for you contribution to a self-sustaining ecosystem.