cocotb 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.. cocotb primarily functions by using coroutines.
Quick links:
Installation:
choco install make
choco install iverilog
uv init
uv add cocotbRunning:
venv\Scripts\Activate.ps1
makeBasics
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). It should be a regular synchronous function if we need it at one particular moment, and it should be async if we want to do it concurrently (i.e., we want to drive a signal in the background).
Tests run sequentially, from start to finish. We use await to suspend execution of the test/function until a trigger happens then return control back to the test.
- The
asyncfunctioncocotb.start(function(param1, param2))schedules the coroutine for concurrent execution, then resumes the calling task. And:start_soon()schedules the new coroutine for future execution.- Note that coroutines can run indefinitely with a
while True.
- Triggers can include timers and signal edges (
Edge,RisingEdge,FallingEdge). We pass in the parameter from thedut— i.e., we canawait RisingEdge(dut.clk)to wait for the rising edge of thedut’s clock.
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
_log—
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.
Resources
https://0bab1.github.io/BRH/posts/TIPS_FOR_COCOTB/
Footnotes
-
The official documentation suggests not using Windows. This suggestion does not matter. ↩