From 82f9fa506e252476a07367ec58f898c6a1b4f12f Mon Sep 17 00:00:00 2001 From: Uri Shaked Date: Sat, 14 Feb 2026 20:04:24 +0200 Subject: feat(timer): ATtiny Timer/Counter1 #143 close #143 --- src/peripherals/timer-attiny.ts | 429 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 src/peripherals/timer-attiny.ts (limited to 'src/peripherals/timer-attiny.ts') 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; +} + +// 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, + ); + } +} -- cgit v1.2.3