Skip to content

Commit 39fe047

Browse files
committed
feat(watchdog): implement watchdog timer #106
1 parent b0695c9 commit 39fe047

File tree

5 files changed

+357
-1
lines changed

5 files changed

+357
-1
lines changed

src/cpu/cpu.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ICPU {
3535

3636
readData(addr: u16): u8;
3737
writeData(addr: u16, value: u8, mask?: u8): void;
38+
onWatchdogReset(): void;
3839
}
3940

4041
export type CPUMemoryHook = (value: u8, oldValue: u8, addr: u16, mask: u8) => boolean | void;
@@ -80,6 +81,14 @@ export class CPU implements ICPU {
8081
readonly gpioPorts = new Set<AVRIOPort>();
8182
readonly gpioByPort: AVRIOPort[] = [];
8283

84+
/**
85+
* This function is called by the WDR instruction. The Watchdog peripheral attaches
86+
* to it to listen for WDR (watchdog reset).
87+
*/
88+
onWatchdogReset = () => {
89+
/* empty by default */
90+
};
91+
8392
pc: u32 = 0;
8493
cycles: u32 = 0;
8594
nextInterrupt: i16 = -1;
@@ -91,8 +100,10 @@ export class CPU implements ICPU {
91100
reset() {
92101
this.data.fill(0);
93102
this.SP = this.data.length - 1;
103+
this.pc = 0;
94104
this.pendingInterrupts.splice(0, this.pendingInterrupts.length);
95105
this.nextInterrupt = -1;
106+
this.nextClockEvent = null;
96107
}
97108

98109
readData(addr: number) {

src/cpu/instruction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,7 @@ export function avrInstruction(cpu: ICPU) {
783783
cpu.data[d] = ((15 & i) << 4) | ((240 & i) >>> 4);
784784
} else if (opcode === 0x95a8) {
785785
/* WDR, 1001 0101 1010 1000 */
786-
/* not implemented */
786+
cpu.onWatchdogReset();
787787
} else if ((opcode & 0xfe0f) === 0x9204) {
788788
/* XCH, 1001 001r rrrr 0100 */
789789
const r = (opcode & 0x1f0) >> 4;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ export {
4949
export * from './peripherals/twi';
5050
export { spiConfig, SPIConfig, SPITransferCallback, AVRSPI } from './peripherals/spi';
5151
export { AVRClock, AVRClockConfig, clockConfig } from './peripherals/clock';
52+
export { AVRWatchdog, watchdogConfig, WatchdogConfig } from './peripherals/watchdog';

src/peripherals/watchdog.spec.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* AVR8 Watchdog Timer Test Suite
3+
* Part of AVR8js
4+
*
5+
* Copyright (C) 2021 Uri Shaked
6+
*/
7+
8+
import { AVRClock, clockConfig } from '..';
9+
import { CPU } from '../cpu/cpu';
10+
import { asmProgram, TestProgramRunner } from '../utils/test-utils';
11+
import { AVRWatchdog, watchdogConfig } from './watchdog';
12+
13+
const R20 = 20;
14+
15+
const MCUSR = 0x54;
16+
const WDRF = 1 << 3;
17+
18+
const WDTCSR = 0x60;
19+
const WDP0 = 1 << 0;
20+
const WDP1 = 1 << 1;
21+
const WDP2 = 1 << 2;
22+
const WDE = 1 << 3;
23+
const WDCE = 1 << 4;
24+
const WDP3 = 1 << 5;
25+
const WDIE = 1 << 6;
26+
27+
const INT_WDT = 0xc;
28+
29+
describe('Watchdog', () => {
30+
it('should correctly calculate the prescaler from WDTCSR', () => {
31+
const cpu = new CPU(new Uint16Array(1024));
32+
const clock = new AVRClock(cpu, 16e6, clockConfig);
33+
const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock);
34+
cpu.writeData(WDTCSR, WDCE | WDE);
35+
cpu.writeData(WDTCSR, 0);
36+
expect(watchdog.prescaler).toEqual(2048);
37+
cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0);
38+
expect(watchdog.prescaler).toEqual(256 * 1024);
39+
cpu.writeData(WDTCSR, WDP3 | WDP0);
40+
expect(watchdog.prescaler).toEqual(1024 * 1024);
41+
});
42+
43+
it('should not change the prescaler unless WDCE is set', () => {
44+
const cpu = new CPU(new Uint16Array(1024));
45+
const clock = new AVRClock(cpu, 16e6, clockConfig);
46+
const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock);
47+
cpu.writeData(WDTCSR, 0);
48+
expect(watchdog.prescaler).toEqual(2048);
49+
cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0);
50+
expect(watchdog.prescaler).toEqual(2048);
51+
52+
cpu.writeData(WDTCSR, WDCE | WDE);
53+
cpu.cycles += 5; // WDCE should expire after 4 cycles
54+
cpu.writeData(WDTCSR, WDP2 | WDP1 | WDP0);
55+
expect(watchdog.prescaler).toEqual(2048);
56+
});
57+
58+
it('should reset the CPU when the timer expires', () => {
59+
const { program } = asmProgram(`
60+
; register addresses
61+
_REPLACE WDTCSR, ${WDTCSR}
62+
63+
; Setup watchdog
64+
ldi r16, ${WDE | WDCE}
65+
sts WDTCSR, r16
66+
ldi r16, ${WDE}
67+
sts WDTCSR, r16
68+
69+
nop
70+
71+
break
72+
`);
73+
const cpu = new CPU(program);
74+
const clock = new AVRClock(cpu, 16e6, clockConfig);
75+
const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock);
76+
const runner = new TestProgramRunner(cpu);
77+
78+
// Setup: enable watchdog timer
79+
runner.runInstructions(4);
80+
expect(watchdog.enabled).toBe(true);
81+
82+
// Now we skip 8ms. Watchdog shouldn't fire, yet
83+
cpu.cycles += 16000 * 8;
84+
runner.runInstructions(1);
85+
86+
// Now we skip an extra 8ms. Watchdog should fire and reset!
87+
cpu.cycles += 16000 * 8;
88+
cpu.tick();
89+
expect(cpu.pc).toEqual(0);
90+
expect(cpu.readData(MCUSR)).toEqual(WDRF);
91+
});
92+
93+
it('should extend the watchdog timeout when executing a WDR instruction', () => {
94+
const { program } = asmProgram(`
95+
; register addresses
96+
_REPLACE WDTCSR, ${WDTCSR}
97+
98+
; Setup watchdog
99+
ldi r16, ${WDE | WDCE}
100+
sts WDTCSR, r16
101+
ldi r16, ${WDE}
102+
sts WDTCSR, r16
103+
104+
wdr
105+
nop
106+
107+
break
108+
`);
109+
const cpu = new CPU(program);
110+
const clock = new AVRClock(cpu, 16e6, clockConfig);
111+
const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock);
112+
const runner = new TestProgramRunner(cpu);
113+
114+
// Setup: enable watchdog timer
115+
runner.runInstructions(4);
116+
expect(watchdog.enabled).toBe(true);
117+
118+
// Now we skip 8ms. Watchdog shouldn't fire, yet
119+
cpu.cycles += 16000 * 8;
120+
runner.runInstructions(1);
121+
122+
// Now we skip an extra 8ms. We extended the timeout with WDR, so watchdog won't fire yet
123+
cpu.cycles += 16000 * 8;
124+
runner.runInstructions(1);
125+
126+
// Finally, another 8ms bring us to 16ms since last WDR, and watchdog should fire
127+
cpu.cycles += 16000 * 8;
128+
cpu.tick();
129+
expect(cpu.pc).toEqual(0);
130+
});
131+
132+
it('should fire an interrupt when the watchdog expires and WDIE is set', () => {
133+
const { program } = asmProgram(`
134+
; register addresses
135+
_REPLACE WDTCSR, ${WDTCSR}
136+
137+
; Setup watchdog
138+
ldi r16, ${WDE | WDCE}
139+
sts WDTCSR, r16
140+
ldi r16, ${WDE | WDIE}
141+
sts WDTCSR, r16
142+
143+
nop
144+
sei
145+
146+
break
147+
`);
148+
const cpu = new CPU(program);
149+
const clock = new AVRClock(cpu, 16e6, clockConfig);
150+
const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock);
151+
const runner = new TestProgramRunner(cpu);
152+
153+
// Setup: enable watchdog timer
154+
runner.runInstructions(4);
155+
expect(watchdog.enabled).toBe(true);
156+
157+
// Now we skip 8ms. Watchdog shouldn't fire, yet
158+
cpu.cycles += 16000 * 8;
159+
runner.runInstructions(1);
160+
161+
// Now we skip an extra 8ms. Watchdog should fire and jump to the interrupt handler
162+
cpu.cycles += 16000 * 8;
163+
runner.runInstructions(1);
164+
165+
expect(cpu.pc).toEqual(INT_WDT);
166+
// The watchdog timer should also clean the WDIE bit, so next timeout will reset the MCU.
167+
expect(cpu.readData(WDTCSR) & WDIE).toEqual(0);
168+
});
169+
170+
it('should not reset the CPU if the watchdog has been disabled', () => {
171+
const { program } = asmProgram(`
172+
; register addresses
173+
_REPLACE WDTCSR, ${WDTCSR}
174+
175+
; Setup watchdog
176+
ldi r16, ${WDE | WDCE}
177+
sts WDTCSR, r16
178+
ldi r16, ${WDE}
179+
sts WDTCSR, r16
180+
181+
; disable watchdog
182+
ldi r16, ${WDE | WDCE}
183+
sts WDTCSR, r16
184+
ldi r16, 0
185+
sts WDTCSR, r16
186+
187+
ldi r20, 55
188+
189+
break
190+
`);
191+
const cpu = new CPU(program);
192+
const clock = new AVRClock(cpu, 16e6, clockConfig);
193+
const watchdog = new AVRWatchdog(cpu, watchdogConfig, clock);
194+
const runner = new TestProgramRunner(cpu);
195+
196+
// Setup: enable watchdog timer
197+
runner.runInstructions(4);
198+
expect(watchdog.enabled).toBe(true);
199+
200+
// Now we skip 8ms. Watchdog shouldn't fire, yet. We disable it.
201+
cpu.cycles += 16000 * 8;
202+
runner.runInstructions(4);
203+
204+
// Now we skip an extra 20ms. Watchdog shouldn't reset!
205+
cpu.cycles += 16000 * 20;
206+
runner.runInstructions(1);
207+
expect(cpu.pc).not.toEqual(0);
208+
expect(cpu.data[R20]).toEqual(55); // assert that `ldi r20, 55` ran
209+
});
210+
});

