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. The model steps at the same time as your firmware. LabWired drives the model at a fixed interval. At each step, LabWired sends the inputs to the model. LabWired then reads the outputs from the model. The manifest routes those inputs and outputs to board pins. Firmware can then drive a circuit. Firmware can read the circuit response.

Use co-simulation to test firmware against a plant that the firmware controls. An RC filter, a motor, a battery pack, or a sensor array is a valid model. The model does not need to run in the LabWired process. The external_process adapter runs a program. The program reads JSON lines. The program writes JSON lines. The analog adapter runs a small circuit solver inside LabWired. The solver also runs in the browser.

The result is a signal path. Firmware writes a pin. The model reacts. A probe reads the result. The result goes 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 adapter for each model.

AdapterRuns whereUse for
mocknative, browserTests and dry runs. Static outputs, no real model.
external_processnative onlyAny program that uses 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 the manifest wiring before the real model exists.

external_process

The external_process adapter starts the program that model names. The adapter uses a JSONL contract on stdin and stdout. LabWired sends one line for each 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. time_ns is not the start of the step. The model returns the value at that time.

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

sudo apt install libngspice0

Any ngspice-compatible model operates. The model can be a vendor model or an open-source model. The netlist loads that model with .include or .lib. One ngspice process contains 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 defines 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 in the LabWired code. It needs no external process. It 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 is ground. Node gnd is ground. A value can use a SPICE suffix. The suffixes are k, meg, u, n, p, and 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 set of elements. A diode, a transistor, or an .include line is outside that set. Manifest validation then fails. Validation names the line. For that model, use adapter: external_process and 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 the oscilloscope 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 to test. The option writes the same trace to a file:

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

Pass a .vcd path instead of a .csv path. You can then 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. The example reads the result on an ADC pin.

The netlist file is rc.cir. The circuit is 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. Firmware reads ADC1 channel 0 every 500 µs. The capacitor charges toward 3.3 V. The charge curve crosses 1.5 V to 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. The oscilloscope shows the v_out charge curve on another row. The two rows use the same time axis.

Limits

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

Use adapter: external_process with the ngspice wrapper for a nonlinear device. Use that adapter for a vendor SPICE model. Use that adapter for a sweep. The ngspice wrapper runs on native builds only. The browser playground supports the analog adapter alone.

Andrii Shylenko
Andrii Shylenko

Founder, LabWired.