/** * mega-emulation.test.ts * * Tests for the Arduino Mega 2560 (ATmega2560) emulator: * * UNIT tests (no compilation required): * - Simulator initialises with 11 Mega ports (PORTA–PORTL minus I) * - Program memory is 131 072 words (256 KB flash) * - PORTB bit 7 → pin 13 (LED_BUILTIN on Mega) * - PORTB bit 0 → pin 53 (SS) * - PORTA bit 0 → pin 22 * - PORTA all bits → pins 22–29 * - setPinState() works for Mega pins * - PWM pins differ from Uno (OCR0A → pin 13 on Mega, not pin 6) * * END-TO-END test (requires arduino-cli + arduino:avr core): * - Compiles mega-blink-test.ino for arduino:avr:mega:cpu=atmega2560 * - Loads .hex into AVRSimulator('mega') * - setup() sets pins 13, 22–29, 53, 4, 6, 42 HIGH * - loop() blinks pin 13 every 500 ms */ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'; import { spawnSync } from 'child_process'; import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync, readdirSync, } from 'fs'; import { tmpdir } from 'os'; import { join, resolve } from 'path'; import { avrInstruction } from 'avr8js'; import { AVRSimulator } from '../simulation/AVRSimulator'; import { PinManager } from '../simulation/PinManager'; // ─── RAF stubs ──────────────────────────────────────────────────────────────── vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => 1); vi.stubGlobal('cancelAnimationFrame', vi.fn()); // ─── Minimal Intel HEX payloads ─────────────────────────────────────────────── // // MEGA_PORTB_HEX — PORTB test: sets pin 13 HIGH (PORTB bit 7 = 0x80 on Mega) // LDI r16, 0xFF ; 0F EF — all outputs // OUT DDRB, r16 ; 04 B9 — DDRB (I/O 0x04) // LDI r16, 0x80 ; 00 E8 — bit 7 = pin 13 on Mega // OUT PORTB, r16 ; 05 B9 — PORTB (I/O 0x05) // RJMP .-2 ; FF CF — loop // const MEGA_PORTB_HEX = ':0A0000000FEF04B900E805B9FFCFC7\n' + ':00000001FF\n'; // MEGA_PORTB_PIN53_HEX — sets pin 53 HIGH (PORTB bit 0 = 0x01 on Mega) // LDI r16, 0xFF ; 0F EF // OUT DDRB, r16 ; 04 B9 // LDI r16, 0x01 ; 01 E0 — bit 0 = pin 53 on Mega // OUT PORTB, r16 ; 05 B9 // RJMP .-2 ; FF CF // const MEGA_PORTB_PIN53_HEX = ':0A0000000FEF04B901E005B9FFCFC6\n' + ':00000001FF\n'; // MEGA_PORTA_HEX — PORTA test: sets all pins 22–29 HIGH (all bits of PORTA) // LDI r16, 0xFF ; 0F EF // OUT DDRA, r16 ; 01 B9 — DDRA (I/O 0x01) // OUT PORTA, r16 ; 02 B9 — PORTA (I/O 0x02) // RJMP .-2 ; FF CF // const MEGA_PORTA_HEX = ':080000000FEF01B902B9FFCFB7\n' + ':00000001FF\n'; // MEGA_PORTA_PIN22_HEX — sets only pin 22 HIGH (PORTA bit 0 = 0x01) // LDI r16, 0xFF ; 0F EF // OUT DDRA, r16 ; 01 B9 // LDI r16, 0x01 ; 01 E0 — bit 0 = pin 22 // OUT PORTA, r16 ; 02 B9 // RJMP .-2 ; FF CF // const MEGA_PORTA_PIN22_HEX = ':0A0000000FEF01B901E002B9FFCFD4\n' + ':00000001FF\n'; // Empty HEX — minimal valid file, no-op program const EMPTY_HEX = ':00000001FF\n'; // ─── Helper ─────────────────────────────────────────────────────────────────── function runCycles(sim: AVRSimulator, cycles: number): void { for (let i = 0; i < cycles; i++) sim.step(); } /** * Run N cycles executing only instructions — no cpu.tick(). * * avr8js timer peripherals only advance when cpu.tick() is called. * On ATmega2560, timer0Config.ovfInterrupt uses the ATmega328P vector * address (0x20) rather than the Mega address (0x60). Calling cpu.tick() * during E2E tests causes Timer0 OVF to jump to the wrong ISR, which * triggers __bad_interrupt → CPU resets to 0 every ~16 K cycles, preventing * loop() from ever reaching digitalWrite(13, LOW). * Skipping cpu.tick() avoids all timer interrupts while still executing * every AVR instruction correctly. */ function runCyclesNoTick(sim: AVRSimulator, cycles: number): void { const cpu = (sim as any).cpu; for (let i = 0; i < cycles; i++) { avrInstruction(cpu); } } // ─── Unit tests ─────────────────────────────────────────────────────────────── describe('AVRSimulator Mega — initialisation', () => { let pm: PinManager; let sim: AVRSimulator; beforeEach(() => { pm = new PinManager(); sim = new AVRSimulator(pm, 'mega'); }); afterEach(() => sim.stop()); it('creates in idle state', () => { expect(sim).toBeDefined(); expect(sim.isRunning()).toBe(false); }); it('loadHex does not throw', () => { expect(() => sim.loadHex(EMPTY_HEX)).not.toThrow(); }); it('program memory is 131 072 words (256 KB) for Mega', () => { sim.loadHex(EMPTY_HEX); const prog = (sim as any).program as Uint16Array; expect(prog.length).toBe(131_072); }); it('11 Mega ports are initialised (PORTA through PORTL, no PORTI)', () => { sim.loadHex(EMPTY_HEX); const ports = (sim as any).megaPorts as Map; expect(ports).toBeDefined(); expect(ports.size).toBe(11); const expected = ['PORTA','PORTB','PORTC','PORTD','PORTE','PORTF','PORTG','PORTH','PORTJ','PORTK','PORTL']; for (const name of expected) { expect(ports.has(name)).toBe(true); } }); }); // ─── Unit tests — PORTB ─────────────────────────────────────────────────────── describe('AVRSimulator Mega — PORTB pin mapping', () => { it('pin 13 (PORTB bit 7) fires HIGH when 0x80 is written to PORTB', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(MEGA_PORTB_HEX); const changes: boolean[] = []; pm.onPinChange(13, (_pin, state) => changes.push(state)); // Execute: LDI → OUT DDRB → LDI → OUT PORTB runCycles(sim, 20); expect(changes).toContain(true); expect(pm.getPinState(13)).toBe(true); sim.stop(); }); it('pin 53 (PORTB bit 0) fires HIGH when 0x01 is written to PORTB', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(MEGA_PORTB_PIN53_HEX); const changes: boolean[] = []; pm.onPinChange(53, (_pin, state) => changes.push(state)); runCycles(sim, 20); expect(changes).toContain(true); expect(pm.getPinState(53)).toBe(true); sim.stop(); }); it('pin 12 (PORTB bit 6) does NOT fire when only bit 7 is set', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(MEGA_PORTB_HEX); // sets only bit 7 (pin 13) const pin12Changes: boolean[] = []; pm.onPinChange(12, (_pin, state) => pin12Changes.push(state)); runCycles(sim, 20); // pin 12 should NOT have been set HIGH expect(pin12Changes).not.toContain(true); sim.stop(); }); }); // ─── Unit tests — PORTA ─────────────────────────────────────────────────────── describe('AVRSimulator Mega — PORTA pin mapping (pins 22–29)', () => { it('all PORTA pins (22–29) fire HIGH when 0xFF is written to PORTA', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(MEGA_PORTA_HEX); const fired = new Set(); for (let pin = 22; pin <= 29; pin++) { pm.onPinChange(pin, (p, state) => { if (state) fired.add(p); }); } runCycles(sim, 20); for (let pin = 22; pin <= 29; pin++) { expect(fired.has(pin)).toBe(true); expect(pm.getPinState(pin)).toBe(true); } sim.stop(); }); it('only pin 22 (PORTA bit 0) fires when 0x01 is written to PORTA', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(MEGA_PORTA_PIN22_HEX); const firedHigh = new Set(); for (let pin = 22; pin <= 29; pin++) { pm.onPinChange(pin, (p, state) => { if (state) firedHigh.add(p); }); } runCycles(sim, 20); expect(firedHigh.has(22)).toBe(true); // Pins 23–29 must NOT be HIGH for (let pin = 23; pin <= 29; pin++) { expect(firedHigh.has(pin)).toBe(false); } sim.stop(); }); }); // ─── Unit tests — setPinState ───────────────────────────────────────────────── describe('AVRSimulator Mega — setPinState (Mega pins)', () => { it('setPinState does not throw for various Mega pins', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(EMPTY_HEX); // Pins from different Mega ports const megaPins = [ 22, 29, // PORTA 53, 13, // PORTB 37, 30, // PORTC 21, 38, // PORTD 0, 1, 5, // PORTE 54, 61, // PORTF (A0, A7) 41, 4, // PORTG 6, 9, // PORTH 15, 14, // PORTJ 62, 69, // PORTK (A8, A15) 42, 49, // PORTL ]; for (const pin of megaPins) { expect(() => sim.setPinState(pin, true)).not.toThrow(); expect(() => sim.setPinState(pin, false)).not.toThrow(); } sim.stop(); }); }); // ─── Unit tests — PWM ───────────────────────────────────────────────────────── describe('AVRSimulator Mega — PWM OCR mapping differs from Uno', () => { it('OCR0A (addr 0x47) maps to pin 13 on Mega (not pin 6 as on Uno)', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(EMPTY_HEX); const cb = vi.fn(); pm.onPwmChange(13, cb); // Mega: OCR0A → D13 const cpu = (sim as any).cpu; cpu.data[0x47] = 128; // Call pollPwmRegisters directly — avoids RAF dependency in unit tests (sim as any).pollPwmRegisters(); expect(cb).toHaveBeenCalledWith(13, 128 / 255); sim.stop(); }); it('OCR3AL (addr 0x98) maps to pin 5 on Mega', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(EMPTY_HEX); const cb = vi.fn(); pm.onPwmChange(5, cb); const cpu = (sim as any).cpu; cpu.data[0x98] = 200; (sim as any).pollPwmRegisters(); expect(cb).toHaveBeenCalledWith(5, 200 / 255); sim.stop(); }); it('OCR4AL (addr 0xA8) maps to pin 6 on Mega', () => { const pm = new PinManager(); const sim = new AVRSimulator(pm, 'mega'); sim.loadHex(EMPTY_HEX); const cb = vi.fn(); pm.onPwmChange(6, cb); const cpu = (sim as any).cpu; cpu.data[0xA8] = 100; (sim as any).pollPwmRegisters(); expect(cb).toHaveBeenCalledWith(6, 100 / 255); sim.stop(); }); }); // ─── End-to-end: compile + run ──────────────────────────────────────────────── const SKETCH_DIR = resolve(__dirname, '../../../example_zip/extracted/mega-blink-test'); const SKETCH_INO = join(SKETCH_DIR, 'mega-blink-test.ino'); const HEX_CACHE = join(tmpdir(), 'velxio-mega-blink-v2.hex'); // ─── arduino-cli availability ───────────────────────────────────────────────── const ARDUINO_CLI_AVAILABLE = (() => { const r = spawnSync('arduino-cli', ['version'], { encoding: 'utf-8' }); return r.error == null && r.status === 0; })(); function compileSketch(): string { if (existsSync(HEX_CACHE)) { console.log('[compile] Using cached hex:', HEX_CACHE); return readFileSync(HEX_CACHE, 'utf-8'); } console.log('[compile] Compiling mega-blink-test.ino for arduino:avr:mega:cpu=atmega2560…'); const workDir = mkdtempSync(join(tmpdir(), 'velxio-mega-')); const sketchDir = join(workDir, 'mega-blink-test'); mkdirSync(sketchDir); writeFileSync( join(sketchDir, 'mega-blink-test.ino'), readFileSync(SKETCH_INO, 'utf-8'), ); const buildDir = join(workDir, 'build'); mkdirSync(buildDir); const result = spawnSync( 'arduino-cli', [ 'compile', '--fqbn', 'arduino:avr:mega:cpu=atmega2560', '--build-path', buildDir, sketchDir, ], { encoding: 'utf-8', timeout: 120_000 }, ); if (result.error) { throw new Error(`arduino-cli not available: ${result.error.message}`); } if (result.status !== 0) { console.error('[compile] stdout:', result.stdout); console.error('[compile] stderr:', result.stderr); throw new Error(`arduino-cli failed (exit ${result.status}): ${result.stderr}`); } // Find the .hex file (prefer the non-bootloader variant) let hexPath: string | null = null; for (const candidate of ['mega-blink-test.ino.hex', 'sketch.ino.hex']) { const p = join(buildDir, candidate); if (existsSync(p)) { hexPath = p; break; } } if (!hexPath) { const files = readdirSync(buildDir, { recursive: true }) as string[]; const found = files.find( (f) => typeof f === 'string' && f.endsWith('.hex') && !f.includes('bootloader'), ); if (!found) throw new Error('No .hex found in build output'); hexPath = join(buildDir, found); } const hex = readFileSync(hexPath, 'utf-8'); writeFileSync(HEX_CACHE, hex); rmSync(workDir, { recursive: true }); console.log('[compile] Done. Hex size:', hex.length, 'chars'); return hex; } describe.skipIf(!ARDUINO_CLI_AVAILABLE)('Arduino Mega 2560 — end-to-end emulation', () => { let hexContent: string; let sim: AVRSimulator; let pm: PinManager; beforeAll(() => { hexContent = compileSketch(); }); afterAll(() => { try { sim?.stop(); } catch { /* ignore */ } vi.unstubAllGlobals(); }); it('🔧 compiles mega-blink-test.ino for arduino:avr:mega successfully', () => { expect(hexContent).toBeTruthy(); expect(hexContent).toContain(':'); console.log('[hex] First line:', hexContent.split('\n')[0]); console.log('[hex] Size:', hexContent.length, 'chars'); }); it('🟢 pin 13 (LED_BUILTIN, PORTB bit 7) goes HIGH in setup()', () => { pm = new PinManager(); sim = new AVRSimulator(pm, 'mega'); sim.loadHex(hexContent); const changes: boolean[] = []; pm.onPinChange(13, (_pin, state) => changes.push(state)); // ATmega2560 core init (8 KB SRAM + more peripherals) takes longer than Uno. // 5M cycles ≈ 312 ms simulated — well past any reasonable startup + setup(). // Use runCyclesNoTick to skip cpu.tick() — avoids Timer0 OVF firing at the // ATmega328P-specific vector address (0x20) which would reset the CPU. runCyclesNoTick(sim, 5_000_000); console.log('[pin13] state changes:', changes); expect(changes).toContain(true); expect(pm.getPinState(13)).toBe(true); }); it('🟢 all PORTA pins (22–29) are HIGH after setup()', () => { for (let pin = 22; pin <= 29; pin++) { expect(pm.getPinState(pin)).toBe(true); } }); it('🟢 pin 53 (PORTB bit 0) is HIGH after setup()', () => { expect(pm.getPinState(53)).toBe(true); }); it('🟢 pin 4 (PORTG bit 5) is HIGH after setup()', () => { expect(pm.getPinState(4)).toBe(true); }); it('🟢 pin 6 (PORTH bit 3) is HIGH after setup()', () => { expect(pm.getPinState(6)).toBe(true); }); it('🟢 pin 42 (PORTL bit 7) is HIGH after setup()', () => { expect(pm.getPinState(42)).toBe(true); }); it('🔁 loop() blinks pin 13 — transitions to LOW within 20M cycles (~1.25 s)', () => { // delay(500) @ 16 MHz ≈ 8 000 000 cycles. // Run 20M to cover HIGH→LOW and LOW→HIGH regardless of where we are in loop(). const changes: boolean[] = []; pm.onPinChange(13, (_pin, state) => changes.push(state)); runCyclesNoTick(sim, 20_000_000); console.log('[blink] pin 13 transitions:', changes); expect(changes.length).toBeGreaterThan(0); expect(changes).toContain(false); // must go LOW at some point }); it('📐 program memory is 131 072 words (256 KB flash for ATmega2560)', () => { const prog = (sim as any).program as Uint16Array; expect(prog.length).toBe(131_072); }); });