Skip to main content

cocotb-vivado

PyPI version lint

A Python/cocotb interface to the Xilinx Vivado Simulator.

Based on cocotb-stub-sim. The Python runner and the value-change manager are derived from vicoco by Kiran Vuksanaj.


Project status

Active development. The Python runner experience is being rebuilt. See CHANGELOG.md for what's landed and MIGRATION.md for breaking changes.

Known limitations:

  • Only top-level ports are accessible (XSI limitation).
  • RisingEdge / FallingEdge / Edge only see changes after Python advances time (Timer / cocotb.clock.Clock). Verilog # delays are not visible to those triggers, so awaiting an edge on a DUT-driven clock with no concurrent Python-driven time advance will not progress.
  • Waveform dump via $dumpfile / $dumpvars is Verilog-only and cannot hook into a VHDL top; with a VHDL top, only the Vivado WDB output (wave_format="wdb") is available.
  • Direct access to the XSI interface via cocotb_vivado.xsi is available for low-level tooling.

Platform support

Linux only. The loader is Linux-specific — cocotb-vivado dlopens the XSim snapshot (xsimk.so) via ctypes and derives an LD_LIBRARY_PATH from Vivado's lib/lnx64.o. XSI itself is not Linux-specific (Vivado's XSim and the XSI C API ship on Windows too), so a Windows port is plausible, but it is untested.

Tested Vivado versions

  • Vivado 2023.1 — minimum supported.
  • Newer versions are expected to work but verify locally.

Installation

pip install cocotb-vivado

Quickstart

from pathlib import Path

import cocotb
from cocotb.triggers import Timer

from cocotb_vivado.runner import get_runner


@cocotb.test()
async def simple_test(dut):
    dut.clk.value = 0
    await Timer(10, units="ns")
    dut.clk.value = 1
    await Timer(10, units="ns")
    assert dut.out.value == 1


def test_simple():
    runner = get_runner("vivado")
    runner.build(
        sources=[Path(__file__).parent / "tb.v"],
        hdl_toplevel="tb",
        always=True,
        timescale=("1ns", "1ps"),
    )
    runner.test(
        hdl_toplevel="tb",
        test_module="test_simple",
        hdl_toplevel_lang="verilog",
        testcase="simple_test",
    )

See examples/ for runnable projects and the tests/ directory for more scenarios.

Before running:

source /path/to/Vivado/<version>/settings64.sh
pytest -s tests/

xelab / xvlog / xvhdl must be on PATH and LD_LIBRARY_PATH must be set (or XILINX_VIVADO set as a courtesy fallback so the runner can synthesize one for the test subprocess).

How the GPI shim is installed

Importing cocotb_vivado replaces cocotb.simulator in sys.modules with the in-process XSI stub, and cocotb caches the simulator handle at its own import time — so the stub has to be installed before cocotb is imported.

This is handled for you and imposes no rule on your testbench. runner.test() spawns python -m cocotb_vivado, which installs the stub before importing cocotb; cocotb then loads your test module. Import cocotb and cocotb_vivado in whatever order your formatter prefers:

import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer

from cocotb_vivado.runner import get_runner

Waveform output

The wave_format kwarg on runner.build() / runner.test() selects the dump format:

runner.build(..., wave_format="vcd")      # Verilog $dumpfile/$dumpvars
runner.build(..., wave_format="fst")      # VCD post-processed by vcd2fst
runner.build(..., wave_format="wdb")      # Vivado native, viewable in xsim --gui
runner.build(...)                          # default: no waves

All formats land in {build_dir}/{hdl_toplevel}.{ext} for symmetry.

Vivado-managed sources (IP / BD / XPR)

Vivado inputs are passed to the runner via source objects from cocotb_vivado.vivado, alongside plain HDL paths in runner.build(sources=[...]). Pick the class by input file format:

Input Class Vivado mechanism
.xci VivadoIp add_files; export_ip_user_files
.bd VivadoBd make_wrapper; launch_simulation -scripts_only
.xpr VivadoProject open_project; launch_simulation -scripts_only
.tcl / pre-extracted VivadoExportedSim runs your TCL

A source object sits in the same sources= list as plain HDL. Here a blk_mem_gen IP is instantiated by a hand-written wrapper, and the wrapper is the toplevel (tests/test_bram.py):

from pathlib import Path

from cocotb_vivado.runner import get_runner
from cocotb_vivado.vivado import VivadoIp

here = Path(__file__).resolve().parent
runner = get_runner("vivado")
runner.build(
    sources=[
        VivadoIp(
            "ip/blk_mem_kilobyte/blk_mem_kilobyte.xci",
            builder_tcl=here / "ip" / "blk_mem_kilobyte" / "regen.tcl",
            part_num="xczu7eg-ffvc1156-2-e",
        ),
        here / "bram_wrap.sv",
    ],
    hdl_toplevel="bram_wrap",
)

