diff options
Diffstat (limited to 'src/peripherals')
| -rw-r--r-- | src/peripherals/gpio.spec.ts | 86 | ||||
| -rw-r--r-- | src/peripherals/gpio.ts | 149 | ||||
| -rw-r--r-- | src/peripherals/timer.spec.ts | 204 | ||||
| -rw-r--r-- | src/peripherals/timer.ts | 244 | ||||
| -rw-r--r-- | src/peripherals/twi.spec.ts | 390 | ||||
| -rw-r--r-- | src/peripherals/twi.ts | 200 | ||||
| -rw-r--r-- | src/peripherals/usart.spec.ts | 159 | ||||
| -rw-r--r-- | src/peripherals/usart.ts | 134 |
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; + } + } +} |
