Welcome to Cocotb’s documentation!¶
Contents:
Introduction¶
What is cocotb?¶
Cocotb is a COroutine based COsimulation TestBench environment for verifying VHDL/Verilog RTL using Python.
Cocotb is completely free, open source (under the BSD License) and hosted on GitHub.
Cocotb requires a simulator to simulate the RTL. Simulators that have been tested and known to work with Cocotb:
Linux Platforms
Windows Platform
- Icarus Verilog
- Aldec Riviera-PRO
- Mentor Modelsim (DE and SE)
An older version of Cocotb can be used live in a web-browser using EDA Playground.
How is Cocotb different?¶
Cocotb encourages the same philosophy of design re-use and randomised testing as UVM, however is implemented in Python rather than SystemVerilog.
In Cocotb VHDL/Verilog/SystemVerilog are only used for the synthesisable design.
Cocotb has built-in support for integrating with the Jenkins continuous integration system.
Cocotb was specifically designed to lower the overhead of creating a test.
Cocotb automatically discovers tests so that no additional step is required to add a test to a regression.
All verification is done using Python which has various advantages over using SystemVerilog or VHDL for verification:
- Writing Python is fast - it’s a very productive language
- It’s easy to interface to other languages from Python
- Python has a huge library of existing code to re-use like packet generation libraries.
- Python is interpreted. Tests can be edited and re-run them without having to recompile the design or exit the simulator GUI.
- Python is popular - far more engineers know Python than SystemVerilog or VHDL
How does Cocotb work?¶
Overview¶
A typical cocotb testbench requires no additional RTL code. The Design Under Test (DUT) is instantiated as the toplevel in the simulator without any wrapper code. Cocotb drives stimulus onto the inputs to the DUT (or further down the hierarchy) and monitors the outputs directly from Python.
A test is simply a Python function. At any given time either the simulator is advancing time or the Python code is executing. The yield keyword is used to indicate when to pass control of execution back to the simulator. A test can spawn multiple coroutines, allowing for independent flows of execution.
Contributors¶
Cocotb was developed by Potential Ventures with the support of Solarflare Communications Ltd and contributions from Gordon McGregor and Finn Grimwood (see contributers for full list of contributions).
Quickstart Guide¶
Installing cocotb¶
Pre-requisites¶
Cocotb has the following requirements:
- Python 2.6+
- Python-dev packages
- GCC and associated development packages
- GNU Make
- A Verilog or VHDL simulator, depending on your source RTL code
Internal development is performed on Linux Mint 17 (x64). We also use RedHat 6.5(x64). Other RedHat and Ubuntu based distributions (x32 and x64) should work too but due fragmented nature of Linux we can not test everything. Instructions are provided for the main distributions we use.
Linux native arch installation¶
Ubuntu based installation
$> sudo apt-get install git make gcc g++ swig python-dev
This will allow building of the cocotb libs for use with a 64-bit native simulator. If a 32-bit simulator is being used then additional steps to install 32-bit development libraries and Python are needed.
RedHat based installation
$> sudo yum install gcc gcc-c++ libstdc++-devel swig python-devel
This will allow building of the cocotb libs for use with a 64-bit native simulator. If a 32-bit simulator is being used then additional steps to install 32-bit development libraries and Python are needed.
32-bit Python¶
Additional development libraries are needed for building 32-bit Python on 64-bit systems.
Ubuntu based installation
$> sudo apt-get install libx32gcc1 gcc-4.8-multilib lib32stdc++-4.8-dev
Replace 4.8 with the version of GCC that was installed on the system in the step above. Unlike on RedHat where 32-bit Python can co-exist with native Python, Ubuntu requires the source to be downloaded and built.
RedHat based installation
$> sudo yum install glibc.i686 glibc-devel.i386 libgcc.i686 libstdc++-devel.i686
Specific releases can be downloaded from https://www.python.org/downloads/ .
$> wget https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz
$> tar xvf Python-2.7.9.tgz
$> cd Python-2.7.9
$> export PY32_DIR=/opt/pym32
$> ./configure CC="gcc -m32" LDFLAGS="-L/lib32 -L/usr/lib32 -Lpwd/lib32 -Wl,-rpath,/lib32 -Wl,-rpath,$PY32_DIR/lib" --prefix=$PY32_DIR --enable-shared
$> make
$> sudo make install
Cocotb can now be built against 32-bit Python by setting the architecture and placing the 32-bit Python ahead of the native version in the path when running a test.
$> export PATH=/opt/pym32/bin
$> cd <cocotb_dir>
$> ARCH=i686 make
Windows 7 installation¶
Work has been done with the support of the cocotb community to enable Windows support using the MinGW/Msys environment. Download the MinGQ installer from.
http://sourceforge.net/projects/mingw/files/latest/download?source=files .
Run the GUI installer and specify a directory you would like the environment installed in. The installer will retrieve a list of possible packages, when this is done press continue. The MinGW Installation Manager is then launched.
The following packages need selecting by checking the tick box and selecting “Mark for installation”
Basic Installation
-- mingw-developer-tools
-- mingw32-base
-- mingw32-gcc-g++
-- msys-base
From the Installation menu then select “Apply Changes”, in the next dialog select “Apply”.
When installed a shell can be opened using the “msys.bat” file located under the <install_dir>/msys/1.0/
Python can be downloaded from https://www.python.org/ftp/python/2.7.9/python-2.7.9.msi, other versions of Python can be used as well. Run the installer and download to your chosen location.
It is beneficial to add the path to Python to the Windows system PATH
variable
so it can be used easily from inside Msys.
Once inside the Msys shell commands as given here will work as expected.
Mac Packages¶
You need a few packages installed to get cocotb running on Mac. Installing a package manager really helps things out here.
Brew seems to be the most popular, so we’ll assume you have that installed.
Running an example¶
$> git clone https://github.com/potentialventures/cocotb
$> cd cocotb/examples/endian_swapper/tests
$> make
To run a test using a different simulator:
$> make SIM=vcs
Running a VHDL example¶
The endian_swapper
example includes both a VHDL and a Verilog RTL implementation.
The cocotb testbench can execute against either implementation using VPI for
Verilog and VHPI/FLI for VHDL. To run the test suite against the VHDL
implementation use the following command (a VHPI or FLI capable simulator must
be used):
$> make SIM=ghdl TOPLEVEL_LANG=vhdl
Using Cocotb¶
A typical cocotb testbench requires no additional RTL code. The Design Under Test (DUT) is instantiated as the toplevel in the simulator without any wrapper code. Cocotb drives stimulus onto the inputs to the DUT and monitors the outputs directly from Python.
Creating a Makefile¶
To create a cocotb test we typically have to create a Makefile. Cocotb provides rules which make it easy to get started. We simply inform cocotb of the source files we need compiling, the toplevel entity to instantiate and the Python test script to load.
VERILOG_SOURCES = $(PWD)/submodule.sv $(PWD)/my_design.sv
TOPLEVEL=my_design # the module name in your Verilog or VHDL file
MODULE=test_my_design # the name of the Python test file
include $(COCOTB)/makefiles/Makefile.inc
include $(COCOTB)/makefiles/Makefile.sim
We would then create a file called test_my_design.py
containing our tests.
Creating a test¶
The test is written in Python. Cocotb wraps your top level with the handle you
pass it. In this documentation, and most of the examples in the project, that
handle is dut
, but you can pass your own preferred name in instead. The
handle is used in all Python files referencing your RTL project. Assuming we
have a toplevel port called clk
we could create a test file containing the
following:
import cocotb
from cocotb.triggers import Timer
@cocotb.test()
def my_first_test(dut):
"""
Try accessing the design
"""
dut._log.info("Running test!")
for cycle in range(10):
dut.clk = 0
yield Timer(1000)
dut.clk = 1
yield Timer(1000)
dut._log.info("Running test!")
This will drive a square wave clock onto the clk
port of the toplevel.
Accessing the design¶
When cocotb initialises it finds the top-level instantiation in the simulator
and creates a handle called dut
. Top-level signals can be accessed using the
“dot” notation used for accessing object attributes in Python. The same mechanism
can be used to access signals inside the design.
# Get a reference to the "clk" signal on the top-level
clk = dut.clk
# Get a reference to a register "count"
# in a sub-block "inst_sub_block"
count = dut.inst_sub_block.count
Assigning values to signals¶
Values can be assigned to signals using either the
value
property of a handle object
or using direct assignment while traversing the hierarchy.
# Get a reference to the "clk" signal and assign a value
clk = dut.clk
clk.value = 1
# Direct assignment through the hierarchy
dut.input_signal <= 12
# Assign a value to a memory deep in the hierarchy
dut.sub_block.memory.array[4] <= 2
The syntax sig <= new_value
is a short form of sig.value = new_value
.
It not only resembles HDL-syntax, but also has the same semantics:
writes are not applied immediately, but delayed until the next write cycle.
Use sig.setimmediatevalue(new_val)
to set a new value immediately
(see setimmediatevalue()
).
Reading values from signals¶
Accessing the value
property of a handle object will return a BinaryValue
object.
Any unresolved bits are preserved and can be accessed using the binstr
attribute,
or a resolved integer value can be accessed using the integer
attribute.
>>> # Read a value back from the DUT
>>> count = dut.counter.value
>>>
>>> print(count.binstr)
1X1010
>>> # Resolve the value to an integer (X or Z treated as 0)
>>> print(count.integer)
42
>>> # Show number of bits in a value
>>> print(count.n_bits)
6
We can also cast the signal handle directly to an integer:
>>> print(int(dut.counter))
42
Parallel and sequential execution of coroutines¶
@cocotb.coroutine
def reset_dut(reset_n, duration):
reset_n <= 0
yield Timer(duration)
reset_n <= 1
reset_n._log.debug("Reset complete")
@cocotb.test()
def parallel_example(dut):
reset_n = dut.reset
# This will call reset_dut sequentially
# Execution will block until reset_dut has completed
yield reset_dut(reset_n, 500)
dut._log.debug("After reset")
# Call reset_dut in parallel with this coroutine
reset_thread = cocotb.fork(reset_dut(reset_n, 500)
yield Timer(250)
dut._log.debug("During reset (reset_n = %s)" % reset_n.value)
# Wait for the other thread to complete
yield reset_thread.join()
dut._log.debug("After reset")
Build options and Environment Variables¶
Make System¶
Makefiles are provided for a variety of simulators in cocotb/makefiles/simulators
. The common Makefile cocotb/makefiles/Makefile.sim
includes the appropriate simulator Makefile based on the contents of the SIM
variable.
Make Targets¶
Makefiles define two targets, regression
and sim
, the default target is sim
.
Both rules create a results file in the calling directory called results.xml
. This file is a JUnit-compatible output file suitable for use with Jenkins. The sim
targets unconditionally re-runs the simulator whereas the regression
target only re-builds if any dependencies have changed.
Make Phases¶
Typically the makefiles provided with Cocotb for various simulators use a separate compile
and run
target. This allows for a rapid re-running of a simulator if none of the RTL source files have changed and therefore the simulator does not need to recompile the RTL.
Make Variables¶
GUI
- Set this to 1 to enable the GUI mode in the simulator (if supported).
SIM
- Selects which simulator Makefile to use. Attempts to include a simulator specific makefile from
cocotb/makefiles/makefile.$(SIM)
VERILOG_SOURCES
- A list of the Verilog source files to include.
VHDL_SOURCES
- A list of the VHDL source files to include.
COMPILE_ARGS
- Any arguments or flags to pass to the compile stage of the simulation. Only applies to simulators with a separate compilation stage (currently Icarus and VCS).
SIM_ARGS
- Any arguments or flags to pass to the execution of the compiled simulation. Only applies to simulators with a separate compilation stage (currently Icarus, VCS and GHDL).
EXTRA_ARGS
- Passed to both the compile and execute phases of simulators with two rules, or passed to the single compile and run command for simulators which don’t have a distinct compilation stage.
CUSTOM_COMPILE_DEPS
- Use to add additional dependencies to the compilation target; useful for defining additional rules to run pre-compilation or if the compilation phase depends on files other than the RTL sources listed in VERILOG_SOURCES or VHDL_SOURCES.
CUSTOM_SIM_DEPS
- Use to add additional dependencies to the simulation target.
COCOTB_NVC_TRACE
- Set this to 1 to enable display of VHPI traces when using the nvc VHDL simulator.
SIM_BUILD
- Use to define a scratch directory for use by the simulator. The path is relative to the Makefile location.
If not provided, the default scratch directory is
sim_build
.
Environment Variables¶
TOPLEVEL
- Used to indicate the instance in the hierarchy to use as the DUT. If this isn’t defined then the first root instance is used.
RANDOM_SEED
Seed the Python random module to recreate a previous test stimulus. At the beginning of every test a message is displayed with the seed used for that execution:
INFO cocotb.gpi __init__.py:89 in _initialise_testbench Seeding Python random module with 1377424946
To recreate the same stimuli use the following:
make RANDOM_SEED=1377424946
COCOTB_ANSI_OUTPUT
Use this to override the default behaviour of annotating Cocotb output with ANSI colour codes if the output is a terminal (
isatty()
).COCOTB_ANSI_OUTPUT=1
forces output to be ANSI regardless of the type stdoutCOCOTB_ANSI_OUTPUT=0
supresses the ANSI output in the log messagesCOCOTB_REDUCED_LOG_FMT
- If defined, log lines displayed in terminal will be shorter. It will print only
time, message type (
INFO
,WARNING
,ERROR
) and log message. MODULE
- The name of the module(s) to search for test functions. Multiple modules can be specified using a comma-separated list.
TESTCASE
The name of the test function(s) to run. If this variable is not defined Cocotb discovers and executes all functions decorated with the
cocotb.test
decorator in the supplied modules.Multiple functions can be specified in a comma-separated list.
Additional Environment Variables¶
COCOTB_ATTACH
- In order to give yourself time to attach a debugger to the simulator process before it starts to run,
you can set the environment variable
COCOTB_ATTACH
to a pause time value in seconds. If set, Cocotb will print the process ID (PID) to attach to and wait the specified time before actually letting the simulator run. COCOTB_ENABLE_PROFILING
Enable performance analysis of the Python portion of Cocotb. When set, a file
test_profile.pstat
will be written which contains statistics about the cumulative time spent in the functions.From this, a callgraph diagram can be generated with gprof2dot and
graphviz
. See theprofile
Make target in theendian_swapper
example on how to set this up.COCOTB_HOOKS
- A comma-separated list of modules that should be executed before the first test.
You can also use the
cocotb.hook
decorator to mark a function to be run before test code. COCOTB_LOG_LEVEL
- Default logging level to use. This is set to
INFO
unless overridden. COCOTB_RESOLVE_X
Defines how to resolve bits with a value of
X
,Z
,U
orW
when being converted to integer. Valid settings are:VALUE_ERROR
- raise a
ValueError
exception ZEROS
- resolve to
0
ONES
- resolve to
1
RANDOM
- randomly resolve to a
0
or a1
Set to
VALUE_ERROR
by default.COCOTB_SCHEDULER_DEBUG
- Enable additional log output of the coroutine scheduler.
MEMCHECK
HTTP port to use for debugging Python’s memory usage. When set to e.g.
8088
, data will be presented at http://localhost:8088.This needs the
cherrypy
anddowser
Python modules installed.SIM_ROOT
- The root directory of the Cocotb installation.
VERSION
- The version of the Cocotb installation. You probably don’t want to modify this.
Coroutines¶
Testbenches built using cocotb use coroutines. While the coroutine is executing
the simulation is paused. The coroutine uses the yield
keyword to
pass control of execution back to the simulator and simulation time can advance
again.
Typically coroutines yield
a Trigger
object which
indicates to the simulator some event which will cause the coroutine to be woken
when it occurs. For example:
@cocotb.coroutine
def wait_10ns():
cocotb.log.info("About to wait for 10ns")
yield Timer(10000)
cocotb.log.info("Simulation time has advanced by 10 ns")
Coroutines may also yield other coroutines:
@cocotb.coroutine
def wait_100ns():
for i in range(10):
yield wait_10ns()
Coroutines can return a value, so that they can be used by other coroutines.
Before Python 3.3, this requires a ReturnValue
to be raised.
@cocotb.coroutine
def get_signal(clk, signal):
yield RisingEdge(clk)
raise ReturnValue(signal.value)
@cocotb.coroutine
def get_signal_python_33(clk, signal):
# newer versions of Python can use return normally
yield RisingEdge(clk)
return signal.value
@cocotb.coroutine
def check_signal_changes(dut):
first = yield get_signal(dut.clk, dut.signal)
second = yield get_signal(dut.clk, dut.signal)
if first == second:
raise TestFailure("Signal did not change")
Coroutines may also yield a list of triggers and coroutines to indicate that execution should resume if any of them fires:
@cocotb.coroutine
def packet_with_timeout(monitor, timeout):
"""Wait for a packet but time out if nothing arrives"""
yield [Timer(timeout), RisingEdge(dut.ready)]
The trigger that caused execution to resume is passed back to the coroutine, allowing them to distinguish which trigger fired:
@cocotb.coroutine
def packet_with_timeout(monitor, timeout):
"""Wait for a packet but time out if nothing arrives"""
tout_trigger = Timer(timeout)
result = yield [tout_trigger, RisingEdge(dut.ready)]
if result is tout_trigger:
raise TestFailure("Timed out waiting for packet")
Coroutines can be forked for parallel operation within a function of that code and the forked code.
@cocotb.test()
def test_act_during_reset(dut):
"""While reset is active, toggle signals"""
tb = uart_tb(dut)
# "Clock" is a built in class for toggling a clock signal
cocotb.fork(Clock(dut.clk, 1000).start())
# reset_dut is a function -
# part of the user-generated "uart_tb" class
cocotb.fork(tb.reset_dut(dut.rstn, 20000))
yield Timer(10000)
print("Reset is still active: %d" % dut.rstn)
yield Timer(15000)
print("Reset has gone inactive: %d" % dut.rstn)
Coroutines can be joined to end parallel operation within a function.
@cocotb.test()
def test_count_edge_cycles(dut, period=1000, clocks=6):
cocotb.fork(Clock(dut.clk, period).start())
yield RisingEdge(dut.clk)
timer = Timer(period + 10)
task = cocotb.fork(count_edges_cycles(dut.clk, clocks))
count = 0
expect = clocks - 1
while True:
result = yield [timer, task.join()]
if count > expect:
raise TestFailure("Task didn't complete in expected time")
if result is timer:
dut._log.info("Count %d: Task still running" % count)
count += 1
else:
break
Coroutines can be killed before they complete, forcing their completion before they’d naturally end.
@cocotb.test()
def test_different_clocks(dut):
clk_1mhz = Clock(dut.clk, 1.0, units='us')
clk_250mhz = Clock(dut.clk, 4.0, units='ns')
clk_gen = cocotb.fork(clk_1mhz.start())
start_time_ns = get_sim_time(units='ns')
yield Timer(1)
yield RisingEdge(dut.clk)
edge_time_ns = get_sim_time(units='ns')
# NOTE: isclose is a python 3.5+ feature
if not isclose(edge_time_ns, start_time_ns + 1000.0):
raise TestFailure("Expected a period of 1 us")
clk_gen.kill()
clk_gen = cocotb.fork(clk_250mhz.start())
start_time_ns = get_sim_time(units='ns')
yield Timer(1)
yield RisingEdge(dut.clk)
edge_time_ns = get_sim_time(units='ns')
# NOTE: isclose is a python 3.5+ feature
if not isclose(edge_time_ns, start_time_ns + 4.0):
raise TestFailure("Expected a period of 4 ns")
Triggers¶
Triggers are used to indicate when the cocotb scheduler should resume coroutine execution.
Typically a coroutine will yield
a trigger or a list of triggers,
while it is waiting for them to complete.
Simulation Timing¶
Timer(time)
:- Registers a timed callback with the simulator to continue execution of the coroutine after a specified simulation time period has elapsed.
ReadOnly()
:- Registers a callback which will continue execution of the coroutine when the current simulation timestep moves to the
ReadOnly
phase of the RTL simulator. TheReadOnly
phase is entered when the current timestep no longer has any further delta steps. This should be a point where all the signal values are stable as there are no more RTL events scheduled for the timestep. The simulator should not allow scheduling of more events in this timestep. Useful for monitors which need to wait for all processes to execute (both RTL and cocotb) to ensure sampled signal values are final.
Python Triggers¶
Event()
:- Can be used to synchronise between coroutines.
Yielding
Event.wait()
will block the coroutine untilEvent.set()
is called somewhere else. Join(coroutine_2)
:- Will block the coroutine until coroutine_2 has completed.
Testbench Tools¶
Logging¶
Cocotb extends the Python logging library. Each DUT, monitor, driver, and
scoreboard (as well as any other function using the coroutine decorator)
implements its own logging
object, and each can be set to its own
logging level. Within a DUT, each hierarchical object can also have individual
logging levels set.
When logging HDL objects, beware that _log
is the preferred way to use
logging. This helps minimize the change of name collisions with an HDL log
component with the Python logging functionality.
Log printing levels can also be set on a per-object basis.
class EndianSwapperTB(object):
def __init__(self, dut, debug=False):
self.dut = dut
self.stream_in = AvalonSTDriver(dut, "stream_in", dut.clk)
self.stream_in_recovered = AvalonSTMonitor(dut, "stream_in", dut.clk,
callback=self.model)
# Set verbosity on our various interfaces
level = logging.DEBUG if debug else logging.WARNING
self.stream_in.log.setLevel(level)
self.stream_in_recovered.log.setLevel(level)
self.dut.reset_n._log.setLevel(logging.DEBUG)
And when the logging is actually called
class AvalonSTPkts(BusMonitor):
...
@coroutine
def _monitor_recv(self):
...
self.log.info("Received a packet of %d bytes" % len(pkt))
class Scoreboard(object):
...
def add_interface(self):
...
self.log.info("Created with reorder_depth %d" % reorder_depth)
class EndianSwapTB(object):
...
@cocotb.coroutine
def reset():
self.dut._log.debug("Resetting DUT")
will display as something like
0.00ns INFO cocotb.scoreboard.endian_swapper_sv scoreboard.py:177 in add_interface Created with reorder_depth 0
0.00ns DEBUG cocotb.endian_swapper_sv .._endian_swapper.py:106 in reset Resetting DUT
160000000000000.00ns INFO cocotb.endian_swapper_sv.stream_out avalon.py:151 in _monitor_recv Received a packet of 125 bytes
Busses¶
Busses are simply defined as collection of signals. The Bus
class
will automatically bundle any group of signals together that are named similar
to dut.<bus_name><separator><signal_name>
. For instance,
dut.stream_in_valid
dut.stream_in_data
have a bus name of stream_in
, a separator of _
, and signal names of
valid
and data
. A list of signal names, or a dictionary mapping attribute
names to signal names is also passed into the Bus
class. Busses can
have values driven onto them, be captured (returning a dictionary), or sampled
and stored into a similar object.
stream_in_bus = Bus(dut, "stream_in", ["valid", "data"]) # '_' is the default separator
Driving Busses¶
Examples and specific bus implementation bus drivers (AMBA, Avalon, XGMII, and
others) exist in the Driver
class enabling a test to append
transactions to perform the serialization of transactions onto a physical
interface. Here is an example using the Avalon bus driver in the endian_swapper
example:
class EndianSwapperTB(object):
def __init__(self, dut, debug=False):
self.dut = dut
self.stream_in = AvalonSTDriver(dut, "stream_in", dut.clk)
def run_test(dut, data_in=None, config_coroutine=None, idle_inserter=None,
backpressure_inserter=None):
cocotb.fork(Clock(dut.clk, 5000).start())
tb = EndianSwapperTB(dut)
yield tb.reset()
dut.stream_out_ready <= 1
if idle_inserter is not None:
tb.stream_in.set_valid_generator(idle_inserter())
# Send in the packets
for transaction in data_in():
yield tb.stream_in.send(transaction)
Monitoring Busses¶
For our testbenches to actually be useful, we have to monitor some of these
busses, and not just drive them. That’s where the Monitor
class
comes in, with prebuilt monitors for Avalon and XGMII busses. The
Monitor class is a base class which you are expected to derive for your
particular purpose. You must create a _monitor_recv()
function which is
responsible for determining 1) at what points in simulation to call the
_recv()
function, and 2) what transaction values to pass to be stored in the
monitors receiving queue. Monitors are good for both outputs of the DUT for
verification, and for the inputs of the DUT, to drive a test model of the DUT
to be compared to the actual DUT. For this purpose, input monitors will often
have a callback function passed that is a model. This model will often generate
expected transactions, which are then compared using the Scoreboard
class.
# ==============================================================================
class BitMonitor(Monitor):
"""Observes single input or output of DUT."""
def __init__(self, name, signal, clock, callback=None, event=None):
self.name = name
self.signal = signal
self.clock = clock
Monitor.__init__(self, callback, event)
@coroutine
def _monitor_recv(self):
clkedge = RisingEdge(self.clock)
while True:
# Capture signal at rising edge of clock
yield clkedge
vec = self.signal.value
self._recv(vec)
# ==============================================================================
def input_gen():
"""Generator for input data applied by BitDriver"""
while True:
yield random.randint(1,5), random.randint(1,5)
# ==============================================================================
class DFF_TB(object):
def __init__(self, dut, init_val):
self.dut = dut
# Create input driver and output monitor
self.input_drv = BitDriver(dut.d, dut.c, input_gen())
self.output_mon = BitMonitor("output", dut.q, dut.c)
# Create a scoreboard on the outputs
self.expected_output = [ init_val ]
# Reconstruct the input transactions from the pins
# and send them to our 'model'
self.input_mon = BitMonitor("input", dut.d, dut.c, callback=self.model)
def model(self, transaction):
"""Model the DUT based on the input transaction."""
# Do not append an output transaction for the last clock cycle of the
# simulation, that is, after stop() has been called.
if not self.stopped:
self.expected_output.append(transaction)
Tracking testbench errors¶
The Scoreboard
class is used to compare the actual outputs to
expected outputs. Monitors are added to the scoreboard for the actual outputs,
and the expected outputs can be either a simple list, or a function that
provides a transaction. Here is some code from the dff
example, similar to
above with the scoreboard added.
class DFF_TB(object):
def __init__(self, dut, init_val):
self.dut = dut
# Create input driver and output monitor
self.input_drv = BitDriver(dut.d, dut.c, input_gen())
self.output_mon = BitMonitor("output", dut.q, dut.c)
# Create a scoreboard on the outputs
self.expected_output = [ init_val ]
self.scoreboard = Scoreboard(dut)
self.scoreboard.add_interface(self.output_mon, self.expected_output)
# Reconstruct the input transactions from the pins
# and send them to our 'model'
self.input_mon = BitMonitor("input", dut.d, dut.c,callback=self.model)
Library Reference¶
Test Results¶
The exceptions in this module can be raised at any point by any code and will terminate the test.
-
cocotb.result.
raise_error
(obj, msg)[source]¶ Creates a
TestError
exception and raises it after printing a traceback.Parameters: - obj – Object with a log method.
- msg (str) – The log message.
-
cocotb.result.
create_error
(obj, msg)[source]¶ Like
raise_error()
, but return the exception rather than raise it, simply to avoid too many levels of nested try/except blocks.Parameters: - obj – Object with a log method.
- msg (str) – The log message.
-
exception
cocotb.result.
ReturnValue
(retval)[source]¶ Helper exception needed for Python versions prior to 3.3.
-
exception
cocotb.result.
TestComplete
(*args, **kwargs)[source]¶ Exception showing that test was completed. Sub-exceptions detail the exit status.
-
exception
cocotb.result.
ExternalException
(exception)[source]¶ Exception thrown by external functions.
-
exception
cocotb.result.
TestError
(*args, **kwargs)[source]¶ Exception showing that test was completed with severity Error.
-
exception
cocotb.result.
TestFailure
(*args, **kwargs)[source]¶ Exception showing that test was completed with severity Failure.
Writing and Generating tests¶
-
class
cocotb.
test
(timeout=None, expect_fail=False, expect_error=False, skip=False, stage=None)[source]¶ Decorator to mark a function as a test
All tests are coroutines. The test decorator provides some common reporting etc, a test timeout and allows us to mark tests as expected failures.
Parameters: - timeout (int, optional) – value representing simulation timeout (not implemented).
- expect_fail (bool, optional) – Don’t mark the result as a failure if the test fails.
- expect_error (bool, optional) – Don’t mark the result as an error if an error is raised. This is for cocotb internal regression use.
- skip (bool, optional) – Don’t execute this test as part of the regression.
- stage (int, optional) – Order tests logically into stages, where multiple tests can share a stage.
-
class
cocotb.
coroutine
(func)[source]¶ Decorator class that allows us to provide common coroutine mechanisms:
log
methods will will log tococotb.coroutines.name
join()
method returns an event which will fire when the coroutine exits
-
class
cocotb.regression.
TestFactory
(test_function, *args, **kwargs)[source]¶ Used to automatically generate tests.
Assuming we have a common test function that will run a test. This test function will take keyword arguments (for example generators for each of the input interfaces) and generate tests that call the supplied function.
This Factory allows us to generate sets of tests based on the different permutations of the possible arguments to the test function.
For example if we have a module that takes backpressure and idles and have some packet generation routines
gen_a
andgen_b
:>>> tf = TestFactory(run_test) >>> tf.add_option('data_in', [gen_a, gen_b]) >>> tf.add_option('backpressure', [None, random_backpressure]) >>> tf.add_option('idles', [None, random_idles]) >>> tf.generate_tests()
- We would get the following tests:
gen_a
with no backpressure and no idlesgen_a
with no backpressure andrandom_idles
gen_a
withrandom_backpressure
and no idlesgen_a
withrandom_backpressure
andrandom_idles
gen_b
with no backpressure and no idlesgen_b
with no backpressure andrandom_idles
gen_b
withrandom_backpressure
and no idlesgen_b
withrandom_backpressure
andrandom_idles
The tests are appended to the calling module for auto-discovery.
Tests are simply named
test_function_N
. The docstring for the test (hence the test description) includes the name and description of each generator.-
generate_tests
(prefix='', postfix='')[source]¶ Generates exhaustive set of tests using the cartesian product of the possible keyword arguments.
The generated tests are appended to the namespace of the calling module.
Parameters: - prefix (str) – Text string to append to start of
test_function
name when naming generated test cases. This allows reuse of a singletest_function
with multipleTestFactories
without name clashes. - postfix (str) – Text string to append to end of
test_function
name when naming generated test cases. This allows reuse of a singletest_function
with multipleTestFactories
without name clashes.
- prefix (str) – Text string to append to start of
Interacting with the Simulator¶
-
class
cocotb.binary.
BinaryRepresentation
[source]¶ -
UNSIGNED
= 0¶ Unsigned format
-
SIGNED_MAGNITUDE
= 1¶ Sign and magnitude format
-
TWOS_COMPLEMENT
= 2¶ Two’s complement format
-
-
class
cocotb.binary.
BinaryValue
(value=None, n_bits=None, bigEndian=True, binaryRepresentation=0, bits=None)[source]¶ Representation of values in binary format.
The underlying value can be set or accessed using these aliasing attributes:
BinaryValue.integer
is an integerBinaryValue.signed_integer
is a signed integerBinaryValue.binstr
is a string of “01xXzZ”BinaryValue.buff
is a binary buffer of bytesBinaryValue.value
is an integer deprecated
For example:
>>> vec = BinaryValue() >>> vec.integer = 42 >>> print(vec.binstr) 101010 >>> print(repr(vec.buff)) '*'
-
assign
(value)[source]¶ Decides how best to assign the value to the vector.
We possibly try to be a bit too clever here by first of all trying to assign the raw string as a binstring, however if the string contains any characters that aren’t
0
,1
,X
orZ
then we interpret the string as a binary buffer.Parameters: value (str or int or long) – The value to assign.
-
is_resolvable
¶ Does the value contain any
X
’s? Inquiring minds want to know.
-
value
¶ Integer access to the value. deprecated
-
integer
¶ The integer representation of the underlying vector.
-
signed_integer
¶ The signed integer representation of the underlying vector.
-
get_buff
()[source]¶ Attribute
buff
represents the value as a binary string buffer.>>> "0100000100101111".buff == "A/" True
-
buff
¶ Access to the value as a buffer.
-
binstr
¶ Access to the binary string.
-
n_bits
¶ Access to the number of bits of the binary value.
-
class
cocotb.bus.
Bus
(entity, name, signals, optional_signals=[], bus_separator='_', array_idx=None)[source]¶ Wraps up a collection of signals.
Assumes we have a set of signals/nets named
entity.<bus_name>_<signal>
.For example a bus
stream_in
with signalsvalid
anddata
is assumed to be nameddut.stream_in_valid
anddut.stream_in_data
.Todo
Support for struct/record ports where signals are member names.
-
drive
(obj, strict=False)[source]¶ Drives values onto the bus.
Parameters: - obj – Object with attribute names that match the bus signals.
- strict (bool, optional) – Check that all signals are being assigned.
Raises: AttributeError
– If not all signals have been assigned whenstrict=True
.
-
capture
()[source]¶ Capture the values from the bus, returning an object representing the capture.
Returns: A dictionary that supports access by attribute, where each attribute corresponds to each signal’s value. Return type: dict Raises: RuntimeError
– If signal not present in bus, or attempt to modify a bus capture.
-
sample
(obj, strict=False)[source]¶ Sample the values from the bus, assigning them to obj.
Parameters: - obj – Object with attribute names that match the bus signals.
- strict (bool, optional) – Check that all signals being sampled are present in obj.
Raises: AttributeError
– If attribute is missing in obj whenstrict=True
.
-
-
class
cocotb.clock.
Clock
(signal, period, units=None)[source]¶ Simple 50:50 duty cycle clock driver.
Instances of this class should call its
start
method and fork the result. This will create a clocking thread that drives the signal at the desired period/frequency.Example:
c = Clock(dut.clk, 10, 'ns') cocotb.fork(c.start())
Parameters:
Triggers¶
Triggers are used to indicate when the scheduler should resume coroutine execution.
Typically a coroutine will yield
a trigger or a list of triggers.
Simulation Timing¶
-
class
cocotb.triggers.
Timer
(time_ps, units=None)[source]¶ Execution will resume when the specified time period expires.
Consumes simulation time.
-
class
cocotb.triggers.
ReadOnly
[source]¶ Execution will resume when the readonly portion of the sim cycles is reached.
Python Triggers¶
-
class
cocotb.triggers.
Event
(name='')[source]¶ Event to permit synchronisation between two coroutines.
Testbench Structure¶
Driver¶
-
class
cocotb.drivers.
Driver
[source]¶ Class defining the standard interface for a driver within a testbench.
The driver is responsible for serialising transactions onto the physical pins of the interface. This may consume simulation time.
-
append
(transaction, callback=None, event=None, **kwargs)[source]¶ Queue up a transaction to be sent over the bus.
Mechanisms are provided to permit the caller to know when the transaction is processed.
Parameters: - transaction (any) – The transaction to be sent.
- callback (callable, optional) – Optional function to be called when the transaction has been sent.
- event (optional) –
Event
to be set when the transaction has been sent. - **kwargs – Any additional arguments used in child class’
_driver_send
method.
-
send
[source]¶ Blocking send call (hence must be “yielded” rather than called).
Sends the transaction over the bus.
Parameters: - transaction (any) – The transaction to be sent.
- sync (bool, optional) – Synchronise the transfer by waiting for rising edge.
- **kwargs (dict) – Additional arguments used in child class’
_driver_send
method.
-
_driver_send
(transaction, sync=True, **kwargs)[source]¶ Actual implementation of the send.
Subclasses should override this method to implement the actual
send()
routine.Parameters: - transaction (any) – The transaction to be sent.
- sync (boolean, optional) – Synchronise the transfer by waiting for rising edge.
- **kwargs – Additional arguments if required for protocol implemented in subclass.
-
_send
[source]¶ Send coroutine.
Parameters: - transaction (any) – The transaction to be sent.
- callback (callable, optional) – Optional function to be called when the transaction has been sent.
- event (optional) – event to be set when the transaction has been sent.
- sync (boolean, optional) – Synchronise the transfer by waiting for rising edge.
- **kwargs – Any additional arguments used in child class’
_driver_send
method.
-
-
class
cocotb.drivers.
BitDriver
(signal, clk, generator=None)[source]¶ Bases:
object
Drives a signal onto a single bit.
Useful for exercising ready / valid.
-
class
cocotb.drivers.
BusDriver
(entity, name, clock, **kwargs)[source]¶ Bases:
cocotb.drivers.Driver
Wrapper around common functionality for busses which have:
- a list of
_signals
(class attribute) - a list of
_optional_signals
(class attribute) - a clock
- a name
- an entity
Parameters: - entity (SimHandle) – A handle to the simulator entity.
- name (str or None) – Name of this bus.
None
for nameless bus, e.g. bus-signals in an interface or a modport. (untested on struct/record, but could work here as well). - clock (SimHandle) – A handle to the clock associated with this bus.
- array_idx (int or None, optional) – Optional index when signal is an array.
-
_wait_for_signal
[source]¶ This method will return with the specified signal has hit logic
1
. The state will be in theReadOnly
phase so sim will need to move toNextTimeStep
before registering more callbacks can occur.
-
_wait_for_nsignal
[source]¶ This method will return with the specified signal has hit logic
0
. The state will be in theReadOnly
phase so sim will need to move toNextTimeStep
before registering more callbacks can occur.
- a list of
Monitor¶
-
class
cocotb.monitors.
Monitor
(callback=None, event=None)[source]¶ Base class for Monitor objects.
Monitors are passive ‘listening’ objects that monitor pins in or out of a DUT. This class should not be used directly, but should be subclassed and the internal
_monitor_recv
method should be overridden and decorated as acoroutine
. This_monitor_recv
method should capture some behavior of the pins, form a transaction, and pass this transaction to the internal_recv
method. The_monitor_recv
method is added to the cocotb scheduler during the__init__
phase, so it should not be yielded anywhere.The primary use of a Monitor is as an interface for a
Scoreboard
.Parameters: - callback (callable) – Callback to be called with each recovered transaction as the argument. If the callback isn’t used, received transactions will be placed on a queue and the event used to notify any consumers.
- event (event) – Object that supports a
set
method that will be called when a transaction is received through the internal_recv
method.
-
class
cocotb.monitors.
BusMonitor
(entity, name, clock, reset=None, reset_n=None, callback=None, event=None, bus_separator='_', array_idx=None)[source]¶ Bases:
cocotb.monitors.Monitor
Wrapper providing common functionality for monitoring busses.
Scoreboard¶
Common scoreboarding capability.
-
class
cocotb.scoreboard.
Scoreboard
(dut, reorder_depth=0, fail_immediately=True)[source]¶ Bases:
object
Generic scoreboarding class.
We can add interfaces by providing a monitor and an expected output queue.
The expected output can either be a function which provides a transaction or a simple list containing the expected output.
Todo
Statistics for end-of-test summary etc.
Parameters: - dut (SimHandle) – Handle to the DUT.
- reorder_depth (int, optional) – Consider up to reorder_depth elements of the expected result list as passing matches. Default is 0, meaning only the first element in the expected result list is considered for a passing match.
- fail_immediately (bool, optional) – Raise
TestFailure
immediately when something is wrong instead of just recording an error. Default isTrue
.
-
result
¶ Determine the test result, do we have any pending data remaining?
Returns: If not all expected output was received or error were recorded during the test. Return type: TestFailure
-
compare
(got, exp, log, strict_type=True)[source]¶ Common function for comparing two transactions.
Can be re-implemented by a subclass.
Parameters: - got – The received transaction.
- exp – The expected transaction.
- log – The logger for reporting messages.
- strict_type (bool, optional) – Require transaction type to match
exactly if
True
, otherwise compare its string representation.
Raises: TestFailure
– If received transaction differed from expected transaction whenfail_immediately
isTrue
. If strict_type isTrue
, also the transaction type must match.
-
add_interface
(monitor, expected_output, compare_fn=None, reorder_depth=0, strict_type=True)[source]¶ Add an interface to be scoreboarded.
Provides a function which the monitor will callback with received transactions.
Simply check against the expected output.
Parameters: - monitor – The monitor object.
- expected_output – Queue of expected outputs.
- compare_fn (callable, optional) –
- reorder_depth (int, optional) – Consider up to reorder_depth elements of the expected result list as passing matches. Default is 0, meaning only the first element in the expected result list is considered for a passing match.
- strict_type (bool, optional) – Require transaction type to match
exactly if
True
, otherwise compare its string representation.
Raises: TypeError
– If no monitor is on the interface or compare_fn is not a callable function.
Clock¶
-
class
cocotb.clock.
Clock
(signal, period, units=None)[source] Simple 50:50 duty cycle clock driver.
Instances of this class should call its
start
method and fork the result. This will create a clocking thread that drives the signal at the desired period/frequency.Example:
c = Clock(dut.clk, 10, 'ns') cocotb.fork(c.start())
Parameters:
Utilities¶
-
cocotb.utils.
get_sim_time
(units=None)[source]¶ Retrieves the simulation time from the simulator.
Parameters: units (str or None, optional) – String specifying the units of the result (one of None
,'fs'
,'ps'
,'ns'
,'us'
,'ms'
,'sec'
).None
will return the raw simulation time.Returns: The simulation time in the specified units.
-
cocotb.utils.
get_time_from_sim_steps
(steps, units)[source]¶ Calculates simulation time in the specified units from the steps based on the simulator precision.
Parameters: Returns: The simulation time in the specified units.
-
cocotb.utils.
get_sim_steps
(time, units=None)[source]¶ Calculates the number of simulation time steps for a given amount of time.
Parameters: Returns: The number of simulation time steps.
Return type: Raises: ValueError
– If given time cannot be represented by simulator precision.
-
cocotb.utils.
pack
(ctypes_obj)[source]¶ Convert a
ctypes
structure into a Python string.Parameters: ctypes_obj (ctypes.Structure) – The ctypes structure to convert to a string. Returns: New Python string containing the bytes from memory holding ctypes_obj.
-
cocotb.utils.
unpack
(ctypes_obj, string, bytes=None)[source]¶ Unpack a Python string into a
ctypes
structure.If the length of string is not the correct size for the memory footprint of the ctypes structure then the bytes keyword argument must be used.
Parameters: - ctypes_obj (ctypes.Structure) – The ctypes structure to pack into.
- string (str) – String to copy over the ctypes_obj memory space.
- bytes (int, optional) – Number of bytes to copy.
Defaults to
None
, meaning the length of string is used.
Raises: ValueError
– If length of string and size of ctypes_obj are not equal.MemoryError
– If bytes is longer than size of ctypes_obj.
-
cocotb.utils.
hexdump
(x)[source]¶ Hexdump a buffer.
Parameters: x – Object that supports conversion via the str
built-in.Returns: A string containing the hexdump. Example:
print(hexdump('this somewhat long string'))
0000 74 68 69 73 20 73 6F 6D 65 77 68 61 74 20 6C 6F this somewhat lo 0010 6E 67 20 73 74 72 69 6E 67 ng string
-
cocotb.utils.
hexdiffs
(x, y)[source]¶ Return a diff string showing differences between two binary strings.
Parameters: - x – Object that supports conversion via the
str
built-in. - y – Object that supports conversion via the
str
built-in.
Example:
print(hexdiffs('this short thing', 'this also short'))
0000 746869732073686F 7274207468696E67 this short thing 0000 7468697320616C73 6F 2073686F7274 this also short
- x – Object that supports conversion via the
-
cocotb.utils.
with_metaclass
(meta, *bases)[source]¶ This provides:
class Foo(with_metaclass(Meta, Base1, Base2)): pass
which is a unifying syntax for:
# python 3 class Foo(Base1, Base2, metaclass=Meta): pass # python 2 class Foo(Base1, Base2): __metaclass__ = Meta
-
class
cocotb.utils.
ParametrizedSingleton
(*args, **kwargs)[source]¶ A metaclass that allows class construction to reuse an existing instance.
We use this so that
RisingEdge(sig)
andJoin(coroutine)
always return the same instance, rather than creating new copies.
Simulation Object Handles¶
-
class
cocotb.handle.
SimHandleBase
(handle, path)[source]¶ Bases:
object
Base class for all simulation objects.
We maintain a handle which we can use for GPI calls.
-
class
cocotb.handle.
RegionObject
(handle, path)[source]¶ Bases:
cocotb.handle.SimHandleBase
Region objects don’t have values, they are effectively scopes or namespaces.
-
class
cocotb.handle.
HierarchyObject
(handle, path)[source]¶ Bases:
cocotb.handle.RegionObject
Hierarchy objects are namespace/scope objects.
-
class
cocotb.handle.
HierarchyArrayObject
(handle, path)[source]¶ Bases:
cocotb.handle.RegionObject
Hierarchy Arrays are containers of Hierarchy Objects.
-
class
cocotb.handle.
NonHierarchyObject
(handle, path)[source]¶ Bases:
cocotb.handle.SimHandleBase
Common base class for all non-hierarchy objects.
-
value
¶ A reference to the value
-
-
class
cocotb.handle.
ConstantObject
(handle, path, handle_type)[source]¶ Bases:
cocotb.handle.NonHierarchyObject
Constant objects have a value that can be read, but not set.
We can also cache the value since it is elaboration time fixed and won’t change within a simulation.
-
class
cocotb.handle.
ModifiableObject
(handle, path)[source]¶ Bases:
cocotb.handle.NonConstantObject
Base class for simulator objects whose values can be modified.
-
setimmediatevalue
(value)[source]¶ Set the value of the underlying simulation object to value.
This operation will fail unless the handle refers to a modifiable object, e.g. net, signal or variable.
We determine the library call to make based on the type of the value because assigning integers less than 32 bits is faster.
Parameters: value (ctypes.Structure, cocotb.binary.BinaryValue, int, double) – The value to drive onto the simulator object. Raises: TypeError
– If target is not wide enough or has an unsupported type for value assignment.
-
-
class
cocotb.handle.
RealObject
(handle, path)[source]¶ Bases:
cocotb.handle.ModifiableObject
Specific object handle for Real signals and variables.
-
setimmediatevalue
(value)[source]¶ Set the value of the underlying simulation object to value.
This operation will fail unless the handle refers to a modifiable object, e.g. net, signal or variable.
Parameters: value (float) – The value to drive onto the simulator object. Raises: TypeError
– If target has an unsupported type for real value assignment.
-
-
class
cocotb.handle.
EnumObject
(handle, path)[source]¶ Bases:
cocotb.handle.ModifiableObject
Specific object handle for enumeration signals and variables.
-
setimmediatevalue
(value)[source]¶ Set the value of the underlying simulation object to value.
This operation will fail unless the handle refers to a modifiable object, e.g. net, signal or variable.
Parameters: value (int) – The value to drive onto the simulator object. Raises: TypeError
– If target has an unsupported type for integer value assignment.
-
-
class
cocotb.handle.
IntegerObject
(handle, path)[source]¶ Bases:
cocotb.handle.ModifiableObject
Specific object handle for Integer and Enum signals and variables.
-
setimmediatevalue
(value)[source]¶ Set the value of the underlying simulation object to value.
This operation will fail unless the handle refers to a modifiable object, e.g. net, signal or variable.
Parameters: value (int) – The value to drive onto the simulator object. Raises: TypeError
– If target has an unsupported type for integer value assignment.
-
-
class
cocotb.handle.
StringObject
(handle, path)[source]¶ Bases:
cocotb.handle.ModifiableObject
Specific object handle for String variables.
-
setimmediatevalue
(value)[source]¶ Set the value of the underlying simulation object to value.
This operation will fail unless the handle refers to a modifiable object, e.g. net, signal or variable.
Parameters: value (str) – The value to drive onto the simulator object. Raises: TypeError
– If target has an unsupported type for string value assignment.
-
Implemented Testbench Structures¶
Drivers¶
AD9361¶
Analog Devices AD9361 RF Transceiver.
-
class
cocotb.drivers.ad9361.
AD9361
(dut, rx_channels=1, tx_channels=1, tx_clock_half_period=16276, rx_clock_half_period=16276, loopback_queue_maxlen=16)[source]¶ Driver for the AD9361 RF Transceiver.
-
send_data
(i_data, q_data, i_data2=None, q_data2=None, binaryRepresentation=BinaryRepresentation.TWOS_COMPLEMENT)[source]¶ Forks the
rx_data_to_ad9361
coroutine to send data.Parameters: - i_data (int) – Data of the I0 channel.
- q_data (int) – Data of the Q0 channel.
- i_data2 (int, optional) – Data of the I1 channel.
- q_data2 (int, optional) – Data of the Q1 channel.
- binaryRepresentation (BinaryRepresentation) – The representation of the binary value.
Default is
TWOS_COMPLEMENT
.
-
rx_data_to_ad9361
(i_data, q_data, i_data2=None, q_data2=None, binaryRepresentation=BinaryRepresentation.TWOS_COMPLEMENT)[source]¶ Receive data to AD9361.
This is a coroutine.
Parameters: - i_data (int) – Data of the I0 channel.
- q_data (int) – Data of the Q0 channel.
- i_data2 (int, optional) – Data of the I1 channel.
- q_data2 (int, optional) – Data of the Q1 channel.
- binaryRepresentation (BinaryRepresentation) – The representation of the binary value.
Default is
TWOS_COMPLEMENT
.
-
AMBA¶
Advanced Microcontroller Bus Architecture.
-
class
cocotb.drivers.amba.
AXI4LiteMaster
(entity, name, clock)[source]¶ AXI4-Lite Master
TODO: Kill all pending transactions if reset is asserted…
-
write
(address, value, byte_enable=0xf, address_latency=0, data_latency=0)[source]¶ Write a value to an address.
Parameters: - address (int) – The address to write to
- value (int) – The data value to write
- byte_enable (int, optional) – Which bytes in value to actually write. Default is to write all bytes.
- address_latency (int, optional) – Delay before setting the address (in clock cycles). Default is no delay.
- data_latency (int, optional) – Delay before setting the data value (in clock cycles). Default is no delay.
- sync (bool, optional) – Wait for rising edge on clock initially. Defaults to True.
Returns: The write response value
Return type: Raises: AXIProtocolError
– If write response from AXI is notOKAY
-
read
(address, sync=True)[source]¶ Read from an address.
Parameters: Returns: The read data value
Return type: Raises: AXIProtocolError
– If read response from AXI is notOKAY
Members: Member-order: bysource -
Avalon¶
-
class
cocotb.drivers.avalon.
AvalonMM
(entity, name, clock, **kwargs)[source]¶ Avalon Memory Mapped Interface (Avalon-MM) Driver.
Currently we only support the mode required to communicate with SF avalon_mapper which is a limited subset of all the signals.
Blocking operation is all that is supported at the moment, and for the near future as well. Posted responses from a slave are not supported.
-
class
cocotb.drivers.avalon.
AvalonMaster
(entity, name, clock, **kwargs)[source]¶ Avalon Memory Mapped Interface (Avalon-MM) Master
-
write
(address, value)[source]¶ Issue a write to the given address with the specified value. See http://www.altera.com/literature/manual/mnl_avalon_spec_1_3.pdf
Parameters: Raises: TestError
– If master is read-only.
-
read
(address, sync=True)[source]¶ Issue a request to the bus and block until this comes back. Simulation time still progresses but syntactically it blocks. See http://www.altera.com/literature/manual/mnl_avalon_spec_1_3.pdf
Parameters: Returns: The read data value.
Return type: Raises: TestError
– If master is write-only.
Members: Member-order: bysource -
-
class
cocotb.drivers.avalon.
AvalonMemory
(entity, name, clock, readlatency_min=1, readlatency_max=1, memory=None, avl_properties={})[source]¶ Emulate a memory, with back-door access.
OPB¶
-
class
cocotb.drivers.opb.
OPBMaster
(entity, name, clock)[source]¶ On-chip peripheral bus master.
-
write
(address, value, sync=True)[source]¶ Issue a write to the given address with the specified value.
Parameters: Raises: OPBException
– If write took longer than 16 cycles
-
read
(address, sync=True)[source]¶ Issue a request to the bus and block until this comes back. Simulation time still progresses but syntactically it blocks.
Parameters: Returns: The read data value.
Return type: Raises: OPBException
– If read took longer than 16 cycles.
Members: Member-order: bysource -
XGMII¶
-
class
cocotb.drivers.xgmii.
XGMII
(signal, clock, interleaved=True)[source]¶ XGMII (10 Gigabit Media Independent Interface) driver.
Monitors¶
Avalon¶
XGMII¶
-
class
cocotb.monitors.xgmii.
XGMII
(signal, clock, interleaved=True, callback=None, event=None)[source]¶ XGMII (10 Gigabit Media Independent Interface) Monitor.
Assumes a single vector, either 4 or 8 bytes plus control bit for each byte.
If interleaved is true then the control bits are adjacent to the bytes.
Tutorial: Endian Swapper¶
In this tutorial we’ll use some of the built-in features of cocotb to quickly create a complex testbench.
Note
All the code and sample output from this example are available on EDA Playground
For the impatient this tutorial is provided as an example with cocotb. You can run this example from a fresh checkout:
cd examples/endian_swapper/tests
make
Design¶
We have a relatively simplistic RTL block called the endian_swapper
.
The DUT has three interfaces, all conforming to the Avalon standard:
The DUT will swap the endianness of packets on the Avalon-ST bus if a configuration bit is set.
For every packet arriving on the stream_in
interface the entire packet will be endian swapped
if the configuration bit is set, otherwise the entire packet will pass through unmodified.
Testbench¶
To begin with we create a class to encapsulate all the common code for the testbench. It is possible to write directed tests without using a testbench class however to encourage code re-use it is good practice to create a distinct class.
class EndianSwapperTB(object):
def __init__(self, dut):
self.dut = dut
self.stream_in = AvalonSTDriver(dut, "stream_in", dut.clk)
self.stream_out = AvalonSTMonitor(dut, "stream_out", dut.clk)
self.csr = AvalonMaster(dut, "csr", dut.clk)
self.expected_output = []
self.scoreboard = Scoreboard(dut)
self.scoreboard.add_interface(self.stream_out, self.expected_output)
# Reconstruct the input transactions from the pins and send them to our 'model'
self.stream_in_recovered = AvalonSTMonitor(dut, "stream_in", dut.clk, callback=self.model)
With the above code we have created a testbench with the following structure:
If we inspect this line-by-line:
self.stream_in = AvalonSTDriver(dut, "stream_in", dut.clk)
Here we are creating an AvalonSTDriver
instance.
The constructor requires 3 arguments - a handle to the entity containing the interface (dut
),
the name of the interface (stream_in
) and the associated clock with which to drive the interface (dut.clk
).
The driver will auto-discover the signals for the interface,
assuming that they follow the naming convention <interface_name>_<signal>
.
In this case we have the following signals defined for the stream_in
interface:
Name | Type | Description (from Avalon Specification) |
---|---|---|
stream_in_data |
data |
The data signal from the source to the sink |
stream_in_empty |
empty |
Indicates the number of symbols that are empty during cycles that contain the end of a packet |
stream_in_valid |
valid |
Asserted by the source to qualify all other source to sink signals |
stream_in_startofpacket |
startofpacket |
Asserted by the source to mark the beginning of a packet |
stream_in_endofpacket |
endofpacket |
Asserted by the source to mark the end of a packet |
stream_in_ready |
ready |
Asserted high to indicate that the sink can accept data |
By following the signal naming convention the driver can find the signals associated with this interface automatically.
self.stream_out = AvalonSTMonitor(dut, "stream_out", dut.clk)
self.csr = AvalonMaster(dut, "csr", dut.clk)
We do the same to create the monitor
on stream_out
and the CSR interface.
self.expected_output = []
self.scoreboard = Scoreboard(dut)
self.scoreboard.add_interface(self.stream_out, self.expected_output)
The above lines create a Scoreboard
instance and attach it to the stream_out
monitor instance.
The scoreboard is used to check that the DUT behaviour is correct.
The call to add_interface()
takes a Monitor instance as the first argument and
the second argument is a mechanism for describing the expected output for that interface.
This could be a callable function but in this example a simple list of expected transactions is sufficient.
# Reconstruct the input transactions from the pins and send them to our 'model'
self.stream_in_recovered = AvalonSTMonitor(dut, "stream_in", dut.clk, callback=self.model)
Finally we create another Monitor instance, this time connected to the stream_in
interface.
This is to reconstruct the transactions being driven into the DUT.
It’s good practice to use a monitor to reconstruct the transactions from the pin interactions
rather than snooping them from a higher abstraction layer as we can gain confidence that our drivers and monitors are functioning correctly.
We also pass the keyword argument callback
to the monitor constructor which will result
in the supplied function being called for each transaction seen on the bus with the transaction as the first argument.
Our model function is quite straightforward in this case - we simply append the transaction to the expected output list and increment a counter:
def model(self, transaction):
"""Model the DUT based on the input transaction"""
self.expected_output.append(transaction)
self.pkts_sent += 1
Test Function¶
There are various ‘knobs’ we can tweak on this testbench to vary the behaviour:
- Packet size
- Backpressure on the
stream_out
interface - Idle cycles on the
stream_in
interface - Configuration switching of the endian swap register during the test.
We want to run different variations of tests but they will all have a very similar structure so we create a common run_test
function.
To generate backpressure on the stream_out
interface we use the BitDriver
class from cocotb.drivers
.
@cocotb.coroutine
def run_test(dut, data_in=None, config_coroutine=None, idle_inserter=None, backpressure_inserter=None):
cocotb.fork(Clock(dut.clk, 5000).start())
tb = EndianSwapperTB(dut)
yield tb.reset()
dut.stream_out_ready <= 1
# Start off any optional coroutines
if config_coroutine is not None:
cocotb.fork(config_coroutine(tb.csr))
if idle_inserter is not None:
tb.stream_in.set_valid_generator(idle_inserter())
if backpressure_inserter is not None:
tb.backpressure.start(backpressure_inserter())
# Send in the packets
for transaction in data_in():
yield tb.stream_in.send(transaction)
# Wait at least 2 cycles where output ready is low before ending the test
for i in range(2):
yield RisingEdge(dut.clk)
while not dut.stream_out_ready.value:
yield RisingEdge(dut.clk)
pkt_count = yield tb.csr.read(1)
if pkt_count.integer != tb.pkts_sent:
raise TestFailure("DUT recorded %d packets but tb counted %d" % (
pkt_count.integer, tb.pkts_sent))
else:
dut._log.info("DUT correctly counted %d packets" % pkt_count.integer)
raise tb.scoreboard.result
We can see that this test function creates an instance of the testbench,
resets the DUT by running the coroutine tb.reset()
and then starts off any optional coroutines passed in using the keyword arguments.
We then send in all the packets from data_in
, ensure that all the packets have been received by waiting 2 cycles at the end.
We read the packet count and compare this with the number of packets.
Finally we use the tb.scoreboard.result
to determine the status of the test.
If any transactions didn’t match the expected output then this member would be an instance of the TestFailure
result.
Test permutations¶
Having defined a test function we can now auto-generate different permutations of tests using the TestFactory
class:
factory = TestFactory(run_test)
factory.add_option("data_in", [random_packet_sizes])
factory.add_option("config_coroutine", [None, randomly_switch_config])
factory.add_option("idle_inserter", [None, wave, intermittent_single_cycles, random_50_percent])
factory.add_option("backpressure_inserter", [None, wave, intermittent_single_cycles, random_50_percent])
factory.generate_tests()
This will generate 32 tests (named run_test_001
to run_test_032
) with all possible permutations of the options provided for each argument.
Note that we utilise some of the built-in generators to toggle backpressure and insert idle cycles.
Tutorial: Ping¶
One of the benefits of Python is the ease with which interfacing is possible. In this tutorial we’ll look at interfacing the standard GNU ping command to the simulator. Using Python we can ping our DUT with fewer than 50 lines of code.
For the impatient this tutorial is provided as an example with Cocotb. You can run this example from a fresh checkout:
cd examples/ping_tun_tap/tests
sudo make
Note
To create a virtual interface the test either needs root permissions or have CAP_NET_ADMIN capability.
Architecture¶
We have a simple RTL block that takes ICMP echo requests and generates an ICMP echo response. To verify this behaviour we want to run the ping utility against our RTL running in the simulator.
In order to achieve this we need to capture the packets that are created by ping, drive them onto the pins of our DUT in simulation, monitor the output of the DUT and send any responses back to the ping process.
Linux has a TUN/TAP virtual network device which we can use for this purpose, allowing ping to run unmodified and unaware that it is communicating with our simulation rather than a remote network endpoint.
Implementation¶
First of all we need to work out how to create a virtual interface. Python has a huge developer base and a quick search of the web reveals a TUN example that looks like an ideal starting point for our testbench. Using this example we write a function that will create our virtual interface:
import subprocess, fcntl, struct
def create_tun(name="tun0", ip="192.168.255.1"):
TUNSETIFF = 0x400454ca
TUNSETOWNER = TUNSETIFF + 2
IFF_TUN = 0x0001
IFF_NO_PI = 0x1000
tun = open('/dev/net/tun', 'r+b')
ifr = struct.pack('16sH', name, IFF_TUN | IFF_NO_PI)
fcntl.ioctl(tun, TUNSETIFF, ifr)
fcntl.ioctl(tun, TUNSETOWNER, 1000)
subprocess.check_call('ifconfig tun0 %s up pointopoint 192.168.255.2 up' % ip, shell=True)
return tun
Now we can get started on the actual test. First of all we’ll create a clock signal and connect up the Avalon driver and monitor to the DUT. To help debug the testbench we’ll enable verbose debug on the drivers and monitors by setting the log level to logging.DEBUG.
import cocotb
from cocotb.clock import Clock
from cocotb.drivers.avalon import AvalonSTPkts as AvalonSTDriver
from cocotb.monitors.avalon import AvalonSTPkts as AvalonSTMonitor
@cocotb.test()
def tun_tap_example_test(dut):
cocotb.fork(Clock(dut.clk, 5000).start())
stream_in = AvalonSTDriver(dut, "stream_in", dut.clk)
stream_out = AvalonSTMonitor(dut, "stream_out", dut.clk)
# Enable verbose logging on the streaming interfaces
stream_in.log.setLevel(logging.DEBUG)
stream_out.log.setLevel(logging.DEBUG)
We also need to reset the DUT and drive some default values onto some of the bus signals. Note that we’ll need to import the Timer and RisingEdge triggers.
# Reset the DUT
dut._log.debug("Resetting DUT")
dut.reset_n <= 0
stream_in.bus.valid <= 0
yield Timer(10000)
yield RisingEdge(dut.clk)
dut.reset_n <= 1
dut.stream_out_ready <= 1
The rest of the test becomes fairly straightforward. We create our TUN interface using our function defined previously. We’ll also use the subprocess module to actually start the ping command.
We then wait for a packet by calling a blocking read call on the TUN file descriptor and simply append that to the queue on the driver. We wait for a packet to arrive on the monitor by yielding on wait_for_recv() and then write the received packet back to the TUN file descriptor.
# Create our interface (destroyed at the end of the test)
tun = create_tun()
fd = tun.fileno()
# Kick off a ping...
subprocess.check_call('ping -c 5 192.168.255.2 &', shell=True)
# Respond to 5 pings, then quit
for i in range(5):
cocotb.log.info("Waiting for packets on tun interface")
packet = os.read(fd, 2048)
cocotb.log.info("Received a packet!")
stream_in.append(packet)
result = yield stream_out.wait_for_recv()
os.write(fd, str(result))
That’s it - simple!
Further work¶
This example is deliberately simplistic to focus on the fundamentals of interfacing to the simulator using TUN/TAP. As an exercise for the reader a useful addition would be to make the file descriptor non-blocking and spawn out separate coroutines for the monitor / driver, thus decoupling the sending and receiving of packets.
Tutorial: Driver Cosimulation¶
Cocotb was designed to provide a common platform for hardware and software developers to interact. By integrating systems early, ideally at the block level, it’s possible to find bugs earlier in the design process.
For any given component that has a software interface there is typically a software abstraction layer or driver which communicates with the hardware. In this tutorial we will call unmodified production software from our testbench and re-use the code written to configure the entity.
For the impatient this tutorial is provided as an example with Cocotb. You can run this example from a fresh checkout:
cd examples/endian_swapper/tests
make MODULE=test_endian_swapper_hal
Note
SWIG is required to compile the example
Difficulties with Driver Co-simulation¶
Co-simulating un-modified production software against a block-level testbench is not trivial - there are a couple of significant obstacles to overcome:
Calling the HAL from a test¶
Typically the software component (often referred to as a Hardware Abstraction Layer or HAL) is written in C. We need to call this software from our test written in Python. There are multiple ways to call C code from Python, in this tutorial we’ll use SWIG to generate Python bindings for our HAL.
Blocking in the driver¶
Another difficulty to overcome is the fact that the HAL is expecting to call
a low-level function to access the hardware, often something like ioread32
.
We need this call to block while simulation time advances and a value is
either read or written on the bus. To achieve this we link the HAL against
a C library that provides the low level read/write functions. These functions
in turn call into Cocotb and perform the relevant access on the DUT.
Cocotb infrastructure¶
There are two decorators provided to enable this flow, which are typically used
together to achieve the required functionality. The cocotb.external
decorator turns a normal function that isn’t a coroutine into a blocking
coroutine (by running the function in a separate thread). The
cocotb.function
decorator allows a coroutine that consumes simulation time
to be called by a normal thread. The call sequence looks like this:
Implementation¶
Register Map¶
The endian swapper has a very simple register map:
Byte Offset | Register | Bits | Access | Description |
---|---|---|---|---|
0 | CONTROL | 0 | R/W | Enable |
31:1 | N/A | Reserved | ||
4 | PACKET_COUNT | 31:0 | RO | Num Packets |
HAL¶
To keep things simple we use the same RTL from the Tutorial: Endian Swapper. We write a simplistic HAL which provides the following functions:
endian_swapper_enable(endian_swapper_state_t *state);
endian_swapper_disable(endian_swapper_state_t *state);
endian_swapper_get_count(endian_swapper_state_t *state);
These functions call IORD
and IOWR
- usually provided by the Altera
NIOS framework.
IO Module¶
This module acts as the bridge between the C HAL and the Python testbench. It
exposes the IORD
and IOWR
calls to link the HAL against, but also
provides a Python interface to allow the read/write bindings to be dynamically
set (through set_write_function
and set_read_function
module functions).
In a more complicated scenario, this could act as an interconnect, dispatching the access to the appropriate driver depending on address decoding, for instance.
Testbench¶
First of all we set up a clock, create an Avalon Master interface and reset
the DUT. Then we create two functions that are wrapped with the
cocotb.function
decorator to be called when the HAL attempts to perform
a read or write. These are then passed to the IO Module:
@cocotb.function
def read(address):
master.log.debug("External source: reading address 0x%08X" % address)
value = yield master.read(address)
master.log.debug("Reading complete: got value 0x%08x" % value)
raise ReturnValue(value)
@cocotb.function
def write(address, value):
master.log.debug("Write called for 0x%08X -> %d" % (address, value))
yield master.write(address, value)
master.log.debug("Write complete")
io_module.set_write_function(write)
io_module.set_read_function(read)
We can then intialise the HAL and call functions, using the cocotb.external
decorator to turn the normal function into a blocking coroutine that we can
yield
:
state = hal.endian_swapper_init(0)
yield cocotb.external(hal.endian_swapper_enable)(state)
The HAL will perform whatever calls it needs, accessing the DUT through the Avalon-MM driver, and control will return to the testbench when the function returns.
Note
The decorator is applied to the function before it is called
Further Work¶
In future tutorials we’ll consider co-simulating unmodified drivers written
using mmap
(for example built upon the UIO framework) and consider
interfacing with emulators like QEMU to allow us to co-simulate when the
software needs to execute on a different processor architecture.
Troubleshooting¶
Increasing Verbosity¶
If things fail in the VPI/VHPI/FLI area, check your simulator’s documentation to see if it has options to
increase its verbosity about what may be wrong. You can then set these options on the make
command line
as COMPILE_ARGS
, SIM_ARGS
or EXTRA_OPTS
(see Build options and Environment Variables for details).
Attaching a Debugger¶
In order to give yourself time to attach a debugger to the simulator process before it starts to run,
you can set the environment variable COCOTB_ATTACH
to a pause time value in seconds.
If set, Cocotb will print the process ID (PID) to attach to and wait the specified time before
actually letting the simulator run.
For the GNU debugger GDB, the command is attach <process-id>
.
Roadmap¶
Cocotb is in active development.
We use GitHub issues to track our pending tasks. Take a look at the open Enhancements to see the work that’s lined up.
If you have a GitHub account you can also raise an enhancement request to suggest new features.
Simulator Support¶
This page documents any known quirks and gotchas in the various simulators.
Icarus¶
Accessing bits of a vector doesn’t work:
dut.stream_in_data[2] <= 1
See “access_single_bit” test in examples/functionality/tests/test_discovery.py.
Synopsys VCS¶
Aldec Riviera-PRO¶
The $LICENSE_QUEUE environmental variable can be used for this simulator - this setting will be mirrored in the TCL license_queue variable to control runtime license checkouts.
Mentor Questa¶
Mentor Modelsim¶
Any ModelSim-PE or ModelSim-PE derivative (like ModelSim Microsemi, Altera, Lattice Edition) does not support the VHDL FLI feature. If you try to run with FLI enabled, you will see a vsim-FLI-3155 error:
** Error (suppressible): (vsim-FLI-3155) The FLI is not enabled in this version of ModelSim.
ModelSim DE and SE (and Questa, of course) supports the FLI.
Cadence Incisive¶
GHDL¶
Support is preliminary.