aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUri Shaked2021-09-10 01:35:06 +0300
committerUri Shaked2021-09-10 01:35:06 +0300
commit39fe0472ce7b7f54438e69f47705086bc60d9716 (patch)
treeb84cd4c3829c87dde6f8ea56fe6342a891fd9525
parent0.17.1 (diff)
downloadavr8js-39fe0472ce7b7f54438e69f47705086bc60d9716.tar.gz
avr8js-39fe0472ce7b7f54438e69f47705086bc60d9716.tar.bz2
avr8js-39fe0472ce7b7f54438e69f47705086bc60d9716.zip
feat(watchdog): implement watchdog timer #106
Diffstat (limited to '')
-rw-r--r--src/cpu/cpu.ts11
-rw-r--r--src/cpu/instruction.ts2
-rw-r--r--src/index.ts1
-rw-r--r--src/peripherals/watchdog.spec.ts210
-rw-r--r--src/peripherals/watchdog.ts134
5 files changed, 357 insertions, 1 deletions
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<AVRIOPort>();
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;
+ }
+}