LabWired The easy way to build hardware
Oscilloscope trace of an RC charge curve next to a firmware GPIO edge in LabWired.

Learn / Track 03

Co-simulation and mixed-signal

What co-simulation does

A co-simulation model is an external circuit or process that steps next to your firmware. LabWired drives the model at a fixed interval. Each step, LabWired sends the model its inputs and reads back its outputs. The manifest routes those inputs and outputs to board pins, so firmware can drive a circuit and read its response.

Use co-simulation to test firmware against a plant it controls. An RC filter, a motor, a battery pack, or a sensor array are all valid models. The model does not need to run in LabWired’s process; the external_process adapter runs any program that reads and writes JSON lines. The analog adapter runs a small circuit solver inside LabWired itself, including in the browser.

The result is a signal path: firmware writes a pin, the model reacts, and a probe reads the result back into firmware or into the oscilloscope.

The manifest block

Add a cosim_models list to system.yaml. Each entry describes one model.

cosim_models:
  - id: rc
    adapter: analog
    step_ns: 100000
    inputs:
      gpio: board.gpio.pa5
    outputs:
      v_out: board.analog.pa0_volts
    config:
      netlist: ./rc.cir
      vdd: 3.3
      probes: { v_out: "v(out)" }
      sources: { gpio: Vgpio }
FieldTypeRequiredMeaning
idstringyesUnique name for this model in the manifest.
adapterstringyesmock, external_process, or analog.
modelstringfor external_processPath to the model program, relative to the manifest. The analog adapter takes its netlist from config.netlist.
step_nsintegeryesStep interval in nanoseconds. Must be greater than zero.
inputsmapyesLabWired input name to signal path, for example gpio: board.gpio.pa5.
outputsmapyesLabWired output name to signal path, for example v_out: board.analog.pa0_volts.
configmapnoAdapter-specific settings. See each adapter below.

Adapters

LabWired supports three adapters. Pick one per model.

AdapterRuns whereUse for
mocknative, browserTests and dry runs. Static outputs, no real model.
external_processnative onlyAny program that speaks the JSONL contract. Includes the ngspice wrapper.
analognative, browserA linear circuit solved in-core, without ngspice.

mock

The mock adapter returns fixed outputs. Declare them under config.outputs.

cosim_models:
  - id: plant_model
    adapter: mock
    step_ns: 10000
    inputs:
      controller_enable: control.enable
    outputs:
      plant_ready: observables.plant_ready
    config:
      outputs:
        plant_ready: true

Use mock to test manifest wiring before the real model exists.

external_process

The external_process adapter starts the program named by model and speaks a JSONL contract over stdin and stdout. LabWired sends one line per step:

{"time_ns": 1000000, "dt_ns": 1000000, "inputs": {"gpio": true}}

The model replies with one line:

{"outputs": {"v_out": 2.08}}

time_ns is the end of the step, not the start. The model returns the value at that time.

LabWired ships a wrapper for ngspice: tools/cosim/labwired_ngspice.py. It loads libngspice in-process and steps a SPICE netlist to each step boundary. Install libngspice0 before you run it:

sudo apt install libngspice0

Any ngspice-compatible model works, including vendor or open-source models pulled in with .include or .lib. One ngspice process holds one circuit. Declare a second cosim_models entry for a second circuit.

cosim_models:
  - id: rc
    adapter: external_process
    model: ./models/rc_lowpass.py
    step_ns: 100000
    inputs:
      gpio: board.gpio.pa5
    outputs:
      v_out: board.analog.pa0_volts

The model program owns the netlist, the source map, and the probe map. See examples/cosim-spice-rc/models/rc_lowpass.py for a three-line stub that calls the wrapper.

analog

The analog adapter solves a linear circuit inside LabWired’s own code. It needs no external process and runs in the browser playground. It supports a fixed set of elements.

ElementLineNotes
ResistorR<name> n1 n2 <value>
CapacitorC<name> n1 n2 <value> [ic=<v>]ic sets the initial voltage.
InductorL<name> n1 n2 <value> [ic=<i>]ic sets the initial current.
Voltage sourceV<name> n+ n- dc <value>Routing overrides the value at run time.
Current sourceI<name> n+ n- dc <value>Routing overrides the value at run time.
SwitchS<name> n1 n2 <ctrl> ron=<r> roff=<r><ctrl> is a routed boolean input.

