An analog simulator bridge for cocotb — open-source mixed-signal co-simulation
cocotbext-ams synchronizes cocotb’s digital simulation with an analog SPICE simulator via shared library APIs. It supports ngspice (default) and Xyce (Sandia’s open-source parallel SPICE), allowing you to co-simulate SPICE netlists alongside Verilog/VHDL testbenches using entirely open-source tools.
cocotb testbench (Python async)
|
v
MixedSignalBridge (orchestrator)
|-- reads Verilog signals via cocotb handles
|-- converts digital <-> analog (threshold-based)
'-- drives simulator via SimulatorInterface
| |
v v
HDL Simulator libngspice.so or libxycecinterface.so
(Icarus/Verilator) (ngspice 45+) (Xyce 7+)
The bridge uses event-driven synchronization: instead of exchanging signals at a fixed interval, it reacts to actual signal changes:
ValueChange monitor coroutines update voltage source
values the instant a Verilog signal changes — no sync overhead needed.Supported simulators:
xyce_simulateUntil())Signal bridging:
Waveform output:
Pass analog_vcd="file.vcd" to record SPICE node voltages as real-typed VCD
signals alongside digitized outputs as wire signals. Load this VCD with the
HDL simulator’s digital VCD to see everything together:

SAR ADC binary search: RC-filtered DAC output converging to vin (top), comparator output q (second), SAR value register (third), and done signal (bottom). See the full tutorial for details.
libngspice.so / libngspice.dylib) or
Xyce shared library (libxycecinterface.so)Ubuntu/Debian:
sudo apt-get install libngspice0-dev
Fedora/RHEL:
sudo dnf install libngspice-devel
macOS (Homebrew):
brew install ngspice
Conda (any platform):
conda install -c conda-forge ngspice
If your distribution doesn’t package the shared library, or you need a specific version:
cd ngspice
mkdir build && cd build
../configure --with-ngshared --enable-xspice --enable-cider
make -j$(nproc) && sudo make install
Xyce is an open-source parallel SPICE simulator from Sandia National Laboratories.
To use Xyce with cocotbext-ams, you need the shared library build
(libxycecinterface.so).
See the Xyce installation guide for build instructions. When building, enable the shared library:
cmake -DBUILD_SHARED_LIBS=ON ...
If the library is installed in a non-standard location, pass the path explicitly:
bridge = MixedSignalBridge(dut, blocks, simulator_lib="/path/to/libxycecinterface.so")
pip install cocotbext-ams
Or install from GitHub for the latest development version:
pip install git+https://github.com/VLSIDA/cocotbext-ams.git
For local development:
git clone https://github.com/VLSIDA/cocotbext-ams.git
pip install -e cocotbext-ams
* my_block.sp
.subckt my_block clk data_in data_out vdd vss
* ... your analog circuit ...
.ends my_block
module my_block(
input wire clk,
input wire data_in,
output reg data_out, // reg so bridge can Force
input wire ain // analog-only, stays X
);
initial data_out = 1'bx;
endmodule
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
from cocotbext.ams import AnalogBlock, DigitalPin, MixedSignalBridge
@cocotb.test()
async def test_my_block(dut):
block = AnalogBlock(
name="dut",
spice_file="my_block.sp",
subcircuit="my_block",
digital_pins={
"clk": DigitalPin("input"),
"data_in": DigitalPin("input"),
"data_out": DigitalPin("output"),
},
analog_inputs={"ain": 0.9},
vdd=1.8,
)
bridge = MixedSignalBridge(dut, [block], max_sync_interval_ns=10)
await bridge.start(duration_ns=50_000, analog_vcd="analog.vcd")
cocotb.start_soon(Clock(dut.clk, 100, "ns").start())
await Timer(1, "us")
# Read result
result = int(dut.data_out.value)
# Change analog input at runtime
bridge.set_analog_input("dut", "ain", 1.2)
await bridge.stop()
The analog_vcd parameter writes a VCD file with real-typed signals at
full simulator resolution. Load it alongside the HDL simulator’s digital VCD
in Surfer, GTKWave, or any viewer that supports real-valued VCD signals to
see analog and digital waveforms together.
block = AnalogBlock(
name="dut",
spice_file="my_block.sp",
subcircuit="my_block",
digital_pins={...},
analog_inputs={"ain": 0.9},
vdd=1.8,
simulator="xyce", # use Xyce instead of ngspice
)
bridge = MixedSignalBridge(dut, [block],
simulator_lib="/path/to/libxycecinterface.so")
await bridge.start(duration_ns=50_000)
The bridge auto-generates a Xyce-compatible netlist (YDAC devices, .TRAN,
.PRINT TRAN) and drives the simulation via Xyce’s explicit stepping API.
DigitalPin(direction, width=1, vdd=1.8, vss=0.0, threshold=None, hysteresis=0.0)Configures how a pin is bridged between digital and analog domains.
| Parameter | Description |
|---|---|
direction |
"input" (digital drives analog) or "output" (analog drives digital) |
width |
Bit width. Multi-bit pins get one SPICE source/probe per bit. |
vdd |
Logic-high voltage level |
vss |
Logic-low voltage level |
threshold |
Voltage threshold for A/D conversion. Default: (vdd + vss) / 2 |
hysteresis |
Total hysteresis band. When > 0, rising transitions require >= threshold + hysteresis/2 and falling transitions require < threshold - hysteresis/2. Prevents rapid oscillation around the threshold. Default: 0.0 |
AnalogBlock(name, spice_file, subcircuit, ...)Describes an analog block (SPICE subcircuit) to be co-simulated.
| Parameter | Description |
|---|---|
name |
Instance name matching the Verilog stub hierarchy |
spice_file |
Path to the SPICE netlist |
subcircuit |
Name of the .subckt |
digital_pins |
dict[str, DigitalPin] — pin name to configuration |
analog_inputs |
dict[str, float] — analog input name to initial voltage (changeable at runtime) |
vdd |
Supply voltage (default 1.8) |
vss |
Ground voltage (default 0.0) |
tran_step |
SPICE transient step size (default "0.1n") |
extra_lines |
Additional SPICE lines for the generated netlist (e.g., .include directives for PDK libraries) |
simulator |
"ngspice" (default) or "xyce" |
MixedSignalBridge(dut, analog_blocks, max_sync_interval_ns=100.0, simulator_lib=None)The main orchestrator.
| Parameter | Description |
|---|---|
dut |
cocotb DUT handle |
analog_blocks |
List of AnalogBlock descriptions |
max_sync_interval_ns |
Maximum time between sync points in nanoseconds (default 100.0) |
simulator_lib |
Path to the simulator shared library (auto-detected if None) |
| Method | Description |
|---|---|
await start(duration_ns, analog_vcd=None, vcd_nodes=None) |
Load circuit, start co-simulation. Pass analog_vcd="file.vcd" to record analog waveforms. vcd_nodes adds extra SPICE nodes beyond the auto-included output pins. |
await stop() |
Halt simulation, release forced signals |
set_analog_input(block, name, voltage) |
Change an analog input voltage at runtime |
get_analog_voltage(block, node) |
Probe any SPICE node voltage |
Migration note: The old
ngspice_libparameter still works but emits aDeprecationWarning. Rename it tosimulator_lib.
Synchronization is primarily event-driven — threshold crossings on analog
outputs trigger immediate sync. The max_sync_interval_ns parameter sets a
ceiling that bounds time drift and ensures digital-side events are processed:
PWM DAC with SAR Controller — A complete walkthrough of a mixed-signal co-simulation: a hardware SAR controller binary-searches PWM duty cycles through an RC filter and sky130 latch comparator to find the voltage matching a reference. Covers both data paths, runtime analog control, VCD export, and waveform viewing.
examples/sar_adc/ — 10-bit SAR ADC with behavioral SPICE modelexamples/pll/ — Charge-pump PLL with digital PFDBoth ngspice and Xyce inherit from SimulatorInterface, which holds all
shared state (voltage source values, node voltages, crossing detection,
VCD writer) and implements common logic (_check_crossings(),
_write_vcd()). Subclasses implement the simulator-specific ctypes wrapper
and control flow.
The bridge uses cocotb’s @bridge / @resume mechanism for thread
synchronization. Both simulators run a blocking simulation in a
@bridge thread and periodically call a @resume function at sync points:
ngspice:
@bridge runs ngspice’s blocking tran command in a dedicated thread.GetVSRCData fires on every ngspice evaluation step, reading the
_vsrc_values dict (updated asynchronously by ValueChange monitors).SendData fires after each accepted timestep — the bridge checks all
output pin voltages against their thresholds (with hysteresis).GetSyncData fires at each internal timestep. If a crossing was detected
(or the fallback interval elapsed), it calls a @resume function that
blocks the ngspice thread and transfers control to the cocotb scheduler.Xyce:
@bridge runs an explicit stepping loop in a dedicated thread.xyce_updateTimeVoltagePairs(),
advance via xyce_simulateUntil(), read voltages via
xyce_obtainResponse(), check crossings.@resume function as ngspice.Common to both:
await Timer(...).@resume function returns, the simulator thread resumes.This is event-driven: sync only happens when analog outputs actually cross a digital threshold, or at the fallback ceiling interval.
The bridge auto-generates a wrapper SPICE deck around the user’s subcircuit, with simulator-specific syntax:
| Feature | ngspice | Xyce |
|---|---|---|
| Runtime sources | v_name node 0 dc 0 external |
YDAC v_name DAC node 0 |
| Output probing | .save v(node) |
.PRINT TRAN v(node) |
| Transient analysis | .tran step stop uic |
.TRAN step stop |
| End marker | .end |
.END |
Power supplies are standard DC sources in both formats.
ngspice may report vector names with plot prefixes (e.g., tran1.v(d0)) or
wrapped in v(). The bridge normalizes lookups so you can query by bare node
name (d0), v(d0), or the full qualified name. Xyce stores both the
expression form and bare name.
FileNotFoundError: Cannot find libngspice shared library.
Install the ngspice or Xyce shared library for your platform (see Prerequisites above). If the library is installed in a non-standard location, pass the path explicitly:
# ngspice
bridge = MixedSignalBridge(dut, blocks, simulator_lib="/path/to/libngspice.so")
# Xyce
bridge = MixedSignalBridge(dut, blocks, simulator_lib="/path/to/libxycecinterface.so")
AttributeError: Cannot find signal 'q' on block 'dut.u_analog'
The block name must match your Verilog hierarchy. If the SPICE stub module
pwm_dac is instantiated as u_analog inside a dut wrapper, use
name="dut.u_analog". The pin name must match a port on the stub module.
If the RC filter output looks like a sawtooth instead of a smooth DC level, the PWM period is too close to the RC time constant. The PWM period should be at least 10-40x smaller than τ:
If the simulation appears to hang, check:
ValueChange support: Some simulators don’t support
ValueChange. The bridge logs a warning and falls back to sync-point
updates. Check your cocotb log output.max_sync_interval_ns values
(< 1ns) can make the simulation extremely slow. Start with 50-100ns.ngspice: stderr warnings (ngspice) or
Xyce error messages.Enable debug logging to see threshold crossings and sync points:
import logging
logging.getLogger("cocotbext.ams").setLevel(logging.DEBUG)
This shows each threshold crossing event with timestamp, pin name, old/new values, and voltages.