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.
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
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.
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:
| Variable | Effect |
|---|---|
COMPILE_ARGS, EXTRA_ARGS | Extra options for ryusim compile. See Compile options. |
SIM_ARGS, COCOTB_PLUSARGS | Extra arguments and plusargs for the simulation run. |
VERILOG_INCLUDE_DIRS | Each directory becomes -I <dir> on the compile line. |
COCOTB_HDL_TIMEUNIT, COCOTB_HDL_TIMEPRECISION | Passed as --timescale <unit>/<precision>. cocotb's defaults give 1ns/1ps. A `timescale directive in the source still wins. |
WAVES=1 | Adds --trace-vcd. See Waveforms. |
RYUSIM_BIN_DIR | Directory 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.
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.
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 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.
Any ryusim compile option can be added to the compile line.
The CLI reference lists them. Where each flow
takes them:
| Flow | Where compile options go |
|---|---|
| Makefile | COMPILE_ARGS or EXTRA_ARGS |
| Python Runner | runner.build(build_args=[...]) |
| pytest plugin | hdl.build_args in the fixture, or --cocotb-build-args |
| cocotb-test | run(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.
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.
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.
| Flow | Default for RyuSim |
|---|---|
| Makefile | 1, unless set in the environment or the Makefile |
| Python Runner and pytest plugin | 1, unless set in the environment or in the runner's extra_env |
| cocotb-test | Not 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.
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.