velxio/docs/wiki/circuit-emulation-architect...

194 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Circuit Emulation — Architecture
## High-level data flow
Two pipelines coexist. Both share the same `avr8js` harness and the same Arduino program fixtures; they differ only in the analog solver.
```
┌────────────────────────── Shared ──────────────────────────┐
│ │
│ Arduino sketch source (.ino) or pre-built .hex │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ AVRHarness │ (wraps avr8js) │
│ │ (CPU + │ │
│ │ PORTs + │ │
│ │ ADC + │ │
│ │ Timers) │ │
│ └──────┬──────┘ │
│ │ │
│ pin state / PWM duty ADC channel voltages │
│ ▼ ▲ │
└──────────────────│────────────────────│─────────────────────┘
│ │
┌──────────────┼────────────────────┼──────────────┐
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ Pipeline A │ │ │
│ │ MNA Solver (JS) │──────────┤ │
│ │ │ │ │
│ │ - Node graph │ │ │
│ │ - Stamps │ │ │
│ │ - Newton loop │ │ │
│ │ - Transient │ │ │
│ │ (back-Euler) │ │ │
│ └─────────────────┘ │ │
│ │ │
│ ┌──────────────────┐ │ │
│ │ Pipeline B │ │ │
│ │ SpiceEngine │─────────┘ │
│ │ (eecircuit-engine│ │
│ │ / ngspice-WASM) │ │
│ │ │ │
│ │ - Netlist in │ │
│ │ - ResultType out │ │
│ │ - AVRSpiceBridge │ │
│ │ for co-sim │ │
│ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
```
## Module layout
```
test/test_circuit/
├── src/
│ ├── solver/ # Pipeline A — hand-rolled MNA
│ │ ├── linalg.js # Gaussian elimination with partial pivoting
│ │ └── Circuit.js # Circuit class, DC + transient solve
│ ├── components/ # Component library for Pipeline A
│ │ ├── passive.js # R, V, I, C, Pot, NTC, Switch
│ │ └── active.js # Diode, LED, BJT
│ ├── avr/ # Shared AVR harness
│ │ ├── intelHex.js # Intel HEX parser (same format as Velxio)
│ │ ├── AVRHarness.js # Thin wrapper over avr8js
│ │ ├── asm.js # Mini AVR assembler (LDI, OUT, STS, LDS, RJMP, SBRC/S, NOP)
│ │ └── programs.js # Hand-assembled Arduino programs (pot→PWM, adcRead)
│ ├── spice/ # Pipeline B — ngspice integration
│ │ ├── SpiceEngine.js # runNetlist() + vec()/dcValue() helpers
│ │ └── AVRSpiceBridge.js # Quasi-static AVR ↔ ngspice co-simulation
│ └── index.js # Re-exports for Pipeline A
├── fixtures/
│ └── blink.hex # Copied from frontend/src/__tests__/fixtures/
├── test/ # 14 test files, 47 tests total
├── plan/ # 8 markdown planning docs
└── autosearch/ # 4 findings docs
```
## Pipeline A — Hand-rolled MNA
Goal: a transparent, minimal-dependency baseline that we fully control. Useful for:
- Understanding every step of the solver
- Cross-checking ngspice results on toy problems
- Providing a fallback if `eecircuit-engine` cannot be loaded in a constrained environment
**Execution path:**
1. User constructs a `Circuit` object and adds `Component` instances.
2. `circuit.solveDC({maxIter, tol})` is called.
3. Solver identifies voltage-source components and allocates extra MNA rows.
4. For each Newton iteration:
- Zero the G matrix and b vector (size `N + M`).
- Add a small shunt conductance `GMIN = 1e-12` on every node for stability.
- Call `component.stampDC(G, b, ctx)` on every component.
- Solve `G · x = b` via Gaussian elimination.
- Apply damping (≤ 0.5 V step per node) and iterate until `max |Δx| < tol` or `maxIter` reached.
5. Store `nodeVoltages` and `branchCurrents` in `state`.
**Transient execution path:**
1. `circuit.runTransient(tEnd, dt, sampleEvery)` seeds `state.prev.nodeVoltages` from each capacitor's `Vinit` (the initial condition).
2. On every step, `state.prev` is the solution from the previous step.
3. The capacitor stamp uses backward Euler: conductance `G_c = C/Δt`, companion current `I_eq = (C/Δt)·V_prev`.
4. Solver runs `solveDC({dt})` with the capacitor stamps using `ctx.prev`.
See [circuit-emulation-mna-solver.md](circuit-emulation-mna-solver.md) for stamp-level detail.
## Pipeline B — ngspice-WASM
Goal: leverage the full, battle-tested ngspice engine (30+ years of development) without reinventing any physics.
**Execution path:**
1. First call to `getEngine()` or `runNetlist()` boots `eecircuit-engine` — downloads and instantiates ~39 MB of WASM+glue; takes ~400 ms.
2. Subsequent calls reuse the same `Simulation` singleton.
3. Each call:
- Compose a full SPICE netlist string (component cards + `.op` / `.tran` / `.ac` / `.dc` + `.end`).
- `sim.setNetList(netlist)`.
- `await sim.runSim()` → returns a `ResultType` with `variableNames[]` and `data[].values[]`.
4. Our wrapper exposes helpers:
- `vec(name)` → array of numbers (real data) or `[{real, img}]` (complex data from `.ac`).
- `dcValue(name)` → first value of the vector.
- `findVar(name)` → index in `variableNames[]`, matching either `"v(node)"` or just `"node"`.
See [circuit-emulation-ngspice.md](circuit-emulation-ngspice.md) for netlist authoring conventions and gotchas.
## Mixed-signal co-simulation
`avr8js` runs the MCU at ~1 MHz (in JS it's slower than real 16 MHz silicon, but cycle-accurate). ngspice runs transient analysis on the analog network. They are **not locked cycle-by-cycle**; instead the bridge operates **quasi-statically** in time slices.
Per slice (default: 1 ms of AVR time = 16 000 cycles):
```
┌─ slice n ────────────────────────────────────────────────┐
│ │
│ 1. avr.runCycles(16_000) │
│ → new pin states, new PWM duty cycles │
│ │
│ 2. snapshot = { pin6:{type:'pwm',duty:0.5}, │
│ pin13:{type:'digital',v:5}, ... } │
│ │
│ 3. buildNetlist(snapshot) → string │
│ V_PIN6 pin6 0 DC 2.5 ← duty × 5 │
│ V_PIN13 pin13 0 DC 5 │
│ ... passive circuit ... │
│ .tran 10u 1m │
│ .end │
│ │
│ 4. await runNetlist(netlist) → result │
│ │
│ 5. for each analog channel: inject final v(node) into │
│ avr.setAnalogVoltage(ch, v) │
│ │
└──────────────────────────────────────────────────────────┘
```
This is good enough for:
- ADC sampling of slow analog sources (NTC, pot, photoresistor)
- PWM filtered to DC via RC
- LED current driven by a digital or PWM pin
- Digital outputs driving logic or switches
It is **not good enough** for:
- Audio-rate feedback loops where the analog network reacts on sub-millisecond timescales
- Oscillators where the analog side drives a digital input and vice versa with tight timing
- Noise / jitter analysis
For the tight-coupling cases, a co-simulation framework would need to arbitrate time advancement between the two engines (see [Velxio Integration](circuit-emulation-velxio-integration.md) for future work).
## Shared Arduino harness
Both pipelines use `AVRHarness` to drive `avr8js`. This class **mirrors exactly** what Velxio's `frontend/src/simulation/AVRSimulator.ts` does:
- `new CPU(programUint16, sramBytes)`
- `new AVRIOPort(cpu, portBConfig/portCConfig/portDConfig)`
- `new AVRADC(cpu, adcConfig)``adc.channelValues[channel] = volts` to inject analog
- `new AVRTimer(cpu, timer0Config/...)`
- `new AVRUSART(cpu, usart0Config, 16_000_000)`
- Listeners per port: `port.addListener((newValue, oldValue) => ...)` with bit-by-bit diff and Arduino pin mapping (D0D7, D8D13, A0A5 via port C).
- PWM duty read via `cpu.data[ocrAddress]` where ocrAddress for Timer0A is `0x47`, Timer1AL is `0x88`, etc.
This parity is important: any lesson learned in the sandbox transfers one-for-one to Velxio's main app.
See [circuit-emulation-avr-bridge.md](circuit-emulation-avr-bridge.md) for the full harness reference.
## Fixtures and hand-assembled programs
To exercise the full sketch path (C++ → avr-gcc → hex) we would normally compile Arduino code. The sandbox does not have `avr-gcc` installed, so we use:
1. **Copied from Velxio**: `fixtures/blink.hex` — the same Intel HEX used by `frontend/src/__tests__/fixtures/avr-blink/avr-blink.ino.hex`. Tests that load this file exercise the full Arduino core init (reset vectors, library init, `setup()`, `loop()`).
2. **Hand-assembled programs**: `src/avr/programs.js` exposes `potToPwmProgram()` and `adcReadProgram()`, built with the mini-assembler in `src/avr/asm.js`. These bypass the Arduino core and directly configure ADC and Timer0 registers. They produce ~1822 instruction words each. Full opcode breakdown is in [circuit-emulation-appendix.md](circuit-emulation-appendix.md).