aboutsummaryrefslogtreecommitdiff
path: root/src/peripherals
diff options
context:
space:
mode:
Diffstat (limited to 'src/peripherals')
-rw-r--r--src/peripherals/gpio.spec.ts86
-rw-r--r--src/peripherals/gpio.ts149
-rw-r--r--src/peripherals/timer.spec.ts204
-rw-r--r--src/peripherals/timer.ts244
-rw-r--r--src/peripherals/twi.spec.ts390
-rw-r--r--src/peripherals/twi.ts200
-rw-r--r--src/peripherals/usart.spec.ts159
-rw-r--r--src/peripherals/usart.ts134
8 files changed, 1566 insertions, 0 deletions
diff --git a/src/peripherals/gpio.spec.ts b/src/peripherals/gpio.spec.ts
new file mode 100644
index 0000000..5a282e6
--- /dev/null
+++ b/src/peripherals/gpio.spec.ts
@@ -0,0 +1,86 @@
+import { CPU } from '../cpu/cpu';
+import { AVRIOPort, portBConfig, PinState } from './gpio';
+
+describe('GPIO', () => {
+ it('should invoke the listeners when the port is written to', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const port = new AVRIOPort(cpu, portBConfig);
+ const listener = jest.fn();
+ port.addListener(listener);
+ cpu.writeData(0x24, 0x0f); // DDRB <- 0x0f
+ cpu.writeData(0x25, 0x55); // PORTB <- 0x55
+ expect(listener).toHaveBeenCalledWith(0x05, 0);
+ expect(cpu.data[0x23]).toEqual(0x5); // PINB should return port value
+ });
+
+ it('should toggle the pin when writing to the PIN register', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const port = new AVRIOPort(cpu, portBConfig);
+ const listener = jest.fn();
+ port.addListener(listener);
+ cpu.writeData(0x24, 0x0f); // DDRB <- 0x0f
+ cpu.writeData(0x25, 0x55); // PORTB <- 0x55
+ cpu.writeData(0x23, 0x01); // PINB <- 0x0f
+ expect(listener).toHaveBeenCalledWith(0x04, 0x5);
+ expect(cpu.data[0x23]).toEqual(0x4); // PINB should return port value
+ });
+
+ describe('removeListener', () => {
+ it('should remove the given listener', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const port = new AVRIOPort(cpu, portBConfig);
+ const listener = jest.fn();
+ port.addListener(listener);
+ cpu.writeData(0x24, 0x0f); // DDRB <- 0x0f
+ port.removeListener(listener);
+ cpu.writeData(0x25, 0x99); // PORTB <- 0x99
+ expect(listener).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('pinState', () => {
+ it('should return PinState.High when the pin set to output and HIGH', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const port = new AVRIOPort(cpu, portBConfig);
+ cpu.writeData(0x24, 0x1); // DDRB <- 0x1
+ cpu.writeData(0x25, 0x1); // PORTB <- 0x1
+ expect(port.pinState(0)).toEqual(PinState.High);
+ });
+
+ it('should return PinState.Low when the pin set to output and LOW', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const port = new AVRIOPort(cpu, portBConfig);
+ cpu.writeData(0x24, 0x8); // DDRB <- 0x8
+ cpu.writeData(0x25, 0xf7); // PORTB <- 0xF7 (~8)
+ expect(port.pinState(3)).toEqual(PinState.Low);
+ });
+
+ it('should return PinState.Input by default (reset state)', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const port = new AVRIOPort(cpu, portBConfig);
+ expect(port.pinState(1)).toEqual(PinState.Input);
+ });
+
+ it('should return PinState.InputPullUp when the pin is set to input with pullup', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const port = new AVRIOPort(cpu, portBConfig);
+ cpu.writeData(0x24, 0); // DDRB <- 0
+ cpu.writeData(0x25, 0x2); // PORTB <- 0x2
+ expect(port.pinState(1)).toEqual(PinState.InputPullUp);
+ });
+
+ it('should reflect the current port state when called inside a listener', () => {
+ // Related issue: https://github.com/wokwi/avr8js/issues/9
+ const cpu = new CPU(new Uint16Array(1024));
+ const port = new AVRIOPort(cpu, portBConfig);
+ const listener = jest.fn(() => {
+ expect(port.pinState(0)).toBe(PinState.High);
+ });
+ port.addListener(listener);
+ expect(port.pinState(0)).toBe(PinState.Input);
+ cpu.writeData(0x24, 0x01); // DDRB <- 0x01
+ cpu.writeData(0x25, 0x01); // PORTB <- 0x01
+ expect(listener).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/peripherals/gpio.ts b/src/peripherals/gpio.ts
new file mode 100644
index 0000000..a667967
--- /dev/null
+++ b/src/peripherals/gpio.ts
@@ -0,0 +1,149 @@
+/**
+ * AVR-8 GPIO Port implementation
+ * Part of AVR8js
+ * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
+ *
+ * Copyright (C) 2019, 2020, Uri Shaked
+ */
+import { CPU } from '../cpu/cpu';
+import { u8 } from '../types';
+
+export interface AVRPortConfig {
+ // Register addresses
+ PIN: u8;
+ DDR: u8;
+ PORT: u8;
+}
+
+export type GPIOListener = (value: u8, oldValue: u8) => void;
+
+export const portAConfig: AVRPortConfig = {
+ PIN: 0x20,
+ DDR: 0x21,
+ PORT: 0x22
+};
+
+export const portBConfig: AVRPortConfig = {
+ PIN: 0x23,
+ DDR: 0x24,
+ PORT: 0x25
+};
+
+export const portCConfig: AVRPortConfig = {
+ PIN: 0x26,
+ DDR: 0x27,
+ PORT: 0x28
+};
+
+export const portDConfig: AVRPortConfig = {
+ PIN: 0x29,
+ DDR: 0x2a,
+ PORT: 0x2b
+};
+
+export const portEConfig: AVRPortConfig = {
+ PIN: 0x2c,
+ DDR: 0x2d,
+ PORT: 0x2e
+};
+
+export const portFConfig: AVRPortConfig = {
+ PIN: 0x2f,
+ DDR: 0x30,
+ PORT: 0x31
+};
+
+export const portGConfig: AVRPortConfig = {
+ PIN: 0x32,
+ DDR: 0x33,
+ PORT: 0x34
+};
+
+export const portHConfig: AVRPortConfig = {
+ PIN: 0x100,
+ DDR: 0x101,
+ PORT: 0x102
+};
+
+export const portJConfig: AVRPortConfig = {
+ PIN: 0x103,
+ DDR: 0x104,
+ PORT: 0x105
+};
+
+export const portKConfig: AVRPortConfig = {
+ PIN: 0x106,
+ DDR: 0x107,
+ PORT: 0x108
+};
+
+export const portLConfig: AVRPortConfig = {
+ PIN: 0x109,
+ DDR: 0x10a,
+ PORT: 0x10b
+};
+
+export enum PinState {
+ Low,
+ High,
+ Input,
+ InputPullUp
+}
+
+export class AVRIOPort {
+ private listeners: GPIOListener[] = [];
+
+ constructor(private cpu: CPU, private portConfig: AVRPortConfig) {
+ cpu.writeHooks[portConfig.PORT] = (value: u8, oldValue: u8) => {
+ const ddrMask = cpu.data[portConfig.DDR];
+ cpu.data[portConfig.PORT] = value;
+ value &= ddrMask;
+ cpu.data[portConfig.PIN] = (cpu.data[portConfig.PIN] & ~ddrMask) | value;
+ this.writeGpio(value, oldValue & ddrMask);
+ return true;
+ };
+ cpu.writeHooks[portConfig.PIN] = (value: u8) => {
+ // Writing to 1 PIN toggles PORT bits
+ const oldPortValue = cpu.data[portConfig.PORT];
+ const ddrMask = cpu.data[portConfig.DDR];
+ const portValue = oldPortValue ^ value;
+ cpu.data[portConfig.PORT] = portValue;
+ cpu.data[portConfig.PIN] = (cpu.data[portConfig.PIN] & ~ddrMask) | (portValue & ddrMask);
+ this.writeGpio(portValue & ddrMask, oldPortValue & ddrMask);
+ return true;
+ };
+ }
+
+ addListener(listener: GPIOListener) {
+ this.listeners.push(listener);
+ }
+
+ removeListener(listener: GPIOListener) {
+ this.listeners = this.listeners.filter((l) => l !== listener);
+ }
+
+ /**
+ * Get the state of a given GPIO pin
+ *
+ * @param index Pin index to return from 0 to 7
+ * @returns PinState.Low or PinState.High if the pin is set to output, PinState.Input if the pin is set
+ * to input, and PinState.InputPullUp if the pin is set to input and the internal pull-up resistor has
+ * been enabled.
+ */
+ pinState(index: number) {
+ const ddr = this.cpu.data[this.portConfig.DDR];
+ const port = this.cpu.data[this.portConfig.PORT];
+ const bitMask = 1 << index;
+ if (ddr & bitMask) {
+ return port & bitMask ? PinState.High : PinState.Low;
+ } else {
+ return port & bitMask ? PinState.InputPullUp : PinState.Input;
+ }
+ }
+
+ private writeGpio(value: u8, oldValue: u8) {
+ for (const listener of this.listeners) {
+ listener(value, oldValue);
+ }
+ }
+}
diff --git a/src/peripherals/timer.spec.ts b/src/peripherals/timer.spec.ts
new file mode 100644
index 0000000..adcef04
--- /dev/null
+++ b/src/peripherals/timer.spec.ts
@@ -0,0 +1,204 @@
+import { CPU } from '../cpu/cpu';
+import { AVRTimer, timer0Config, timer2Config } from './timer';
+
+describe('timer', () => {
+ let cpu: CPU;
+
+ beforeEach(() => {
+ cpu = new CPU(new Uint16Array(0x1000));
+ });
+
+ it('should update timer every tick when prescaler is 1', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(1); // TCNT should be 1
+ });
+
+ it('should update timer every 64 ticks when prescaler is 3', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x45] = 0x3; // TCCR0B.CS <- 3
+ cpu.cycles = 64;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(1); // TCNT should be 1
+ });
+
+ it('should not update timer if it has been disabled', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x45] = 0; // TCCR0B.CS <- 0
+ cpu.cycles = 100000;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0); // TCNT should stay 0
+ });
+
+ it('should set TOV if timer overflows', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x46] = 0xff; // TCNT0 <- 0xff
+ cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0); // TCNT should be 0
+ expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR
+ });
+
+ it('should set TOV if timer overflows in PWM Phase Correct mode', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x46] = 0xff; // TCNT0 <- 0xff
+ cpu.writeData(0x47, 0x7f); // OCRA <- 0x7f
+ cpu.writeData(0x44, 0x1); // WGM0 <- 1 (PWM, Phase Correct)
+ cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0); // TCNT should be 0
+ expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR
+ });
+
+ it('should set TOV if timer overflows in FAST PWM mode', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x46] = 0xff; // TCNT0 <- 0xff
+ cpu.writeData(0x47, 0x7f); // OCRA <- 0x7f
+ cpu.writeData(0x44, 0x3); // WGM0 <- 3 (FAST PWM)
+ cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0); // TCNT should be 0
+ expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR
+ });
+
+ it('should generate an overflow interrupt if timer overflows and interrupts enabled', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x46] = 0xff; // TCNT0 <- 0xff
+ cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
+ cpu.data[0x6e] = 0x1; // TIMSK0: TOIE0
+ cpu.data[95] = 0x80; // SREG: I-------
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0); // TCNT should be 0
+ expect(cpu.data[0x35]).toEqual(0); // TOV bit in TIFR should be clear
+ expect(cpu.pc).toEqual(0x20);
+ expect(cpu.cycles).toEqual(3);
+ });
+
+ it('should not generate an overflow interrupt when global interrupts disabled', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x46] = 0xff; // TCNT0 <- 0xff
+ cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
+ cpu.data[0x6e] = 0x1; // TIMSK0: TOIE0
+ cpu.data[95] = 0x0; // SREG: --------
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR should be set
+ expect(cpu.pc).toEqual(0);
+ expect(cpu.cycles).toEqual(1);
+ });
+
+ it('should not generate an overflow interrupt when TOIE0 is clear', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.data[0x46] = 0xff; // TCNT0 <- 0xff
+ cpu.data[0x45] = 0x1; // TCCR0B.CS <- 1
+ cpu.data[0x6e] = 0; // TIMSK0: clear
+ cpu.data[95] = 0x80; // SREG: I-------
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x35]).toEqual(1); // TOV bit in TIFR should be set
+ expect(cpu.pc).toEqual(0);
+ expect(cpu.cycles).toEqual(1);
+ });
+
+ it('should set OCF0A flag when timer equals OCRA', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.writeData(0x46, 0x10); // TCNT0 <- 0x10
+ cpu.writeData(0x47, 0x11); // OCR0A <- 0x11
+ cpu.writeData(0x44, 0x0); // WGM0 <- 0 (Normal)
+ cpu.writeData(0x45, 0x1); // TCCR0B.CS <- 1
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x35]).toEqual(2); // TIFR0 should have OCF0A bit on
+ expect(cpu.pc).toEqual(0);
+ expect(cpu.cycles).toEqual(1);
+ });
+
+ it('should clear the timer in CTC mode if it equals to OCRA', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.writeData(0x46, 0x10); // TCNT0 <- 0x10
+ cpu.writeData(0x47, 0x11); // OCR0A <- 0x11
+ cpu.writeData(0x44, 0x2); // WGM0 <- 2 (CTC)
+ cpu.writeData(0x45, 0x1); // TCCR0B.CS <- 1
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0); // TCNT should be 0
+ expect(cpu.pc).toEqual(0);
+ expect(cpu.cycles).toEqual(1);
+ });
+
+ it('should set OCF0B flag when timer equals OCRB', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.writeData(0x46, 0x10); // TCNT0 <- 0x50
+ cpu.writeData(0x48, 0x11); // OCR0B <- 0x51
+ cpu.writeData(0x44, 0x0); // WGM0 <- 0 (Normal)
+ cpu.writeData(0x45, 0x1); // TCCR0B.CS <- 1
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x35]).toEqual(4); // TIFR0 should have OCF0B bit on
+ expect(cpu.pc).toEqual(0);
+ expect(cpu.cycles).toEqual(1);
+ });
+
+ it('should generate Timer Compare A interrupt when TCNT0 == TCNTA', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.writeData(0x46, 0x20); // TCNT0 <- 0x20
+ cpu.writeData(0x47, 0x21); // OCR0A <- 0x21
+ cpu.writeData(0x45, 0x1); // TCCR0B.CS <- 1
+ cpu.writeData(0x6e, 0x2); // TIMSK0: OCIEA
+ cpu.writeData(95, 0x80); // SREG: I-------
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0x21); // TCNT should be 0x21
+ expect(cpu.data[0x35]).toEqual(0); // OCFA bit in TIFR should be clear
+ expect(cpu.pc).toEqual(0x1c);
+ expect(cpu.cycles).toEqual(3);
+ });
+
+ it('should not generate Timer Compare A interrupt when OCIEA is disabled', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.writeData(0x46, 0x20); // TCNT0 <- 0x20
+ cpu.writeData(0x47, 0x21); // OCR0A <- 0x21
+ cpu.writeData(0x45, 0x1); // TCCR0B.CS <- 1
+ cpu.writeData(0x6e, 0); // TIMSK0
+ cpu.writeData(95, 0x80); // SREG: I-------
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0x21); // TCNT should be 0x21
+ expect(cpu.pc).toEqual(0);
+ expect(cpu.cycles).toEqual(1);
+ });
+
+ it('should generate Timer Compare B interrupt when TCNT0 == TCNTB', () => {
+ const timer = new AVRTimer(cpu, timer0Config);
+ cpu.writeData(0x46, 0x20); // TCNT0 <- 0x20
+ cpu.writeData(0x48, 0x21); // OCR0B <- 0x21
+ cpu.writeData(0x45, 0x1); // TCCR0B.CS <- 1
+ cpu.writeData(0x6e, 0x4); // TIMSK0: OCIEB
+ cpu.writeData(95, 0x80); // SREG: I-------
+ cpu.cycles = 1;
+ timer.tick();
+ expect(cpu.data[0x46]).toEqual(0x21); // TCNT should be 0x21
+ expect(cpu.data[0x35]).toEqual(0); // OCFB bit in TIFR should be clear
+ expect(cpu.pc).toEqual(0x1e);
+ expect(cpu.cycles).toEqual(3);
+ });
+
+ it('timer2 should count every 256 ticks when prescaler is 6 (issue #5)', () => {
+ const timer = new AVRTimer(cpu, timer2Config);
+ cpu.data[0xb1] = 0x6; // TCCR1B.CS <- 6
+
+ cpu.cycles = 511;
+ timer.tick();
+ expect(cpu.data[0xb2]).toEqual(1); // TCNT2 should be 2
+
+ cpu.cycles = 512;
+ timer.tick();
+ expect(cpu.data[0xb2]).toEqual(2); // TCNT2 should be 2
+ });
+});
diff --git a/src/peripherals/timer.ts b/src/peripherals/timer.ts
new file mode 100644
index 0000000..7e563c2
--- /dev/null
+++ b/src/peripherals/timer.ts
@@ -0,0 +1,244 @@
+/**
+ * AVR-8 Timers
+ * Part of AVR8js
+ * Reference: http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
+ *
+ * Copyright (C) 2019, Uri Shaked
+ */
+
+import { CPU } from '../cpu/cpu';
+import { avrInterrupt } from '../cpu/interrupt';
+
+const timer01Dividers = {
+ 0: 0,
+ 1: 1,
+ 2: 8,
+ 3: 64,
+ 4: 256,
+ 5: 1024,
+ 6: 0, // TODO: External clock source on T0 pin. Clock on falling edge.
+ 7: 0 // TODO: External clock source on T0 pin. Clock on rising edge.
+};
+
+const WGM_NORMAL = 0;
+const WGM_PWM_PHASE_CORRECT = 1;
+const WGM_CTC = 2;
+const WGM_FASTPWM = 3;
+
+const TOV = 1;
+const OCFA = 2;
+const OCFB = 4;
+
+const TOIE = 1;
+const OCIEA = 2;
+const OCIEB = 4;
+
+type u8 = number;
+
+interface TimerDividers {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+}
+
+interface AVRTimerConfig {
+ bits: 8 | 16;
+ captureInterrupt: u8;
+ compAInterrupt: u8;
+ compBInterrupt: u8;
+ ovfInterrupt: u8;
+
+ // Register addresses
+ TIFR: u8;
+ OCRA: u8;
+ OCRB: u8;
+ ICR: u8;
+ TCNT: u8;
+ TCCRA: u8;
+ TCCRB: u8;
+ TCCRC: u8;
+ TIMSK: u8;
+
+ dividers: TimerDividers;
+}
+
+export const timer0Config: AVRTimerConfig = {
+ bits: 8,
+ captureInterrupt: 0, // not available
+ compAInterrupt: 0x1c,
+ compBInterrupt: 0x1e,
+ ovfInterrupt: 0x20,
+ TIFR: 0x35,
+ OCRA: 0x47,
+ OCRB: 0x48,
+ ICR: 0, // not available
+ TCNT: 0x46,
+ TCCRA: 0x44,
+ TCCRB: 0x45,
+ TCCRC: 0, // not available
+ TIMSK: 0x6e,
+ dividers: timer01Dividers
+};
+
+export const timer1Config: AVRTimerConfig = {
+ bits: 16,
+ captureInterrupt: 0x14,
+ compAInterrupt: 0x16,
+ compBInterrupt: 0x18,
+ ovfInterrupt: 0x1a,
+ TIFR: 0x36,
+ OCRA: 0x88,
+ OCRB: 0x8a,
+ ICR: 0x86,
+ TCNT: 0x84,
+ TCCRA: 0x80,
+ TCCRB: 0x81,
+ TCCRC: 0x82,
+ TIMSK: 0x6f,
+ dividers: timer01Dividers
+};
+
+export const timer2Config: AVRTimerConfig = {
+ bits: 8,
+ captureInterrupt: 0, // not available
+ compAInterrupt: 0x0e,
+ compBInterrupt: 0x10,
+ ovfInterrupt: 0x12,
+ TIFR: 0x37,
+ OCRA: 0xb3,
+ OCRB: 0xb4,
+ ICR: 0, // not available
+ TCNT: 0xb2,
+ TCCRA: 0xb0,
+ TCCRB: 0xb1,
+ TCCRC: 0, // not available
+ TIMSK: 0x70,
+ dividers: {
+ 0: 1,
+ 1: 1,
+ 2: 8,
+ 3: 32,
+ 4: 64,
+ 5: 128,
+ 6: 256,
+ 7: 1024
+ }
+};
+
+export class AVRTimer {
+ private mask = (1 << this.config.bits) - 1;
+ private lastCycle = 0;
+ private ocrA: u8 = 0;
+ private ocrB: u8 = 0;
+
+ constructor(private cpu: CPU, private config: AVRTimerConfig) {
+ cpu.writeHooks[config.TCNT] = (value: u8) => {
+ this.TCNT = value;
+ this.timerUpdated(value);
+ return true;
+ };
+ cpu.writeHooks[config.OCRA] = (value: u8) => {
+ // TODO implement buffering when timer running in PWM mode
+ this.ocrA = value;
+ };
+ cpu.writeHooks[config.OCRB] = (value: u8) => {
+ this.ocrB = value;
+ };
+ }
+
+ reset() {
+ this.lastCycle = 0;
+ this.ocrA = 0;
+ this.ocrB = 0;
+ }
+
+ get TIFR() {
+ return this.cpu.data[this.config.TIFR];
+ }
+
+ set TIFR(value: u8) {
+ this.cpu.data[this.config.TIFR] = value;
+ }
+
+ get TCNT() {
+ return this.cpu.data[this.config.TCNT];
+ }
+
+ set TCNT(value: u8) {
+ this.cpu.data[this.config.TCNT] = value;
+ }
+
+ get TCCRA() {
+ return this.cpu.data[this.config.TCCRA];
+ }
+
+ get TCCRB() {
+ return this.cpu.data[this.config.TCCRB];
+ }
+
+ get TIMSK() {
+ return this.cpu.data[this.config.TIMSK];
+ }
+
+ get CS() {
+ return (this.TCCRB & 0x7) as 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
+ }
+
+ get WGM() {
+ return ((this.TCCRB & 0x8) >> 1) | (this.TCCRA & 0x3);
+ }
+
+ tick() {
+ const divider = this.config.dividers[this.CS];
+ const delta = this.cpu.cycles - this.lastCycle;
+ if (divider && delta >= divider) {
+ const counterDelta = Math.floor(delta / divider);
+ this.lastCycle += counterDelta * divider;
+ const val = this.TCNT;
+ const newVal = (val + counterDelta) & this.mask;
+ this.TCNT = newVal;
+ this.timerUpdated(newVal);
+ if (
+ (this.WGM === WGM_NORMAL ||
+ this.WGM === WGM_PWM_PHASE_CORRECT ||
+ this.WGM === WGM_FASTPWM) &&
+ val > newVal
+ ) {
+ this.TIFR |= TOV;
+ }
+ }
+ if (this.cpu.interruptsEnabled) {
+ if (this.TIFR & TOV && this.TIMSK & TOIE) {
+ avrInterrupt(this.cpu, this.config.ovfInterrupt);
+ this.TIFR &= ~TOV;
+ }
+ if (this.TIFR & OCFA && this.TIMSK & OCIEA) {
+ avrInterrupt(this.cpu, this.config.compAInterrupt);
+ this.TIFR &= ~OCFA;
+ }
+ if (this.TIFR & OCFB && this.TIMSK & OCIEB) {
+ avrInterrupt(this.cpu, this.config.compBInterrupt);
+ this.TIFR &= ~OCFB;
+ }
+ }
+ }
+
+ private timerUpdated(value: u8) {
+ if (this.ocrA && value === this.ocrA) {
+ this.TIFR |= OCFA;
+ if (this.WGM === WGM_CTC) {
+ // Clear Timer on Compare Match (CTC) Mode
+ this.TCNT = 0;
+ this.TIFR |= TOV;
+ }
+ }
+ if (this.ocrB && value === this.ocrB) {
+ this.TIFR |= OCFB;
+ }
+ }
+}
diff --git a/src/peripherals/twi.spec.ts b/src/peripherals/twi.spec.ts
new file mode 100644
index 0000000..542cf2a
--- /dev/null
+++ b/src/peripherals/twi.spec.ts
@@ -0,0 +1,390 @@
+import { CPU } from '../cpu/cpu';
+import { AVRTWI, twiConfig } from './twi';
+import { assemble } from '../utils/assembler';
+import { avrInstruction } from '../cpu/instruction';
+
+const FREQ_16MHZ = 16e6;
+
+function asmProgram(source: string) {
+ const { bytes, errors, lines } = assemble(source);
+ if (errors.length) {
+ throw new Error('Assembly failed: ' + errors);
+ }
+ return { program: new Uint16Array(bytes.buffer), lines };
+}
+
+function runInstructions(cpu: CPU, twi: AVRTWI, count: number) {
+ for (let i = 0; i < count; i++) {
+ if (cpu.progMem[cpu.pc] === 0x9598) {
+ console.log(cpu.data[0xbc].toString(16));
+ console.log(cpu.data[16]);
+ throw new Error('BREAK instruction encountered');
+ }
+ avrInstruction(cpu);
+ twi.tick();
+ }
+}
+
+describe('TWI', () => {
+ const TWINT = 7;
+ const TWSTA = 5;
+ const TWEN = 2;
+
+ it('should correctly calculate the sclFrequency from TWBR', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ);
+ cpu.writeData(0xb8, 0x48); // TWBR <- 0x48
+ cpu.writeData(0xb9, 0); // TWSR <- 0 (prescaler: 1)
+ expect(twi.sclFrequency).toEqual(100000);
+ });
+
+ it('should take the prescaler into consideration when calculating sclFrequency', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ);
+ cpu.writeData(0xb8, 0x03); // TWBR <- 0x03
+ cpu.writeData(0xb9, 0x01); // TWSR <- 1 (prescaler: 4)
+ expect(twi.sclFrequency).toEqual(400000);
+ });
+
+ it('should trigger data an interrupt if TWINT is set', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ);
+ cpu.writeData(0xbc, 0x81); // TWCR <- TWINT | TWIE
+ cpu.data[95] = 0x80; // SREG: I-------
+ twi.tick();
+ expect(cpu.pc).toEqual(0x30); // 2-wire Serial Interface Vector
+ expect(cpu.cycles).toEqual(2);
+ expect(cpu.data[0xbc] & 0x80).toEqual(0); // UCSR0A should clear TWINT
+ });
+
+ describe('Master mode', () => {
+ it('should call the startEvent handler when TWSTA bit is written 1', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ);
+ jest.spyOn(twi.eventHandler, 'start');
+ cpu.writeData(0xbc, (1 << TWINT) | (1 << TWSTA) | (1 << TWEN));
+ twi.tick();
+ expect(twi.eventHandler.start).toHaveBeenCalledWith(false);
+ });
+
+ it('should successfully transmit a byte to a slave', () => {
+ // based on the example in page 225 of the datasheet:
+ // https://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
+ const { program } = asmProgram(`
+ ; register addresses
+ _REPLACE TWSR, 0xb9
+ _REPLACE TWDR, 0xbb
+ _REPLACE TWCR, 0xbc
+
+ ; TWCR bits
+ _REPLACE TWEN, 0x04
+ _REPLACE TWSTO, 0x10
+ _REPLACE TWSTA, 0x20
+ _REPLACE TWINT, 0x80
+
+ ; TWSR states
+ _REPLACE START, 0x8 ; TWI start
+ _REPLACE MT_SLA_ACK, 0x18 ; Slave Adresss ACK has been received
+ _REPLACE MT_DATA_ACK, 0x28 ; Data ACK has been received
+
+ ; Send start condition
+ ldi r16, TWEN
+ sbr r16, TWSTA
+ sbr r16, TWINT
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the START condition has been transmitted
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. Mask prescaler bits. If status different from START go to ERROR
+ lds r16, TWSR
+ andi r16, 0xf8
+ cpi r16, START
+ brne error
+
+ ; Load SLA_W into TWDR Register. Clear TWINT bit in TWCR to start transmission of address
+ ; 0x44 = Address 0x22, write mode (R/W bit clear)
+ _REPLACE SLA_W, 0x44
+ ldi r16, SLA_W
+ sts TWDR, r16
+ ldi r16, TWINT
+ sbr r16, TWEN
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the SLA+W has been transmitted, and ACK/NACK has been received.
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_SLA_ACK go to ERROR
+ lds r16, TWSR
+ andi r16, 0xf8
+ cpi r16, MT_SLA_ACK
+ brne error
+
+ ; Load DATA into TWDR Register. Clear TWINT bit in TWCR to start transmission of data
+ _replace DATA, 0x55
+ ldi r16, DATA
+ sts TWDR, r16
+ ldi r16, TWINT
+ sbr r16, TWEN
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the DATA has been transmitted, and ACK/NACK has been received
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_DATA_ACK go to ERROR
+ lds r16, TWSR
+ andi r16, 0xf8
+ cpi r16, MT_DATA_ACK
+ brne error
+
+ ; Transmit STOP condition
+ ldi r16, TWINT
+ sbr r16, TWEN
+ sbr r16, TWSTO
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the STOP condition has been sent
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. The masked value should be 0xf8 once done
+ lds r16, TWSR
+ andi r16, 0xf8
+ cpi r16, 0xf8
+ brne error
+
+ ; Indicate success by loading 0x42 into r17
+ ldi r17, 0x42
+
+ loop:
+ jmp loop
+
+ ; Busy-waits for the TWINT flag to be set
+ wait_for_twint:
+ lds r16, TWCR
+ andi r16, TWINT
+ breq wait_for_twint
+ ret
+
+ ; In case of an error, toggle a breakpoint
+ error:
+ break
+ `);
+ const cpu = new CPU(program);
+ const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ);
+ twi.eventHandler = {
+ start: jest.fn(),
+ stop: jest.fn(),
+ connectToSlave: jest.fn(),
+ writeByte: jest.fn(),
+ readByte: jest.fn()
+ };
+
+ // Step 1: wait for start condition
+ runInstructions(cpu, twi, 4);
+ expect(twi.eventHandler.start).toHaveBeenCalledWith(false);
+
+ runInstructions(cpu, twi, 16);
+ twi.completeStart();
+
+ // Step 2: wait for slave connect in write mode
+ runInstructions(cpu, twi, 16);
+ expect(twi.eventHandler.connectToSlave).toHaveBeenCalledWith(0x22, true);
+
+ runInstructions(cpu, twi, 16);
+ twi.completeConnect(true);
+
+ // Step 3: wait for first data byte
+ runInstructions(cpu, twi, 16);
+ expect(twi.eventHandler.writeByte).toHaveBeenCalledWith(0x55);
+
+ runInstructions(cpu, twi, 16);
+ twi.completeWrite(true);
+
+ // Step 4: wait for stop condition
+ runInstructions(cpu, twi, 16);
+ expect(twi.eventHandler.stop).toHaveBeenCalled();
+
+ runInstructions(cpu, twi, 16);
+ twi.completeStop();
+
+ // Step 5: wait for the assembly code to indicate success by settings r17 to 0x42
+ runInstructions(cpu, twi, 16);
+ expect(cpu.data[17]).toEqual(0x42);
+ });
+
+ it('should successfully receive a byte from a slave', () => {
+ const { program } = asmProgram(`
+ ; register addresses
+ _REPLACE TWSR, 0xb9
+ _REPLACE TWDR, 0xbb
+ _REPLACE TWCR, 0xbc
+
+ ; TWCR bits
+ _REPLACE TWEN, 0x04
+ _REPLACE TWSTO, 0x10
+ _REPLACE TWSTA, 0x20
+ _REPLACE TWEA, 0x40
+ _REPLACE TWINT, 0x80
+
+ ; TWSR states
+ _REPLACE START, 0x8 ; TWI start
+ _REPLACE MT_SLAR_ACK, 0x40 ; Slave Adresss ACK has been received
+ _REPLACE MT_DATA_RECV, 0x50 ; Data has been received
+ _REPLACE MT_DATA_RECV_NACK, 0x58 ; Data has been received, NACK has been returned
+
+ ; Send start condition
+ ldi r16, TWEN
+ sbr r16, TWSTA
+ sbr r16, TWINT
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the START condition has been transmitted
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. Mask prescaler bits. If status different from START go to ERROR
+ lds r16, TWSR
+ andi r16, 0xf8
+ ldi r18, START
+ cpse r16, r18
+ jmp error ; only jump if r16 != r18 (START)
+
+ ; Load SLA_R into TWDR Register. Clear TWINT bit in TWCR to start transmission of address
+ ; 0xa1 = Address 0x50, read mode (R/W bit set)
+ _REPLACE SLA_R, 0xa1
+ ldi r16, SLA_R
+ sts TWDR, r16
+ ldi r16, TWINT
+ sbr r16, TWEN
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the SLA+W has been transmitted, and ACK/NACK has been received.
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_SLA_ACK go to ERROR
+ lds r16, TWSR
+ andi r16, 0xf8
+ cpi r16, MT_SLAR_ACK
+ brne error
+
+ ; Clear TWINT bit in TWCR to receive the next byte, set TWEA to send ACK
+ ldi r16, TWINT
+ sbr r16, TWEA
+ sbr r16, TWEN
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the DATA has been received, and ACK has been transmitted
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_DATA_RECV go to ERROR
+ lds r16, TWSR
+ andi r16, 0xf8
+ cpi r16, MT_DATA_RECV
+ brne error
+
+ ; Validate that we recieved the desired data - first byte should be 0x66
+ lds r16, TWDR
+ cpi r16, 0x66
+ brne error
+
+ ; Clear TWINT bit in TWCR to receive the next byte, this time we don't ACK
+ ldi r16, TWINT
+ sbr r16, TWEN
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the DATA has been received, and NACK has been transmitted
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. Mask prescaler bits. If status different from MT_DATA_RECV_NACK go to ERROR
+ lds r16, TWSR
+ andi r16, 0xf8
+ cpi r16, MT_DATA_RECV_NACK
+ brne error
+
+ ; Validate that we recieved the desired data - second byte should be 0x77
+ lds r16, TWDR
+ cpi r16, 0x77
+ brne error
+
+ ; Transmit STOP condition
+ ldi r16, TWINT
+ sbr r16, TWEN
+ sbr r16, TWSTO
+ sts TWCR, r16
+
+ ; Wait for TWINT Flag set. This indicates that the STOP condition has been sent
+ call wait_for_twint
+
+ ; Check value of TWI Status Register. The masked value should be 0xf8 once done
+ lds r16, TWSR
+ andi r16, 0xf8
+ cpi r16, 0xf8
+ brne error
+
+ ; Indicate success by loading 0x42 into r17
+ ldi r17, 0x42
+
+ loop:
+ jmp loop
+
+ ; Busy-waits for the TWINT flag to be set
+ wait_for_twint:
+ lds r16, TWCR
+ andi r16, TWINT
+ breq wait_for_twint
+ ret
+
+ ; In case of an error, toggle a breakpoint
+ error:
+ break
+ `);
+ const cpu = new CPU(program);
+ const twi = new AVRTWI(cpu, twiConfig, FREQ_16MHZ);
+ twi.eventHandler = {
+ start: jest.fn(),
+ stop: jest.fn(),
+ connectToSlave: jest.fn(),
+ writeByte: jest.fn(),
+ readByte: jest.fn()
+ };
+
+ // Step 1: wait for start condition
+ runInstructions(cpu, twi, 4);
+ expect(twi.eventHandler.start).toHaveBeenCalledWith(false);
+
+ runInstructions(cpu, twi, 16);
+ twi.completeStart();
+
+ // Step 2: wait for slave connect in read mode
+ runInstructions(cpu, twi, 16);
+ expect(twi.eventHandler.connectToSlave).toHaveBeenCalledWith(0x50, false);
+
+ runInstructions(cpu, twi, 16);
+ twi.completeConnect(true);
+
+ // Step 3: send the first byte to the master, expect ack
+ runInstructions(cpu, twi, 16);
+ expect(twi.eventHandler.readByte).toHaveBeenCalledWith(true);
+
+ runInstructions(cpu, twi, 16);
+ twi.completeRead(0x66);
+
+ // Step 4: send the first byte to the master, expect nack
+ runInstructions(cpu, twi, 16);
+ expect(twi.eventHandler.readByte).toHaveBeenCalledWith(false);
+
+ runInstructions(cpu, twi, 16);
+ twi.completeRead(0x77);
+
+ // Step 5: wait for stop condition
+ runInstructions(cpu, twi, 24);
+ expect(twi.eventHandler.stop).toHaveBeenCalled();
+
+ runInstructions(cpu, twi, 16);
+ twi.completeStop();
+
+ // Step 6: wait for the assembly code to indicate success by settings r17 to 0x42
+ runInstructions(cpu, twi, 16);
+ expect(cpu.data[17]).toEqual(0x42);
+ });
+ });
+});
diff --git a/src/peripherals/twi.ts b/src/peripherals/twi.ts
new file mode 100644
index 0000000..ec0a206
--- /dev/null
+++ b/src/peripherals/twi.ts
@@ -0,0 +1,200 @@
+import { CPU } from '../cpu/cpu';
+import { avrInterrupt } from '../cpu/interrupt';
+import { u8 } from '../types';
+
+export interface TWIEventHandler {
+ start(repeated: boolean): void;
+
+ stop(): void;
+
+ connectToSlave(addr: u8, write: boolean): void;
+
+ writeByte(value: u8): void;
+
+ readByte(ack: boolean): void;
+}
+
+export interface TWIConfig {
+ twiInterrupt: u8;
+
+ TWBR: u8;
+ TWCR: u8;
+ TWSR: u8;
+ TWDR: u8;
+ TWAR: u8;
+ TWAMR: u8;
+}
+
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// Register bits:
+const TWCR_TWINT = 0x80; // TWI Interrupt Flag
+const TWCR_TWEA = 0x40; // TWI Enable Acknowledge Bit
+const TWCR_TWSTA = 0x20; // TWI START Condition Bit
+const TWCR_TWSTO = 0x10; // TWI STOP Condition Bit
+const TWCR_TWWC = 0x8; //TWI Write Collision Flag
+const TWCR_TWEN = 0x4; // TWI Enable Bit
+const TWCR_TWIE = 0x1; // TWI Interrupt Enable
+const TWSR_TWS_MASK = 0xf8; // TWI Status
+const TWSR_TWPS1 = 0x2; // TWI Prescaler Bits
+const TWSR_TWPS0 = 0x1; // TWI Prescaler Bits
+const TWSR_TWPS_MASK = TWSR_TWPS1 | TWSR_TWPS0; // TWI Prescaler mask
+const TWAR_TWA_MASK = 0xfe; // TWI (Slave) Address Register
+const TWAR_TWGCE = 0x1; // TWI General Call Recognition Enable Bit
+
+const STATUS_BUS_ERROR = 0x0;
+const STATUS_TWI_IDLE = 0xf8;
+// Master states
+const STATUS_START = 0x08;
+const STATUS_REPEATED_START = 0x10;
+const STATUS_SLAW_ACK = 0x18;
+const STATUS_SLAW_NACK = 0x20;
+const STATUS_DATA_SENT_ACK = 0x28;
+const STATUS_DATA_SENT_NACK = 0x30;
+const STATUS_DATA_LOST_ARBITRATION = 0x38;
+const STATUS_SLAR_ACK = 0x40;
+const STATUS_SLAR_NACK = 0x48;
+const STATUS_DATA_RECEIVED_ACK = 0x50;
+const STATUS_DATA_RECEIVED_NACK = 0x58;
+// TODO: add slave states
+/* eslint-enable @typescript-eslint/no-unused-vars */
+
+export const twiConfig: TWIConfig = {
+ twiInterrupt: 0x30,
+ TWBR: 0xb8,
+ TWSR: 0xb9,
+ TWAR: 0xba,
+ TWDR: 0xbb,
+ TWCR: 0xbc,
+ TWAMR: 0xbd
+};
+
+// A simple TWI Event Handler that sends a NACK for all events
+export class NoopTWIEventHandler implements TWIEventHandler {
+ constructor(protected twi: AVRTWI) {}
+
+ start() {
+ this.twi.completeStart();
+ }
+
+ stop() {
+ this.twi.completeStop();
+ }
+
+ connectToSlave() {
+ this.twi.completeConnect(false);
+ }
+
+ writeByte() {
+ this.twi.completeWrite(false);
+ }
+
+ readByte() {
+ this.twi.completeRead(0xff);
+ }
+}
+
+export class AVRTWI {
+ public eventHandler: TWIEventHandler = new NoopTWIEventHandler(this);
+
+ private nextTick: (() => void) | null = null;
+
+ constructor(private cpu: CPU, private config: TWIConfig, private freqMHz: number) {
+ this.updateStatus(STATUS_TWI_IDLE);
+ this.cpu.writeHooks[config.TWCR] = (value) => {
+ const clearInt = value & TWCR_TWINT;
+ if (clearInt) {
+ value &= ~TWCR_TWINT;
+ }
+ const { status } = this;
+ if (clearInt && value & TWCR_TWEN) {
+ const twdrValue = this.cpu.data[this.config.TWDR];
+ this.nextTick = () => {
+ if (value & TWCR_TWSTA) {
+ this.eventHandler.start(status !== STATUS_TWI_IDLE);
+ } else if (value & TWCR_TWSTO) {
+ this.eventHandler.stop();
+ } else if (status === STATUS_START) {
+ this.eventHandler.connectToSlave(twdrValue >> 1, twdrValue & 0x1 ? false : true);
+ } else if (status === STATUS_SLAW_ACK || status === STATUS_DATA_SENT_ACK) {
+ this.eventHandler.writeByte(twdrValue);
+ } else if (status === STATUS_SLAR_ACK || status === STATUS_DATA_RECEIVED_ACK) {
+ const ack = !!(value & TWCR_TWEA);
+ this.eventHandler.readByte(ack);
+ }
+ };
+ this.cpu.data[config.TWCR] = value;
+ return true;
+ }
+ };
+ }
+
+ tick() {
+ if (this.nextTick) {
+ this.nextTick();
+ this.nextTick = null;
+ }
+ if (this.cpu.interruptsEnabled) {
+ const { TWCR, twiInterrupt } = this.config;
+ if (this.cpu.data[TWCR] & TWCR_TWIE && this.cpu.data[TWCR] & TWCR_TWINT) {
+ avrInterrupt(this.cpu, twiInterrupt);
+ this.cpu.data[TWCR] &= ~TWCR_TWINT;
+ }
+ }
+ }
+
+ get prescaler() {
+ switch (this.cpu.data[this.config.TWSR] & TWSR_TWPS_MASK) {
+ case 0:
+ return 1;
+ case 1:
+ return 4;
+ case 2:
+ return 16;
+ case 3:
+ return 64;
+ }
+ // We should never get here:
+ throw new Error('Invalid prescaler value!');
+ }
+
+ get sclFrequency() {
+ return this.freqMHz / (16 + 2 * this.cpu.data[this.config.TWBR] * this.prescaler);
+ }
+
+ completeStart() {
+ this.updateStatus(this.status === STATUS_TWI_IDLE ? STATUS_START : STATUS_REPEATED_START);
+ }
+
+ completeStop() {
+ this.cpu.data[this.config.TWCR] &= ~TWCR_TWSTO;
+ this.updateStatus(STATUS_TWI_IDLE);
+ }
+
+ completeConnect(ack: boolean) {
+ if (this.cpu.data[this.config.TWDR] & 0x1) {
+ this.updateStatus(ack ? STATUS_SLAR_ACK : STATUS_SLAR_NACK);
+ } else {
+ this.updateStatus(ack ? STATUS_SLAW_ACK : STATUS_SLAW_NACK);
+ }
+ }
+
+ completeWrite(ack: boolean) {
+ this.updateStatus(ack ? STATUS_DATA_SENT_ACK : STATUS_DATA_SENT_NACK);
+ }
+
+ completeRead(value: u8) {
+ const ack = !!(this.cpu.data[this.config.TWCR] & TWCR_TWEA);
+ this.cpu.data[this.config.TWDR] = value;
+ this.updateStatus(ack ? STATUS_DATA_RECEIVED_ACK : STATUS_DATA_RECEIVED_NACK);
+ }
+
+ private get status() {
+ return this.cpu.data[this.config.TWSR] & TWSR_TWS_MASK;
+ }
+
+ private updateStatus(value: u8) {
+ const { TWCR, TWSR } = this.config;
+ this.cpu.data[TWSR] = (this.cpu.data[TWSR] & ~TWSR_TWS_MASK) | value;
+ this.cpu.data[TWCR] |= TWCR_TWINT;
+ }
+}
diff --git a/src/peripherals/usart.spec.ts b/src/peripherals/usart.spec.ts
new file mode 100644
index 0000000..d9843dc
--- /dev/null
+++ b/src/peripherals/usart.spec.ts
@@ -0,0 +1,159 @@
+import { CPU } from '../cpu/cpu';
+import { AVRUSART, usart0Config } from './usart';
+
+const FREQ_16MHZ = 16e6;
+const FREQ_11_0529MHZ = 11059200;
+
+describe('USART', () => {
+ it('should correctly calculate the baudRate from UBRR', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_11_0529MHZ);
+ cpu.writeData(0xc5, 0); // UBRR0H <- 0
+ cpu.writeData(0xc4, 5); // UBRR0L <- 5
+ expect(usart.baudRate).toEqual(115200);
+ });
+
+ it('should correctly calculate the baudRate from UBRR in double-speed mode', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc5, 3); // UBRR0H <- 3
+ cpu.writeData(0xc4, 64); // UBRR0L <- 64
+ cpu.writeData(0xc0, 2); // UCSR0A: U2X0
+ expect(usart.baudRate).toEqual(2400);
+ });
+
+ it('should return 5-bits per byte when UCSZ = 0', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc0, 0);
+ expect(usart.bitsPerChar).toEqual(5);
+ });
+
+ it('should return 6-bits per byte when UCSZ = 1', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc0, 0x2);
+ expect(usart.bitsPerChar).toEqual(6);
+ });
+
+ it('should return 7-bits per byte when UCSZ = 2', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc0, 0x4);
+ expect(usart.bitsPerChar).toEqual(7);
+ });
+
+ it('should return 8-bits per byte when UCSZ = 3', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc0, 0x6);
+ expect(usart.bitsPerChar).toEqual(8);
+ });
+
+ it('should return 9-bits per byte when UCSZ = 7', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc0, 0x6);
+ cpu.writeData(0xc1, 0x4);
+ expect(usart.bitsPerChar).toEqual(9);
+ });
+
+ it('should invoke onByteTransmit when UDR0 is written to', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ usart.onByteTransmit = jest.fn();
+ cpu.writeData(0xc1, 0x8); // UCSR0B <- TXEN
+ cpu.writeData(0xc6, 0x61); // UDR0
+ expect(usart.onByteTransmit).toHaveBeenCalledWith(0x61);
+ });
+
+ it('should set UDRE and TXC flags after UDR0', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc1, 0x8); // UCSR0B <- TXEN
+ cpu.writeData(0xc0, 0); // UCSR0A <- 0
+ cpu.writeData(0xc6, 0x61); // UDR0
+ expect(cpu.data[0xc0]).toEqual(0x40 | 0x20); // UCSR0A: TXC | UDRE
+ });
+
+ describe('tick()', () => {
+ it('should trigger data register empty interrupt if UDRE is set', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc1, 0x28); // UCSR0B <- UDRIE | TXEN
+ cpu.writeData(0xc6, 0x61); // UDR0
+ cpu.data[95] = 0x80; // SREG: I-------
+ usart.tick();
+ expect(cpu.pc).toEqual(0x26);
+ expect(cpu.cycles).toEqual(2);
+ expect(cpu.data[0xc0] & 0x20).toEqual(0); // UCSR0A should clear UDRE
+ });
+
+ it('should trigger data tx complete interrupt if TXCIE is set', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc1, 0x48); // UCSR0B <- TXCIE | TXEN
+ cpu.writeData(0xc6, 0x61); // UDR0
+ cpu.data[95] = 0x80; // SREG: I-------
+ usart.tick();
+ expect(cpu.pc).toEqual(0x28);
+ expect(cpu.cycles).toEqual(2);
+ expect(cpu.data[0xc0] & 0x40).toEqual(0); // UCSR0A should clear TXC
+ });
+
+ it('should not trigger any interrupt if interrupts are disabled', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ cpu.writeData(0xc1, 0x28); // UCSR0B <- UDRIE | TXEN
+ cpu.writeData(0xc6, 0x61); // UDR0
+ cpu.data[95] = 0; // SREG: 0 (disable interrupts)
+ usart.tick();
+ expect(cpu.pc).toEqual(0);
+ expect(cpu.cycles).toEqual(0);
+ expect(cpu.data[0xc0]).toEqual(0x40 | 0x20); // UCSR0A: TXC | UDRE
+ });
+ });
+
+ describe('onLineTransmit', () => {
+ it('should call onLineTransmit with the current line buffer after every newline', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ usart.onLineTransmit = jest.fn();
+ cpu.writeData(0xc1, 0x8); // UCSR0B <- TXEN
+ cpu.writeData(0xc6, 0x48); // 'H'
+ cpu.writeData(0xc6, 0x65); // 'e'
+ cpu.writeData(0xc6, 0x6c); // 'l'
+ cpu.writeData(0xc6, 0x6c); // 'l'
+ cpu.writeData(0xc6, 0x6f); // 'o'
+ cpu.writeData(0xc6, 0xa); // '\n'
+ expect(usart.onLineTransmit).toHaveBeenCalledWith('Hello');
+ });
+
+ it('should not call onLineTransmit if no newline was received', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ usart.onLineTransmit = jest.fn();
+ cpu.writeData(0xc1, 0x8); // UCSR0B <- TXEN
+ cpu.writeData(0xc6, 0x48); // 'H'
+ cpu.writeData(0xc6, 0x69); // 'i'
+ expect(usart.onLineTransmit).not.toHaveBeenCalled();
+ });
+
+ it('should clear the line buffer after each call to onLineTransmit', () => {
+ const cpu = new CPU(new Uint16Array(1024));
+ const usart = new AVRUSART(cpu, usart0Config, FREQ_16MHZ);
+ usart.onLineTransmit = jest.fn();
+ cpu.writeData(0xc1, 0x8); // UCSR0B <- TXEN
+ cpu.writeData(0xc6, 0x48); // 'H'
+ cpu.writeData(0xc6, 0x69); // 'i'
+ cpu.writeData(0xc6, 0xa); // '\n'
+ cpu.writeData(0xc6, 0x74); // 't'
+ cpu.writeData(0xc6, 0x68); // 'h'
+ cpu.writeData(0xc6, 0x65); // 'e'
+ cpu.writeData(0xc6, 0x72); // 'r'
+ cpu.writeData(0xc6, 0x65); // 'e'
+ cpu.writeData(0xc6, 0xa); // '\n'
+ expect(usart.onLineTransmit).toHaveBeenCalledWith('there');
+ });
+ });
+});
diff --git a/src/peripherals/usart.ts b/src/peripherals/usart.ts
new file mode 100644
index 0000000..b93c0ea
--- /dev/null
+++ b/src/peripherals/usart.ts
@@ -0,0 +1,134 @@
+import { CPU } from '../cpu/cpu';
+import { avrInterrupt } from '../cpu/interrupt';
+import { u8 } from '../types';
+
+export interface USARTConfig {
+ rxCompleteInterrupt: u8;
+ dataRegisterEmptyInterrupt: u8;
+ txCompleteInterrupt: u8;
+
+ UCSRA: u8;
+ UCSRB: u8;
+ UCSRC: u8;
+ UBRRL: u8;
+ UBRRH: u8;
+ UDR: u8;
+}
+
+export const usart0Config: USARTConfig = {
+ rxCompleteInterrupt: 0x24,
+ dataRegisterEmptyInterrupt: 0x26,
+ txCompleteInterrupt: 0x28,
+ UCSRA: 0xc0,
+ UCSRB: 0xc1,
+ UCSRC: 0xc2,
+ UBRRL: 0xc4,
+ UBRRH: 0xc5,
+ UDR: 0xc6
+};
+
+export type USARTTransmitCallback = (value: u8) => void;
+export type USARTLineTransmitCallback = (value: string) => void;
+
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// Register bits:
+const UCSRA_RXC = 0x80; // USART Receive Complete
+const UCSRA_TXC = 0x40; // USART Transmit Complete
+const UCSRA_UDRE = 0x20; // USART Data Register Empty
+const UCSRA_FE = 0x10; // Frame Error
+const UCSRA_DOR = 0x8; // Data OverRun
+const UCSRA_UPE = 0x4; // USART Parity Error
+const UCSRA_U2X = 0x2; // Double the USART Transmission Speed
+const UCSRA_MPCM = 0x1; // Multi-processor Communication Mode
+const UCSRB_RXCIE = 0x80; // RX Complete Interrupt Enable
+const UCSRB_TXCIE = 0x40; // TX Complete Interrupt Enable
+const UCSRB_UDRIE = 0x20; // USART Data Register Empty Interrupt Enable
+const UCSRB_RXEN = 0x10; // Receiver Enable
+const UCSRB_TXEN = 0x8; // Transmitter Enable
+const UCSRB_UCSZ2 = 0x4; // Character Size 2
+const UCSRB_RXB8 = 0x2; // Receive Data Bit 8
+const UCSRB_TXB8 = 0x1; // Transmit Data Bit 8
+const UCSRC_UMSEL1 = 0x80; // USART Mode Select 1
+const UCSRC_UMSEL0 = 0x40; // USART Mode Select 0
+const UCSRC_UPM1 = 0x20; // Parity Mode 1
+const UCSRC_UPM0 = 0x10; // Parity Mode 0
+const UCSRC_USBS = 0x8; // Stop Bit Select
+const UCSRC_UCSZ1 = 0x4; // Character Size 1
+const UCSRC_UCSZ0 = 0x2; // Character Size 0
+const UCSRC_UCPOL = 0x1; // Clock Polarity
+/* eslint-enable @typescript-eslint/no-unused-vars */
+
+export class AVRUSART {
+ public onByteTransmit: USARTTransmitCallback | null = null;
+ public onLineTransmit: USARTLineTransmitCallback | null = null;
+
+ private lineBuffer = '';
+
+ constructor(private cpu: CPU, private config: USARTConfig, private freqMHz: number) {
+ this.cpu.writeHooks[config.UCSRA] = (value) => {
+ this.cpu.data[config.UCSRA] = value | UCSRA_UDRE | UCSRA_TXC;
+ return true;
+ };
+ this.cpu.writeHooks[config.UCSRB] = (value, oldValue) => {
+ if (value & UCSRB_TXEN && !(oldValue & UCSRB_TXEN)) {
+ // Enabling the transmission - mark UDR as empty
+ this.cpu.data[config.UCSRA] |= UCSRA_UDRE;
+ }
+ };
+ this.cpu.writeHooks[config.UDR] = (value) => {
+ if (this.onByteTransmit) {
+ this.onByteTransmit(value);
+ }
+ if (this.onLineTransmit) {
+ const ch = String.fromCharCode(value);
+ if (ch === '\n') {
+ this.onLineTransmit(this.lineBuffer);
+ this.lineBuffer = '';
+ } else {
+ this.lineBuffer += ch;
+ }
+ }
+ this.cpu.data[config.UCSRA] |= UCSRA_UDRE | UCSRA_TXC;
+ };
+ }
+
+ tick() {
+ if (this.cpu.interruptsEnabled) {
+ const ucsra = this.cpu.data[this.config.UCSRA];
+ const ucsrb = this.cpu.data[this.config.UCSRB];
+ if (ucsra & UCSRA_UDRE && ucsrb & UCSRB_UDRIE) {
+ avrInterrupt(this.cpu, this.config.dataRegisterEmptyInterrupt);
+ this.cpu.data[this.config.UCSRA] &= ~UCSRA_UDRE;
+ }
+ if (ucsrb & UCSRA_TXC && ucsrb & UCSRB_TXCIE) {
+ avrInterrupt(this.cpu, this.config.txCompleteInterrupt);
+ this.cpu.data[this.config.UCSRA] &= ~UCSRA_TXC;
+ }
+ }
+ }
+
+ get baudRate() {
+ const UBRR = (this.cpu.data[this.config.UBRRH] << 8) | this.cpu.data[this.config.UBRRL];
+ const multiplier = this.cpu.data[this.config.UCSRA] & UCSRA_U2X ? 8 : 16;
+ return Math.floor(this.freqMHz / (multiplier * (1 + UBRR)));
+ }
+
+ get bitsPerChar() {
+ const ucsz =
+ ((this.cpu.data[this.config.UCSRA] & (UCSRC_UCSZ1 | UCSRC_UCSZ0)) >> 1) |
+ (this.cpu.data[this.config.UCSRB] & UCSRB_UCSZ2);
+ switch (ucsz) {
+ case 0:
+ return 5;
+ case 1:
+ return 6;
+ case 2:
+ return 7;
+ case 3:
+ return 8;
+ default: // 4..6 are reserved
+ case 7:
+ return 9;
+ }
+ }
+}