Skip to content

Commit 9c1288f

Browse files
committed
perf!: centeral timekeeping
This should improve performance, especially when running simulations with multiple peripherals. For instance, the demo project now runs at ~322%, up from ~185% in AVR8js 0.13.1. BREAKING CHANGE: `tick()` methods were removed from individual peripherals. You now need to call `cpu.tick()` instead.
1 parent 36c4134 commit 9c1288f

File tree

13 files changed

+222
-225
lines changed

13 files changed

+222
-225
lines changed

demo/src/execute.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ export class AVRRunner {
5050
const cyclesToRun = this.cpu.cycles + this.workUnitCycles;
5151
while (this.cpu.cycles < cyclesToRun) {
5252
avrInstruction(this.cpu);
53-
this.timer0.tick();
54-
this.timer1.tick();
55-
this.timer2.tick();
56-
this.usart.tick();
5753
this.cpu.tick();
5854
}
5955

src/cpu/cpu.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ export interface AVRInterruptConfig {
5555
constant?: boolean;
5656
}
5757

58+
export type AVRClockEventCallback = () => void;
59+
60+
interface AVRClockEventEntry {
61+
cycles: number;
62+
callback: AVRClockEventCallback;
63+
}
64+
5865
export class CPU implements ICPU {
5966
readonly data: Uint8Array = new Uint8Array(this.sramBytes + registerSpace);
6067
readonly data16 = new Uint16Array(this.data.buffer);
@@ -63,6 +70,7 @@ export class CPU implements ICPU {
6370
readonly readHooks: CPUMemoryReadHooks = [];
6471
readonly writeHooks: CPUMemoryHooks = [];
6572
private readonly pendingInterrupts: AVRInterruptConfig[] = [];
73+
private readonly clockEvents: AVRClockEventEntry[] = [];
6674
readonly pc22Bits = this.progBytes.length > 0x20000;
6775

6876
// This lets the Timer Compare output override GPIO pins:
@@ -71,6 +79,7 @@ export class CPU implements ICPU {
7179
pc: u32 = 0;
7280
cycles: u32 = 0;
7381
nextInterrupt: i16 = -1;
82+
private nextClockEvent: u32 = 0;
7483

7584
constructor(public progMem: Uint16Array, private sramBytes = 8192) {
7685
this.reset();
@@ -164,10 +173,44 @@ export class CPU implements ICPU {
164173
}
165174
}
166175

176+
private updateClockEvents() {
177+
this.clockEvents.sort((a, b) => a.cycles - b.cycles);
178+
this.nextClockEvent = this.clockEvents[0]?.cycles ?? 0;
179+
}
180+
181+
addClockEvent(callback: AVRClockEventCallback, cycles: number) {
182+
const entry = { cycles: this.cycles + Math.max(1, cycles), callback };
183+
this.clockEvents.push(entry);
184+
this.updateClockEvents();
185+
return callback;
186+
}
187+
188+
updateClockEvent(callback: AVRClockEventCallback, cycles: number) {
189+
const entry = this.clockEvents.find((item) => (item.callback = callback));
190+
if (entry) {
191+
entry.cycles = this.cycles + Math.max(1, cycles);
192+
this.updateClockEvents();
193+
return true;
194+
}
195+
return false;
196+
}
197+
198+
clearClockEvent(callback: AVRClockEventCallback) {
199+
const index = this.clockEvents.findIndex((item) => (item.callback = callback));
200+
if (index >= 0) {
201+
this.clockEvents.splice(index, 1);
202+
this.updateClockEvents();
203+
}
204+
}
205+
167206
tick() {
207+
if (this.nextClockEvent && this.nextClockEvent <= this.cycles) {
208+
const clockEvent = this.clockEvents.shift();
209+
clockEvent?.callback();
210+
this.nextClockEvent = this.clockEvents[0]?.cycles ?? 0;
211+
}
168212
if (this.interruptsEnabled && this.nextInterrupt >= 0) {
169213
const interrupt = this.pendingInterrupts[this.nextInterrupt];
170-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
171214
avrInterrupt(this, interrupt.address);
172215
if (!interrupt.constant) {
173216
this.clearInterrupt(interrupt);

src/peripherals/eeprom.spec.ts

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ describe('EEPROM', () => {
2323
describe('Reading the EEPROM', () => {
2424
it('should return 0xff when reading from an empty location', () => {
2525
const cpu = new CPU(new Uint16Array(0x1000));
26-
const eeprom = new AVREEPROM(cpu, new EEPROMMemoryBackend(1024));
26+
new AVREEPROM(cpu, new EEPROMMemoryBackend(1024));
2727
cpu.writeData(EEARL, 0);
2828
cpu.writeData(EEARH, 0);
2929
cpu.writeData(EECR, EERE);
30-
eeprom.tick();
3130
cpu.tick();
3231
expect(cpu.cycles).toEqual(4);
3332
expect(cpu.data[EEDR]).toEqual(0xff);
@@ -36,12 +35,11 @@ describe('EEPROM', () => {
3635
it('should return the value stored at the given EEPROM address', () => {
3736
const cpu = new CPU(new Uint16Array(0x1000));
3837
const eepromBackend = new EEPROMMemoryBackend(1024);
39-
const eeprom = new AVREEPROM(cpu, eepromBackend);
38+
new AVREEPROM(cpu, eepromBackend);
4039
eepromBackend.memory[0x250] = 0x42;
4140
cpu.writeData(EEARL, 0x50);
4241
cpu.writeData(EEARH, 0x2);
4342
cpu.writeData(EECR, EERE);
44-
eeprom.tick();
4543
cpu.tick();
4644
expect(cpu.data[EEDR]).toEqual(0x42);
4745
});
@@ -51,13 +49,12 @@ describe('EEPROM', () => {
5149
it('should write a byte to the given EEPROM address', () => {
5250
const cpu = new CPU(new Uint16Array(0x1000));
5351
const eepromBackend = new EEPROMMemoryBackend(1024);
54-
const eeprom = new AVREEPROM(cpu, eepromBackend);
52+
new AVREEPROM(cpu, eepromBackend);
5553
cpu.writeData(EEDR, 0x55);
5654
cpu.writeData(EEARL, 15);
5755
cpu.writeData(EEARH, 0);
5856
cpu.writeData(EECR, EEMPE);
5957
cpu.writeData(EECR, EEPE);
60-
eeprom.tick();
6158
cpu.tick();
6259
expect(cpu.cycles).toEqual(2);
6360
expect(eepromBackend.memory[15]).toEqual(0x55);
@@ -84,10 +81,10 @@ describe('EEPROM', () => {
8481

8582
const cpu = new CPU(program);
8683
const eepromBackend = new EEPROMMemoryBackend(1024);
87-
const eeprom = new AVREEPROM(cpu, eepromBackend);
84+
new AVREEPROM(cpu, eepromBackend);
8885
eepromBackend.memory[9] = 0x0f; // high four bits are cleared
8986

90-
const runner = new TestProgramRunner(cpu, eeprom);
87+
const runner = new TestProgramRunner(cpu);
9188
runner.runInstructions(instructionCount);
9289

9390
// EEPROM was 0x0f, and our program wrote 0x55.
@@ -98,22 +95,20 @@ describe('EEPROM', () => {
9895
it('should clear the EEPE bit and fire an interrupt when write has been completed', () => {
9996
const cpu = new CPU(new Uint16Array(0x1000));
10097
const eepromBackend = new EEPROMMemoryBackend(1024);
101-
const eeprom = new AVREEPROM(cpu, eepromBackend);
98+
new AVREEPROM(cpu, eepromBackend);
10299
cpu.writeData(EEDR, 0x55);
103100
cpu.writeData(EEARL, 15);
104101
cpu.writeData(EEARH, 0);
105102
cpu.writeData(EECR, EEMPE | EERIE);
106103
cpu.data[SREG] = 0x80; // SREG: I-------
107104
cpu.writeData(EECR, EEPE);
108105
cpu.cycles += 1000;
109-
eeprom.tick();
110106
cpu.tick();
111107
// At this point, write shouldn't be complete yet
112108
expect(cpu.data[EECR] & EEPE).toEqual(EEPE);
113109
expect(cpu.pc).toEqual(0);
114110
cpu.cycles += 10000000;
115111
// And now, 10 million cycles later, it should.
116-
eeprom.tick();
117112
cpu.tick();
118113
expect(eepromBackend.memory[15]).toEqual(0x55);
119114
expect(cpu.data[EECR] & EEPE).toEqual(0);
@@ -123,16 +118,14 @@ describe('EEPROM', () => {
123118
it('should skip the write if EEMPE is clear', () => {
124119
const cpu = new CPU(new Uint16Array(0x1000));
125120
const eepromBackend = new EEPROMMemoryBackend(1024);
126-
const eeprom = new AVREEPROM(cpu, eepromBackend);
121+
new AVREEPROM(cpu, eepromBackend);
127122
cpu.writeData(EEDR, 0x55);
128123
cpu.writeData(EEARL, 15);
129124
cpu.writeData(EEARH, 0);
130125
cpu.writeData(EECR, EEMPE);
131126
cpu.cycles = 8; // waiting for more than 4 cycles should clear EEMPE
132-
eeprom.tick();
133127
cpu.tick();
134128
cpu.writeData(EECR, EEPE);
135-
eeprom.tick();
136129
cpu.tick();
137130
// Ensure that nothing was written, and EEPE bit is clear
138131
expect(cpu.cycles).toEqual(8);
@@ -143,15 +136,14 @@ describe('EEPROM', () => {
143136
it('should skip the write if another write is already in progress', () => {
144137
const cpu = new CPU(new Uint16Array(0x1000));
145138
const eepromBackend = new EEPROMMemoryBackend(1024);
146-
const eeprom = new AVREEPROM(cpu, eepromBackend);
139+
new AVREEPROM(cpu, eepromBackend);
147140

148141
// Write 0x55 to address 15
149142
cpu.writeData(EEDR, 0x55);
150143
cpu.writeData(EEARL, 15);
151144
cpu.writeData(EEARH, 0);
152145
cpu.writeData(EECR, EEMPE);
153146
cpu.writeData(EECR, EEPE);
154-
eeprom.tick();
155147
cpu.tick();
156148
expect(cpu.cycles).toEqual(2);
157149

@@ -161,7 +153,6 @@ describe('EEPROM', () => {
161153
cpu.writeData(EEARH, 0);
162154
cpu.writeData(EECR, EEMPE);
163155
cpu.writeData(EECR, EEPE);
164-
eeprom.tick();
165156
cpu.tick();
166157

167158
// Ensure that second write didn't happen
@@ -173,21 +164,19 @@ describe('EEPROM', () => {
173164
it('should write two bytes sucessfully', () => {
174165
const cpu = new CPU(new Uint16Array(0x1000));
175166
const eepromBackend = new EEPROMMemoryBackend(1024);
176-
const eeprom = new AVREEPROM(cpu, eepromBackend);
167+
new AVREEPROM(cpu, eepromBackend);
177168

178169
// Write 0x55 to address 15
179170
cpu.writeData(EEDR, 0x55);
180171
cpu.writeData(EEARL, 15);
181172
cpu.writeData(EEARH, 0);
182173
cpu.writeData(EECR, EEMPE);
183174
cpu.writeData(EECR, EEPE);
184-
eeprom.tick();
185175
cpu.tick();
186176
expect(cpu.cycles).toEqual(2);
187177

188178
// wait long enough time for the first write to finish
189179
cpu.cycles += 10000000;
190-
eeprom.tick();
191180
cpu.tick();
192181

193182
// Write 0x66 to address 16
@@ -196,7 +185,6 @@ describe('EEPROM', () => {
196185
cpu.writeData(EEARH, 0);
197186
cpu.writeData(EECR, EEMPE);
198187
cpu.writeData(EECR, EEPE);
199-
eeprom.tick();
200188
cpu.tick();
201189

202190
// Ensure both writes took place
@@ -226,10 +214,10 @@ describe('EEPROM', () => {
226214

227215
const cpu = new CPU(program);
228216
const eepromBackend = new EEPROMMemoryBackend(1024);
229-
const eeprom = new AVREEPROM(cpu, eepromBackend);
217+
new AVREEPROM(cpu, eepromBackend);
230218
eepromBackend.memory[9] = 0x22;
231219

232-
const runner = new TestProgramRunner(cpu, eeprom);
220+
const runner = new TestProgramRunner(cpu);
233221
runner.runInstructions(instructionCount);
234222

235223
expect(eepromBackend.memory[9]).toEqual(0xff);

src/peripherals/eeprom.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@ export class AVREEPROM {
9595
}
9696

9797
if (eecr & EEMPE) {
98-
this.writeEnabledCycles = this.cpu.cycles + 4;
98+
const eempeCycles = 4;
99+
this.writeEnabledCycles = this.cpu.cycles + eempeCycles;
100+
this.cpu.addClockEvent(() => {
101+
this.cpu.data[EECR] &= ~EEMPE;
102+
}, eempeCycles);
99103
}
100104

101105
// Read
@@ -134,6 +138,11 @@ export class AVREEPROM {
134138
}
135139

136140
this.cpu.data[EECR] |= EEPE;
141+
142+
this.cpu.addClockEvent(() => {
143+
this.cpu.setInterruptFlag(this.EER);
144+
}, this.writeCompleteCycles - this.cpu.cycles);
145+
137146
// When EEPE has been set, the CPU is halted for two cycles before the
138147
// next instruction is executed.
139148
this.cpu.cycles += 2;
@@ -143,15 +152,4 @@ export class AVREEPROM {
143152
return false;
144153
};
145154
}
146-
147-
tick() {
148-
const { EECR } = this.config;
149-
150-
if (this.writeEnabledCycles && this.cpu.cycles > this.writeEnabledCycles) {
151-
this.cpu.data[EECR] &= ~EEMPE;
152-
}
153-
if (this.writeCompleteCycles && this.cpu.cycles > this.writeCompleteCycles) {
154-
this.cpu.setInterruptFlag(this.EER);
155-
}
156-
}
157155
}

src/peripherals/spi.spec.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ describe('SPI', () => {
160160
return 0x5b; // we copy this byte to
161161
};
162162

163-
const runner = new TestProgramRunner(cpu, spi);
163+
const runner = new TestProgramRunner(cpu);
164164
runner.runToBreak();
165165

166166
// 16 cycles per clock * 8 bits = 128
@@ -172,11 +172,10 @@ describe('SPI', () => {
172172

173173
it('should set the WCOL bit in SPSR if writing to SPDR while SPI is already transmitting', () => {
174174
const cpu = new CPU(new Uint16Array(1024));
175-
const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
175+
new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
176176

177177
cpu.writeData(SPCR, SPE | MSTR);
178178
cpu.writeData(SPDR, 0x50);
179-
spi.tick();
180179
cpu.tick();
181180
expect(cpu.readData(SPSR) & WCOL).toEqual(0);
182181

@@ -186,21 +185,19 @@ describe('SPI', () => {
186185

187186
it('should clear the SPIF bit and fire an interrupt when SPI transfer completes', () => {
188187
const cpu = new CPU(new Uint16Array(1024));
189-
const spi = new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
188+
new AVRSPI(cpu, spiConfig, FREQ_16MHZ);
190189

191190
cpu.writeData(SPCR, SPE | SPIE | MSTR);
192191
cpu.writeData(SPDR, 0x50);
193192
cpu.data[SREG] = 0x80; // SREG: I-------
194193

195194
// At this point, write shouldn't be complete yet
196195
cpu.cycles += 10;
197-
spi.tick();
198196
cpu.tick();
199197
expect(cpu.pc).toEqual(0);
200198

201199
// 100 cycles later, it should (8 bits * 8 cycles per bit = 64).
202200
cpu.cycles += 100;
203-
spi.tick();
204201
cpu.tick();
205202
expect(cpu.data[SPSR] & SPIF).toEqual(0);
206203
expect(cpu.pc).toEqual(0x22); // SPI Ready interrupt
@@ -215,12 +212,10 @@ describe('SPI', () => {
215212
cpu.writeData(SPDR, 0x8f);
216213

217214
cpu.cycles = 10;
218-
spi.tick();
219215
cpu.tick();
220216
expect(cpu.readData(SPDR)).toEqual(0);
221217

222218
cpu.cycles = 32; // 4 cycles per bit * 8 bits = 32
223-
spi.tick();
224219
cpu.tick();
225220
expect(cpu.readData(SPDR)).toEqual(0x88);
226221
});

0 commit comments

Comments
 (0)