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

76 lines
2.8 KiB
JavaScript

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;
}
}