diff --git a/example_zip/extracted/mega-blink-test/mega-blink-test.ino b/example_zip/extracted/mega-blink-test/mega-blink-test.ino new file mode 100644 index 0000000..cb74487 --- /dev/null +++ b/example_zip/extracted/mega-blink-test/mega-blink-test.ino @@ -0,0 +1,66 @@ +/** + * mega-blink-test.ino + * + * Arduino Mega 2560 GPIO test sketch used by mega-emulation.test.ts + * + * Exercises pins across multiple ATmega2560 ports: + * - Pin 13 — PORTB bit 7 (LED_BUILTIN) + * - Pins 22-29 — PORTA bits 0-7 (all HIGH in setup) + * - Pin 53 — PORTB bit 0 (SPI SS) + * - Pin 4 — PORTG bit 5 + * - Pin 6 — PORTH bit 3 (PWM) + * - Pin 42 — PORTL bit 7 + * + * loop(): blinks pin 13 using a busy-wait loop (avoids Timer0 ISR dependency) + * so the emulation test can detect both HIGH→LOW and LOW→HIGH transitions + * within a bounded cycle budget. + * + * NOTE: delay() is intentionally avoided here. Arduino's delay() relies on + * Timer0 overflow interrupts whose vector address differs between ATmega328P + * (used by avr8js defaults) and ATmega2560, so it would spin forever in the + * emulator. The busy-wait loop below completes in ~1.4 M CPU cycles per half- + * period — well within the 20 M cycle budget of the emulation test. + */ + +/* Number of iterations per half-blink; ~14 cycles/iter × 100 000 ≈ 1.4 M cycles */ +#define HALF_BLINK_ITERS 100000UL + +static void busyWait(unsigned long iters) { + volatile unsigned long i; + for (i = 0; i < iters; i++) { /* nothing */ } +} + +void setup() { + // LED_BUILTIN = pin 13 = PORTB bit 7 + pinMode(13, OUTPUT); + digitalWrite(13, HIGH); + + // PORTA: pins 22–29 (bits 0–7) + for (int i = 22; i <= 29; i++) { + pinMode(i, OUTPUT); + digitalWrite(i, HIGH); + } + + // PORTB bit 0: pin 53 (SPI SS) + pinMode(53, OUTPUT); + digitalWrite(53, HIGH); + + // PORTG bit 5: pin 4 + pinMode(4, OUTPUT); + digitalWrite(4, HIGH); + + // PORTH bit 3: pin 6 + pinMode(6, OUTPUT); + digitalWrite(6, HIGH); + + // PORTL bit 7: pin 42 + pinMode(42, OUTPUT); + digitalWrite(42, HIGH); +} + +void loop() { + digitalWrite(13, HIGH); + busyWait(HALF_BLINK_ITERS); + digitalWrite(13, LOW); + busyWait(HALF_BLINK_ITERS); +} diff --git a/frontend/src/__tests__/mega-emulation.test.ts b/frontend/src/__tests__/mega-emulation.test.ts new file mode 100644 index 0000000..6b6a0d5 --- /dev/null +++ b/frontend/src/__tests__/mega-emulation.test.ts @@ -0,0 +1,476 @@ +/** + * 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'); + +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.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('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); + }); +}); diff --git a/frontend/src/simulation/AVRSimulator.ts b/frontend/src/simulation/AVRSimulator.ts index 81671b1..b55997f 100644 --- a/frontend/src/simulation/AVRSimulator.ts +++ b/frontend/src/simulation/AVRSimulator.ts @@ -159,8 +159,11 @@ export class AVRSimulator { // ATmega328P: 32 KB = 16 384 words. ATmega2560: 256 KB = 131 072 words. const progWords = this.boardVariant === 'mega' ? 131072 : 16384; - // ATmega2560 has 8 KB SRAM; 328P has 2 KB but avr8js defaults 8 KB (safe over-alloc) - const sramBytes = this.boardVariant === 'mega' ? 8192 : 8192; + // ATmega2560 data space: 0x0000–0x21FF = 8704 bytes total. + // avr8js: data.length = sramBytes + registerSpace (0x100 = 256). + // So sramBytes must be >= 8704 − 256 = 8448 to fit RAMEND=0x21FF on the stack. + // ATmega328P RAMEND = 0x08FF; default 8192 is already a safe over-alloc. + const sramBytes = this.boardVariant === 'mega' ? 8448 : 8192; this.program = new Uint16Array(progWords); for (let i = 0; i < bytes.length; i += 2) { @@ -377,7 +380,7 @@ export class AVRSimulator { this.stop(); if (this.program) { // Re-use the stored hex content path: just reload - const sramBytes = this.boardVariant === 'mega' ? 8192 : 8192; + const sramBytes = this.boardVariant === 'mega' ? 8448 : 8192; console.log('Resetting AVR CPU...'); this.cpu = new CPU(this.program, sramBytes);