/** * AVRSimulator Tests * * Tests the ATmega328p emulator (Arduino Uno backend) including: * - Lifecycle: create, loadHex, start, stop, reset * - ADC initialization (AVRADC) * - Timer1 / Timer2 instantiation * - PWM OCR register monitoring * - Pin state changes via step() execution * - Integration: real HEX that toggles pin 13 */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { AVRSimulator } from '../simulation/AVRSimulator'; import { PinManager } from '../simulation/PinManager'; // ─── Mock requestAnimationFrame (not available in node) ────────────────────── // Depth-limited: calls cb once synchronously but prevents re-entrancy so that // animation loops (start() → execute → requestAnimationFrame(execute) → …) // do not cause infinite recursion / stack overflow in tests. beforeEach(() => { let counter = 0; let depth = 0; vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { if (depth === 0) { depth++; cb(0); depth--; } return ++counter; }); vi.stubGlobal('cancelAnimationFrame', vi.fn()); }); afterEach(() => vi.unstubAllGlobals()); // ─── Minimal Intel HEX payloads ─────────────────────────────────────────────── // // BLINK_HEX: 5 AVR instructions that set pin 13 HIGH then loop: // LDI r16, 0xFF ; 0F EF — load 0xFF // OUT DDRB, r16 ; 04 B9 — set PORTB as output (I/O 0x04 = DDRB) // LDI r16, 0x20 ; 00 E2 — load 0x20 (bit 5 = pin 13) // OUT PORTB, r16 ; 05 B9 — set pin 13 HIGH (I/O 0x05 = PORTB) // RJMP .-2 ; FF CF — infinite loop (jump to self) // // Checksum verified manually. const BLINK_HEX = ':0A0000000FEF04B900E205B9FFCFCD\n' + ':00000001FF\n'; // EOF-only HEX — minimal valid file, empty program const EMPTY_HEX = ':00000001FF\n'; // ─── Basic lifecycle ────────────────────────────────────────────────────────── describe('AVRSimulator — lifecycle', () => { let pm: PinManager; let sim: AVRSimulator; beforeEach(() => { pm = new PinManager(); sim = new AVRSimulator(pm); }); afterEach(() => sim.stop()); it('starts in idle state', () => { expect(sim).toBeDefined(); expect(sim.isRunning()).toBe(false); expect(sim.getSpeed()).toBe(1.0); }); it('loads a valid HEX without throwing', () => { expect(() => sim.loadHex(EMPTY_HEX)).not.toThrow(); expect(() => sim.loadHex(BLINK_HEX)).not.toThrow(); }); it('start() transitions to running state', () => { sim.loadHex(EMPTY_HEX); sim.start(); expect(sim.isRunning()).toBe(true); sim.stop(); expect(sim.isRunning()).toBe(false); }); it('stop() is idempotent (safe to call when not running)', () => { expect(() => sim.stop()).not.toThrow(); expect(sim.isRunning()).toBe(false); }); it('reset() stops the simulation and re-initializes', () => { sim.loadHex(BLINK_HEX); sim.start(); expect(sim.isRunning()).toBe(true); sim.reset(); expect(sim.isRunning()).toBe(false); }); it('clamps speed within [0.1, 10.0]', () => { sim.setSpeed(0.001); expect(sim.getSpeed()).toBe(0.1); sim.setSpeed(999); expect(sim.getSpeed()).toBe(10.0); sim.setSpeed(2.5); expect(sim.getSpeed()).toBe(2.5); }); }); // ─── ADC ───────────────────────────────────────────────────────────────────── describe('AVRSimulator — ADC', () => { it('getADC() returns null before loadHex()', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); expect(sim.getADC()).toBeNull(); }); it('getADC() returns an AVRADC instance after loadHex()', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(EMPTY_HEX); const adc = sim.getADC(); expect(adc).not.toBeNull(); expect(adc).toBeDefined(); }); it('can inject voltage on ADC channels', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(EMPTY_HEX); const adc = sim.getADC(); expect(adc).not.toBeNull(); // channelValues is an array of floats (0..5V) adc!.channelValues[0] = 2.5; // A0 expect(adc!.channelValues[0]).toBe(2.5); adc!.channelValues[3] = 5.0; // A3 expect(adc!.channelValues[3]).toBe(5.0); }); it('ADC state is preserved after reset()', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(EMPTY_HEX); sim.getADC()!.channelValues[0] = 3.3; sim.reset(); // After reset a fresh AVRADC is created — channel[0] is unset (undefined) or 0 expect(sim.getADC()!.channelValues[0] ?? 0).toBe(0); }); }); // ─── PWM monitoring ─────────────────────────────────────────────────────────── describe('AVRSimulator — PWM OCR monitoring', () => { it('PinManager receives PWM update when OCR register changes', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(EMPTY_HEX); const pwmCb = vi.fn(); pm.onPwmChange(9, pwmCb); // D9 = OCR1AL (0x88) // Directly write a value to the OCR1AL register in CPU data memory const cpu = (sim as any).cpu; cpu.data[0x88] = 128; // 50% duty cycle // Simulate a frame tick to trigger OCR polling sim.start(); sim.stop(); expect(pwmCb).toHaveBeenCalledWith(9, 128 / 255); }); it('PWM covers all six Arduino PWM pins', () => { // OCR addr → Arduino pin mapping const PWM_MAP = [ { addr: 0x47, pin: 6 }, // OCR0A { addr: 0x48, pin: 5 }, // OCR0B { addr: 0x88, pin: 9 }, // OCR1A { addr: 0x8a, pin: 10 }, // OCR1B { addr: 0xb3, pin: 11 }, // OCR2A { addr: 0xb4, pin: 3 }, // OCR2B ]; const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(EMPTY_HEX); const cbs: Record> = {}; PWM_MAP.forEach(({ pin }) => { cbs[pin] = vi.fn(); pm.onPwmChange(pin, cbs[pin]); }); const cpu = (sim as any).cpu; PWM_MAP.forEach(({ addr }, i) => { cpu.data[addr] = (i + 1) * 25; // 25, 50, 75, 100, 125, 150 }); sim.start(); sim.stop(); PWM_MAP.forEach(({ pin }, i) => { const expected = ((i + 1) * 25) / 255; expect(cbs[pin]).toHaveBeenCalledWith(pin, expected); }); }); }); // ─── Pin output via setPinState ─────────────────────────────────────────────── describe('AVRSimulator — external pin driving', () => { it('setPinState drives PORTD pins 0-7 without throwing', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(EMPTY_HEX); // setPinState drives external INPUT on the pin (like a button press). // It does NOT directly update PinManager — PinManager reflects CPU PORT OUTPUT. expect(() => sim.setPinState(0, true)).not.toThrow(); expect(() => sim.setPinState(4, true)).not.toThrow(); expect(() => sim.setPinState(7, false)).not.toThrow(); }); it('setPinState drives PORTB pins 8-13', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(EMPTY_HEX); const cb = vi.fn(); pm.onPinChange(13, cb); // D13 = PORTB bit 5 sim.setPinState(13, true); // Note: setPinState calls portB.setPin which may not immediately update the // PORTB listener in test context, but state is reflected internally expect(() => sim.setPinState(13, false)).not.toThrow(); }); it('setPinState drives PORTC analog pins 14-19', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(EMPTY_HEX); expect(() => sim.setPinState(14, true)).not.toThrow(); // A0 expect(() => sim.setPinState(19, false)).not.toThrow(); // A5 }); }); // ─── Integration: real HEX execution ───────────────────────────────────────── describe('AVRSimulator — integration with real HEX', () => { it('executes BLINK_HEX and sets pin 13 HIGH after 4 steps', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(BLINK_HEX); const changes: boolean[] = []; pm.onPinChange(13, (_pin, state) => changes.push(state)); // Step through: LDI → OUT DDRB → LDI → OUT PORTB sim.step(); // LDI r16, 0xFF sim.step(); // OUT DDRB, r16 sim.step(); // LDI r16, 0x20 sim.step(); // OUT PORTB, r16 → triggers PORTB listener expect(changes).toContain(true); expect(pm.getPinState(13)).toBe(true); }); it('pin 13 is LOW before executing any instructions', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(BLINK_HEX); expect(pm.getPinState(13)).toBe(false); }); it('pin 13 stays HIGH after RJMP loop', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm); sim.loadHex(BLINK_HEX); sim.step(); // LDI sim.step(); // OUT DDRB sim.step(); // LDI sim.step(); // OUT PORTB (pin 13 → HIGH) // Execute 10 more steps (RJMP loop, OUT PORTB again, etc.) for (let i = 0; i < 10; i++) sim.step(); expect(pm.getPinState(13)).toBe(true); }); });