292 lines
9.6 KiB
TypeScript
292 lines
9.6 KiB
TypeScript
/**
|
|
* 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<number, ReturnType<typeof vi.fn>> = {};
|
|
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);
|
|
});
|
|
});
|