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 function start() 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.

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 of HDL_TIMEPRECISION, but we can overload with a particular time unit.

Footnotes

  1. The official documentation suggests not using Windows. This suggestion does not matter.