velxio/frontend/src/simulation/AVRSimulator.ts

283 lines
8.2 KiB
TypeScript
Raw Normal View History

import { CPU, AVRTimer, timer0Config, timer1Config, timer2Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, AVRADC, adcConfig } from 'avr8js';
import { PinManager } from './PinManager';
import { hexToUint8Array } from '../utils/hexParser';
/**
* AVRSimulator - Emulates Arduino Uno (ATmega328p) using avr8js
*
* Features:
* - CPU emulation at 16MHz
* - Timer0/Timer1/Timer2 support (enables millis(), delay(), PWM)
* - USART support (Serial)
* - GPIO ports (PORTB, PORTC, PORTD)
* - ADC support (analogRead())
* - PWM monitoring via OCR register polling
* - Pin state tracking via PinManager
*/
// OCR register addresses → Arduino pin mapping for PWM
const PWM_PINS = [
{ 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
{ ocrAddr: 0x8A, pin: 10, label: 'OCR1BL' }, // Timer1B low byte → D10
{ ocrAddr: 0xB3, pin: 11, label: 'OCR2A' }, // Timer2A → D11
{ ocrAddr: 0xB4, pin: 3, label: 'OCR2B' }, // Timer2B → D3
];
export class AVRSimulator {
private cpu: CPU | null = null;
/** Peripherals kept alive by reference so GC doesn't collect their CPU hooks */
private peripherals: unknown[] = [];
private portB: AVRIOPort | null = null;
private portC: AVRIOPort | null = null;
private portD: AVRIOPort | null = null;
private adc: AVRADC | 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;
private lastOcrValues: number[] = new Array(PWM_PINS.length).fill(-1);
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 (kept alive so their CPU hooks are not GC'd)
this.peripherals = [
new AVRTimer(this.cpu, timer0Config),
new AVRTimer(this.cpu, timer1Config),
new AVRTimer(this.cpu, timer2Config),
new AVRUSART(this.cpu, usart0Config, 16000000),
];
// Initialize ADC (analogRead support)
this.adc = new AVRADC(this.cpu, adcConfig);
// Initialize IO 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);
// Set up pin change hooks
this.setupPinHooks();
console.log(`AVR CPU initialized (${this.peripherals.length} peripherals, ADC + Timer1/Timer2 enabled)`);
}
/**
* Expose ADC instance so components (potentiometer, etc.) can inject voltages
*/
getADC(): AVRADC | null {
return this.adc;
}
/**
* Monitor pin changes and update component states
*/
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;
}
});
// 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;
}
});
console.log('Pin hooks configured successfully');
}
/**
* Poll OCR registers and notify PinManager of PWM duty cycle changes
*/
private pollPwmRegisters(): void {
if (!this.cpu) return;
for (let i = 0; i < PWM_PINS.length; i++) {
const { ocrAddr, pin } = PWM_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);
}
}
}
/**
* 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...');
let frameCount = 0;
const execute = (_timestamp: number) => {
if (!this.running || !this.cpu) return;
// 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++) {
avrInstruction(this.cpu); // Execute the AVR instruction
this.cpu.tick(); // Update peripheral timers and cycles
}
// Poll PWM registers every frame
this.pollPwmRegisters();
frameCount++;
if (frameCount % 60 === 0) {
console.log(`[CPU] Frame ${frameCount}, PC: ${this.cpu.pc}, Cycles: ${this.cpu.cycles}`);
}
} catch (error) {
console.error('Simulation error:', error);
this.stop();
return;
}
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...');
this.cpu = new CPU(this.program);
this.peripherals = [
new AVRTimer(this.cpu, timer0Config),
new AVRTimer(this.cpu, timer1Config),
new AVRTimer(this.cpu, timer2Config),
new AVRUSART(this.cpu, usart0Config, 16000000),
];
this.adc = new AVRADC(this.cpu, adcConfig);
this.portB = new AVRIOPort(this.cpu, portBConfig);
this.portC = new AVRIOPort(this.cpu, portCConfig);
this.portD = new AVRIOPort(this.cpu, portDConfig);
this.lastPortBValue = 0;
this.lastPortCValue = 0;
this.lastPortDValue = 0;
this.lastOcrValues = new Array(PWM_PINS.length).fill(-1);
this.setupPinHooks();
console.log('AVR CPU reset complete');
}
}
isRunning(): boolean {
return this.running;
}
setSpeed(speed: number): void {
this.speed = Math.max(0.1, Math.min(10.0, speed));
console.log(`Simulation speed set to ${this.speed}x`);
}
getSpeed(): number {
return this.speed;
}
step(): void {
if (!this.cpu) return;
avrInstruction(this.cpu);
this.cpu.tick();
}
/**
* 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);
}
}
}