velxio/frontend/src/simulation/AVRSimulator.ts

264 lines
7.8 KiB
TypeScript
Raw Normal View History

import { CPU, AVRTimer, timer0Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, PinState } from 'avr8js';
import { PinManager } from './PinManager';
import { hexToUint8Array } from '../utils/hexParser';
/**
* AVRSimulator - Emulates Arduino Uno (ATmega328p) using avr8js
*
* Features:
* - CPU emulation at 16MHz
* - Timer0 support
* - USART support (Serial)
* - GPIO ports (PORTB, PORTC, PORTD)
* - Pin state tracking via PinManager
*/
export class AVRSimulator {
private cpu: CPU | null = null;
private timer0: AVRTimer | null = null;
private usart: AVRUSART | null = null;
private portB: AVRIOPort | null = null;
private portC: AVRIOPort | null = null;
private portD: AVRIOPort | null = null;
private program: Uint16Array | null = null;
private running = false;
private animationFrame: number | null = null;
public pinManager: PinManager;
private speed = 1.0; // Simulation speed multiplier
private lastPortBValue = 0;
private lastPortCValue = 0;
private lastPortDValue = 0;
constructor(pinManager: PinManager) {
this.pinManager = pinManager;
}
/**
* Load compiled hex file into simulator
*/
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);
// Load bytes into program memory (little-endian, 16-bit words)
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);
}
console.log(`Loaded ${bytes.length} bytes into program memory`);
// Initialize CPU (ATmega328p @ 16MHz)
this.cpu = new CPU(this.program);
// Initialize peripherals
this.timer0 = new AVRTimer(this.cpu, timer0Config);
this.usart = new AVRUSART(this.cpu, usart0Config, 16000000); // 16MHz
// Initialize IO ports
this.portB = new AVRIOPort(this.cpu, portBConfig);
this.portC = new AVRIOPort(this.cpu, portCConfig);
this.portD = new AVRIOPort(this.cpu, portDConfig);
// Set up pin change hooks
this.setupPinHooks();
console.log('AVR CPU initialized successfully');
}
/**
* Monitor pin changes and update component states
*/
private setupPinHooks(): void {
if (!this.cpu) return;
console.log('Setting up pin hooks...');
console.log('Initial PORTB:', this.portB);
console.log('Initial PORTC:', this.portC);
console.log('Initial PORTD:', this.portD);
// PORTB (Digital pins 8-13)
// Pin 13 (LED_BUILTIN) = PORTB5
this.portB!.addListener((value, _oldValue) => {
console.log(`[PORTB LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`);
console.log(` Binary: ${value.toString(2).padStart(8, '0')}`);
console.log(` Pin 13 (bit 5) state: ${this.portB!.pinState(5) === PinState.High ? 'HIGH' : 'LOW'}`);
if (value !== this.lastPortBValue) {
this.pinManager.updatePort('PORTB', value, this.lastPortBValue);
this.lastPortBValue = value;
}
});
// PORTC (Analog pins A0-A5)
this.portC!.addListener((value, _oldValue) => {
console.log(`[PORTC LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`);
if (value !== this.lastPortCValue) {
this.pinManager.updatePort('PORTC', value, this.lastPortCValue);
this.lastPortCValue = value;
}
});
// PORTD (Digital pins 0-7)
this.portD!.addListener((value, _oldValue) => {
console.log(`[PORTD LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`);
if (value !== this.lastPortDValue) {
this.pinManager.updatePort('PORTD', value, this.lastPortDValue);
this.lastPortDValue = value;
}
});
console.log('Pin hooks configured successfully');
}
/**
* Start simulation loop
*/
start(): void {
if (this.running || !this.cpu) {
console.warn('Simulator already running or not initialized');
return;
}
this.running = true;
console.log('Starting AVR simulation...');
console.log('CPU state:', {
pc: this.cpu.pc,
cycles: this.cpu.cycles,
data: this.cpu.data.slice(0, 10) // First 10 bytes of RAM
});
console.log('Program loaded:', this.program?.length, 'words');
let frameCount = 0;
const execute = (timestamp: number) => {
if (!this.running || !this.cpu) return;
// Execute instructions in batches for performance
// ATmega328p @ 16MHz = 16M cycles/sec
// At 60fps: 16,000,000 / 60 ≈ 267,000 cycles per frame
const cyclesPerFrame = Math.floor(267000 * this.speed);
try {
for (let i = 0; i < cyclesPerFrame; i++) {
// Execute one AVR instruction and update peripherals
avrInstruction(this.cpu); // CRITICAL: Execute the actual instruction
this.cpu.tick(); // Update peripheral timers and cycles
}
// Log every 60 frames (once per second at 60fps)
frameCount++;
if (frameCount % 60 === 0) {
console.log(`[CPU] Frame ${frameCount}, PC: ${this.cpu.pc}, Cycles: ${this.cpu.cycles}`);
console.log(`[CPU] PORTB register value: 0x${this.cpu.data[0x25].toString(16).padStart(2, '0')}`);
}
} catch (error) {
console.error('Simulation error:', error);
this.stop();
return;
}
// Schedule next frame
this.animationFrame = requestAnimationFrame(execute);
};
this.animationFrame = requestAnimationFrame(execute);
}
/**
* Stop simulation
*/
stop(): void {
if (!this.running) return;
this.running = false;
if (this.animationFrame !== null) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
console.log('AVR simulation stopped');
}
/**
* Reset simulator
*/
reset(): void {
this.stop();
if (this.cpu && this.program) {
console.log('Resetting AVR CPU...');
// Reinitialize CPU
this.cpu = new CPU(this.program);
this.timer0 = new AVRTimer(this.cpu, timer0Config);
this.usart = new AVRUSART(this.cpu, usart0Config, 16000000);
// Reinitialize ports
this.portB = new AVRIOPort(this.cpu, portBConfig);
this.portC = new AVRIOPort(this.cpu, portCConfig);
this.portD = new AVRIOPort(this.cpu, portDConfig);
// Reset port values
this.lastPortBValue = 0;
this.lastPortCValue = 0;
this.lastPortDValue = 0;
this.setupPinHooks();
console.log('AVR CPU reset complete');
}
}
/**
* Check if simulator is running
*/
isRunning(): boolean {
return this.running;
}
/**
* Set simulation speed (1.0 = normal, 0.5 = half speed, 2.0 = double speed)
*/
setSpeed(speed: number): void {
this.speed = Math.max(0.1, Math.min(10.0, speed));
console.log(`Simulation speed set to ${this.speed}x`);
}
/**
* Get current simulation speed
*/
getSpeed(): number {
return this.speed;
}
/**
* Execute a single instruction (for step-by-step debugging)
*/
step(): void {
if (!this.cpu) return;
avrInstruction(this.cpu); // Execute the instruction
this.cpu.tick(); // Update peripherals
}
/**
* Set the state of an Arduino pin externally (e.g. from a UI button)
*/
setPinState(arduinoPin: number, state: boolean): void {
if (arduinoPin >= 0 && arduinoPin <= 7 && this.portD) {
this.portD.setPin(arduinoPin, state);
} else if (arduinoPin >= 8 && arduinoPin <= 13 && this.portB) {
this.portB.setPin(arduinoPin - 8, state);
} else if (arduinoPin >= 14 && arduinoPin <= 19 && this.portC) {
this.portC.setPin(arduinoPin - 14, state);
}
}
}