coco-tb is a Python-based, platform- and simulator-agnostic testbench framework for digital hardware.1 It’s a modern, reusable way to test the correctness of designs.
Quick links:
Basics
Each test is defined as an asynchronous Python function, annotated with the @cocotb.test()
decorator. All tests are discovered if they’re annotated (much like how Cargo works), and they all take an input object dut
that allows us to access the internals of the design (i.e., signals, ports, parameters). The decorator also supports a few input parameters:
- Timeout parameters
- Expected fails
Tests can call other Python functions (like to drive particular signals). Ideally these too should be async
so we can do things concurrently. Tests run sequentially, from start to finish. We use await
to suspend execution of the test/function (like the half period of a square wave) then return control back to the test.
- The
async
functionstart()
schedules the coroutine for concurrent execution, then resumes the calling task. And:start_soon()
schedules the new coroutine for future execution.- We can also
kill()
a task before they terminate. - Note that coroutines can run indefinitely with a
while True
.
The methods of the dut
object are:
.<SIGNAL_NAME>
— we can get a reference to a specific port..value
— allows us to read or write a signal value.- Note that this doesn’t apply the write immediately, much like HDLs. It delays until the next write cycle. We can force this with
sig.setimmediatevalue(val)
. - Assignment makes no assumptions about the signedness of the signal, so it takes a valid range between the minimum signed number to the maximum unsigned number.
- Note that this doesn’t apply the write immediately, much like HDLs. It delays until the next write cycle. We can force this with
Signals have a few operations too:
- Forcing and freezing signal values — to remain fixed until a particular time.
Force(val)
,Release()
,Freeze()
. Release essentially unlocks it while maintaining this value.
Finally, tests will fail if any assert
fails. With Pytest, a stack trace is printed where the assert fails. Otherwise, the test will probably pass.
Build
There are two main approaches used to run coco-tb tests. The first is via an extension of traditional Makefiles, and the other is via a Pytest runner interface. In both cases, they specify important information about the design/test, like the simulator (SIM
), design language (TOPLEVEL_LANG
), source files (VERILOG_SOURCES
, VERILOG_INCLUDE_DIRS
), top-level modules (TOPLEVEL
), and the test file (MODULE
). We can also specify nice-to-haves, like timescale precision (HDL_TIMEUNIT
and HDL_TIMEPRECISION
), arguments, and visual waveforms (GUI
and WAVES
).
For the Makefile, we just run make
. This will compile and run the unit test. Standard make
options may be unsupported (like parallel compilation). clean
still works. The Pytest runner seems to be the preferred way to do things, but it’s still an experimental feature.
Triggers
In general, we can await for most things in the cocotb.triggers
library. We can also await any stock Python coroutines.
Edge(sig)
— any edge change.RisingEdge(sig)
,FallingEdge(sig)
ClockCycles(sig, num_cycles, rising=True)
— fires after a certain number of clock transitions.Timer(val, units='step')
— after a specified time period has passed. By default, this takes the value ofHDL_TIMEPRECISION
, but we can overload with a particular time unit.
Footnotes
-
The official documentation suggests not using Windows. This suggestion does not matter. ↩