A block design or a project defines its own toplevel instead, and reports the names to use (tests/test_bd_axi.py):

from cocotb_vivado.vivado import VivadoBd

bd = VivadoBd(
    "ip/bd_axi/bd_axi.bd",
    builder_tcl=here / "ip" / "bd_axi" / "regen.tcl",
    part_num="xczu7eg-ffvc1156-2-e",
)
runner.build(
    sources=[bd],
    hdl_toplevel=bd.top,        # "bd_axi_wrapper" — Vivado's wrapper name
    hdl_library=bd.library,     # "xil_defaultlib"
)

Note the path convention: a relative .xci / .bd path resolves under the build directory — that is where builder_tcl deposits it — while builder_tcl itself is an input you already have, so pass it absolute (relative resolves against the current working directory).

One design-defining source per build. hdl_library is a single setting for the whole build — every plain source is compiled into it and the toplevel is elaborated from it — so a VivadoBd and your own RTL necessarily share one library. VivadoBd, VivadoProject and VivadoExportedSim each extract into xil_defaultlib, so combining two of them in one build merges two independently generated structural HDL sets and risks name collisions. Use at most one of those three, plus any number of VivadoIp and plain HDL sources.

Each source self-orchestrates its Vivado batch invocation (with mtime-based caching) and returns a parsed view of the resulting Vivado-emitted xsim/ directory: per-language .prj files (consumed via xvlog -prj / xvhdl -prj) and a sibling *.sh script whose xelab command line contributes the -L library set and any <lib>.glbl modules. VivadoBd, VivadoProject and VivadoExportedSim go through launch_simulation -scripts_only -absolute_path; VivadoIp uses export_ip_user_files.

Block designs and interface flattening. XSI exposes only top-level scalar ports — there is no hierarchical or interface access. A block design whose top is built from create_bd_intf_port (AXI/AXIS/BRAM) is therefore not directly drivable. VivadoBd defaults to wrapper=True, generating Vivado's RTL wrapper (make_wrapper -top) that flattens each interface bundle into discrete scalar ports — the form cocotbext-axi binds to. bd.top reports the resulting <bd>_wrapper name so you need not hardcode it. For a BD whose top is all plain signals (create_bd_port), pass wrapper=False to elaborate the BD module directly (bd.top is then the bare <bd>).

part_num is required on VivadoIp and VivadoBd (the IP/BD generator needs a set_part target). VivadoProject reads the part from the XPR and accepts an optional part_num to retarget in-memory for simulation. VivadoExportedSim's TCL controls its own part. It falls back to the COCOTB_DEFAULT_PART_NUM environment variable, and cocotb_vivado.vivado.discover_default_part() is an opt-in helper that queries Vivado once and caches the answer. Pure-RTL builds without any VivadoSource instance never touch the vivado binary.

The .bd can be committed alongside the test, or regenerated on first build by passing builder_tcl (a script that constructs it and save_bd_designs) — the latter keeps the fixture Vivado-version- agnostic. To use a block design as a sub-component under your own top-level RTL, list the VivadoBd alongside your HDL and name your module as the top: sources=[VivadoBd("x.bd"), "my_tb.sv"], hdl_toplevel="my_tb".

See tests/test_bd_axi.py for a VivadoBd example that flattens an AXI-Lite + AXIS block design, and tests/test_fw.py for the equivalent via a full VivadoProject.

cocotb extensions

Extensions like cocotbext-axi work as long as the DUT is clocked by a Python-driven Clock (XSI exposes no native GPI clock). AXI bus accesses are routed through the cocotb scheduler the extension expects.

Contributing

See CONTRIBUTING.md.

Acknowledgment

We'd like to thank our employer, Dectris for supporting this work.

Metadata

Release files for cocotb-vivado 0.0.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cocotb-vivado 0.0.6
File Size Uploaded
cocotb_vivado-0.0.6.tar.gz 44.9 kB Details

Release files / cocotb_vivado-0.0.6.tar.gz

Download URL cocotb_vivado-0.0.6.tar.gz
Size 44.9 kB
Tags Source
SHA-256 checksum
How to use checksums
bd87b82fab567651cfe98e8fb5b0b38477c6d0cca7749aed017178d44bceeb1a
BLAKE2b-256 checksum
How to use checksums
a518641d8d46d87f7f590ee10664d4f373a285d045c6e6dc45200c42ce63510a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

0.1.1

2 release files

0.1.0

2 release files

This release

0.0.6 This release

1 release file

0.0.5

1 release file

0.0.4

1 release file

0.0.3

1 release file

0.0.2

1 release file

0.0.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page