From 3a38607f11575482e74cdba37b26c9957532e8d2 Mon Sep 17 00:00:00 2001 From: Uri Shaked Date: Thu, 7 May 2020 13:03:15 +0300 Subject: [PATCH] feat(timer): Compare Match Output --- src/cpu/cpu.ts | 4 + src/peripherals/gpio.ts | 64 ++++++++++--- src/peripherals/timer.spec.ts | 158 ++++++++++++++++++++++++++++--- src/peripherals/timer.ts | 173 +++++++++++++++++++++++++++------- 4 files changed, 342 insertions(+), 57 deletions(-) diff --git a/src/cpu/cpu.ts b/src/cpu/cpu.ts index 5663a1c..ed88e71 100644 --- a/src/cpu/cpu.ts +++ b/src/cpu/cpu.ts @@ -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); @@ -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; diff --git a/src/peripherals/gpio.ts b/src/peripherals/gpio.ts index e3c8eaa..cb21669 100644 --- a/src/peripherals/gpio.ts +++ b/src/peripherals/gpio.ts @@ -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) => { @@ -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) { @@ -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; } @@ -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); + } } } } diff --git a/src/peripherals/timer.spec.ts b/src/peripherals/timer.spec.ts index 8733f73..f4b0c69 100644 --- a/src/peripherals/timer.spec.ts +++ b/src/peripherals/timer.spec.ts @@ -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; @@ -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 @@ -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); @@ -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); + }); }); }); diff --git a/src/peripherals/timer.ts b/src/peripherals/timer.ts index 4e75fb1..4293a49 100644 --- a/src/peripherals/timer.ts +++ b/src/peripherals/timer.ts @@ -8,6 +8,7 @@ import { CPU } from '../cpu/cpu'; import { avrInterrupt } from '../cpu/interrupt'; +import { portBConfig, portDConfig, PinOverrideMode } from './gpio'; const timer01Dividers = { 0: 0, @@ -61,6 +62,12 @@ interface AVRTimerConfig { TIMSK: u8; dividers: TimerDividers; + + // Output compare pins + compPortA: u16; + compPinA: u8; + compPortB: u16; + compPinB: u8; } export const timer0Config: AVRTimerConfig = { @@ -79,6 +86,10 @@ export const timer0Config: AVRTimerConfig = { TCCRC: 0, // not available TIMSK: 0x6e, dividers: timer01Dividers, + compPortA: portDConfig.PORT, + compPinA: 6, + compPortB: portDConfig.PORT, + compPinB: 5, }; export const timer1Config: AVRTimerConfig = { @@ -97,6 +108,10 @@ export const timer1Config: AVRTimerConfig = { TCCRC: 0x82, TIMSK: 0x6f, dividers: timer01Dividers, + compPortA: portBConfig.PORT, + compPinA: 1, + compPortB: portBConfig.PORT, + compPinB: 2, }; export const timer2Config: AVRTimerConfig = { @@ -124,6 +139,10 @@ export const timer2Config: AVRTimerConfig = { 6: 256, 7: 1024, }, + compPortA: portBConfig.PORT, + compPinA: 3, + compPortB: portDConfig.PORT, + compPinB: 3, }; /* All the following types and constants are related to WGM (Waveform Generation Mode) bits: */ @@ -185,14 +204,36 @@ const wgmModes16Bit: WGMConfig[] = [ /*15*/ [TimerMode.FastPWM, TopOCRA, OCRUpdateMode.Bottom, TOVUpdateMode.Top], ]; +type CompBitsValue = 0 | 1 | 2 | 3; + +function compToOverride(comp: CompBitsValue) { + switch (comp) { + case 1: + return PinOverrideMode.Toggle; + case 2: + return PinOverrideMode.Clear; + case 3: + return PinOverrideMode.Set; + default: + return PinOverrideMode.Enable; + } +} + export class AVRTimer { private lastCycle = 0; private ocrA: u16 = 0; private ocrB: u16 = 0; + private icr: u16 = 0; // only for 16-bit timers private timerMode: TimerMode; private topValue: TimerTopValue; private tcnt: u16 = 0; + private compA: CompBitsValue; + private compB: CompBitsValue; private tcntUpdated = false; + private countingUp = true; + + // This is the temporary register used to access 16-bit registers (section 16.3 of the datasheet) + private highByteTemp: u8 = 0; constructor(private cpu: CPU, private config: AVRTimerConfig) { this.updateWGMConfig(); @@ -205,20 +246,36 @@ export class AVRTimer { }; this.cpu.writeHooks[config.TCNT] = (value: u8) => { - const highByte = this.config.bits === 16 ? this.cpu.data[config.TCNT + 1] : 0; - this.tcnt = (highByte << 8) | value; + this.tcnt = (this.highByteTemp << 8) | value; this.tcntUpdated = true; this.timerUpdated(); }; - this.registerHook(config.OCRA, (value: u16) => { + this.cpu.writeHooks[config.OCRA] = (value: u8) => { // TODO implement buffering when timer running in PWM mode - this.ocrA = value; - }); - this.registerHook(config.OCRB, (value: u16) => { - this.ocrB = value; - }); + this.ocrA = (this.highByteTemp << 8) | value; + }; + this.cpu.writeHooks[config.OCRB] = (value: u8) => { + // TODO implement buffering when timer running in PWM mode + this.ocrB = (this.highByteTemp << 8) | value; + }; + this.cpu.writeHooks[config.ICR] = (value: u8) => { + this.icr = (this.highByteTemp << 8) | value; + }; + if (this.config.bits === 16) { + const updateTempRegister = (value: u8) => { + this.highByteTemp = value; + }; + this.cpu.writeHooks[config.TCNT + 1] = updateTempRegister; + this.cpu.writeHooks[config.OCRA + 1] = updateTempRegister; + this.cpu.writeHooks[config.OCRB + 1] = updateTempRegister; + this.cpu.writeHooks[config.ICR + 1] = updateTempRegister; + } cpu.writeHooks[config.TCCRA] = (value) => { this.cpu.data[config.TCCRA] = value; + this.compA = ((value >> 6) & 0x3) as CompBitsValue; + this.updateCompA(this.compA ? PinOverrideMode.Enable : PinOverrideMode.None); + this.compB = ((value >> 4) & 0x3) as CompBitsValue; + this.updateCompB(this.compB ? PinOverrideMode.Enable : PinOverrideMode.None); this.updateWGMConfig(); return true; }; @@ -255,11 +312,6 @@ export class AVRTimer { return this.cpu.data[this.config.TIMSK]; } - get ICR() { - // Only available for 16-bit timers - return (this.cpu.data[this.config.ICR + 1] << 8) | this.cpu.data[this.config.ICR]; - } - get CS() { return (this.TCCRB & 0x7) as 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; } @@ -274,21 +326,12 @@ export class AVRTimer { case TopOCRA: return this.ocrA; case TopICR: - return this.ICR; + return this.icr; default: return this.topValue; } } - private registerHook(address: number, hook: (value: u16) => void) { - if (this.config.bits === 16) { - this.cpu.writeHooks[address] = (value: u8) => hook((this.cpu.data[address + 1] << 8) | value); - this.cpu.writeHooks[address + 1] = (value: u8) => hook((value << 8) | this.cpu.data[address]); - } else { - this.cpu.writeHooks[address] = hook; - } - } - private updateWGMConfig() { const wgmModes = this.config.bits === 16 ? wgmModes16Bit : wgmModes8Bit; const [timerMode, topValue] = wgmModes[this.WGM]; @@ -303,20 +346,18 @@ export class AVRTimer { const counterDelta = Math.floor(delta / divider); this.lastCycle += counterDelta * divider; const val = this.tcnt; - const newVal = (val + counterDelta) % (this.TOP + 1); + const { timerMode } = this; + const phasePwm = + timerMode === TimerMode.PWMPhaseCorrect || timerMode === TimerMode.PWMPhaseFrequencyCorrect; + const newVal = phasePwm + ? this.phasePwmCount(val, counterDelta) + : (val + counterDelta) % (this.TOP + 1); // A CPU write overrides (has priority over) all counter clear or count operations. if (!this.tcntUpdated) { this.tcnt = newVal; this.timerUpdated(); } - const { timerMode } = this; - if ( - (timerMode === TimerMode.Normal || - timerMode === TimerMode.PWMPhaseCorrect || - timerMode === TimerMode.PWMPhaseFrequencyCorrect || - timerMode === TimerMode.FastPWM) && - val > newVal - ) { + if ((timerMode === TimerMode.Normal || timerMode === TimerMode.FastPWM) && val > newVal) { this.TIFR |= TOV; } } @@ -337,8 +378,28 @@ export class AVRTimer { } } + private phasePwmCount(value: u16, delta: u8) { + while (delta > 0) { + if (this.countingUp) { + value++; + if (value === this.TOP && !this.tcntUpdated) { + this.countingUp = false; + } + } else { + value--; + if (!value && !this.tcntUpdated) { + this.countingUp = true; + this.TIFR |= TOV; + } + } + delta--; + } + return value; + } + private timerUpdated() { const value = this.tcnt; + if (this.ocrA && value === this.ocrA) { this.TIFR |= OCFA; if (this.timerMode === TimerMode.CTC) { @@ -346,9 +407,57 @@ export class AVRTimer { this.tcnt = 0; this.TIFR |= TOV; } + if (this.compA) { + this.updateCompPin(this.compA, 'A'); + } } if (this.ocrB && value === this.ocrB) { this.TIFR |= OCFB; + if (this.compB) { + this.updateCompPin(this.compB, 'B'); + } + } + } + + private updateCompPin(compValue: CompBitsValue, pinName: 'A' | 'B') { + let newValue: PinOverrideMode = PinOverrideMode.None; + const inverted = compValue === 3; + const isSet = this.countingUp === inverted; + switch (this.timerMode) { + case TimerMode.Normal: + case TimerMode.CTC: + case TimerMode.FastPWM: + newValue = compToOverride(compValue); + break; + + case TimerMode.PWMPhaseCorrect: + case TimerMode.PWMPhaseFrequencyCorrect: + newValue = isSet ? PinOverrideMode.Set : PinOverrideMode.Clear; + break; + } + + if (newValue !== PinOverrideMode.None) { + if (pinName === 'A') { + this.updateCompA(newValue); + } else { + this.updateCompB(newValue); + } + } + } + + private updateCompA(value: PinOverrideMode) { + const { compPortA, compPinA } = this.config; + const hook = this.cpu.gpioTimerHooks[compPortA]; + if (hook) { + hook(compPinA, value, compPortA); + } + } + + private updateCompB(value: PinOverrideMode) { + const { compPortB, compPinB } = this.config; + const hook = this.cpu.gpioTimerHooks[compPortB]; + if (hook) { + hook(compPinB, value, compPortB); } } }