Skip to content

Commit 3685e1a

Browse files
committed
feat(eeprom): implement EEPROM peripheral
close #15
1 parent 32c0290 commit 3685e1a

File tree

6 files changed

+408
-42
lines changed

6 files changed

+408
-42
lines changed

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,11 @@ export {
2626
PinState,
2727
} from './peripherals/gpio';
2828
export { AVRUSART, usart0Config } from './peripherals/usart';
29+
export {
30+
AVREEPROM,
31+
AVREEPROMConfig,
32+
EEPROMBackend,
33+
EEPROMMemoryBackend,
34+
eepromConfig,
35+
} from './peripherals/eeprom';
2936
export * from './peripherals/twi';

src/peripherals/eeprom.spec.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { CPU } from '../cpu/cpu';
2+
import { AVREEPROM, EEPROMMemoryBackend } from './eeprom';
3+
import { asmProgram, TestProgramRunner } from '../utils/test-utils';
4+
5+
// EEPROM Registers
6+
const EECR = 0x3f;
7+
const EEDR = 0x40;
8+
const EEARL = 0x41;
9+
const EEARH = 0x42;
10+
const SREG = 95;
11+
12+
// Register bit names
13+
/* eslint-disable @typescript-eslint/no-unused-vars */
14+
const EERE = 1;
15+
const EEPE = 2;
16+
const EEMPE = 4;
17+
const EERIE = 8;
18+
const EEPM0 = 16;
19+
const EEPM1 = 32;
20+
/* eslint-enable @typescript-eslint/no-unused-vars */
21+
22+
describe('EEPROM', () => {
23+
describe('Reading the EEPROM', () => {
24+
it('should return 0xff when reading from an empty location', () => {
25+
const cpu = new CPU(new Uint16Array(0x1000));
26+
const eeprom = new AVREEPROM(cpu, new EEPROMMemoryBackend(1024));
27+
cpu.writeData(EEARL, 0);
28+
cpu.writeData(EEARH, 0);
29+
cpu.writeData(EECR, EERE);
30+
eeprom.tick();
31+
expect(cpu.cycles).toEqual(4);
32+
expect(cpu.data[EEDR]).toEqual(0xff);
33+
});
34+
35+
it('should return the value stored at the given EEPROM address', () => {
36+
const cpu = new CPU(new Uint16Array(0x1000));
37+
const eepromBackend = new EEPROMMemoryBackend(1024);
38+
const eeprom = new AVREEPROM(cpu, eepromBackend);
39+
eepromBackend.memory[0x250] = 0x42;
40+
cpu.writeData(EEARL, 0x50);
41+
cpu.writeData(EEARH, 0x2);
42+
cpu.writeData(EECR, EERE);
43+
eeprom.tick();
44+
expect(cpu.data[EEDR]).toEqual(0x42);
45+
});
46+
});
47+
48+
describe('Writing to the EEPROM', () => {
49+
it('should write a byte to the given EEPROM address', () => {
50+
const cpu = new CPU(new Uint16Array(0x1000));
51+
const eepromBackend = new EEPROMMemoryBackend(1024);
52+
const eeprom = new AVREEPROM(cpu, eepromBackend);
53+
cpu.writeData(EEDR, 0x55);
54+
cpu.writeData(EEARL, 15);
55+
cpu.writeData(EEARH, 0);
56+
cpu.writeData(EECR, EEMPE);
57+
cpu.writeData(EECR, EEPE);
58+
eeprom.tick();
59+
expect(cpu.cycles).toEqual(2);
60+
expect(eepromBackend.memory[15]).toEqual(0x55);
61+
expect(cpu.data[EECR] & EEPE).toEqual(EEPE);
62+
});
63+
64+
it('should not erase the memory when writing if EEPM1 is high', () => {
65+
// We subtract 0x20 to translate from RAM address space to I/O register space
66+
const { program } = asmProgram(`
67+
; register addresses
68+
_REPLACE TWSR, ${EECR - 0x20}
69+
_REPLACE EEARL, ${EEARL - 0x20}
70+
_REPLACE EEDR, ${EEDR - 0x20}
71+
_REPLACE EECR, ${EECR - 0x20}
72+
73+
LDI r16, 0x55
74+
OUT EEDR, r16
75+
LDI r16, 9
76+
OUT EEARL, r16
77+
SBI EECR, 5 ; EECR |= EEPM1
78+
SBI EECR, 2 ; EECR |= EEMPE
79+
SBI EECR, 1 ; EECR |= EEPE
80+
`);
81+
82+
const cpu = new CPU(program);
83+
const eepromBackend = new EEPROMMemoryBackend(1024);
84+
const eeprom = new AVREEPROM(cpu, eepromBackend);
85+
eepromBackend.memory[9] = 0x0f; // high four bits are cleared
86+
87+
const runner = new TestProgramRunner(cpu, eeprom);
88+
runner.runInstructions(program.length);
89+
90+
// EEPROM was 0x0f, and our program wrote 0x55.
91+
// Since write (without erase) only clears bits, we expect 0x05 now.
92+
expect(eepromBackend.memory[9]).toEqual(0x05);
93+
});
94+
95+
it('should clear the EEPE bit and fire an interrupt when write has been completed', () => {
96+
const cpu = new CPU(new Uint16Array(0x1000));
97+
const eepromBackend = new EEPROMMemoryBackend(1024);
98+
const eeprom = new AVREEPROM(cpu, eepromBackend);
99+
cpu.writeData(EEDR, 0x55);
100+
cpu.writeData(EEARL, 15);
101+
cpu.writeData(EEARH, 0);
102+
cpu.writeData(EECR, EEMPE | EERIE);
103+
cpu.data[SREG] = 0x80; // SREG: I-------
104+
cpu.writeData(EECR, EEPE);
105+
cpu.cycles += 1000;
106+
eeprom.tick();
107+
// At this point, write shouldn't be complete yet
108+
expect(cpu.data[EECR] & EEPE).toEqual(EEPE);
109+
expect(cpu.pc).toEqual(0);
110+
cpu.cycles += 10000000;
111+
// And now, 10 million cycles later, it should.
112+
eeprom.tick();
113+
expect(eepromBackend.memory[15]).toEqual(0x55);
114+
expect(cpu.data[EECR] & EEPE).toEqual(0);
115+
expect(cpu.pc).toEqual(0x2c); // EEPROM Ready interrupt
116+
});
117+
118+
it('should skip the write if EEMPE is clear', () => {
119+
const cpu = new CPU(new Uint16Array(0x1000));
120+
const eepromBackend = new EEPROMMemoryBackend(1024);
121+
const eeprom = new AVREEPROM(cpu, eepromBackend);
122+
cpu.writeData(EEDR, 0x55);
123+
cpu.writeData(EEARL, 15);
124+
cpu.writeData(EEARH, 0);
125+
cpu.writeData(EECR, EEMPE);
126+
cpu.cycles = 8; // waiting for more than 4 cycles should clear EEMPE
127+
eeprom.tick();
128+
cpu.writeData(EECR, EEPE);
129+
eeprom.tick();
130+
// Ensure that nothing was written, and EEPE bit is clear
131+
expect(cpu.cycles).toEqual(8);
132+
expect(eepromBackend.memory[15]).toEqual(0xff);
133+
expect(cpu.data[EECR] & EEPE).toEqual(0);
134+
});
135+
136+
it('should skip the write if another write is already in progress', () => {
137+
const cpu = new CPU(new Uint16Array(0x1000));
138+
const eepromBackend = new EEPROMMemoryBackend(1024);
139+
const eeprom = new AVREEPROM(cpu, eepromBackend);
140+
141+
// Write 0x55 to address 15
142+
cpu.writeData(EEDR, 0x55);
143+
cpu.writeData(EEARL, 15);
144+
cpu.writeData(EEARH, 0);
145+
cpu.writeData(EECR, EEMPE);
146+
cpu.writeData(EECR, EEPE);
147+
eeprom.tick();
148+
expect(cpu.cycles).toEqual(2);
149+
150+
// Write 0x66 to address 16 (first write is still in progress)
151+
cpu.writeData(EEDR, 0x66);
152+
cpu.writeData(EEARL, 16);
153+
cpu.writeData(EEARH, 0);
154+
cpu.writeData(EECR, EEMPE);
155+
cpu.writeData(EECR, EEPE);
156+
eeprom.tick();
157+
158+
// Ensure that second write didn't happen
159+
expect(cpu.cycles).toEqual(2);
160+
expect(eepromBackend.memory[15]).toEqual(0x55);
161+
expect(eepromBackend.memory[16]).toEqual(0xff);
162+
});
163+
});
164+
165+
describe('EEPROM erase', () => {
166+
it('should only erase the memory when EEPM0 is high', () => {
167+
// We subtract 0x20 to translate from RAM address space to I/O register space
168+
const { program } = asmProgram(`
169+
; register addresses
170+
_REPLACE TWSR, ${EECR - 0x20}
171+
_REPLACE EEARL, ${EEARL - 0x20}
172+
_REPLACE EEDR, ${EEDR - 0x20}
173+
_REPLACE EECR, ${EECR - 0x20}
174+
175+
LDI r16, 0x55
176+
OUT EEDR, r16
177+
LDI r16, 9
178+
OUT EEARL, r16
179+
SBI EECR, 4 ; EECR |= EEPM0
180+
SBI EECR, 2 ; EECR |= EEMPE
181+
SBI EECR, 1 ; EECR |= EEPE
182+
`);
183+
184+
const cpu = new CPU(program);
185+
const eepromBackend = new EEPROMMemoryBackend(1024);
186+
const eeprom = new AVREEPROM(cpu, eepromBackend);
187+
eepromBackend.memory[9] = 0x22;
188+
189+
const runner = new TestProgramRunner(cpu, eeprom);
190+
runner.runInstructions(program.length);
191+
192+
expect(eepromBackend.memory[9]).toEqual(0xff);
193+
});
194+
});
195+
});

