76 lines
2.8 KiB
JavaScript
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;
|
|
}
|
|
}
|