src/peripherals/watchdog.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* AVR8 Watchdog Timer
3+
* Part of AVR8js
4+
* Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
5+
*
6+
* Copyright (C) 2021 Uri Shaked
7+
*/
8+
9+
import { AVRClock } from '..';
10+
import { AVRInterruptConfig, CPU } from '../cpu/cpu';
11+
import { u8 } from '../types';
12+
13+
export interface WatchdogConfig {
14+
watchdogInterrupt: u8;
15+
MCUSR: u8;
16+
WDTCSR: u8;
17+
}
18+
19+
// Register bits:
20+
const MCUSR_WDRF = 0x8; // Watchdog System Reset Flag
21+
22+
const WDTCSR_WDIF = 0x80;
23+
const WDTCSR_WDIE = 0x40;
24+
const WDTCSR_WDP3 = 0x20;
25+
const WDTCSR_WDCE = 0x10; // Watchdog Change Enable
26+
const WDTCSR_WDE = 0x8;
27+
const WDTCSR_WDP2 = 0x4;
28+
const WDTCSR_WDP1 = 0x2;
29+
const WDTCSR_WDP0 = 0x1;
30+
const WDTCSR_WDP210 = WDTCSR_WDP2 | WDTCSR_WDP1 | WDTCSR_WDP0;
31+
32+
const WDTCSR_PROTECT_MASK = WDTCSR_WDE | WDTCSR_WDP3 | WDTCSR_WDP210;
33+
34+
export const watchdogConfig: WatchdogConfig = {
35+
watchdogInterrupt: 0x0c,
36+
MCUSR: 0x54,
37+
WDTCSR: 0x60,
38+
};
39+
40+
export class AVRWatchdog {
41+
readonly clockFrequency = 128000;
42+
43+
/**
44+
* Used to keep track on the last write to WDCE. Once written, the WDE/WDP* bits can be changed.
45+
*/
46+
private changeEnabledCycles = 0;
47+
private watchdogTimeout = 0;
48+
private enabledValue = false;
49+
private scheduled = false;
50+
51+
// Interrupts
52+
private Watchdog: AVRInterruptConfig = {
53+
address: this.config.watchdogInterrupt,
54+
flagRegister: this.config.WDTCSR,
55+
flagMask: WDTCSR_WDIF,
56+
enableRegister: this.config.WDTCSR,
57+
enableMask: WDTCSR_WDIE,
58+
};
59+
60+
constructor(private cpu: CPU, private config: WatchdogConfig, private clock: AVRClock) {
61+
const { WDTCSR } = config;
62+
this.cpu.onWatchdogReset = () => {
63+
this.resetWatchdog();
64+
};
65+
cpu.writeHooks[WDTCSR] = (value: u8, oldValue: u8) => {
66+
if (value & WDTCSR_WDCE && value & WDTCSR_WDE) {
67+
this.changeEnabledCycles = this.cpu.cycles + 4;
68+
value = value & ~WDTCSR_PROTECT_MASK;
69+
} else {
70+
if (this.cpu.cycles >= this.changeEnabledCycles) {
71+
value = (value & ~WDTCSR_PROTECT_MASK) | (oldValue & WDTCSR_PROTECT_MASK);
72+
}
73+
this.enabledValue = !!(value & WDTCSR_WDE || value & WDTCSR_WDIE);
74+
this.cpu.data[WDTCSR] = value;
75+
}
76+
77+
if (this.enabled) {
78+
this.resetWatchdog();
79+
}
80+
81+
if (this.enabled && !this.scheduled) {
82+
this.cpu.addClockEvent(this.checkWatchdog, this.watchdogTimeout - this.cpu.cycles);
83+
}
84+
85+
this.cpu.clearInterruptByFlag(this.Watchdog, value);
86+
return true;
87+
};
88+
}
89+
90+
resetWatchdog() {
91+
const cycles = Math.floor((this.clock.frequency / this.clockFrequency) * this.prescaler);
92+
this.watchdogTimeout = this.cpu.cycles + cycles;
93+
}
94+
95+
checkWatchdog = () => {
96+
if (this.enabled && this.cpu.cycles >= this.watchdogTimeout) {
97+
// Watchdog timed out!
98+
const wdtcsr = this.cpu.data[this.config.WDTCSR];
99+
if (wdtcsr & WDTCSR_WDIE) {
100+
this.cpu.setInterruptFlag(this.Watchdog);
101+
}
102+
if (wdtcsr & WDTCSR_WDE) {
103+
if (wdtcsr & WDTCSR_WDIE) {
104+
this.cpu.data[this.config.WDTCSR] &= ~WDTCSR_WDIE;
105+
} else {
106+
this.cpu.reset();
107+
this.scheduled = false;
108+
this.cpu.data[this.config.MCUSR] |= MCUSR_WDRF;
109+
return;
110+
}
111+
}
112+
this.resetWatchdog();
113+
}
114+
if (this.enabled) {
115+
this.scheduled = true;
116+
this.cpu.addClockEvent(this.checkWatchdog, this.watchdogTimeout - this.cpu.cycles);
117+
} else {
118+
this.scheduled = false;
119+
}
120+
};
121+
122+
get enabled() {
123+
return this.enabledValue;
124+
}
125+
126+
/**
127+
* The base clock frequency is 128KHz. Thus, a prescaler of 2048 gives 16ms timeout.
128+
*/
129+
get prescaler() {
130+
const wdtcsr = this.cpu.data[this.config.WDTCSR];
131+
const value = ((wdtcsr & WDTCSR_WDP3) >> 2) | (wdtcsr & WDTCSR_WDP210);
132+
return 2048 << value;
133+
}
134+
}

0 commit comments

Comments
 (0)