From 39fe0472ce7b7f54438e69f47705086bc60d9716 Mon Sep 17 00:00:00 2001 From: Uri Shaked Date: Fri, 10 Sep 2021 01:35:06 +0300 Subject: feat(watchdog): implement watchdog timer #106 --- src/cpu/cpu.ts | 11 ++ src/cpu/instruction.ts | 2 +- src/index.ts | 1 + src/peripherals/watchdog.spec.ts | 210 +++++++++++++++++++++++++++++++++++++++ src/peripherals/watchdog.ts | 134 +++++++++++++++++++++++++ 5 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 src/peripherals/watchdog.spec.ts create mode 100644 src/peripherals/watchdog.ts (limited to 'src') diff --git a/src/cpu/cpu.ts b/src/cpu/cpu.ts index de5b294..7a518b8 100644 --- a/src/cpu/cpu.ts +++ b/src/cpu/cpu.ts @@ -35,6 +35,7 @@ export interface ICPU { readData(addr: u16): u8; writeData(addr: u16, value: u8, mask?: u8): void; + onWatchdogReset(): void; } export type CPUMemoryHook = (value: u8, oldValue: u8, addr: u16, mask: u8) => boolean | void; @@ -80,6 +81,14 @@ export class CPU implements ICPU { readonly gpioPorts = new Set(); readonly gpioByPort: AVRIOPort[] = []; + /** + * This function is called by the WDR instruction. The Watchdog peripheral attaches + * to it to listen for WDR (watchdog reset). + */ + onWatchdogReset = () => { + /* empty by default */ + }; + pc: u32 = 0; cycles: u32 = 0; nextInterrupt: i16 = -1; @@ -91,8 +100,10 @@ export class CPU implements ICPU { reset() { this.data.fill(0); this.SP = this.data.length - 1; + this.pc = 0; this.pendingInterrupts.splice(0, this.pendingInterrupts.length); this.nextInterrupt = -1; + this.nextClockEvent = null; } readData(addr: number) { diff --git a/src/cpu/instruction.ts b/src/cpu/instruction.ts index c3cdb92..9937ec9 100644 --- a/src/cpu/instruction.ts +++ b/src/cpu/instruction.ts @@ -783,7 +783,7 @@ export function avrInstruction(cpu: ICPU) { cpu.data[d] = ((15 & i) << 4) | ((240 & i) >>> 4); } else if (opcode === 0x95a8) { /* WDR, 1001 0101 1010 1000 */ - /* not implemented */ + cpu.onWatchdogReset(); } else if ((opcode & 0xfe0f) === 0x9204) { /* XCH, 1001 001r rrrr 0100 */ const r = (opcode & 0x1f0) >> 4; diff --git a/src/index.ts b/src/index.ts index 01af188..8ea70d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,3 +49,4 @@ export { export * from './peripherals/twi'; export { spiConfig, SPIConfig, SPITransferCallback, AVRSPI } from './peripherals/spi'; export { AVRClock, AVRClockConfig, clockConfig } from './peripherals/clock'; +export { AVRWatchdog, watchdogConfig, WatchdogConfig } from './peripherals/watchdog'; diff --git a/src/peripherals/watchdog.spec.ts b/src/peripherals/watchdog.spec.ts new file mode 100644 index 0000000..6fea3b3 --- /dev/null +++ b/src/peripherals/watchdog.spec.ts @@ -0,0 +1,210 @@ +/** + * AVR8 Watchdog Timer Test Suite + * Part of AVR8js + * + * Copyright (C) 2021 Uri Shaked + */ + +import { AVRClock, clockConfig } from '..'; +import { CPU } from '../cpu/cpu'; +import { asmProgram, TestProgramRunner } from '../utils/test-utils'; +import { AVRWatchdog, watchdogConfig } from './watchdog'; + +const R20 = 20; + +const MCUSR = 0x54; +const WDRF = 1 << 3; + +const WDTCSR = 0x60; +const WDP0 = 1 << 0; +const WDP1 = 1 << 1; +const WDP2 = 1 << 2; +const WDE = 1 << 3; +const WDCE = 1 << 4; +const WDP3 = 1 << 5; +const WDIE = 1 << 6; + +const INT_WDT = 0xc; + +describe('Watchdog', () => { + it('should correctly calculate the prescaler from WDTCSR', () => { + const cpu = new CPU(new Uint16Array(1024)); + const clock = new AVRClock(cpu, 16e6, clockConfig); + const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); + cpu.writeData(WDTCSR, WDCE | WDE); + cpu.writeData(WDTCSR, 0); + expect(watchdog.prescaler).toEqual(2048); + cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0); + expect(watchdog.prescaler).toEqual(256 * 1024); + cpu.writeData(WDTCSR, WDP3 | WDP0); + expect(watchdog.prescaler).toEqual(1024 * 1024); + }); + + it('should not change the prescaler unless WDCE is set', () => { + const cpu = new CPU(new Uint16Array(1024)); + const clock = new AVRClock(cpu, 16e6, clockConfig); + const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); + cpu.writeData(WDTCSR, 0); + expect(watchdog.prescaler).toEqual(2048); + cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0); + expect(watchdog.prescaler).toEqual(2048); + + cpu.writeData(WDTCSR, WDCE | WDE); + cpu.cycles += 5; // WDCE should expire after 4 cycles + cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0); + expect(watchdog.prescaler).toEqual(2048); + }); + + it('should reset the CPU when the timer expires', () => { + const { program } = asmProgram(` + ; register addresses + _REPLACE WDTCSR, ${WDTCSR} + + ; Setup watchdog + ldi r16, ${WDE | WDCE} + sts WDTCSR, r16 + ldi r16, ${WDE} + sts WDTCSR, r16 + + nop + + break + `); + const cpu = new CPU(program); + const clock = new AVRClock(cpu, 16e6, clockConfig); + const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); + const runner = new TestProgramRunner(cpu); + + // Setup: enable watchdog timer + runner.runInstructions(4); + expect(watchdog.enabled).toBe(true); + + // Now we skip 8ms. Watchdog shouldn't fire, yet + cpu.cycles += 16000 * 8; + runner.runInstructions(1); + + // Now we skip an extra 8ms. Watchdog should fire and reset! + cpu.cycles += 16000 * 8; + cpu.tick(); + expect(cpu.pc).toEqual(0); + expect(cpu.readData(MCUSR)).toEqual(WDRF); + }); + + it('should extend the watchdog timeout when executing a WDR instruction', () => { + const { program } = asmProgram(` + ; register addresses + _REPLACE WDTCSR, ${WDTCSR} + + ; Setup watchdog + ldi r16, ${WDE | WDCE} + sts WDTCSR, r16 + ldi r16, ${WDE} + sts WDTCSR, r16 + + wdr + nop + + break + `); + const cpu = new CPU(program); + const clock = new AVRClock(cpu, 16e6, clockConfig); + const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); + const runner = new TestProgramRunner(cpu); + + // Setup: enable watchdog timer + runner.runInstructions(4); + expect(watchdog.enabled).toBe(true); + + // Now we skip 8ms. Watchdog shouldn't fire, yet + cpu.cycles += 16000 * 8; + runner.runInstructions(1); + + // Now we skip an extra 8ms. We extended the timeout with WDR, so watchdog won't fire yet + cpu.cycles += 16000 * 8; + runner.runInstructions(1); + + // Finally, another 8ms bring us to 16ms since last WDR, and watchdog should fire + cpu.cycles += 16000 * 8; + cpu.tick(); + expect(cpu.pc).toEqual(0); + }); + + it('should fire an interrupt when the watchdog expires and WDIE is set', () => { + const { program } = asmProgram(` + ; register addresses + _REPLACE WDTCSR, ${WDTCSR} + + ; Setup watchdog + ldi r16, ${WDE | WDCE} + sts WDTCSR, r16 + ldi r16, ${WDE | WDIE} + sts WDTCSR, r16 + + nop + sei + + break + `); + const cpu = new CPU(program); + const clock = new AVRClock(cpu, 16e6, clockConfig); + const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); + const runner = new TestProgramRunner(cpu); + + // Setup: enable watchdog timer + runner.runInstructions(4); + expect(watchdog.enabled).toBe(true); + + // Now we skip 8ms. Watchdog shouldn't fire, yet + cpu.cycles += 16000 * 8; + runner.runInstructions(1); + + // Now we skip an extra 8ms. Watchdog should fire and jump to the interrupt handler + cpu.cycles += 16000 * 8; + runner.runInstructions(1); + + expect(cpu.pc).toEqual(INT_WDT); + // The watchdog timer should also clean the WDIE bit, so next timeout will reset the MCU. + expect(cpu.readData(WDTCSR) & WDIE).toEqual(0); + }); + + it('should not reset the CPU if the watchdog has been disabled', () => { + const { program } = asmProgram(` + ; register addresses + _REPLACE WDTCSR, ${WDTCSR} + + ; Setup watchdog + ldi r16, ${WDE | WDCE} + sts WDTCSR, r16 + ldi r16, ${WDE} + sts WDTCSR, r16 + + ; disable watchdog + ldi r16, ${WDE | WDCE} + sts WDTCSR, r16 + ldi r16, 0 + sts WDTCSR, r16 + + ldi r20, 55 + + break + `); + const cpu = new CPU(program); + const clock = new AVRClock(cpu, 16e6, clockConfig); + const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock); + const runner = new TestProgramRunner(cpu); + + // Setup: enable watchdog timer + runner.runInstructions(4); + expect(watchdog.enabled).toBe(true); + + // Now we skip 8ms. Watchdog shouldn't fire, yet. We disable it. + cpu.cycles += 16000 * 8; + runner.runInstructions(4); + + // Now we skip an extra 20ms. Watchdog shouldn't reset! + cpu.cycles += 16000 * 20; + runner.runInstructions(1); + expect(cpu.pc).not.toEqual(0); + expect(cpu.data[R20]).toEqual(55); // assert that `ldi r20, 55` ran + }); +}); diff --git a/src/peripherals/watchdog.ts b/src/peripherals/watchdog.ts new file mode 100644 index 0000000..dc66220 --- /dev/null +++ b/src/peripherals/watchdog.ts @@ -0,0 +1,134 @@ +/** + * AVR8 Watchdog Timer + * Part of AVR8js + * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf + * + * Copyright (C) 2021 Uri Shaked + */ + +import { AVRClock } from '..'; +import { AVRInterruptConfig, CPU } from '../cpu/cpu'; +import { u8 } from '../types'; + +export interface WatchdogConfig { + watchdogInterrupt: u8; + MCUSR: u8; + WDTCSR: u8; +} + +// Register bits: +const MCUSR_WDRF = 0x8; // Watchdog System Reset Flag + +const WDTCSR_WDIF = 0x80; +const WDTCSR_WDIE = 0x40; +const WDTCSR_WDP3 = 0x20; +const WDTCSR_WDCE = 0x10; // Watchdog Change Enable +const WDTCSR_WDE = 0x8; +const WDTCSR_WDP2 = 0x4; +const WDTCSR_WDP1 = 0x2; +const WDTCSR_WDP0 = 0x1; +const WDTCSR_WDP210 = WDTCSR_WDP2 | WDTCSR_WDP1 | WDTCSR_WDP0; + +const WDTCSR_PROTECT_MASK = WDTCSR_WDE | WDTCSR_WDP3 | WDTCSR_WDP210; + +export const watchdogConfig: WatchdogConfig = { + watchdogInterrupt: 0x0c, + MCUSR: 0x54, + WDTCSR: 0x60, +}; + +export class AVRWatchdog { + readonly clockFrequency = 128000; + + /** + * Used to keep track on the last write to WDCE. Once written, the WDE/WDP* bits can be changed. + */ + private changeEnabledCycles = 0; + private watchdogTimeout = 0; + private enabledValue = false; + private scheduled = false; + + // Interrupts + private Watchdog: AVRInterruptConfig = { + address: this.config.watchdogInterrupt, + flagRegister: this.config.WDTCSR, + flagMask: WDTCSR_WDIF, + enableRegister: this.config.WDTCSR, + enableMask: WDTCSR_WDIE, + }; + + constructor(private cpu: CPU, private config: WatchdogConfig, private clock: AVRClock) { + const { WDTCSR } = config; + this.cpu.onWatchdogReset = () => { + this.resetWatchdog(); + }; + cpu.writeHooks[WDTCSR] = (value: u8, oldValue: u8) => { + if (value & WDTCSR_WDCE && value & WDTCSR_WDE) { + this.changeEnabledCycles = this.cpu.cycles + 4; + value = value & ~WDTCSR_PROTECT_MASK; + } else { + if (this.cpu.cycles >= this.changeEnabledCycles) { + value = (value & ~WDTCSR_PROTECT_MASK) | (oldValue & WDTCSR_PROTECT_MASK); + } + this.enabledValue = !!(value & WDTCSR_WDE || value & WDTCSR_WDIE); + this.cpu.data[WDTCSR] = value; + } + + if (this.enabled) { + this.resetWatchdog(); + } + + if (this.enabled && !this.scheduled) { + this.cpu.addClockEvent(this.checkWatchdog, this.watchdogTimeout - this.cpu.cycles); + } + + this.cpu.clearInterruptByFlag(this.Watchdog, value); + return true; + }; + } + + resetWatchdog() { + const cycles = Math.floor((this.clock.frequency / this.clockFrequency) * this.prescaler); + this.watchdogTimeout = this.cpu.cycles + cycles; + } + + checkWatchdog = () => { + if (this.enabled && this.cpu.cycles >= this.watchdogTimeout) { + // Watchdog timed out! + const wdtcsr = this.cpu.data[this.config.WDTCSR]; + if (wdtcsr & WDTCSR_WDIE) { + this.cpu.setInterruptFlag(this.Watchdog); + } + if (wdtcsr & WDTCSR_WDE) { + if (wdtcsr & WDTCSR_WDIE) { + this.cpu.data[this.config.WDTCSR] &= ~WDTCSR_WDIE; + } else { + this.cpu.reset(); + this.scheduled = false; + this.cpu.data[this.config.MCUSR] |= MCUSR_WDRF; + return; + } + } + this.resetWatchdog(); + } + if (this.enabled) { + this.scheduled = true; + this.cpu.addClockEvent(this.checkWatchdog, this.watchdogTimeout - this.cpu.cycles); + } else { + this.scheduled = false; + } + }; + + get enabled() { + return this.enabledValue; + } + + /** + * The base clock frequency is 128KHz. Thus, a prescaler of 2048 gives 16ms timeout. + */ + get prescaler() { + const wdtcsr = this.cpu.data[this.config.WDTCSR]; + const value = ((wdtcsr & WDTCSR_WDP3) >> 2) | (wdtcsr & WDTCSR_WDP210); + return 2048 << value; + } +} -- cgit v1.2.3