velxio/test/test_circuit/src/avr/AVRHarness.js

151 lines
4.6 KiB
JavaScript
Raw Normal View History

feat: electrical simulation via ngspice-WASM (eecircuit-engine) Adds full SPICE-accurate electrical simulation to Velxio, behind a lazy- loaded ⚡ toolbar toggle. Arduino / ESP32 / RP2040 sketches now co-simulate with real analog behaviour: correct voltages on wires, real I–V curves on LEDs, working potentiometers, NTC thermistors read by analogRead(), PWM driving RC filters, transistors, op-amps, diodes, MOSFETs, etc. Engine: eecircuit-engine (ngspice compiled to WebAssembly). Main bundle stays at 2.4 MB; the 20 MB SPICE chunk only loads when the user activates electrical mode. Disabled at build time via VITE_ELECTRICAL_SIM=false. Frontend additions: - simulation/spice/: SpiceEngine wrapper + lazy entry, NetlistBuilder with UnionFind over wires, componentToSpice mapping (24 metadataIds incl. real part numbers: 2N2222, 2N3055, BC547, IRF540, 2N7000, 1N4148, 1N4007, 1N4733, LEDs, NTC, op-amp ideal), CircuitScheduler with debounced coalescing, AVRSpiceBridge for quasi-static co-simulation. - store/useElectricalStore: Zustand slice, feature-flag aware. - components/analog-ui/: ⚡ toolbar toggle + SVG voltage overlay. - components/components-instruments/: Voltmeter, Ammeter probes. - 62 tests (spice-*, netlist-builder, component-to-spice, instruments). Sandbox (test/test_circuit/): 47-test validation sandbox that proved the approach (hand-rolled MNA baseline + ngspice pipeline) before porting to the app. Kept as reference. Docs: docs/wiki/circuit-emulation-*.md (13 engineering pages covering architecture, solvers, components, AVR bridge, gotchas, performance, integration plan, API reference, appendix) + electrical-simulation- user-guide.md (end-user facing). Reference plan: test/test_circuit/plan/phase_8_velxio_implementation.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:11:54 +07:00
/**
* 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('');
}
}