aboutsummaryrefslogtreecommitdiff
path: root/src/peripherals
diff options
context:
space:
mode:
authorlironh2020-03-21 09:52:53 +0200
committerlironh2020-03-22 21:08:30 +0200
commit8934a7566a038a74464d3d8df9d04fd875e5b1d7 (patch)
treee131c263938081e6c89f39f141f3f20f6da8f851 /src/peripherals
parentMerge pull request #19 from gfeun/main-execute-loop-optimization (diff)
downloadavr8js-8934a7566a038a74464d3d8df9d04fd875e5b1d7.tar.gz
avr8js-8934a7566a038a74464d3d8df9d04fd875e5b1d7.tar.bz2
avr8js-8934a7566a038a74464d3d8df9d04fd875e5b1d7.zip
refactor: added peripherals and cpu feature folders
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;
+ }
+ }
+}
If you find this content useful please consider a small donation via bitcoin: bitcoin:1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2?amount=0.001&message=Donation Scraping git contents via cgit is very resource intensive. The continued hosting of these git repositories is entirely financed by your bitcoin contributions. For donations of a least 1 mBTC you include your IP address in the message of your bitcoin donation. This will remove this message in the future for requests from your IP, which will significantly reduce token usage. In any case, any contributions to bitcoin address 1mxKyQsHHugqRxPKgwaA7wUwJEGCNthn2 are very welcome. Thanks in advance for you contribution to a self-sustaining ecosystem.