151 lines
4.6 KiB
JavaScript
151 lines
4.6 KiB
JavaScript
/**
|
|
* Thin wrapper around avr8js that mirrors how Velxio's AVRSimulator drives
|
|
* the ATmega328P. Exposes:
|
|
* - load(hexText)
|
|
* - runCycles(n)
|
|
* - getPin(pin) — digital 0/1
|
|
* - getPWMDuty(pin) — 0..1 estimated duty for a PWM pin
|
|
* - setAnalogVoltage(ch, v) — inject voltage on ADC channel (0..5)
|
|
* - onPinChange(pin, cb)
|
|
*/
|
|
import {
|
|
CPU, AVRIOPort, AVRTimer, AVRADC, AVRUSART,
|
|
portBConfig, portCConfig, portDConfig,
|
|
timer0Config, timer1Config, timer2Config,
|
|
adcConfig, usart0Config,
|
|
avrInstruction,
|
|
} from 'avr8js';
|
|
import { parseIntelHex, bytesToProgramWords } from './intelHex.js';
|
|
|
|
// ATmega328P PWM OCR addresses → Arduino Uno pin
|
|
const PWM_PINS = [
|
|
{ ocrAddr: 0x47, pin: 6, label: 'OCR0A' },
|
|
{ ocrAddr: 0x48, pin: 5, label: 'OCR0B' },
|
|
{ ocrAddr: 0x88, pin: 9, label: 'OCR1AL' },
|
|
{ ocrAddr: 0x8A, pin: 10, label: 'OCR1BL' },
|
|
{ ocrAddr: 0xB3, pin: 11, label: 'OCR2A' },
|
|
{ ocrAddr: 0xB4, pin: 3, label: 'OCR2B' },
|
|
];
|
|
|
|
// Arduino Uno pin ↔ (port, bit)
|
|
// PORTD bit 0..7 → D0..D7
|
|
// PORTB bit 0..5 → D8..D13
|
|
// PORTC bit 0..5 → A0..A5 (pins 14..19)
|
|
const PIN_MAP = {};
|
|
for (let i = 0; i < 8; i++) PIN_MAP[i] = { portName: 'D', bit: i };
|
|
for (let i = 0; i < 6; i++) PIN_MAP[8 + i] = { portName: 'B', bit: i };
|
|
for (let i = 0; i < 6; i++) PIN_MAP[14 + i] = { portName: 'C', bit: i };
|
|
|
|
export class AVRHarness {
|
|
constructor() {
|
|
this.cpu = null;
|
|
this.ports = { B: null, C: null, D: null };
|
|
this.adc = null;
|
|
this.usart = null;
|
|
this.timers = [];
|
|
this.ocrValues = new Array(PWM_PINS.length).fill(0);
|
|
this.pinListeners = new Map(); // pin → Set<fn>
|
|
this.portValues = { B: 0, C: 0, D: 0 };
|
|
this.serialOut = [];
|
|
}
|
|
|
|
load(hexText) {
|
|
const bytes = parseIntelHex(hexText);
|
|
const program = bytesToProgramWords(bytes);
|
|
this._bindCpu(program);
|
|
}
|
|
|
|
/** Load a pre-assembled Uint16Array of instruction words. */
|
|
loadProgram(words) {
|
|
const program = new Uint16Array(0x8000 / 2);
|
|
program.set(words);
|
|
this._bindCpu(program);
|
|
}
|
|
|
|
_bindCpu(program) {
|
|
this.cpu = new CPU(program, 8192);
|
|
|
|
this.ports.B = new AVRIOPort(this.cpu, portBConfig);
|
|
this.ports.C = new AVRIOPort(this.cpu, portCConfig);
|
|
this.ports.D = new AVRIOPort(this.cpu, portDConfig);
|
|
this.adc = new AVRADC(this.cpu, adcConfig);
|
|
|
|
this.usart = new AVRUSART(this.cpu, usart0Config, 16_000_000);
|
|
this.usart.onByteTransmit = (v) => this.serialOut.push(String.fromCharCode(v));
|
|
|
|
this.timers = [
|
|
new AVRTimer(this.cpu, timer0Config),
|
|
new AVRTimer(this.cpu, timer1Config),
|
|
new AVRTimer(this.cpu, timer2Config),
|
|
];
|
|
|
|
for (const name of ['B', 'C', 'D']) {
|
|
const port = this.ports[name];
|
|
port.addListener((value, _oldValue) => {
|
|
const old = this.portValues[name];
|
|
this.portValues[name] = value;
|
|
const changed = old ^ value;
|
|
for (let bit = 0; bit < 8; bit++) {
|
|
if (changed & (1 << bit)) {
|
|
const arduinoPin = this._portBitToArduinoPin(name, bit);
|
|
if (arduinoPin == null) continue;
|
|
const state = (value >> bit) & 1;
|
|
const set = this.pinListeners.get(arduinoPin);
|
|
if (set) set.forEach(cb => cb(state));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
_portBitToArduinoPin(portName, bit) {
|
|
if (portName === 'B' && bit < 6) return 8 + bit;
|
|
if (portName === 'C' && bit < 6) return 14 + bit;
|
|
if (portName === 'D' && bit < 8) return bit;
|
|
return null;
|
|
}
|
|
|
|
runCycles(n) {
|
|
const end = this.cpu.cycles + n;
|
|
while (this.cpu.cycles < end) {
|
|
avrInstruction(this.cpu);
|
|
this.cpu.tick();
|
|
}
|
|
}
|
|
|
|
getPin(pin) {
|
|
const m = PIN_MAP[pin];
|
|
if (!m) return 0;
|
|
const port = this.ports[m.portName];
|
|
return (port.pinState(m.bit) === 3 || port.pinState(m.bit) === 1) ? 1 : 0;
|
|
// pinState: 0=input low, 1=input high, 2=output low, 3=output high
|
|
}
|
|
|
|
onPinChange(pin, cb) {
|
|
if (!this.pinListeners.has(pin)) this.pinListeners.set(pin, new Set());
|
|
this.pinListeners.get(pin).add(cb);
|
|
return () => this.pinListeners.get(pin).delete(cb);
|
|
}
|
|
|
|
/** Inject an analog voltage (0..5 V) onto ADC channel 0..5 (A0..A5). */
|
|
setAnalogVoltage(channel, volts) {
|
|
if (!this.adc) return;
|
|
this.adc.channelValues[channel] = Math.max(0, Math.min(5, volts));
|
|
}
|
|
|
|
/**
|
|
* Estimate PWM duty cycle on a supported pin by reading the OCR register.
|
|
* Returns 0..1. Returns null if the pin is not a PWM pin.
|
|
*/
|
|
getPWMDuty(pin) {
|
|
const entry = PWM_PINS.find(p => p.pin === pin);
|
|
if (!entry) return null;
|
|
const ocrVal = this.cpu.data[entry.ocrAddr];
|
|
return ocrVal / 255;
|
|
}
|
|
|
|
getSerialOutput() {
|
|
return this.serialOut.join('');
|
|
}
|
|
}
|