diff options
Diffstat (limited to '')
| -rw-r--r-- | src/index.ts | 2 | ||||
| -rw-r--r-- | src/peripherals/timer-attiny.spec.ts | 170 | ||||
| -rw-r--r-- | src/peripherals/timer-attiny.ts | 429 |
3 files changed, 601 insertions, 0 deletions
diff --git a/src/index.ts b/src/index.ts index 9f8d27a..f951897 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,6 +47,8 @@ export { AVRSPI, spiConfig } from './peripherals/spi'; export type { SPIConfig, SPITransferCallback } from './peripherals/spi'; export { AVRTimer, timer0Config, timer1Config, timer2Config } from './peripherals/timer'; export type { AVRTimerConfig } from './peripherals/timer'; +export { ATtinyTimer1, attinyTimer1Config } from './peripherals/timer-attiny'; +export type { ATtinyTimer1Config } from './peripherals/timer-attiny'; export * from './peripherals/twi'; export { AVRUSART, usart0Config } from './peripherals/usart'; export { AVRUSI } from './peripherals/usi'; diff --git a/src/peripherals/timer-attiny.spec.ts b/src/peripherals/timer-attiny.spec.ts new file mode 100644 index 0000000..e81a6e8 --- /dev/null +++ b/src/peripherals/timer-attiny.spec.ts @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) Uri Shaked and contributors + +import { describe, expect, it } from 'vitest'; +import { CPU } from '../cpu/cpu'; +import { AVRIOPort } from './gpio'; +import { ATtinyTimer1, attinyTimer1Config } from './timer-attiny'; + +const attinyPortB = { + PIN: 0x36, + DDR: 0x37, + PORT: 0x38, + externalInterrupts: [], +}; + +const TCCR1 = attinyTimer1Config.TCCR1; +const TCNT1 = attinyTimer1Config.TCNT1; +const OCR1A = attinyTimer1Config.OCR1A; +const OCR1B = attinyTimer1Config.OCR1B; +const OCR1C = attinyTimer1Config.OCR1C; +const TIFR = attinyTimer1Config.TIFR; +const TIMSK = attinyTimer1Config.TIMSK; + +const TOV1 = attinyTimer1Config.TOV1; +const OCF1A = attinyTimer1Config.OCF1A; +const OCF1B = attinyTimer1Config.OCF1B; +const OCIE1A = attinyTimer1Config.OCIE1A; + +const CTC1 = 1 << 7; +const CS10 = 1; +const CS13 = 1 << 3; + +const SREG = 95; + +function createTimer() { + const cpu = new CPU(new Uint16Array(0x1000)); + new AVRIOPort(cpu, attinyPortB); + const timer = new ATtinyTimer1(cpu, attinyTimer1Config); + return { cpu, timer }; +} + +describe('ATtiny Timer1', () => { + it('should update timer every tick when prescaler is 1 (CS=1)', () => { + const { cpu } = createTimer(); + cpu.writeData(TCCR1, CS10); + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 2; + cpu.tick(); + expect(cpu.readData(TCNT1)).toEqual(1); + }); + + it('should update timer every 128 ticks when prescaler is 128 (CS=8)', () => { + const { cpu } = createTimer(); + cpu.writeData(TCCR1, CS13); + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 1 + 128; + cpu.tick(); + expect(cpu.readData(TCNT1)).toEqual(1); + }); + + it('should not update timer when disabled (CS=0)', () => { + const { cpu } = createTimer(); + cpu.writeData(TCCR1, 0); + cpu.cycles = 100000; + cpu.tick(); + expect(cpu.readData(TCNT1)).toEqual(0); + }); + + describe('CTC mode', () => { + it('should clear timer on OCR1C match when CTC1 is set', () => { + const { cpu } = createTimer(); + cpu.writeData(OCR1C, 9); + cpu.writeData(TCCR1, CTC1 | CS10); + cpu.writeData(TCNT1, 8); + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 1 + 3; + cpu.tick(); + expect(cpu.readData(TCNT1)).toEqual(1); + }); + + it('should set TOV1 when timer overflows past OCR1C', () => { + const { cpu } = createTimer(); + cpu.writeData(OCR1C, 9); + cpu.writeData(TCNT1, 9); + cpu.writeData(TCCR1, CTC1 | CS10); + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 2; + cpu.tick(); + expect(cpu.readData(TCNT1)).toEqual(0); + expect(cpu.data[TIFR] & TOV1).toEqual(TOV1); + }); + + it('should set OCF1A when timer matches OCR1A', () => { + const { cpu } = createTimer(); + cpu.writeData(OCR1C, 249); + cpu.writeData(OCR1A, 5); + cpu.writeData(TCCR1, CTC1 | CS10); + cpu.writeData(TCNT1, 4); + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 3; + cpu.tick(); + expect(cpu.data[TIFR] & OCF1A).toEqual(OCF1A); + }); + + it('should set OCF1B when timer matches OCR1B', () => { + const { cpu } = createTimer(); + cpu.writeData(OCR1C, 249); + cpu.writeData(OCR1B, 10); + cpu.writeData(TCCR1, CTC1 | CS10); + cpu.writeData(TCNT1, 9); + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 3; + cpu.tick(); + expect(cpu.data[TIFR] & OCF1B).toEqual(OCF1B); + }); + + it('should fire COMPA interrupt when enabled', () => { + const { cpu } = createTimer(); + cpu.writeData(OCR1C, 249); + cpu.writeData(OCR1A, 0); + cpu.writeData(TCCR1, CTC1 | CS10); + cpu.writeData(TCNT1, 248); + cpu.writeData(TIMSK, OCIE1A); + cpu.data[SREG] = 0x80; + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 3; + cpu.tick(); + expect(cpu.pc).toEqual(0x03); + }); + + it('should overflow after a full period with prescaler 128', () => { + const { cpu } = createTimer(); + cpu.writeData(TCCR1, CTC1 | CS13); + cpu.writeData(OCR1C, 249); + cpu.writeData(TIMSK, OCIE1A); + cpu.data[SREG] = 0x80; + + // Full timer period: 250 * 128 = 32000 cycles + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 32001; + cpu.tick(); + + expect(cpu.data[TIFR] & TOV1).not.toEqual(0); + }); + }); + + describe('clearing interrupt flags', () => { + it('should clear TOV1 by writing 1 to TIFR', () => { + const { cpu } = createTimer(); + cpu.writeData(OCR1C, 9); + cpu.writeData(TCNT1, 9); + cpu.writeData(TCCR1, CTC1 | CS10); + cpu.cycles = 1; + cpu.tick(); + cpu.cycles = 2; + cpu.tick(); + expect(cpu.data[TIFR] & TOV1).toEqual(TOV1); + cpu.writeData(TIFR, TOV1); + expect(cpu.data[TIFR] & TOV1).toEqual(0); + }); + }); +}); diff --git a/src/peripherals/timer-attiny.ts b/src/peripherals/timer-attiny.ts new file mode 100644 index 0000000..98bf473 --- /dev/null +++ b/src/peripherals/timer-attiny.ts @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) Uri Shaked and contributors + +/** + * ATtiny25/45/85 Timer/Counter1 + * Part of AVR8js + * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-2586-AVR-8-bit-Microcontroller-ATtiny25-ATtiny45-ATtiny85_Datasheet.pdf + */ + +import { AVRInterruptConfig, CPU, CPUMemoryHook } from '../cpu/cpu'; +import { PinOverrideMode } from './gpio'; + +type u8 = number; +type u16 = number; + +export interface ATtinyTimer1Config { + TCCR1: u8; + GTCCR: u8; + TCNT1: u8; + OCR1A: u8; + OCR1B: u8; + OCR1C: u8; + TIFR: u8; + TIMSK: u8; + + ovfInterrupt: u8; + compAInterrupt: u8; + compBInterrupt: u8; + + TOV1: u8; + OCF1A: u8; + OCF1B: u8; + + TOIE1: u8; + OCIE1A: u8; + OCIE1B: u8; + + compPortB: u16; + compPinA: u8; + compPinB: u8; + + dividers: Record<number, number>; +} + +// TCCR1 bits +const CTC1 = 1 << 7; +const PWM1A = 1 << 6; +const CS_MASK = 0x0f; + +// GTCCR bits +const PWM1B_BIT = 1 << 6; +const FOC1B = 1 << 3; +const FOC1A = 1 << 2; +const PSR1 = 1 << 1; + +export const attinyTimer1Config: ATtinyTimer1Config = { + TCCR1: 0x50, + GTCCR: 0x4c, + TCNT1: 0x4f, + OCR1A: 0x4e, + OCR1B: 0x4b, + OCR1C: 0x4d, + TIFR: 0x58, + TIMSK: 0x59, + + ovfInterrupt: 0x04, + compAInterrupt: 0x03, + compBInterrupt: 0x09, + + TOV1: 1 << 2, + OCF1A: 1 << 6, + OCF1B: 1 << 5, + + TOIE1: 1 << 2, + OCIE1A: 1 << 6, + OCIE1B: 1 << 5, + + compPortB: 0x38, + compPinA: 1, // PB1 + compPinB: 4, // PB4 + + dividers: { + 0: 0, + 1: 1, + 2: 2, + 3: 4, + 4: 8, + 5: 16, + 6: 32, + 7: 64, + 8: 128, + 9: 256, + 10: 512, + 11: 1024, + 12: 2048, + 13: 4096, + 14: 8192, + 15: 16384, + }, +}; + +export class ATtinyTimer1 { + private lastCycle = 0; + private tcnt = 0; + private tcntNext = 0; + private tcntUpdated = false; + private ocrA = 0; + private ocrB = 0; + private ocrC = 0; + private divider = 0; + private updateDivider = false; + private countingUp = true; + + private readonly OVF: AVRInterruptConfig = { + address: this.config.ovfInterrupt, + flagRegister: this.config.TIFR, + flagMask: this.config.TOV1, + enableRegister: this.config.TIMSK, + enableMask: this.config.TOIE1, + }; + + private readonly OCFA: AVRInterruptConfig = { + address: this.config.compAInterrupt, + flagRegister: this.config.TIFR, + flagMask: this.config.OCF1A, + enableRegister: this.config.TIMSK, + enableMask: this.config.OCIE1A, + }; + + private readonly OCFB: AVRInterruptConfig = { + address: this.config.compBInterrupt, + flagRegister: this.config.TIFR, + flagMask: this.config.OCF1B, + enableRegister: this.config.TIMSK, + enableMask: this.config.OCIE1B, + }; + + constructor( + private cpu: CPU, + private config: ATtinyTimer1Config, + ) { + const { TCCR1, GTCCR, TCNT1, OCR1A, OCR1B, OCR1C, TIFR, TIMSK } = config; + + cpu.readHooks[TCNT1] = () => { + this.count(false); + return (cpu.data[TCNT1] = this.tcnt & 0xff); + }; + + cpu.writeHooks[TCNT1] = (value: number) => { + this.tcntNext = value; + this.countingUp = true; + this.tcntUpdated = true; + cpu.updateClockEvent(this.count, 0); + if (this.divider) { + this.timerUpdated(this.tcntNext, this.tcntNext); + } + }; + + cpu.writeHooks[OCR1A] = (value: number) => { + this.ocrA = value; + }; + cpu.writeHooks[OCR1B] = (value: number) => { + this.ocrB = value; + }; + cpu.writeHooks[OCR1C] = (value: number) => { + this.ocrC = value; + }; + + cpu.writeHooks[TCCR1] = (value: number) => { + cpu.data[TCCR1] = value; + this.updateDivider = true; + cpu.clearClockEvent(this.count); + cpu.addClockEvent(this.count, 0); + this.updateCompConfig(); + return true; + }; + + // GTCCR is shared with Timer0 (PSR0) — chain with existing hook + const prevGtccrHook = cpu.writeHooks[GTCCR] as CPUMemoryHook | undefined; + cpu.writeHooks[GTCCR] = (value: number, oldValue: number, addr: number, mask: number) => { + if (value & FOC1A) { + this.forceCompare('A'); + } + if (value & FOC1B) { + this.forceCompare('B'); + } + if (value & PSR1) { + this.lastCycle = this.cpu.cycles; + } + value &= ~(FOC1A | FOC1B | PSR1); + + if (prevGtccrHook) { + prevGtccrHook(value, oldValue, addr, mask); + } else { + cpu.data[GTCCR] = value; + } + + this.updateCompConfig(); + return true; + }; + + // TIFR/TIMSK are shared with Timer0 — chain with existing hooks + const prevTifrHook = cpu.writeHooks[TIFR] as CPUMemoryHook | undefined; + cpu.writeHooks[TIFR] = (value: number, oldValue: number, addr: number, mask: number) => { + if (prevTifrHook) { + prevTifrHook(value, oldValue, addr, mask); + } else { + cpu.data[TIFR] = value; + } + cpu.clearInterruptByFlag(this.OVF, value); + cpu.clearInterruptByFlag(this.OCFA, value); + cpu.clearInterruptByFlag(this.OCFB, value); + return true; + }; + + const prevTimskHook = cpu.writeHooks[TIMSK] as CPUMemoryHook | undefined; + cpu.writeHooks[TIMSK] = (value: number, oldValue: number, addr: number, mask: number) => { + if (prevTimskHook) { + prevTimskHook(value, oldValue, addr, mask); + } + cpu.updateInterruptEnable(this.OVF, value); + cpu.updateInterruptEnable(this.OCFA, value); + cpu.updateInterruptEnable(this.OCFB, value); + }; + } + + private get tccr1() { + return this.cpu.data[this.config.TCCR1]; + } + + private get gtccr() { + return this.cpu.data[this.config.GTCCR]; + } + + private get CS() { + return this.tccr1 & CS_MASK; + } + + private get ctcMode() { + return !!(this.tccr1 & CTC1); + } + + private get pwmA() { + return !!(this.tccr1 & PWM1A); + } + + private get pwmB() { + return !!(this.gtccr & PWM1B_BIT); + } + + private get comA(): number { + return (this.tccr1 >> 4) & 0x3; + } + + private get comB(): number { + return (this.gtccr >> 4) & 0x3; + } + + /** TOP = OCR1C in CTC/PWM modes, 0xFF in Normal mode */ + private get TOP() { + if (this.ctcMode || this.pwmA || this.pwmB) { + return this.ocrC; + } + return 0xff; + } + + count = (reschedule = true) => { + const { divider, lastCycle, cpu } = this; + const { cycles } = cpu; + const delta = cycles - lastCycle; + + if (divider && delta >= divider) { + const counterDelta = Math.floor(delta / divider); + this.lastCycle += counterDelta * divider; + const val = this.tcnt; + const top = this.TOP; + const phasePwm = (this.pwmA || this.pwmB) && !this.ctcMode; + + const newVal = phasePwm + ? this.phasePwmCount(val, counterDelta) + : (val + counterDelta) % (top + 1); + const overflow = val + counterDelta > top; + + if (!this.tcntUpdated) { + this.tcnt = newVal; + if (!phasePwm) { + this.timerUpdated(newVal, val); + } + } + + if (!phasePwm && overflow) { + cpu.setInterruptFlag(this.OVF); + } + } + + if (this.tcntUpdated) { + this.tcnt = this.tcntNext; + this.tcntUpdated = false; + } + + if (this.updateDivider) { + const cs = this.CS; + const newDivider = this.config.dividers[cs] ?? 0; + this.lastCycle = newDivider ? this.cpu.cycles : 0; + this.updateDivider = false; + this.divider = newDivider; + if (newDivider) { + cpu.addClockEvent(this.count, this.lastCycle + newDivider - cpu.cycles); + } + return; + } + + if (reschedule && divider) { + cpu.addClockEvent(this.count, this.lastCycle + divider - cpu.cycles); + } + }; + + private phasePwmCount(value: number, delta: number): number { + const top = this.TOP; + + while (delta > 0) { + if (this.countingUp) { + value++; + if (value >= top) { + value = top; + this.countingUp = false; + } + } else { + value--; + if (value <= 0) { + value = 0; + this.countingUp = true; + this.cpu.setInterruptFlag(this.OVF); + } + } + + if (!this.tcntUpdated) { + if (value === this.ocrA) { + this.cpu.setInterruptFlag(this.OCFA); + this.updateCompPinPwm('A'); + } + if (value === this.ocrB) { + this.cpu.setInterruptFlag(this.OCFB); + this.updateCompPinPwm('B'); + } + } + delta--; + } + return value & 0xff; + } + + private timerUpdated(value: number, prevValue: number) { + const { ocrA, ocrB } = this; + const overflow = prevValue > value; + if (((prevValue < ocrA || overflow) && value >= ocrA) || (prevValue < ocrA && overflow)) { + this.cpu.setInterruptFlag(this.OCFA); + if (this.comA && !this.pwmA) { + this.updateCompPinNonPwm('A'); + } + } + if (((prevValue < ocrB || overflow) && value >= ocrB) || (prevValue < ocrB && overflow)) { + this.cpu.setInterruptFlag(this.OCFB); + if (this.comB && !this.pwmB) { + this.updateCompPinNonPwm('B'); + } + } + } + + private forceCompare(channel: 'A' | 'B') { + if (channel === 'A' && !this.pwmA && this.comA) { + this.updateCompPinNonPwm('A'); + } else if (channel === 'B' && !this.pwmB && this.comB) { + this.updateCompPinNonPwm('B'); + } + } + + private updateCompPinNonPwm(channel: 'A' | 'B') { + const com = channel === 'A' ? this.comA : this.comB; + const pin = channel === 'A' ? this.config.compPinA : this.config.compPinB; + let mode: PinOverrideMode; + switch (com) { + case 1: + mode = PinOverrideMode.Toggle; + break; + case 2: + mode = PinOverrideMode.Clear; + break; + case 3: + mode = PinOverrideMode.Set; + break; + default: + return; + } + this.cpu.gpioByPort[this.config.compPortB]?.timerOverridePin(pin, mode); + } + + private updateCompPinPwm(channel: 'A' | 'B') { + const com = channel === 'A' ? this.comA : this.comB; + const pin = channel === 'A' ? this.config.compPinA : this.config.compPinB; + const invertingMode = com === 3; + const isSet = this.countingUp === invertingMode; + let mode: PinOverrideMode; + switch (com) { + case 1: + mode = PinOverrideMode.Toggle; + break; + case 2: + case 3: + mode = isSet ? PinOverrideMode.Set : PinOverrideMode.Clear; + break; + default: + return; + } + this.cpu.gpioByPort[this.config.compPortB]?.timerOverridePin(pin, mode); + } + + private updateCompConfig() { + const port = this.cpu.gpioByPort[this.config.compPortB]; + if (!port) return; + port.timerOverridePin( + this.config.compPinA, + this.comA ? PinOverrideMode.Enable : PinOverrideMode.None, + ); + port.timerOverridePin( + this.config.compPinB, + this.comB ? PinOverrideMode.Enable : PinOverrideMode.None, + ); + } +} |
