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

12 KiB
Raw Permalink Blame History

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 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 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 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 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.