feat: add Arduino Mega support to simulator
- Introduced ArduinoMega component for rendering in the simulator. - Updated SimulatorCanvas to handle Arduino Mega board type. - Enhanced AVRSimulator to support ATmega2560 architecture, including PWM pin mapping and port management. - Modified PinManager to accommodate Mega's non-linear pin mapping. - Updated boardPinMapping utility to include Mega analog pins. - Adjusted Wokwi import/export functionality to recognize and handle Arduino Mega. - Updated useSimulatorStore to initialize AVRSimulator with the correct board variant.pull/10/head
parent
faa6f6b7b3
commit
1018609ed4
|
|
@ -0,0 +1,397 @@
|
|||
/**
|
||||
* ili9341-emulation.test.ts
|
||||
*
|
||||
* End-to-end test for ILI9341 TFT display emulation:
|
||||
*
|
||||
* 1. Compile ili9341-test-sketch.ino with arduino-cli (arduino:avr:nano)
|
||||
* 2. Load the resulting .hex into AVRSimulator
|
||||
* 3. Intercept the SPI bus with a VirtualILI9341
|
||||
* 4. Run ~5 million simulated cycles
|
||||
* 5. Assert the display received pixel data (RAMWR command)
|
||||
* 6. Verify colored regions (red fill, white rect, blue circle)
|
||||
*
|
||||
* Requirements:
|
||||
* - arduino-cli in PATH
|
||||
* - "arduino:avr" core installed
|
||||
* - "Adafruit ILI9341" and "Adafruit GFX Library" installed
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, 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 { AVRSimulator } from '../simulation/AVRSimulator';
|
||||
import { PinManager } from '../simulation/PinManager';
|
||||
|
||||
// ─── ImageData polyfill ───────────────────────────────────────────────────────
|
||||
|
||||
if (typeof globalThis.ImageData === 'undefined') {
|
||||
class ImageDataPoly {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
readonly data: Uint8ClampedArray;
|
||||
constructor(w: number, h: number) {
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
this.data = new Uint8ClampedArray(w * h * 4);
|
||||
}
|
||||
}
|
||||
(globalThis as any).ImageData = ImageDataPoly;
|
||||
}
|
||||
|
||||
// ─── RAF stub ────────────────────────────────────────────────────────────────
|
||||
|
||||
vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => 1);
|
||||
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
||||
|
||||
// ─── Paths ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const SKETCH_DIR = resolve(
|
||||
__dirname,
|
||||
'../../../example_zip/extracted/ili9341-test-sketch',
|
||||
);
|
||||
const SKETCH_INO = join(SKETCH_DIR, 'ili9341-test-sketch.ino');
|
||||
|
||||
// ─── Hex cache ───────────────────────────────────────────────────────────────
|
||||
|
||||
const HEX_CACHE = join(tmpdir(), 'velxio-ili9341-nano.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 ili9341-test-sketch.ino for Arduino Nano…');
|
||||
|
||||
const workDir = mkdtempSync(join(tmpdir(), 'velxio-ili9341-'));
|
||||
const sketchDir = join(workDir, 'ili9341-test-sketch');
|
||||
mkdirSync(sketchDir);
|
||||
writeFileSync(join(sketchDir, 'ili9341-test-sketch.ino'), readFileSync(SKETCH_INO, 'utf-8'));
|
||||
|
||||
const buildDir = join(workDir, 'build');
|
||||
mkdirSync(buildDir);
|
||||
|
||||
const result = spawnSync(
|
||||
'arduino-cli',
|
||||
[
|
||||
'compile',
|
||||
'--fqbn', 'arduino:avr:nano:cpu=atmega328old',
|
||||
'--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 .hex (prefer non-bootloader)
|
||||
let hexPath: string | null = null;
|
||||
for (const candidate of ['ili9341-test-sketch.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;
|
||||
}
|
||||
|
||||
// ─── VirtualILI9341 SPI monitor ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Intercepts the AVRSimulator SPI bus and decodes ILI9341 commands/data.
|
||||
*
|
||||
* ILI9341 protocol (hardware SPI):
|
||||
* - DC pin LOW → command byte
|
||||
* - DC pin HIGH → data byte
|
||||
* - Commands: 0x2A CASET, 0x2B PASET, 0x2C RAMWR, 0x01 SWRESET, etc.
|
||||
* - Pixel data: RGB-565 (2 bytes per pixel) streamed after RAMWR
|
||||
*/
|
||||
class VirtualILI9341 {
|
||||
static readonly WIDTH = 240;
|
||||
static readonly HEIGHT = 320;
|
||||
|
||||
// Raw framebuffer: RGB-565 per pixel (0 = black / unwritten)
|
||||
readonly framebuffer = new Uint16Array(VirtualILI9341.WIDTH * VirtualILI9341.HEIGHT);
|
||||
|
||||
// Statistics
|
||||
commandCount = 0;
|
||||
ramwrCount = 0; // number of RAMWR (0x2C) commands received
|
||||
pixelCount = 0; // total pixels written
|
||||
|
||||
// DC pin state (must be injected from AVRSimulator PinManager)
|
||||
dcHigh = false;
|
||||
|
||||
// ILI9341 address window
|
||||
private colStart = 0; private colEnd = VirtualILI9341.WIDTH - 1;
|
||||
private rowStart = 0; private rowEnd = VirtualILI9341.HEIGHT - 1;
|
||||
private curX = 0; private curY = 0;
|
||||
|
||||
// Command state machine
|
||||
private currentCmd = -1;
|
||||
private dataBytes: number[] = [];
|
||||
private inRamWrite = false;
|
||||
private pixelHiByte = 0;
|
||||
private pixelByteIdx = 0;
|
||||
|
||||
/**
|
||||
* Process one SPI byte. Call with the byte value BEFORE calling
|
||||
* spi.completeTransfer() so we see it first.
|
||||
*/
|
||||
processByte(value: number): void {
|
||||
if (!this.dcHigh) {
|
||||
// Command byte
|
||||
this.commandCount++;
|
||||
this.currentCmd = value;
|
||||
this.dataBytes = [];
|
||||
this.inRamWrite = (value === 0x2C);
|
||||
this.pixelByteIdx = 0;
|
||||
if (value === 0x2C) this.ramwrCount++;
|
||||
if (value === 0x01) { // SWRESET
|
||||
this.framebuffer.fill(0);
|
||||
this.pixelCount = 0;
|
||||
}
|
||||
} else {
|
||||
// Data byte
|
||||
if (this.inRamWrite) {
|
||||
if (this.pixelByteIdx === 0) {
|
||||
this.pixelHiByte = value;
|
||||
this.pixelByteIdx = 1;
|
||||
} else {
|
||||
this.writePixel(this.pixelHiByte, value);
|
||||
this.pixelByteIdx = 0;
|
||||
}
|
||||
} else {
|
||||
this.dataBytes.push(value);
|
||||
this.applyCmd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyCmd(): void {
|
||||
const d = this.dataBytes;
|
||||
switch (this.currentCmd) {
|
||||
case 0x2A: // CASET
|
||||
if (d.length === 2) this.colStart = (d[0] << 8) | d[1];
|
||||
if (d.length === 4) { this.colEnd = (d[2] << 8) | d[3]; this.curX = this.colStart; }
|
||||
break;
|
||||
case 0x2B: // PASET
|
||||
if (d.length === 2) this.rowStart = (d[0] << 8) | d[1];
|
||||
if (d.length === 4) { this.rowEnd = (d[2] << 8) | d[3]; this.curY = this.rowStart; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private writePixel(hi: number, lo: number): void {
|
||||
if (this.curX > this.colEnd || this.curY > this.rowEnd ||
|
||||
this.curX >= VirtualILI9341.WIDTH || this.curY >= VirtualILI9341.HEIGHT) return;
|
||||
|
||||
const rgb565 = (hi << 8) | lo;
|
||||
this.framebuffer[this.curY * VirtualILI9341.WIDTH + this.curX] = rgb565;
|
||||
this.pixelCount++;
|
||||
|
||||
this.curX++;
|
||||
if (this.curX > this.colEnd) {
|
||||
this.curX = this.colStart;
|
||||
this.curY++;
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert RGB-565 pixel to R,G,B channels */
|
||||
static rgb565ToRGB(p: number): [number, number, number] {
|
||||
return [
|
||||
((p >> 11) & 0x1F) * 8,
|
||||
((p >> 5) & 0x3F) * 4,
|
||||
( p & 0x1F) * 8,
|
||||
];
|
||||
}
|
||||
|
||||
/** True if any pixel in a region matches a specific colour (within tolerance) */
|
||||
regionHasColor(
|
||||
x0: number, y0: number, x1: number, y1: number,
|
||||
targetRGB565: number, tolerance = 20,
|
||||
): boolean {
|
||||
const [tr, tg, tb] = VirtualILI9341.rgb565ToRGB(targetRGB565);
|
||||
for (let y = y0; y <= y1; y++) {
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
const p = this.framebuffer[y * VirtualILI9341.WIDTH + x];
|
||||
if (p === 0) continue;
|
||||
const [r, g, b] = VirtualILI9341.rgb565ToRGB(p);
|
||||
if (
|
||||
Math.abs(r - tr) <= tolerance &&
|
||||
Math.abs(g - tg) <= tolerance &&
|
||||
Math.abs(b - tb) <= tolerance
|
||||
) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count pixels whose colour is within `tolerance` of targetRGB565.
|
||||
*/
|
||||
countColor(targetRGB565: number, tolerance = 20): number {
|
||||
const [tr, tg, tb] = VirtualILI9341.rgb565ToRGB(targetRGB565);
|
||||
let n = 0;
|
||||
for (const p of this.framebuffer) {
|
||||
if (p === 0) continue;
|
||||
const [r, g, b] = VirtualILI9341.rgb565ToRGB(p);
|
||||
if (
|
||||
Math.abs(r - tr) <= tolerance &&
|
||||
Math.abs(g - tg) <= tolerance &&
|
||||
Math.abs(b - tb) <= tolerance
|
||||
) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** ILI9341_RED (RGB-565: 0xF800) */
|
||||
static readonly COLOR_RED = 0xF800;
|
||||
/** ILI9341_WHITE (RGB-565: 0xFFFF) */
|
||||
static readonly COLOR_WHITE = 0xFFFF;
|
||||
/** ILI9341_BLUE (RGB-565: 0x001F) */
|
||||
static readonly COLOR_BLUE = 0x001F;
|
||||
/** ILI9341_BLACK (RGB-565: 0x0000) */
|
||||
static readonly COLOR_BLACK = 0x0000;
|
||||
/** ILI9341_YELLOW (RGB-565: 0xFFE0) */
|
||||
static readonly COLOR_YELLOW = 0xFFE0;
|
||||
|
||||
/** Summary string for console output */
|
||||
summary(): string {
|
||||
return (
|
||||
`ILI9341 stats: commands=${this.commandCount}, ` +
|
||||
`RAMWR=${this.ramwrCount}, pixelsWritten=${this.pixelCount}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Run N cycles ─────────────────────────────────────────────────────────────
|
||||
|
||||
function runCycles(sim: AVRSimulator, cycles: number): void {
|
||||
for (let i = 0; i < cycles; i++) sim.step();
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('ILI9341 emulation — full end-to-end', () => {
|
||||
let hexContent: string;
|
||||
let sim: AVRSimulator;
|
||||
let display: VirtualILI9341;
|
||||
|
||||
beforeAll(() => {
|
||||
hexContent = compileSketch();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try { sim?.stop(); } catch { /* ignore */ }
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('🔧 compiles ili9341-test-sketch.ino successfully', () => {
|
||||
expect(hexContent).toBeTruthy();
|
||||
expect(hexContent).toContain(':');
|
||||
console.log('[hex] First line:', hexContent.split('\n')[0]);
|
||||
});
|
||||
|
||||
it('🖥️ boots, initialises ILI9341, and fills screen within 15M cycles', () => {
|
||||
const pm = new PinManager();
|
||||
sim = new AVRSimulator(pm);
|
||||
sim.loadHex(hexContent);
|
||||
|
||||
// ── Attach VirtualILI9341 to the SPI bus ──────────────────────────────
|
||||
display = new VirtualILI9341();
|
||||
|
||||
// DC pin: Arduino pin 9 → PORTB bit 1 (D9 = PB1)
|
||||
// We track DC state by watching pin 9 in the pinManager.
|
||||
// PinManager.onPinChange gives us the logical state (true = HIGH).
|
||||
const DC_ARDUINO_PIN = 9;
|
||||
pm.onPinChange(DC_ARDUINO_PIN, (_pin: number, state: boolean) => {
|
||||
display.dcHigh = state;
|
||||
});
|
||||
|
||||
// Intercept SPI: wrap the existing onByte
|
||||
sim.spi!.onByte = (value: number) => {
|
||||
display.processByte(value);
|
||||
sim.spi!.completeTransfer(0xFF);
|
||||
};
|
||||
|
||||
// 15M cycles = ~937ms simulated @16MHz.
|
||||
// Breakdown:
|
||||
// ~3M – Arduino startup + Serial.begin + tft.begin() init commands (~20+ cmd)
|
||||
// ~2M – fillScreen(RED) = 76800px × 2bytes × ~4CPU/byte + overhead
|
||||
// ~6M – fillRect + fillCircle + fillTriangle
|
||||
// ~4M – margin
|
||||
runCycles(sim, 15_000_000);
|
||||
|
||||
console.log(`[init] ${display.summary()}`);
|
||||
console.log(`[init] Pixels written: ${display.pixelCount}`);
|
||||
|
||||
// The Adafruit_ILI9341::begin() sends ~20+ init commands
|
||||
expect(display.commandCount).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it('🎨 fillScreen(RED) was issued (pixels present after 15M cycles)', () => {
|
||||
// By the time we reach this test, 15M cycles have already run.
|
||||
// fillScreen alone writes 76800 pixels; we accept ≥10000 to be lenient.
|
||||
runCycles(sim, 0); // no-op; just reads current state
|
||||
|
||||
console.log(`[fill] ${display.summary()}`);
|
||||
|
||||
const redCount = display.countColor(VirtualILI9341.COLOR_RED, 30);
|
||||
console.log(`[fill] Red pixels: ${redCount} / ${VirtualILI9341.WIDTH * VirtualILI9341.HEIGHT}`);
|
||||
|
||||
// At least the majority of the screen should be red
|
||||
expect(redCount).toBeGreaterThan(10_000);
|
||||
});
|
||||
|
||||
it('⬜ draws white rectangle at (20,20,200,80)', () => {
|
||||
// White rect was drawn before circle — should already be in framebuffer
|
||||
const hasWhiteRect = display.regionHasColor(20, 20, 220, 100, VirtualILI9341.COLOR_WHITE, 10);
|
||||
console.log(`[rect] White rect region has white pixels: ${hasWhiteRect}`);
|
||||
expect(hasWhiteRect).toBe(true);
|
||||
});
|
||||
|
||||
it('🔵 draws blue circle center (120,200) r=50', () => {
|
||||
// Check a small region within the circle bounds
|
||||
const hasBluePx = display.regionHasColor(90, 170, 150, 230, VirtualILI9341.COLOR_BLUE, 30);
|
||||
console.log(`[circle] Blue circle region has blue pixels: ${hasBluePx}`);
|
||||
expect(hasBluePx).toBe(true);
|
||||
});
|
||||
|
||||
it('🖼️ RAMWR received multiple times (multi-shape drawing)', () => {
|
||||
console.log(`[ramwr] RAMWR count: ${display.ramwrCount}`);
|
||||
// fillScreen + fillRect + fillCircle + fillTriangle → at least 4 RAMWR
|
||||
expect(display.ramwrCount).toBeGreaterThan(3);
|
||||
});
|
||||
|
||||
it('📊 total pixel writes cover most of the 240×320 screen', () => {
|
||||
const total = VirtualILI9341.WIDTH * VirtualILI9341.HEIGHT;
|
||||
const coverage = display.pixelCount / total;
|
||||
console.log(
|
||||
`[coverage] pixelCount=${display.pixelCount} / ${total} = ${(coverage * 100).toFixed(1)}%`
|
||||
);
|
||||
// fillScreen alone should cover 100%; we expect at least 50% to account
|
||||
// for cases where address windows overlap (pixels counted once per write)
|
||||
expect(display.pixelCount).toBeGreaterThan(total * 0.5);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import '@wokwi/elements';
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
interface ArduinoMegaProps {
|
||||
id?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
led13?: boolean;
|
||||
}
|
||||
|
||||
export const ArduinoMega = ({
|
||||
id = 'arduino-mega',
|
||||
x = 0,
|
||||
y = 0,
|
||||
led13 = false,
|
||||
}: ArduinoMegaProps) => {
|
||||
const megaRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (megaRef.current) {
|
||||
(megaRef.current as any).led13 = led13;
|
||||
}
|
||||
}, [led13]);
|
||||
|
||||
return (
|
||||
<wokwi-arduino-mega
|
||||
id={id}
|
||||
ref={megaRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ import type { BoardType } from '../../store/useSimulatorStore';
|
|||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { ArduinoUno } from '../components-wokwi/ArduinoUno';
|
||||
import { ArduinoNano } from '../components-wokwi/ArduinoNano';
|
||||
import { ArduinoMega } from '../components-wokwi/ArduinoMega';
|
||||
import { NanoRP2040 } from '../components-wokwi/NanoRP2040';
|
||||
import { ComponentPickerModal } from '../ComponentPickerModal';
|
||||
import { ComponentPropertyDialog } from './ComponentPropertyDialog';
|
||||
|
|
@ -625,6 +626,12 @@ export const SimulatorCanvas = () => {
|
|||
y={boardPosition.y}
|
||||
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
|
||||
/>
|
||||
) : boardType === 'arduino-mega' ? (
|
||||
<ArduinoMega
|
||||
x={boardPosition.x}
|
||||
y={boardPosition.y}
|
||||
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
|
||||
/>
|
||||
) : (
|
||||
<NanoRP2040
|
||||
x={boardPosition.x}
|
||||
|
|
@ -640,8 +647,8 @@ export const SimulatorCanvas = () => {
|
|||
position: 'absolute',
|
||||
left: boardPosition.x,
|
||||
top: boardPosition.y,
|
||||
width: boardType === 'arduino-uno' ? 360 : boardType === 'arduino-nano' ? 175 : 280,
|
||||
height: boardType === 'arduino-uno' ? 250 : boardType === 'arduino-nano' ? 70 : 180,
|
||||
width: boardType === 'arduino-uno' ? 360 : boardType === 'arduino-nano' ? 175 : boardType === 'arduino-mega' ? 530 : 280,
|
||||
height: boardType === 'arduino-uno' ? 250 : boardType === 'arduino-nano' ? 70 : boardType === 'arduino-mega' ? 195 : 180,
|
||||
cursor: 'move',
|
||||
zIndex: 1,
|
||||
}}
|
||||
|
|
@ -659,7 +666,7 @@ export const SimulatorCanvas = () => {
|
|||
|
||||
{/* Board pin overlay */}
|
||||
<PinOverlay
|
||||
componentId={boardType === 'arduino-uno' ? 'arduino-uno' : boardType === 'arduino-nano' ? 'arduino-nano' : 'nano-rp2040'}
|
||||
componentId={boardType === 'arduino-uno' ? 'arduino-uno' : boardType === 'arduino-nano' ? 'arduino-nano' : boardType === 'arduino-mega' ? 'arduino-mega' : 'nano-rp2040'}
|
||||
componentX={boardPosition.x}
|
||||
componentY={boardPosition.y}
|
||||
onPinClick={handlePinClick}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CPU, AVRTimer, timer0Config, timer1Config, timer2Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, AVRADC, adcConfig, AVRSPI, spiConfig, AVRTWI, twiConfig } from 'avr8js';
|
||||
import { CPU, AVRTimer, timer0Config, timer1Config, timer2Config, AVRUSART, usart0Config, AVRIOPort, portAConfig, portBConfig, portCConfig, portDConfig, portEConfig, portFConfig, portGConfig, portHConfig, portJConfig, portKConfig, portLConfig, avrInstruction, AVRADC, adcConfig, AVRSPI, spiConfig, AVRTWI, twiConfig } from 'avr8js';
|
||||
import { PinManager } from './PinManager';
|
||||
import { hexToUint8Array } from '../utils/hexParser';
|
||||
import { I2CBusManager } from './I2CBusManager';
|
||||
|
|
@ -17,8 +17,8 @@ import type { I2CDevice } from './I2CBusManager';
|
|||
* - Pin state tracking via PinManager
|
||||
*/
|
||||
|
||||
// OCR register addresses → Arduino pin mapping for PWM
|
||||
const PWM_PINS = [
|
||||
// OCR register addresses → Arduino pin mapping for PWM (ATmega328P / Uno / Nano)
|
||||
const PWM_PINS_UNO = [
|
||||
{ ocrAddr: 0x47, pin: 6, label: 'OCR0A' }, // Timer0A → D6
|
||||
{ ocrAddr: 0x48, pin: 5, label: 'OCR0B' }, // Timer0B → D5
|
||||
{ ocrAddr: 0x88, pin: 9, label: 'OCR1AL' }, // Timer1A low byte → D9
|
||||
|
|
@ -27,6 +27,87 @@ const PWM_PINS = [
|
|||
{ ocrAddr: 0xB4, pin: 3, label: 'OCR2B' }, // Timer2B → D3
|
||||
];
|
||||
|
||||
// OCR register addresses → Arduino Mega pin mapping for PWM (ATmega2560)
|
||||
// Timers 0/1/2 same addresses; Timers 3/4/5 at higher addresses.
|
||||
const PWM_PINS_MEGA = [
|
||||
{ ocrAddr: 0x47, pin: 13, label: 'OCR0A' }, // Timer0A → D13
|
||||
{ ocrAddr: 0x48, pin: 4, label: 'OCR0B' }, // Timer0B → D4
|
||||
{ ocrAddr: 0x88, pin: 11, label: 'OCR1AL' }, // Timer1A → D11
|
||||
{ ocrAddr: 0x8A, pin: 12, label: 'OCR1BL' }, // Timer1B → D12
|
||||
{ ocrAddr: 0xB3, pin: 10, label: 'OCR2A' }, // Timer2A → D10
|
||||
{ ocrAddr: 0xB4, pin: 9, label: 'OCR2B' }, // Timer2B → D9
|
||||
// Timer3 (0x80–0x8D, but OCR3A/B/C at 0x98/0x9A/0x9C)
|
||||
{ ocrAddr: 0x98, pin: 5, label: 'OCR3AL' }, // Timer3A → D5
|
||||
{ ocrAddr: 0x9A, pin: 2, label: 'OCR3BL' }, // Timer3B → D2
|
||||
{ ocrAddr: 0x9C, pin: 3, label: 'OCR3CL' }, // Timer3C → D3
|
||||
// Timer4 (OCR4A/B/C at 0xA8/0xAA/0xAC)
|
||||
{ ocrAddr: 0xA8, pin: 6, label: 'OCR4AL' }, // Timer4A → D6
|
||||
{ ocrAddr: 0xAA, pin: 7, label: 'OCR4BL' }, // Timer4B → D7
|
||||
{ ocrAddr: 0xAC, pin: 8, label: 'OCR4CL' }, // Timer4C → D8
|
||||
// Timer5 (OCR5A/B/C at 0x128/0x12A/0x12C — extended I/O)
|
||||
{ ocrAddr: 0x128, pin: 46, label: 'OCR5AL' }, // Timer5A → D46
|
||||
{ ocrAddr: 0x12A, pin: 45, label: 'OCR5BL' }, // Timer5B → D45
|
||||
{ ocrAddr: 0x12C, pin: 44, label: 'OCR5CL' }, // Timer5C → D44
|
||||
];
|
||||
|
||||
/**
|
||||
* ATmega2560 port-bit → Arduino Mega pin mapping.
|
||||
* Index = bit position (0–7). -1 = not exposed on the Arduino Mega header.
|
||||
*/
|
||||
const MEGA_PORT_BIT_MAP: Record<string, number[]> = {
|
||||
// PA0-PA7 → D22-D29
|
||||
'PORTA': [22, 23, 24, 25, 26, 27, 28, 29],
|
||||
// PB0=D53(SS), PB1=D52(SCK), PB2=D51(MOSI), PB3=D50(MISO), PB4-PB7=D10-D13
|
||||
'PORTB': [53, 52, 51, 50, 10, 11, 12, 13],
|
||||
// PC0-PC7 → D37, D36, D35, D34, D33, D32, D31, D30 (reversed)
|
||||
'PORTC': [37, 36, 35, 34, 33, 32, 31, 30],
|
||||
// PD0=D21(SCL), PD1=D20(SDA), PD2=D19(RX1), PD3=D18(TX1), PD7=D38
|
||||
'PORTD': [21, 20, 19, 18, -1, -1, -1, 38],
|
||||
// PE0=D0(RX0), PE1=D1(TX0), PE3=D5, PE4=D2, PE5=D3
|
||||
'PORTE': [0, 1, -1, 5, 2, 3, -1, -1],
|
||||
// PF0-PF7 → A0-A7 (pin numbers 54-61)
|
||||
'PORTF': [54, 55, 56, 57, 58, 59, 60, 61],
|
||||
// PG0=D41, PG1=D40, PG2=D39, PG5=D4
|
||||
'PORTG': [41, 40, 39, -1, -1, 4, -1, -1],
|
||||
// PH0=D17(RX2), PH1=D16(TX2), PH3=D6, PH4=D7, PH5=D8, PH6=D9
|
||||
'PORTH': [17, 16, -1, 6, 7, 8, 9, -1],
|
||||
// PJ0=D15(RX3), PJ1=D14(TX3)
|
||||
'PORTJ': [15, 14, -1, -1, -1, -1, -1, -1],
|
||||
// PK0-PK7 → A8-A15 (pin numbers 62-69)
|
||||
'PORTK': [62, 63, 64, 65, 66, 67, 68, 69],
|
||||
// PL0=D49, PL1=D48, PL2=D47, PL3=D46, PL4=D45, PL5=D44, PL6=D43, PL7=D42
|
||||
'PORTL': [49, 48, 47, 46, 45, 44, 43, 42],
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverse of MEGA_PORT_BIT_MAP: Arduino Mega pin → { portName, bit }.
|
||||
* Pre-built for fast setPinState() lookups.
|
||||
*/
|
||||
const MEGA_PIN_TO_PORT = (() => {
|
||||
const map: Record<number, { portName: string; bit: number; port?: AVRIOPort }> = {};
|
||||
for (const [portName, pins] of Object.entries(MEGA_PORT_BIT_MAP)) {
|
||||
pins.forEach((pin, bit) => {
|
||||
if (pin >= 0) map[pin] = { portName, bit };
|
||||
});
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
|
||||
/** Ordered list of Mega ports with their avr8js configs */
|
||||
const MEGA_PORT_CONFIGS = [
|
||||
{ name: 'PORTA', config: portAConfig },
|
||||
{ name: 'PORTB', config: portBConfig },
|
||||
{ name: 'PORTC', config: portCConfig },
|
||||
{ name: 'PORTD', config: portDConfig },
|
||||
{ name: 'PORTE', config: portEConfig },
|
||||
{ name: 'PORTF', config: portFConfig },
|
||||
{ name: 'PORTG', config: portGConfig },
|
||||
{ name: 'PORTH', config: portHConfig },
|
||||
{ name: 'PORTJ', config: portJConfig },
|
||||
{ name: 'PORTK', config: portKConfig },
|
||||
{ name: 'PORTL', config: portLConfig },
|
||||
];
|
||||
|
||||
export class AVRSimulator {
|
||||
private cpu: CPU | null = null;
|
||||
/** Peripherals kept alive by reference so GC doesn't collect their CPU hooks */
|
||||
|
|
@ -34,6 +115,9 @@ export class AVRSimulator {
|
|||
private portB: AVRIOPort | null = null;
|
||||
private portC: AVRIOPort | null = null;
|
||||
private portD: AVRIOPort | null = null;
|
||||
/** Extra ports used by the Mega (A, E–L); keyed by port name */
|
||||
private megaPorts: Map<string, AVRIOPort> = new Map();
|
||||
private megaPortValues: Map<string, number> = new Map();
|
||||
private adc: AVRADC | null = null;
|
||||
public spi: AVRSPI | null = null;
|
||||
public usart: AVRUSART | null = null;
|
||||
|
|
@ -43,7 +127,9 @@ export class AVRSimulator {
|
|||
private running = false;
|
||||
private animationFrame: number | null = null;
|
||||
public pinManager: PinManager;
|
||||
private speed = 1.0; // Simulation speed multiplier
|
||||
private speed = 1.0;
|
||||
/** 'uno' for ATmega328P boards (Uno, Nano); 'mega' for ATmega2560 */
|
||||
private boardVariant: 'uno' | 'mega';
|
||||
|
||||
/** Serial output buffer — subscribers receive each byte or line */
|
||||
public onSerialData: ((char: string) => void) | null = null;
|
||||
|
|
@ -52,10 +138,15 @@ export class AVRSimulator {
|
|||
private lastPortBValue = 0;
|
||||
private lastPortCValue = 0;
|
||||
private lastPortDValue = 0;
|
||||
private lastOcrValues: number[] = new Array(PWM_PINS.length).fill(-1);
|
||||
private lastOcrValues: number[] = [];
|
||||
|
||||
constructor(pinManager: PinManager) {
|
||||
constructor(pinManager: PinManager, boardVariant: 'uno' | 'mega' = 'uno') {
|
||||
this.pinManager = pinManager;
|
||||
this.boardVariant = boardVariant;
|
||||
}
|
||||
|
||||
private get pwmPins() {
|
||||
return this.boardVariant === 'mega' ? PWM_PINS_MEGA : PWM_PINS_UNO;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,45 +155,33 @@ export class AVRSimulator {
|
|||
loadHex(hexContent: string): void {
|
||||
console.log('Loading HEX file...');
|
||||
|
||||
// Parse Intel HEX format to Uint8Array
|
||||
const bytes = hexToUint8Array(hexContent);
|
||||
|
||||
// Create program memory (ATmega328p has 32KB = 16K words)
|
||||
this.program = new Uint16Array(16384);
|
||||
// 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;
|
||||
|
||||
// Load bytes into program memory (little-endian, 16-bit words)
|
||||
this.program = new Uint16Array(progWords);
|
||||
for (let i = 0; i < bytes.length; i += 2) {
|
||||
const low = bytes[i] || 0;
|
||||
const high = bytes[i + 1] || 0;
|
||||
this.program[i >> 1] = low | (high << 8);
|
||||
this.program[i >> 1] = (bytes[i] || 0) | ((bytes[i + 1] || 0) << 8);
|
||||
}
|
||||
|
||||
console.log(`Loaded ${bytes.length} bytes into program memory`);
|
||||
|
||||
// Initialize CPU (ATmega328p @ 16MHz)
|
||||
this.cpu = new CPU(this.program);
|
||||
this.cpu = new CPU(this.program, sramBytes);
|
||||
|
||||
// Initialize peripherals (kept alive so their CPU hooks are not GC'd)
|
||||
this.spi = new AVRSPI(this.cpu, spiConfig, 16000000);
|
||||
// Default onByte: complete transfer immediately (no external device)
|
||||
this.spi.onByte = (value) => {
|
||||
this.spi!.completeTransfer(value);
|
||||
};
|
||||
this.spi.onByte = (value) => { this.spi!.completeTransfer(value); };
|
||||
|
||||
// USART (Serial) — hook onByteTransmit to forward output
|
||||
this.usart = new AVRUSART(this.cpu, usart0Config, 16000000);
|
||||
this.usart.onByteTransmit = (value: number) => {
|
||||
if (this.onSerialData) {
|
||||
this.onSerialData(String.fromCharCode(value));
|
||||
}
|
||||
if (this.onSerialData) this.onSerialData(String.fromCharCode(value));
|
||||
};
|
||||
this.usart.onConfigurationChange = () => {
|
||||
if (this.onBaudRateChange && this.usart) {
|
||||
this.onBaudRateChange(this.usart.baudRate);
|
||||
}
|
||||
if (this.onBaudRateChange && this.usart) this.onBaudRateChange(this.usart.baudRate);
|
||||
};
|
||||
|
||||
// TWI (I2C)
|
||||
this.twi = new AVRTWI(this.cpu, twiConfig, 16000000);
|
||||
this.i2cBus = new I2CBusManager(this.twi);
|
||||
|
||||
|
|
@ -115,21 +194,31 @@ export class AVRSimulator {
|
|||
this.twi,
|
||||
];
|
||||
|
||||
// Initialize ADC (analogRead support)
|
||||
this.adc = new AVRADC(this.cpu, adcConfig);
|
||||
|
||||
// Initialize IO ports
|
||||
// ── GPIO ports ────────────────────────────────────────────────────────
|
||||
this.portB = new AVRIOPort(this.cpu, portBConfig);
|
||||
this.portC = new AVRIOPort(this.cpu, portCConfig);
|
||||
this.portD = new AVRIOPort(this.cpu, portDConfig);
|
||||
|
||||
// Reset OCR tracking
|
||||
this.lastOcrValues = new Array(PWM_PINS.length).fill(-1);
|
||||
if (this.boardVariant === 'mega') {
|
||||
this.megaPorts.clear();
|
||||
this.megaPortValues.clear();
|
||||
for (const { name, config } of MEGA_PORT_CONFIGS) {
|
||||
this.megaPorts.set(name, new AVRIOPort(this.cpu, config));
|
||||
this.megaPortValues.set(name, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPortBValue = 0;
|
||||
this.lastPortCValue = 0;
|
||||
this.lastPortDValue = 0;
|
||||
this.lastOcrValues = new Array(this.pwmPins.length).fill(-1);
|
||||
|
||||
// Set up pin change hooks
|
||||
this.setupPinHooks();
|
||||
|
||||
console.log(`AVR CPU initialized (${this.peripherals.length} peripherals, ADC + Timer1/Timer2 enabled)`);
|
||||
const board = this.boardVariant === 'mega' ? 'ATmega2560' : 'ATmega328P';
|
||||
console.log(`AVR CPU initialized (${board}, ${this.peripherals.length} peripherals)`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -144,32 +233,42 @@ export class AVRSimulator {
|
|||
*/
|
||||
private setupPinHooks(): void {
|
||||
if (!this.cpu) return;
|
||||
|
||||
console.log('Setting up pin hooks...');
|
||||
|
||||
// PORTB (Digital pins 8-13)
|
||||
this.portB!.addListener((value, _oldValue) => {
|
||||
if (value !== this.lastPortBValue) {
|
||||
this.pinManager.updatePort('PORTB', value, this.lastPortBValue);
|
||||
this.lastPortBValue = value;
|
||||
if (this.boardVariant === 'mega') {
|
||||
// Mega: use explicit per-bit pin maps for all 11 ports
|
||||
for (const [portName, port] of this.megaPorts) {
|
||||
const pinMap = MEGA_PORT_BIT_MAP[portName];
|
||||
this.megaPortValues.set(portName, 0);
|
||||
port.addListener((value) => {
|
||||
const old = this.megaPortValues.get(portName) ?? 0;
|
||||
if (value !== old) {
|
||||
this.pinManager.updatePort(portName, value, old, pinMap);
|
||||
this.megaPortValues.set(portName, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PORTC (Analog pins A0-A5)
|
||||
this.portC!.addListener((value, _oldValue) => {
|
||||
if (value !== this.lastPortCValue) {
|
||||
this.pinManager.updatePort('PORTC', value, this.lastPortCValue);
|
||||
this.lastPortCValue = value;
|
||||
}
|
||||
});
|
||||
|
||||
// PORTD (Digital pins 0-7)
|
||||
this.portD!.addListener((value, _oldValue) => {
|
||||
if (value !== this.lastPortDValue) {
|
||||
this.pinManager.updatePort('PORTD', value, this.lastPortDValue);
|
||||
this.lastPortDValue = value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Uno / Nano: simple 3-port setup
|
||||
this.portB!.addListener((value) => {
|
||||
if (value !== this.lastPortBValue) {
|
||||
this.pinManager.updatePort('PORTB', value, this.lastPortBValue);
|
||||
this.lastPortBValue = value;
|
||||
}
|
||||
});
|
||||
this.portC!.addListener((value) => {
|
||||
if (value !== this.lastPortCValue) {
|
||||
this.pinManager.updatePort('PORTC', value, this.lastPortCValue);
|
||||
this.lastPortCValue = value;
|
||||
}
|
||||
});
|
||||
this.portD!.addListener((value) => {
|
||||
if (value !== this.lastPortDValue) {
|
||||
this.pinManager.updatePort('PORTD', value, this.lastPortDValue);
|
||||
this.lastPortDValue = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Pin hooks configured successfully');
|
||||
}
|
||||
|
|
@ -179,14 +278,13 @@ export class AVRSimulator {
|
|||
*/
|
||||
private pollPwmRegisters(): void {
|
||||
if (!this.cpu) return;
|
||||
|
||||
for (let i = 0; i < PWM_PINS.length; i++) {
|
||||
const { ocrAddr, pin } = PWM_PINS[i];
|
||||
const pins = this.pwmPins;
|
||||
for (let i = 0; i < pins.length; i++) {
|
||||
const { ocrAddr, pin } = pins[i];
|
||||
const ocrValue = this.cpu.data[ocrAddr];
|
||||
if (ocrValue !== this.lastOcrValues[i]) {
|
||||
this.lastOcrValues[i] = ocrValue;
|
||||
const dutyCycle = ocrValue / 255;
|
||||
this.pinManager.updatePwm(pin, dutyCycle);
|
||||
this.pinManager.updatePwm(pin, ocrValue / 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -273,15 +371,16 @@ export class AVRSimulator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reset simulator
|
||||
* Reset simulator (re-run program from scratch without recompiling)
|
||||
*/
|
||||
reset(): void {
|
||||
this.stop();
|
||||
|
||||
if (this.cpu && this.program) {
|
||||
if (this.program) {
|
||||
// Re-use the stored hex content path: just reload
|
||||
const sramBytes = this.boardVariant === 'mega' ? 8192 : 8192;
|
||||
console.log('Resetting AVR CPU...');
|
||||
|
||||
this.cpu = new CPU(this.program);
|
||||
this.cpu = new CPU(this.program, sramBytes);
|
||||
|
||||
this.spi = new AVRSPI(this.cpu, spiConfig, 16000000);
|
||||
this.spi.onByte = (value) => { this.spi!.completeTransfer(value); };
|
||||
|
|
@ -291,9 +390,7 @@ export class AVRSimulator {
|
|||
if (this.onSerialData) this.onSerialData(String.fromCharCode(value));
|
||||
};
|
||||
this.usart.onConfigurationChange = () => {
|
||||
if (this.onBaudRateChange && this.usart) {
|
||||
this.onBaudRateChange(this.usart.baudRate);
|
||||
}
|
||||
if (this.onBaudRateChange && this.usart) this.onBaudRateChange(this.usart.baudRate);
|
||||
};
|
||||
|
||||
this.twi = new AVRTWI(this.cpu, twiConfig, 16000000);
|
||||
|
|
@ -303,9 +400,7 @@ export class AVRSimulator {
|
|||
new AVRTimer(this.cpu, timer0Config),
|
||||
new AVRTimer(this.cpu, timer1Config),
|
||||
new AVRTimer(this.cpu, timer2Config),
|
||||
this.usart,
|
||||
this.spi,
|
||||
this.twi,
|
||||
this.usart, this.spi, this.twi,
|
||||
];
|
||||
this.adc = new AVRADC(this.cpu, adcConfig);
|
||||
|
||||
|
|
@ -313,11 +408,19 @@ export class AVRSimulator {
|
|||
this.portC = new AVRIOPort(this.cpu, portCConfig);
|
||||
this.portD = new AVRIOPort(this.cpu, portDConfig);
|
||||
|
||||
if (this.boardVariant === 'mega') {
|
||||
this.megaPorts.clear();
|
||||
this.megaPortValues.clear();
|
||||
for (const { name, config } of MEGA_PORT_CONFIGS) {
|
||||
this.megaPorts.set(name, new AVRIOPort(this.cpu, config));
|
||||
this.megaPortValues.set(name, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPortBValue = 0;
|
||||
this.lastPortCValue = 0;
|
||||
this.lastPortDValue = 0;
|
||||
this.lastOcrValues = new Array(PWM_PINS.length).fill(-1);
|
||||
|
||||
this.lastOcrValues = new Array(this.pwmPins.length).fill(-1);
|
||||
this.setupPinHooks();
|
||||
|
||||
console.log('AVR CPU reset complete');
|
||||
|
|
@ -347,6 +450,15 @@ export class AVRSimulator {
|
|||
* Set the state of an Arduino pin externally (e.g. from a UI button)
|
||||
*/
|
||||
setPinState(arduinoPin: number, state: boolean): void {
|
||||
if (this.boardVariant === 'mega') {
|
||||
const entry = MEGA_PIN_TO_PORT[arduinoPin];
|
||||
if (entry) {
|
||||
const port = this.megaPorts.get(entry.portName);
|
||||
port?.setPin(entry.bit, state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Uno / Nano
|
||||
if (arduinoPin >= 0 && arduinoPin <= 7 && this.portD) {
|
||||
this.portD.setPin(arduinoPin, state);
|
||||
} else if (arduinoPin >= 8 && arduinoPin <= 13 && this.portB) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
/**
|
||||
* PinManager - Manages Arduino pin states and notifies listeners
|
||||
*
|
||||
* Maps AVR PORT registers to Arduino pin numbers:
|
||||
* Maps AVR PORT registers to Arduino pin numbers.
|
||||
*
|
||||
* Arduino Uno / Nano (ATmega328P):
|
||||
* - PORTB (0x25) → Digital pins 8-13
|
||||
* - PORTC (0x28) → Analog pins A0-A5 (14-19)
|
||||
* - PORTD (0x2B) → Digital pins 0-7
|
||||
*
|
||||
* Arduino Mega 2560 (ATmega2560): uses explicit per-bit pin maps
|
||||
* for non-linear port ↔ Arduino-pin relationships.
|
||||
*
|
||||
* Also supports:
|
||||
* - Analog voltage injection (for potentiometers, sensors)
|
||||
* - PWM duty cycle tracking (for servos, RGB LEDs, buzzers)
|
||||
|
|
@ -41,13 +46,17 @@ export class PinManager {
|
|||
|
||||
/**
|
||||
* Update port register and notify digital pin listeners.
|
||||
*
|
||||
* @param portName Human-readable port name for log output (e.g. 'PORTB').
|
||||
* @param newValue New 8-bit port value.
|
||||
* @param oldValue Previous 8-bit port value (default 0).
|
||||
* @param pinMap Optional per-bit Arduino pin numbers (length 8).
|
||||
* Use -1 for bits that are not exposed as Arduino pins.
|
||||
* When omitted the legacy Uno/Nano fixed offsets are used:
|
||||
* PORTB→8, PORTC→14, PORTD→0.
|
||||
*/
|
||||
updatePort(portName: 'PORTB' | 'PORTC' | 'PORTD', newValue: number, oldValue: number = 0) {
|
||||
const pinOffset = {
|
||||
'PORTB': 8,
|
||||
'PORTC': 14,
|
||||
'PORTD': 0,
|
||||
}[portName];
|
||||
updatePort(portName: string, newValue: number, oldValue: number = 0, pinMap?: number[]) {
|
||||
const legacyOffsets: Record<string, number> = { 'PORTB': 8, 'PORTC': 14, 'PORTD': 0 };
|
||||
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
const mask = 1 << bit;
|
||||
|
|
@ -55,7 +64,9 @@ export class PinManager {
|
|||
const newState = (newValue & mask) !== 0;
|
||||
|
||||
if (oldState !== newState) {
|
||||
const arduinoPin = pinOffset + bit;
|
||||
const arduinoPin = pinMap ? pinMap[bit] : (legacyOffsets[portName] ?? 0) + bit;
|
||||
if (arduinoPin < 0) continue; // unmapped bit
|
||||
|
||||
this.pinStates.set(arduinoPin, newState);
|
||||
|
||||
const callbacks = this.listeners.get(arduinoPin);
|
||||
|
|
|
|||
|
|
@ -7,17 +7,19 @@ import type { RP2040I2CDevice } from '../simulation/RP2040Simulator';
|
|||
import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
|
||||
import { calculatePinPosition } from '../utils/pinPositionCalculator';
|
||||
|
||||
export type BoardType = 'arduino-uno' | 'arduino-nano' | 'raspberry-pi-pico';
|
||||
export type BoardType = 'arduino-uno' | 'arduino-nano' | 'arduino-mega' | 'raspberry-pi-pico';
|
||||
|
||||
export const BOARD_FQBN: Record<BoardType, string> = {
|
||||
'arduino-uno': 'arduino:avr:uno',
|
||||
'arduino-nano': 'arduino:avr:nano:cpu=atmega328',
|
||||
'arduino-mega': 'arduino:avr:mega',
|
||||
'raspberry-pi-pico': 'rp2040:rp2040:rpipico',
|
||||
};
|
||||
|
||||
export const BOARD_LABELS: Record<BoardType, string> = {
|
||||
'arduino-uno': 'Arduino Uno',
|
||||
'arduino-nano': 'Arduino Nano',
|
||||
'arduino-mega': 'Arduino Mega 2560',
|
||||
'raspberry-pi-pico': 'Raspberry Pi Pico',
|
||||
};
|
||||
|
||||
|
|
@ -191,8 +193,8 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
if (running) {
|
||||
get().stopSimulation();
|
||||
}
|
||||
const simulator = (type === 'arduino-uno' || type === 'arduino-nano')
|
||||
? new AVRSimulator(pinManager)
|
||||
const simulator = (type === 'arduino-uno' || type === 'arduino-nano' || type === 'arduino-mega')
|
||||
? new AVRSimulator(pinManager, type === 'arduino-mega' ? 'mega' : 'uno')
|
||||
: new RP2040Simulator(pinManager);
|
||||
// Wire serial output callback for both simulator types
|
||||
simulator.onSerialData = (char: string) => {
|
||||
|
|
@ -207,8 +209,8 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
|
||||
initSimulator: () => {
|
||||
const { boardType } = get();
|
||||
const simulator = (boardType === 'arduino-uno' || boardType === 'arduino-nano')
|
||||
? new AVRSimulator(pinManager)
|
||||
const simulator = (boardType === 'arduino-uno' || boardType === 'arduino-nano' || boardType === 'arduino-mega')
|
||||
? new AVRSimulator(pinManager, boardType === 'arduino-mega' ? 'mega' : 'uno')
|
||||
: new RP2040Simulator(pinManager);
|
||||
// Wire serial output callback for both simulator types
|
||||
simulator.onSerialData = (char: string) => {
|
||||
|
|
|
|||
|
|
@ -50,8 +50,19 @@ const ARDUINO_UNO_ANALOG_MAP: Record<string, number> = {
|
|||
'A7': 21,
|
||||
};
|
||||
|
||||
/**
|
||||
* Arduino Mega analog pin names → AVR pin numbers.
|
||||
* A0–A15 map to physical pins 54–69 on the ATmega2560.
|
||||
*/
|
||||
const ARDUINO_MEGA_ANALOG_MAP: Record<string, number> = {
|
||||
'A0': 54, 'A1': 55, 'A2': 56, 'A3': 57,
|
||||
'A4': 58, 'A5': 59, 'A6': 60, 'A7': 61,
|
||||
'A8': 62, 'A9': 63, 'A10': 64, 'A11': 65,
|
||||
'A12': 66, 'A13': 67, 'A14': 68, 'A15': 69,
|
||||
};
|
||||
|
||||
/** All known board component IDs in the simulator */
|
||||
export const BOARD_COMPONENT_IDS = ['arduino-uno', 'arduino-nano', 'nano-rp2040'];
|
||||
export const BOARD_COMPONENT_IDS = ['arduino-uno', 'arduino-nano', 'arduino-mega', 'nano-rp2040'];
|
||||
|
||||
/**
|
||||
* Check whether a componentId represents a board (not an external component).
|
||||
|
|
@ -82,6 +93,17 @@ export function boardPinToNumber(boardId: string, pinName: string): number | nul
|
|||
return ARDUINO_UNO_ANALOG_MAP[pinName] ?? null;
|
||||
}
|
||||
|
||||
if (boardId === 'arduino-mega') {
|
||||
// Digital pins D0–D53 parsed numerically
|
||||
const num = parseInt(pinName, 10);
|
||||
if (!isNaN(num) && num >= 0 && num <= 53) return num;
|
||||
if (pinName.startsWith('D')) {
|
||||
const d = parseInt(pinName.substring(1), 10);
|
||||
if (!isNaN(d) && d <= 53) return d;
|
||||
}
|
||||
return ARDUINO_MEGA_ANALOG_MAP[pinName] ?? null;
|
||||
}
|
||||
|
||||
if (boardId === 'nano-rp2040') {
|
||||
return NANO_RP2040_PIN_MAP[pinName] ?? null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export interface VelxioComponent {
|
|||
}
|
||||
|
||||
export interface ImportResult {
|
||||
boardType: 'arduino-uno' | 'arduino-nano' | 'raspberry-pi-pico';
|
||||
boardType: 'arduino-uno' | 'arduino-nano' | 'arduino-mega' | 'raspberry-pi-pico';
|
||||
boardPosition: { x: number; y: number };
|
||||
components: VelxioComponent[];
|
||||
wires: Wire[];
|
||||
|
|
@ -53,10 +53,10 @@ export interface ImportResult {
|
|||
// ── Board mappings ────────────────────────────────────────────────────────────
|
||||
|
||||
// Wokwi board type → Velxio boardType
|
||||
const WOKWI_TYPE_TO_BOARD: Record<string, 'arduino-uno' | 'arduino-nano' | 'raspberry-pi-pico'> = {
|
||||
const WOKWI_TYPE_TO_BOARD: Record<string, 'arduino-uno' | 'arduino-nano' | 'arduino-mega' | 'raspberry-pi-pico'> = {
|
||||
'wokwi-arduino-uno': 'arduino-uno',
|
||||
'wokwi-arduino-nano': 'arduino-nano',
|
||||
'wokwi-arduino-mega': 'arduino-uno',
|
||||
'wokwi-arduino-mega': 'arduino-mega',
|
||||
'wokwi-raspberry-pi-pico': 'raspberry-pi-pico',
|
||||
};
|
||||
|
||||
|
|
@ -64,6 +64,7 @@ const WOKWI_TYPE_TO_BOARD: Record<string, 'arduino-uno' | 'arduino-nano' | 'rasp
|
|||
const BOARD_TO_WOKWI_TYPE: Record<string, string> = {
|
||||
'arduino-uno': 'wokwi-arduino-uno',
|
||||
'arduino-nano': 'wokwi-arduino-nano',
|
||||
'arduino-mega': 'wokwi-arduino-mega',
|
||||
'raspberry-pi-pico': 'wokwi-raspberry-pi-pico',
|
||||
};
|
||||
|
||||
|
|
@ -71,6 +72,7 @@ const BOARD_TO_WOKWI_TYPE: Record<string, string> = {
|
|||
const BOARD_TO_WOKWI_ID: Record<string, string> = {
|
||||
'arduino-uno': 'uno',
|
||||
'arduino-nano': 'nano',
|
||||
'arduino-mega': 'mega',
|
||||
'raspberry-pi-pico': 'pico',
|
||||
};
|
||||
|
||||
|
|
@ -217,6 +219,7 @@ export async function importFromWokwiZip(file: File): Promise<ImportResult> {
|
|||
const VELXIO_BOARD_ID: Record<string, string> = {
|
||||
'arduino-uno': 'arduino-uno',
|
||||
'arduino-nano': 'arduino-nano',
|
||||
'arduino-mega': 'arduino-mega',
|
||||
'raspberry-pi-pico': 'nano-rp2040',
|
||||
};
|
||||
const velxioBoardId = VELXIO_BOARD_ID[boardType] ?? 'arduino-uno';
|
||||
|
|
|
|||
Loading…
Reference in New Issue