cocotb

Run cocotb testbenches on RyuSim through the Makefile flow, the Python Runner, the pytest plugin or cocotb-test.

cocotb drives a design from Python through VPI. RyuSim support for cocotb comes from the cocotbext-ryusim package, which adds the simulator name ryusim to cocotb's Makefile flow, its Python Runner and its pytest plugin. The package needs cocotb 2.1 (>=2.1,<2.2) and Python 3.9 or later. This page runs one design and one test through each flow, then covers the settings they share.

You need ryusim on your PATH and the Clang toolchain it compiles with. Getting Started installs both.

Install

Create a virtual environment and install the package into it. pip pulls in a cocotb release in the supported range, and cocotb pulls in pytest. The pytest>=8.4 constraint is for the pytest plugin. On Debian and Ubuntu, python3 -m venv needs the python3-venv package.

python3 -m venv .venv source .venv/bin/activate pip install cocotbext-ryusim "pytest>=8.4"

Check what was installed:

cocotbext-ryusim-config --version cocotb-config --version
0.1.0 2.1.0

The example design and test

Every flow below runs the same two files. Save the design as counter.sv:

module counter #(parameter int WIDTH = 8) ( input logic clk, input logic rst, input logic en, output logic [WIDTH-1:0] count ); always_ff @(posedge clk) if (rst) count <= '0; else if (en) count <= count + 1'b1; endmodule

Save the test as test_counter.py. It holds reset for two clock cycles, enables the counter for five, and checks the count after the fifth edge has settled:

import cocotb from cocotb.clock import Clock from cocotb.triggers import ClockCycles, ReadOnly @cocotb.test() async def counts_when_enabled(dut): cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) dut.rst.value = 1 dut.en.value = 0 await ClockCycles(dut.clk, 2) dut.rst.value = 0 dut.en.value = 1 await ClockCycles(dut.clk, 5) await ReadOnly() assert dut.count.value == 5

Each flow builds into its own directory. A flow skips compilation when the simulation library in its build directory is newer than the sources, so two flows that share a directory would run the first flow's build.

Makefile flow

Include the package's Makefile.sim in place of cocotb's. With SIM=ryusim it compiles and runs the design with RyuSim; with any other SIM it includes cocotb's own Makefile.sim unchanged, so one Makefile serves every simulator. Save this as Makefile:

SIM ?= ryusim TOPLEVEL_LANG = verilog VERILOG_SOURCES = $(PWD)/counter.sv COCOTB_TOPLEVEL = counter COCOTB_TEST_MODULES = test_counter include $(shell cocotbext-ryusim-config --makefiles)/Makefile.sim

Run it:

make

The build goes to sim_build/. The compile line and the end of the output look like this:

ryusim compile --top counter -o sim_build --timescale 1ns/1ps /home/user/counter/counter.sv ... RyuSim - Simulation of counter_W8 ... 0.00ns INFO cocotb.regression running test_counter.counts_when_enabled (1/1) 60.00ns INFO cocotb.regression test_counter.counts_when_enabled passed ... ** TESTS=1 PASS=1 FAIL=0 SKIP=0 60.00 0.00 68845.95 **

make exits non-zero when a test fails. The Makefile flow reads these RyuSim-specific variables:

VariableEffect
COMPILE_ARGS, EXTRA_ARGSExtra options for ryusim compile. See Compile options.
SIM_ARGS, COCOTB_PLUSARGSExtra arguments and plusargs for the simulation run.
VERILOG_INCLUDE_DIRSEach directory becomes -I <dir> on the compile line.
COCOTB_HDL_TIMEUNIT, COCOTB_HDL_TIMEPRECISIONPassed as --timescale <unit>/<precision>. cocotb's defaults give 1ns/1ps. A `timescale directive in the source still wins.
WAVES=1Adds --trace-vcd. See Waveforms.
RYUSIM_BIN_DIRDirectory that holds the ryusim to use. Without it, ryusim is looked up on PATH.

RyuSim simulates Verilog and SystemVerilog only. With TOPLEVEL_LANG=vhdl or any VHDL_SOURCES, the Makefile prints Skipping simulation as only Verilog is supported on simulator=ryusim and runs nothing.

Python Runner

cocotb's Python Runner (cocotb_tools.runner) finds a simulator by name. Importing cocotbext.ryusim adds ryusim to the names it knows. Save this as run_counter.py:

import sys import cocotbext.ryusim # registers the "ryusim" runner from cocotb_tools.check_results import get_results from cocotb_tools.runner import get_runner runner = get_runner("ryusim") runner.build( sources=["counter.sv"], hdl_toplevel="counter", build_dir="sim_build_runner", timescale=("1ns", "1ps"), ) results = runner.test(hdl_toplevel="counter", test_module="test_counter") num_tests, num_failed = get_results(results) sys.exit(1 if num_failed else 0)
python3 run_counter.py

Outside pytest, runner.test() returns the path of the results file and does not raise when a test fails; the last two lines turn a failure into a non-zero exit status. Without the cocotbext.ryusim import, get_runner fails:

ValueError: Simulator 'ryusim' is not in supported list: icarus, questa, questa-qisqrun, ghdl, riviera, activehdl, verilator, xcelium, nvc, vcs, dsim

The RyuSim runner takes these build() arguments: hdl_toplevel (required), sources or verilog_sources, includes (-I), defines (-D), parameters (-G), timescale (--timescale, only when given), build_args (passed to ryusim compile as they are), waves and always. test() does not support pre_cmd. The runner calls the ryusim it finds on PATH; it does not read RYUSIM_BIN_DIR.

pytest plugin

