Cocotb Simulation Quickstart
A practical guide to writing and running cocotb tests using the RouteRTL SDK — for both VHDL and SystemVerilog projects.
Install via
pip install routertl. Your project layout is: directory below does not apply to you. The pip package installs everything globally — justfrom routertl.sim import Tb, run_simulationand go. Your project layout is simply:my_project/ <- pip-installed layout ├── project.yml ├── src/ └── sim/cocotb/tests/The submodule layout below is for advanced users who embed the SDK as a Git submodule.
1. Prerequisites
| Requirement | Minimum Version | Notes |
|---|---|---|
| Python | 3.10+ | With pip |
| cocotb | 2.0+ | pip install cocotb |
| Verilator | 5.x | For Verilog/SV projects |
| GHDL or NVC | Latest | For VHDL projects (NVC recommended) |
| RouteRTL SDK | Latest | pip install routertl |
Quick install:
pip install cocotb cocotb-bus
2. Project Structure
The SDK expects this layout in your project:
my_project/ ├── project.yml ← SDK discovers PROJECT_ROOT from here ├── src/ │ └── my_ip/ │ ├── includes/ ← .vh / .svh headers │ └── src/ ← .sv / .vhd source files └── sim/ └── cocotb/ ├── tests/ ← Your test files go here │ ├── __init__.py ← Required for dotted module imports │ └── ip/ │ ├── __init__.py │ └── test_my_ip.py ├── waves/ ← Generated waveforms (auto-created) └── logs/ ← Simulation logs (auto-created)
Key: The SDK walks up from the test file's directory, looking for
project.yml, to auto-detectPROJECT_ROOT. No environment variables needed.
3. Minimal Working Example
Here is the simplest possible test file for a SystemVerilog IP:
"""Smoke test for my_ip module."""
import os
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, ClockCycles
@cocotb.test()
async def test_reset(dut):
"""Verify the module resets cleanly."""
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
dut.rst_n.value = 0
await ClockCycles(dut.clk, 10)
dut.rst_n.value = 1
await ClockCycles(dut.clk, 5)
assert dut.ready.value == 1, "Expected ready=1 after reset"
dut._log.info("✅ Reset smoke test PASSED")
# ============================================================
# Runner: executed when this file is run as a standalone script
# ============================================================
if __name__ == "__main__":
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(script_dir, "../../../../"))
from routertl.sim.cocotb.engine.simulation import run_simulation
ip_dir = os.path.join(project_root, "src/my_ip")
run_simulation(
top_level="my_ip_top", # Top-level module name
module="tests.ip.test_my_ip", # Dotted Python module path
top_level_lang="verilog", # "verilog" or "vhdl"
hdl_sources=[
os.path.join(ip_dir, "includes/defines.vh"),
os.path.join(ip_dir, "src/my_ip_pkg.sv"),
os.path.join(ip_dir, "src/my_ip_top.sv"),
],
custom_libraries={}, # Empty = no VHDL libs needed
)
Run it:
cd my_project
python sim/cocotb/tests/ip/test_my_ip.py
4. API Reference: run_simulation()
Import Path
# From a CLIENT PROJECT (via submodule):
from routertl.sim.cocotb.engine.simulation import run_simulation
# From a pip-installed SDK (recommended for new projects):
from routertl.sim import run_simulation
Both paths resolve to the same function.
from routertl.sim import run_simulationandfrom routertl.sim.cocotb.engine.simulation import run_simulationare identical — use whichever is shortest.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
top_level | str | required | Top-level entity/module name |
module | str | required | Python test module (dotted path, e.g. tests.ip.test_foo) |
top_level_lang | str | "vhdl" | "vhdl" or "verilog" |
generics | dict | None | VHDL generics / Verilog parameters, e.g. {"WIDTH": 32} |
hdl_sources | list[str] | None | Additional HDL source files (absolute or relative to project root) |
custom_libraries | dict | None | Custom VHDL library map: {"lib": ["file.vhd"]}. Set to {} to skip default libs |
simulator | str | None | Force simulator: "ghdl", "nvc", "verilator", or "icarus". Falls back to $SIM env var |
extra_env | dict | None | Extra environment variables for the simulator subprocess |
waves | bool | True | Generate waveform files (VCD/GHW/FST) |
verbose | bool | None | Detailed logging. Auto-reads $COCOTB_VERBOSE if not set |
build_args | list[str] | None | Extra compiler flags, e.g. ["-Wno-WIDTHEXPAND"] for Verilator |
includes | list[str] | None | Verilog include dirs (-I). Usually auto-derived from .vh/.svh files |
strict_warnings | bool | False | If False, Verilator auto-injects -Wno-fatal |
seed | int | None | Random seed for reproducibility (COCOTB_RANDOM_SEED) |
test_args | list[str] | None | Extra args forwarded to the simulator's test/run phase (e.g. ["-t", "1ps"] for Questa) |
Return & Exceptions
- Returns:
None(test results printed to stdout by cocotb) - Raises:
ValueErrorfor mixed VHDL+Verilog sources,RuntimeErrorfor compilation/simulation failures
5. Auto-Magic Features
The SDK does several things automatically so you don't have to:
5.1 PROJECT_ROOT Auto-Detection
The SDK walks up from os.getcwd() looking for project.yml. Once found, that directory becomes PROJECT_ROOT. No need to set CLIENT_PROJECT_ROOT manually.
5.2 Source Ordering
When you pass hdl_sources, the SDK auto-sorts them:
- Headers (
.vh,.svh) — compiled first - Packages (
*_pkg.sv,*_pkg.vhd) — compiled second - Modules (everything else) — compiled last
You can list sources in any order; the SDK will fix the compilation order.
5.3 Include Path Derivation
For every .vh or .svh file in hdl_sources, the SDK adds its parent directory to the include path (-I flag). You usually don't need to set includes manually.
5.4 sys.path Injection
The SDK adds your project's cocotb root to sys.path so that dotted module
names like tests.ip.test_foo resolve correctly. If your project uses a
non-standard layout (e.g. hw/sim/cocotb/tests), set simulation.test_dir
in project.yml — the SDK strips the trailing /tests segment to derive
the cocotb root automatically:
# project.yml
simulation:
test_dir: hw/sim/cocotb/tests # SDK injects hw/sim/cocotb/ into sys.path
If test_dir is not set, the SDK falls back to PROJECT_ROOT/sim/cocotb/.
5.5 Build Artifact Placement
Build artifacts go to sim/work/<simulator>/ inside your project — never inside the SDK submodule.
5.6 Default -Wno-fatal for Verilator
With strict_warnings=False (default), Verilator won't abort on benign width/style warnings. Set strict_warnings=True for stricter CI checks.
6. Simulator Selection
| Language | Recommended | Alternative | Commercial |
|---|---|---|---|
| VHDL | NVC | GHDL | Questa, Riviera-PRO |
| Verilog/SV | Verilator | Icarus Verilog | Questa, Riviera-PRO |
| Mixed VHDL+SV | Not supported | — | Questa, Riviera-PRO |
Zynq users: Most Zynq SoC designs can simulate the VHDL portions independently with NVC. For mixed-language simulation, use
routertl docker shell questawith a license server, or stub the SystemVerilog wrappers. See §14 (Docker) in the SDK User Guide for setup.
Setting the simulator:
# Option 1: In the run_simulation() call
run_simulation(..., simulator="verilator")
# Option 2: Environment variable
export SIM=verilator
python test.py
# Option 3: Auto-detect (default)
# SDK checks $SIM, then falls back to "ghdl"
7. VHDL Example
For VHDL projects, the SDK manages library compilation automatically:
if __name__ == "__main__":
from routertl.sim.cocotb.engine.simulation import run_simulation
run_simulation(
top_level="uart_sniffer",
module="tests.test_uart",
top_level_lang="vhdl",
generics={"BAUD_RATE": 115200, "DATA_BITS": 8},
simulator="nvc",
# No hdl_sources needed — DEFAULT_LIBRARIES handles SDK VHDL files
# For custom VHDL files, use:
# hdl_sources=["src/my_pkg.vhd", "src/my_entity.vhd"],
)
8. Common Patterns
8.1 Collecting Sources with glob
import glob
ip_dir = os.path.join(project_root, "src/ip/my_core")
sv_sources = sorted(glob.glob(os.path.join(ip_dir, "**/*.sv"), recursive=True))
vh_sources = sorted(glob.glob(os.path.join(ip_dir, "**/*.vh"), recursive=True))
run_simulation(
top_level="my_core",
module="tests.ip.test_my_core",
top_level_lang="verilog",
hdl_sources=vh_sources + sv_sources, # Order doesn't matter — SDK auto-sorts
custom_libraries={},
)
8.2 Overriding Generics/Parameters
run_simulation(
top_level="fifo_wrapper",
module="tests.test_fifo",
top_level_lang="verilog",
generics={"DEPTH": 512, "WIDTH": 64},
hdl_sources=[...],
custom_libraries={},
)
8.3 Extra Verilator Flags
run_simulation(
...,
build_args=["-Wno-WIDTHEXPAND", "-Wno-UNUSEDSIGNAL", "--trace"],
strict_warnings=False, # Already default, prevents -Wno-fatal override
)
8.4 Verbose Debugging
COCOTB_VERBOSE=1 python sim/cocotb/tests/ip/test_my_ip.py
This prints every SDK decision: path resolution, source ordering, include derivation, sys.path injection.
8.5 Mixed VHDL + SystemVerilog (Questa/Riviera)
Mixed-language simulations require a commercial simulator (Questa, Riviera-PRO, Xcelium, or Active-HDL). The SDK auto-detects mixed sources and switches to a compatible simulator if one is available.
The most common gotcha is the timescale mismatch between SystemVerilog
( `timescale 1ns/1ps) and VHDL (which has no timescale directive). Questa
requires -t 1ps to resolve this — otherwise it errors with
Fatal: (vsim-3693).
Use test_args to pass the timescale resolution:
run_simulation(
top_level="tb_mixed_pipe",
module="tests.integration.test_mixed_pipe",
top_level_lang="vhdl",
simulator="questa",
hdl_sources=vhdl_sources + sv_sources,
custom_libraries={"mylib": vhdl_sources},
test_args=["-t", "1ps"], # Required for mixed VHDL/SV in Questa
)
Always pass
test_args=["-t", "1ps"]when mixing VHDL and SystemVerilog with Questa or Riviera-PRO. Without it,vsimaborts with a timescale resolution error.
9. Protocol Drivers (BFMs)
The SDK provides native pure-Python Bus Functional Models (BFMs) for common protocols, allowing you to drive and respond to interfaces without compiled C/C++ dependencies. These are located in sim/cocotb/tb/drivers/.
9.1 AXI-Lite & Avalon-MM Mapped Interfaces
To drive a memory-mapped interface, instantiate a Master BFM and pass it the TbEnv and the signal prefix. For responders, use the Slave BFM which implements a sparse background memory model.
from routertl.sim.cocotb.tb.drivers.axi_lite import AxiLiteMaster, AxiLiteSlave
from routertl.sim.cocotb.tb.drivers.avalon_mm import AvalonMMMaster, AvalonMMSlave
@cocotb.test()
async def test_memory_mapped(dut):
env = TbEnv(dut)
# AXI-Lite Example
axi_master = AxiLiteMaster(env, prefix="S_AXI")
await axi_master.write(0x100, 0xDEADBEEF)
data, resp = await axi_master.read(0x100)
# Avalon-MM Example
avl_master = AvalonMMMaster(env, prefix="M_AVL")
# Slave automatically responds to reads/writes in the background
avl_slave = AvalonMMSlave(env, prefix="S_AVL")
cocotb.start_soon(avl_slave.start())
await avl_master.write(0x200, 0xCAFEBABE)
data, resp = await avl_master.read(0x200)
Drivers automatically handle protocol handshakes (e.g., AXI VALID/READY or Avalon waitrequest backpressure) and report throughput/latency statistics.
9.2 Serial Interfaces
The SDK also includes basic BFMs for serial protocols:
- SPI (API reference ·
sim/cocotb/tb/drivers/spi_master.py): Full-duplex SPI Master and Slave BFMs with CPOL/CPHA mode support.await spi.write([0x01, 0x02, 0x03]) - UART (API reference ·
sim/cocotb/tb/drivers/uart_master.py): UART Source (transmitter) and Sink (receiver) with configurable baud, data bits (5–9), parity, and error injection.await uart.write_bytes([0xDE, 0xAD, 0xBE, 0xEF])
10. Protocol Monitors
The SDK ships passive protocol monitors for AXI-Lite and Avalon-MM buses. They run as background coroutines, check specification compliance on every clock edge, and can assert zero violations at test end.
10.1 Attaching Monitors
Call env.attach_monitors() after reset, specifying which bus prefixes to watch:
env.attach_monitors(
axi_lite = {"S": {"rst_active_low": True}},
avalon_mm = {"M_AVL": {"rst_active_low": True}},
)
Each key is a signal prefix — the same string you pass to AxiLiteMaster(prefix=...)
or AvalonMMSlave(prefix=...). The kwarg dict is forwarded to the monitor constructor.
10.2 Asserting Zero Violations
At the end of every test call env.check_monitors():
@cocotb.test()
async def test_write_read(dut):
env, master, slave = await setup(dut)
await master.write(0x100, 0xDEADBEEF)
data, resp = await master.read(0x100)
assert data == 0xDEADBEEF
env.check_monitors() # raises AssertionError if any violation was detected
check_monitors() logs the full report for every monitor (pass or fail) and
aggregates all violations into a single AssertionError message if any are found.
10.3 What the Monitors Check
AXI-Lite (AxiLiteMonitor) — per AMBA IHI0022E:
| Check ID | Rule |
|---|---|
AXI_STAB_{AW,W,AR,B,R} | Data/ctrl stable while VALID=1 & READY=0 |
AXI_XZ_{channel} | No X/Z on signals during active transactions |
AXI_RST_{channel} | VALID must deassert during reset |
AXI_DEADLOCK_{channel} | VALID acknowledged within timeout_cycles |
Avalon-MM (AvalonMMMonitor) — per Intel Avalon §3.5:
| Check ID | Rule |
|---|---|
AVL_WR_HOLD | Write signals stable during waitrequest |
AVL_RD_HOLD | Read address stable during waitrequest |
AVL_RD_VALID | readdatavalid only after an outstanding read |
AVL_OVERLAP | read + write not both asserted |
AVL_RST | Control signals deassert during reset |
11. Troubleshooting
No module named 'tests.ip'
Cause: Missing __init__.py files in the test directory hierarchy.
Fix: Create __init__.py in every directory from sim/cocotb/tests/ down to your test file:
touch sim/cocotb/tests/__init__.py
touch sim/cocotb/tests/ip/__init__.py
ModuleNotFoundError: routertl
Cause: The SDK submodule isn't on Python's import path.
Fix: Ensure vendor/ is importable. The SDK auto-adds it if project.yml is found. If running from an unusual directory, set:
export PYTHONPATH=$PYTHONPATH:/path/to/my_project/vendor
Verilator: error: Package not found
Cause: Source files compiled in wrong order (packages after modules that import them).
Fix: Ensure package files are in your hdl_sources list. The SDK auto-sorts them, but they must be listed.
Build artifacts in unexpected location
Cause: PROJECT_ROOT not detected (no project.yml found).
Fix: Ensure project.yml exists at your project root. Run from the project root directory:
cd my_project
python sim/cocotb/tests/ip/test_my_ip.py
Questa: Fatal: (vsim-3693) minimum time resolution limit
Cause: Mixed VHDL + SystemVerilog simulation without explicit timescale resolution.
Fix: Pass test_args=["-t", "1ps"] to run_simulation(). See §8.5 for a full example.
Pre-commit hook fails with "Pre-compilation FAILED"
Cause: VHDL packages found on disk trigger VHDL pre-compilation, but no VHDL compiler is installed.
Fix: For pure SV projects, ensure hardware.language: systemverilog is set in project.yml, and that no stale .vhd files exist in src/.
12. Waveform Configuration
The SDK provides an independent waveform pipeline. Waveforms are generated by default — you don't need to pass any extra flags.
Output Location
Output directories are resolved in this priority:
run_simulation(waves_dir=...)kwarg (highest)simulation.waves.output_dirinproject.yml$ROUTERTL_WAVES_DIRenvironment variable- Default:
<PROJECT_ROOT>/sim/waves/
Waveforms are grouped per module: sim/waves/<module_name>/<top_level>.fst.
project.yml Configuration
Configure waveform behavior in the nested simulation.waves block:
simulation:
waves:
formats: [fst] # Available: "fst", "vcd", "ghw" (default: fst)
output_dir: sim/waves # Output directory (relative to project root)
enabled: true # Set to false to disable waveform generation
Disabling waveforms: Set
simulation.waves.enabled: falseinproject.yml, or passWAVES=0as an environment variable for CI overrides. The--viewCLI flag only controls whether the viewer opens after simulation — it does not affect waveform generation.
Verilator Trace Format
The SDK auto-injects --trace-fst into the Verilator build when fst
is listed in simulation.waves.formats. For VCD output, set formats: [vcd].
Do not manually pass raw Verilator flags like
VM_TRACE_FST=1or--trace-fstthrough your build environment. The SDK engine manages the trace pipeline natively.
13. Further Reading
- Simulator Backends & CI/CD — Licensing, CI strategy, custom backends
tools/bridge-gen/VERIFICATION_ARCHITECTURE.md— Architectural diagrams of an end-to-end parametric regression pipeline- cocotb documentation — Testbench writing guide