velxio/test/test_circuit/src/spice/AVRSpiceBridge.js

76 lines
2.8 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
import { runNetlist } from './SpiceEngine.js';
/**
* Co-simulation bridge between avr8js (digital MCU) and ngspice (analog circuit).
*
* Workflow:
* 1. Host runs the AVR for a slice of time (e.g. 1 ms of cycles).
* 2. Host records the GPIO pin states (and PWM duties) at that instant.
* 3. Host builds a PWL voltage source per AVR pin and a netlist for the circuit.
* 4. ngspice runs a transient analysis covering that slice.
* 5. Host reads the voltage at ADC-connected nodes and injects into avr8js.
* 6. Repeat.
*
* This is quasi-static: the analog and digital clocks aren't locked cycle-to-cycle,
* but the feedback loop refreshes at ~1 kHz which is enough for most educational
* circuits (PWM filtering, sensors, LED drivers, etc.).
*/
export class AVRSpiceBridge {
constructor(avr, { sliceMs = 1, analogChannels = [] } = {}) {
this.avr = avr;
this.sliceMs = sliceMs;
this.analogChannels = analogChannels; // [{ channel, node }]
this.analogPinVoltageHistory = new Map(); // pin → [{t, v}]
this.adcSamples = [];
this.t = 0;
}
/**
* Run co-simulation for `totalMs` milliseconds.
*
* `buildNetlist(pinSnapshots, sliceStartMs, sliceEndMs)` must return a
* complete ngspice netlist for the circuit. `pinSnapshots` is an object
* mapping pin number {type:'digital', v:0|5} or {type:'pwm', duty:0..1}.
*
* The bridge will patch in `.tran` automatically if not provided.
*/
async run(totalMs, buildNetlist) {
const slices = Math.ceil(totalMs / this.sliceMs);
const cyclesPerSlice = Math.round(16_000_000 * (this.sliceMs / 1000));
const timeline = [];
for (let s = 0; s < slices; s++) {
const t0 = s * this.sliceMs;
const t1 = (s + 1) * this.sliceMs;
// 1. Run the MCU
this.avr.runCycles(cyclesPerSlice);
// 2. Snapshot pin states
const pinSnapshots = {};
for (let p = 0; p <= 13; p++) {
const duty = this.avr.getPWMDuty(p);
if (duty !== null && duty > 0) {
pinSnapshots[p] = { type: 'pwm', duty };
} else {
pinSnapshots[p] = { type: 'digital', v: this.avr.getPin(p) ? 5 : 0 };
}
}
// 3. Build and run netlist
const netlist = buildNetlist(pinSnapshots, t0, t1);
const result = await runNetlist(netlist);
// 4. Sample the voltages at the end of the slice on the configured ADC channels
const time = result.vec('time');
for (const { channel, node } of this.analogChannels) {
const varname = `v(${node})`;
let vec;
try { vec = result.vec(varname); }
catch { continue; }
const vEnd = vec[vec.length - 1];
this.avr.setAnalogVoltage(channel, vEnd);
this.adcSamples.push({ t: t1 / 1000, channel, node, v: vEnd });
}
timeline.push({ t0, t1, pinSnapshots, result });
}
return timeline;
}
}