cocotb 2.1 ships a pytest plugin, cocotb_tools._pytest.plugin, that builds the design and runs the cocotb tests from a pytest test. cocotb's documentation marks it as under active development. The plugin needs pytest 8.4 or newer: with pytest 8.3 it fails to load with cannot import name 'TerminalReporter' from 'pytest', and cocotb 2.1.0's package metadata does not state that minimum, so an environment that already has pytest 8.3 keeps it unless you ask for 8.4, as the install command does. cocotbext-ryusim registers itself with pytest when it is installed and adds ryusim to the plugin's --cocotb-simulator choices. Save this as test_counter_pytest.py:

from pathlib import Path import pytest from cocotb_tools._pytest.hdl import HDL HERE = Path(__file__).parent @pytest.fixture(name="counter") def counter_fixture(hdl: HDL) -> HDL: hdl.toplevel = "counter" hdl.sources = (HERE / "counter.sv",) hdl.build() return hdl @pytest.mark.cocotb_runner("test_counter") def test_counter(counter: HDL) -> None: counter.test()

Load cocotb's plugin with -p and select RyuSim:

python3 -m pytest -p cocotb_tools._pytest.plugin --cocotb-simulator ryusim test_counter_pytest.py
collected 1 item test_counter_pytest.py . [100%] ======================= 1 passed cocotb runner in 1.85s ========================

The build goes to sim_build/test_counter_pytest/test_counter/, one directory per pytest node. The argument to cocotb_runner names the module that holds the cocotb tests; without it, the plugin loads the pytest file itself. The default --cocotb-simulator auto picks the first simulator it finds on PATH, and RyuSim comes last in that search, so pass ryusim explicitly when another simulator is installed. To load the plugin without -p, see cocotb's pytest plugin documentation.

cocotb-test

cocotb-test runs cocotb from a pytest test with cocotb_test.simulator.run(). The upstream release has no RyuSim support. The Seiraiyu/cocotb-test tag v0.2.7-ryusim adds it. Install it into the same environment:

pip install "cocotb-test @ git+https://github.com/Seiraiyu/cocotb-test.git@v0.2.7-ryusim"

Save this as test_counter_cocotb_test.py:

from cocotb_test.simulator import run def test_counter(): run( simulator="ryusim", verilog_sources=["counter.sv"], toplevel="counter", module="test_counter", sim_build="sim_build_cocotb_test", timescale="1ns/1ps", )
python3 -m pytest test_counter_cocotb_test.py

A SIM environment variable overrides the simulator argument. Extra compile options go in compile_args; waves=True or WAVES=1 adds --trace-vcd. cocotb-test does not set COCOTB_TRUST_INERTIAL_WRITES; see below.

Compile options

Any ryusim compile option can be added to the compile line. The CLI reference lists them. Where each flow takes them:

FlowWhere compile options go
MakefileCOMPILE_ARGS or EXTRA_ARGS
Python Runnerrunner.build(build_args=[...])
pytest pluginhdl.build_args in the fixture, or --cocotb-build-args
cocotb-testrun(compile_args=[...])

In the Makefile flow, set COMPILE_ARGS in the Makefile or in the environment, not as a make argument. The package's makefile appends --timescale and, with WAVES=1, --trace-vcd to COMPILE_ARGS. A variable given on the make command line overrides those appends, so make COMPILE_ARGS=--top-only compiles without either flag.

COMPILE_ARGS += --top-only include $(shell cocotbext-ryusim-config --makefiles)/Makefile.sim

--top-only matters when the source list also holds a module that instantiates the cocotb top level, such as a SystemVerilog testbench or a wrapper. With --top alone, RyuSim still elaborates that other module and generates and compiles C++ for it, although the simulation starts at the top level. With --top-only, only the hierarchy under the top level is elaborated and compiled. See the CLI reference for the exact rule.

Waveforms

WAVES=1 in the Makefile flow and waves=True in the Runner and cocotb-test add --trace-vcd to the compile line. The simulation writes trace.vcd in the directory it runs in: the Makefile's directory for the Makefile flow, the build directory for the Runner. The option is compiled into the model, and a flow does not rebuild when only WAVES changes, so remove the build directory first:

rm -rf sim_build make WAVES=1 ls trace.vcd

For FST output, other file names and trace depth, pass the --trace-* options through the compile options above. The Waveforms page covers them.

COCOTB_TRUST_INERTIAL_WRITES

This cocotb environment variable decides how a Python write such as dut.en.value = 1 reaches the simulator. When it is 1, cocotb passes each write straight to the simulator through VPI, and Clock uses cocotb's C++ clock. When it is 0 or unset, cocotb queues writes and applies them at the next ReadWrite phase, and Clock runs as a Python coroutine.

FlowDefault for RyuSim
Makefile1, unless set in the environment or the Makefile
Python Runner and pytest plugin1, unless set in the environment or in the runner's extra_env
cocotb-testNot set, so cocotb queues writes. Export COCOTB_TRUST_INERTIAL_WRITES=1 to match the other flows.

The example test passes with either setting. A test that depends on exactly when a write lands can behave differently between the two, so use the same setting in every flow that runs it.

How a run starts

ryusim compile builds the model as lib<top>.so in the build directory. The flow then runs that library directly with --vpi-load pointing at cocotb's VPI library, libcocotbvpi_verilator.so. RyuSim uses cocotb's stock Verilator VPI library; no RyuSim-specific cocotb library exists. The Makefile and Runner flows add cocotb's library directory to LD_LIBRARY_PATH and set PYGPI_PYTHON_BIN, so the simulation embeds the same Python that started the flow.