Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/cpu/cpu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type CPUMemoryReadHook = (addr: u16) => u8;
export interface CPUMemoryReadHooks {
[key: number]: CPUMemoryReadHook;
}

export class CPU implements ICPU {
readonly data: Uint8Array = new Uint8Array(this.sramBytes + registerSpace);
readonly data16 = new Uint16Array(this.data.buffer);
Expand All @@ -53,6 +54,9 @@ export class CPU implements ICPU {
readonly writeHooks: CPUMemoryHooks = [];
readonly pc22Bits = this.progBytes.length > 0x20000;

// This lets the Timer Compare output override GPIO pins:
readonly gpioTimerHooks: CPUMemoryHooks = [];

pc = 0;
cycles = 0;

Expand Down
64 changes: 53 additions & 11 deletions src/peripherals/gpio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,34 @@ export enum PinState {
InputPullUp,
}

/* This mechanism allows timers to override specific GPIO pins */
export enum PinOverrideMode {
None,
Enable,
Set,
Clear,
Toggle,
}

export class AVRIOPort {
private listeners: GPIOListener[] = [];
private pinValue: u8 = 0;
private overrideMask: u8 = 0xff;
private overrideValue: u8;
private lastValue: u8 = 0;
private lastDdr: u8 = 0;

constructor(private cpu: CPU, private portConfig: AVRPortConfig) {
cpu.writeHooks[portConfig.DDR] = (value, oldValue) => {
cpu.writeHooks[portConfig.DDR] = (value: u8) => {
const portValue = cpu.data[portConfig.PORT];
this.updatePinRegister(portValue, value);
this.writeGpio(value & portValue, oldValue & oldValue);
this.writeGpio(portValue, value);
};
cpu.writeHooks[portConfig.PORT] = (value: u8, oldValue: u8) => {
cpu.writeHooks[portConfig.PORT] = (value: u8) => {
const ddrMask = cpu.data[portConfig.DDR];
cpu.data[portConfig.PORT] = value;
value &= ddrMask;
cpu.data[portConfig.PIN] = (cpu.data[portConfig.PIN] & ~ddrMask) | value;
this.updatePinRegister(value, ddrMask);
this.writeGpio(value, oldValue & ddrMask);
this.writeGpio(value, ddrMask);
return true;
};
cpu.writeHooks[portConfig.PIN] = (value: u8) => {
Expand All @@ -116,9 +127,34 @@ export class AVRIOPort {
const portValue = oldPortValue ^ value;
cpu.data[portConfig.PORT] = portValue;
cpu.data[portConfig.PIN] = (cpu.data[portConfig.PIN] & ~ddrMask) | (portValue & ddrMask);
this.writeGpio(portValue & ddrMask, oldPortValue & ddrMask);
this.writeGpio(portValue, ddrMask);
return true;
};
// The following hook is used by the timer compare output to override GPIO pins:
cpu.gpioTimerHooks[portConfig.PORT] = (pin: u8, mode: PinOverrideMode) => {
const pinMask = 1 << pin;
if (mode == PinOverrideMode.None) {
this.overrideMask |= pinMask;
} else {
this.overrideMask &= ~pinMask;
switch (mode) {
case PinOverrideMode.Enable:
this.overrideValue &= ~pinMask;
this.overrideValue |= cpu.data[portConfig.PORT] & pinMask;
break;
case PinOverrideMode.Set:
this.overrideValue |= pinMask;
break;
case PinOverrideMode.Clear:
this.overrideValue &= ~pinMask;
break;
case PinOverrideMode.Toggle:
this.overrideValue ^= pinMask;
break;
}
}
this.writeGpio(cpu.data[portConfig.PORT], cpu.data[portConfig.DDR]);
};
}

addListener(listener: GPIOListener) {
Expand All @@ -142,7 +178,7 @@ export class AVRIOPort {
const port = this.cpu.data[this.portConfig.PORT];
const bitMask = 1 << index;
if (ddr & bitMask) {
return port & bitMask ? PinState.High : PinState.Low;
return this.lastValue & bitMask ? PinState.High : PinState.Low;
} else {
return port & bitMask ? PinState.InputPullUp : PinState.Input;
}
Expand All @@ -165,9 +201,15 @@ export class AVRIOPort {
this.cpu.data[this.portConfig.PIN] = (this.pinValue & ~ddr) | (port & ddr);
}

private writeGpio(value: u8, oldValue: u8) {
for (const listener of this.listeners) {
listener(value, oldValue);
private writeGpio(value: u8, ddr: u8) {
const newValue = ((value & this.overrideMask) | this.overrideValue) & ddr;
const prevValue = this.lastValue;
if (newValue !== prevValue || ddr !== this.lastDdr) {
this.lastValue = newValue;
this.lastDdr = ddr;
for (const listener of this.listeners) {
listener(newValue, prevValue);
}
}
}
}
158 changes: 144 additions & 14 deletions src/peripherals/timer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CPU } from '../cpu/cpu';
import { avrInstruction } from '../cpu/instruction';
import { assemble } from '../utils/assembler';
import { AVRTimer, timer0Config, timer1Config, timer2Config } from './timer';
import { PinOverrideMode } from './gpio';

describe('timer', () => {
let cpu: CPU;
Expand Down Expand Up @@ -57,20 +58,6 @@ describe('timer', () => {
expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR
});

it('should set TOV if timer overflows in PWM Phase Correct mode', () => {
const timer = new AVRTimer(cpu, timer0Config);
cpu.writeData(0x46, 0xff); // TCNT0 <- 0xff
timer.tick();
cpu.writeData(0x47, 0x7f); // OCRA <- 0x7f
cpu.writeData(0x44, 0x1); // WGM0 <- 1 (PWM, Phase Correct)
cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
cpu.cycles = 1;
timer.tick();
const tcnt = cpu.readData(0x46);
expect(tcnt).toEqual(0); // TCNT should be 0
expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR
});

it('should set TOV if timer overflows in FAST PWM mode', () => {
const timer = new AVRTimer(cpu, timer0Config);
cpu.writeData(0x46, 0xff); // TCNT0 <- 0xff
Expand Down Expand Up @@ -273,6 +260,108 @@ describe('timer', () => {
expect(cpu.data[1]).toEqual(2); // r1 should equal 2
});

describe('Phase-correct PWM mode', () => {
it('should count up to TOP, down to 0, and then set TOV flag', () => {
const program = [
// Set waveform generation mode (WGM) to PWM, Phase Correct, top OCR0A
'LDI r16, 0x1', // TCCR0A = 1 << WGM00;
'OUT 0x24, r16',
'LDI r16, 0x9', // TCCR0B = (1 << WGM02) | (1 << CS00);
'OUT 0x25, r16',
'LDI r16, 0x3', // OCR0A = 0x3;
'OUT 0x27, r16',
'LDI r16, 0x2', // TCNT0 = 0x2;
'OUT 0x26, r16',
];
const nops = [
'NOP', // TCNT0 will be 3
'NOP', // TCNT0 will be 2
'NOP', // TCNT0 will be 1
'NOP', // TCNT0 will be 0
'NOP', // TCNT0 will be 1 (end of test)
];
loadProgram(...program, ...nops);
const timer = new AVRTimer(cpu, timer0Config);

for (let i = 0; i < program.length; i++) {
avrInstruction(cpu);
timer.tick();
}
expect(cpu.readData(0x46)).toEqual(2); // TCNT should be 2

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x46)).toEqual(3); // TCNT should be 3

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x46)).toEqual(2); // TCNT should be 2

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x46)).toEqual(1); // TCNT should be 1
expect(cpu.data[0x35] & 0x1).toEqual(0); // TIFR should have TOV bit clear

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x46)).toEqual(0); // TCNT should be 0
expect(cpu.data[0x35] & 0x1).toEqual(1); // TIFR should have TOV bit set

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x46)).toEqual(1); // TCNT should be 1
});

it('should clear OC0A when TCNT0=OCR0A and counting up', () => {
const program = [
// Set waveform generation mode (WGM) to PWM, Phase Correct
'LDI r16, 0x81', // TCCR0A = (1 << COM0A1) || (1 << WGM01);
'OUT 0x24, r16',
'LDI r16, 0x1', // TCCR0B = (1 << CS00);
'OUT 0x25, r16',
'LDI r16, 0xfe', // OCR0A = 0xfe;
'OUT 0x27, r16',
'LDI r16, 0xfd', // TCNT0 = 0xfd;
'OUT 0x26, r16',
];
const nops = [
'NOP', // TCNT0 will be 0xfe
'NOP', // TCNT0 will be 0xff
'NOP', // TCNT0 will be 0xfe again (end of test)
];
loadProgram(...program, ...nops);
const timer = new AVRTimer(cpu, timer0Config);

// Listen to Port D's internal callback
const gpioCallback = jest.fn();
cpu.gpioTimerHooks[0x2b] = gpioCallback;

for (let i = 0; i < program.length; i++) {
avrInstruction(cpu);
timer.tick();
}
expect(cpu.readData(0x46)).toEqual(0xfd); // TCNT0 should be 0xfd
expect(gpioCallback).toHaveBeenCalledWith(6, PinOverrideMode.Enable, 0x2b);
gpioCallback.mockClear();

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x46)).toEqual(0xfe); // TCNT should be 0xfe
expect(gpioCallback).toHaveBeenCalledWith(6, PinOverrideMode.Clear, 0x2b);
gpioCallback.mockClear();

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x46)).toEqual(0xff); // TCNT should be 0xff
expect(gpioCallback).not.toHaveBeenCalled();

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x46)).toEqual(0xfe); // TCNT should be 0xfe
expect(gpioCallback).toHaveBeenCalledWith(6, PinOverrideMode.Set, 0x2b);
});
});