src/peripherals/eeprom.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { CPU } from '../cpu/cpu';
2+
import { avrInterrupt } from '../cpu/interrupt';
3+
import { u8, u16, u32 } from '../types';
4+
5+
export interface EEPROMBackend {
6+
readMemory(addr: u16): u8;
7+
writeMemory(addr: u16, value: u8): void;
8+
eraseMemory(addr: u16): void;
9+
}
10+
11+
export class EEPROMMemoryBackend implements EEPROMBackend {
12+
readonly memory: Uint8Array;
13+
14+
constructor(size: u16) {
15+
this.memory = new Uint8Array(size);
16+
this.memory.fill(0xff);
17+
}
18+
19+
readMemory(addr: u16) {
20+
return this.memory[addr];
21+
}
22+
23+
writeMemory(addr: u16, value: u8) {
24+
this.memory[addr] &= value;
25+
}
26+
27+
eraseMemory(addr: u16) {
28+
this.memory[addr] = 0xff;
29+
}
30+
}
31+
32+
export interface AVREEPROMConfig {
33+
eepromReadyInterrupt: u8;
34+
35+
EECR: u8;
36+
EEDR: u8;
37+
EEARL: u8;
38+
EEARH: u8;
39+
40+
/** The amount of clock cycles erase takes */
41+
eraseCycles: u32;
42+
/** The amount of clock cycles a write takes */
43+
writeCycles: u32;
44+
}
45+
46+
export const eepromConfig: AVREEPROMConfig = {
47+
eepromReadyInterrupt: 0x2c,
48+
EECR: 0x3f,
49+
EEDR: 0x40,
50+
EEARL: 0x41,
51+
EEARH: 0x42,
52+
eraseCycles: 28800, // 1.8ms at 16MHz
53+
writeCycles: 28800, // 1.8ms at 16MHz
54+
};
55+
56+
const EERE = 1 << 0;
57+
const EEPE = 1 << 1;
58+
const EEMPE = 1 << 2;
59+
const EERIE = 1 << 3;
60+
const EEPM0 = 1 << 4;
61+
const EEPM1 = 1 << 5;
62+
63+
export class AVREEPROM {
64+
/**
65+
* Used to keep track on the last write to EEMPE. From the datasheet:
66+
* The EEMPE bit determines whether setting EEPE to one causes the EEPROM to be written.
67+
* When EEMPE is set, setting EEPE within four clock cycles will write data to the EEPROM
68+
* at the selected address If EEMPE is zero, setting EEPE will have no effect.
69+
*/
70+
private writeEnabledCycles = 0;
71+
72+
private writeCompleteCycles = 0;
73+
74+
constructor(
75+
private cpu: CPU,
76+
private backend: EEPROMBackend,
77+
private config: AVREEPROMConfig = eepromConfig
78+
) {
79+
this.cpu.writeHooks[this.config.EECR] = (eecr) => {
80+
const { EEARH, EEARL, EECR, EEDR } = this.config;
81+
82+
const addr = (this.cpu.data[EEARH] << 8) | this.cpu.data[EEARL];
83+
84+
if (eecr & EEMPE) {
85+
this.writeEnabledCycles = this.cpu.cycles + 4;
86+
}
87+
88+
// Read
89+
if (eecr & EERE) {
90+
this.cpu.data[EEDR] = this.backend.readMemory(addr);
91+
// When the EEPROM is read, the CPU is halted for four cycles before the
92+
// next instruction is executed.
93+
this.cpu.cycles += 4;
94+
return true;
95+
}
96+
97+
// Write
98+
if (eecr & EEPE) {
99+
// If EEMPE is zero, setting EEPE will have no effect.
100+
if (this.cpu.cycles >= this.writeEnabledCycles) {
101+
return true;
102+
}
103+
// Check for write-in-progress
104+
if (this.writeCompleteCycles) {
105+
return true;
106+
}
107+
108+
const eedr = this.cpu.data[EEDR];
109+
110+
this.writeCompleteCycles = this.cpu.cycles;
111+
112+
// Erase
113+
if (!(eecr & EEPM1)) {
114+
this.backend.eraseMemory(addr);
115+
this.writeCompleteCycles += this.config.eraseCycles;
116+
}
117+
// Write
118+
if (!(eecr & EEPM0)) {
119+
this.backend.writeMemory(addr, eedr);
120+
this.writeCompleteCycles += this.config.writeCycles;
121+
}
122+
123+
this.cpu.data[EECR] |= EEPE;
124+
// When EEPE has been set, the CPU is halted for two cycles before the
125+
// next instruction is executed.
126+
this.cpu.cycles += 2;
127+
return true;
128+
}
129+
130+
return false;
131+
};
132+
}
133+
134+
tick() {
135+
const { EECR, eepromReadyInterrupt } = this.config;
136+
137+
if (this.writeEnabledCycles && this.cpu.cycles > this.writeEnabledCycles) {
138+
this.cpu.data[EECR] &= ~EEMPE;
139+
}
140+
if (this.writeCompleteCycles && this.cpu.cycles > this.writeCompleteCycles) {
141+
this.cpu.data[EECR] &= ~EEPE;
142+
if (this.cpu.interruptsEnabled && this.cpu.data[EECR] & EERIE) {
143+
avrInterrupt(this.cpu, eepromReadyInterrupt);
144+
}
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)