Node 0 and node gnd are both ground. Values accept SPICE suffixes: k, meg, u, n, p, m.

cosim_models:
  - id: rc
    adapter: analog
    step_ns: 100000
    inputs:
      gpio: board.gpio.pa5
    outputs:
      v_out: board.analog.pa0_volts
    config:
      netlist: ./rc.cir
      vdd: 3.3
      substeps: 10
      integration: be
      probes: { v_out: "v(out)" }
      sources: { gpio: Vgpio }
Config keyMeaning
netlistPath to a netlist file, relative to the manifest.
netlist_textInline netlist text, as an alternative to netlist.
vddVoltage a boolean input drives when true.
substepsInternal solver steps per step_ns. Default 10.
integrationbe (backward Euler, default) or trap (trapezoidal).
probesOutput name to node or current, for example v_out: "v(out)".
sourcesInput name to element name, for example gpio: Vgpio.
trace_samplesRing buffer size for the oscilloscope trace. Default 20000.

The analog adapter parses a fixed element set. A diode, a transistor, or an .include line is outside that set. Manifest validation then fails and names the line. Switch that model to adapter: external_process with the ngspice wrapper.

Signal paths

LabWired routes co-simulation inputs and outputs through three signal path forms.

PathDirectionExample
board.gpio.<pin>Firmware pin state into the modelboard.gpio.pa5 carries the PA5 output level into inputs.gpio.
board.analog.<pin>_voltsModel voltage into a board ADC pinboard.analog.pa0_volts sets the voltage the ADC on PA0 reads.
board.gpio_in.<pin>Model output into a firmware input pinboard.gpio_in.pb0 drives PB0 high or low from a model output.

Oscilloscope

The oscilloscope instrument shows analog channels and digital pins on one time axis.

Open it from the Tools menu in the playground, or load a lab with ?panel=scope in the URL.

FeatureDescription
ChannelsAny analog channel from outputs, or a digital pin with pin:<name>.
Triggernone, rising, falling, or level, on a chosen channel.
CursorsTwo movable cursors, A and B, with a Δt and ΔV readout.
ExportSave the visible trace as CSV.

On the command line, add --analog-trace to run or test to write the same trace to a file:

labwired run examples/rc-oscilloscope-lab/system.yaml --analog-trace out.csv

Pass a .vcd path instead of .csv to open the trace in GTKWave or PulseView.

Worked example: the RC lab

The rc-oscilloscope-lab example toggles a GPIO into an RC low-pass filter and reads the result back on an ADC pin.

Netlist (rc.cir), a 10 kΩ / 100 nF low-pass, τ = 1 ms:

Vgpio in 0 dc 0
R1 in out 10k
C1 out 0 100n
.end

Manifest entry:

cosim_models:
  - id: rc
    adapter: analog
    step_ns: 100000
    inputs:
      gpio: board.gpio.pa5
    outputs:
      v_out: board.analog.pa0_volts
    config:
      netlist: ./rc.cir
      vdd: 3.3
      probes: { v_out: "v(out)" }
      sources: { gpio: Vgpio }

Firmware toggles PA5 every 5 ms and reads ADC1 channel 0 every 500 µs. The charge curve crosses 1.5 V to 3 V as the capacitor charges toward 3.3 V.

Run it natively:

labwired run examples/rc-oscilloscope-lab/system.yaml --analog-trace out.csv

Open it in the playground:

https://app.labwired.com/?board=rc-oscilloscope-lab&panel=scope

The oscilloscope shows the PA5 edge on one row and the v_out charge curve on another, on the same time axis.

Limits

The analog adapter solves linear circuits only. It does not model diodes, transistors, or any nonlinear device. It does not run an AC or DC sweep. It has no .include or .lib support.

For a nonlinear device, a vendor SPICE model, or a sweep, use adapter: external_process with the ngspice wrapper. The ngspice wrapper runs on native builds only; the browser playground supports the analog adapter alone.

Andrii Shylenko
Andrii Shylenko

Founder, LabWired.