describe('16 bit timers', () => {
it('should increment 16-bit TCNT by 1', () => {
const timer = new AVRTimer(cpu, timer1Config);
Expand Down Expand Up @@ -365,5 +454,46 @@ describe('timer', () => {
const timerLow = cpu.readData(0x84);
expect((timerHigh << 8) | timerLow).toEqual(0xff00);
});

it('should toggle OC1B on Compare Match', () => {
const program = [
// Set waveform generation mode (WGM) to Normal, top 0xFFFF
'LDI r16, 0x10', // TCCR1A = (1 << COM1B0);
'STS 0x80, r16',
'LDI r16, 0x1', // TCCR1B = (1 << CS00);
'STS 0x81, r16',
'LDI r16, 0x0', // OCR1BH = 0x0;
'STS 0x8B, r16',
'LDI r16, 0x4a', // OCR1BL = 0x4a;
'STS 0x8A, r16',
'LDI r16, 0x0', // TCNT1H = 0x0;
'STS 0x85, r16',
'LDI r16, 0x49', // TCNT1L = 0x49;
'STS 0x84, r16',
];
const nops = [
'NOP', // TCNT1 will be 0x49
'NOP', // TCNT1 will be 0x4a
];
loadProgram(...program, ...nops);
const timer = new AVRTimer(cpu, timer1Config);

// Listen to Port B's internal callback
const gpioCallback = jest.fn();
cpu.gpioTimerHooks[0x25] = gpioCallback;

for (let i = 0; i < program.length; i++) {
avrInstruction(cpu);
timer.tick();
}
expect(cpu.readData(0x84)).toEqual(0x49); // TCNT1 should be 0x49
expect(gpioCallback).toHaveBeenCalledWith(2, PinOverrideMode.Enable, 0x25);
gpioCallback.mockClear();

avrInstruction(cpu);
timer.tick();
expect(cpu.readData(0x84)).toEqual(0x4a); // TCNT1 should be 0x4a
expect(gpioCallback).toHaveBeenCalledWith(2, PinOverrideMode.Toggle, 0x25);
});
});
});
Loading