Verilog RTL verification
CoCoTB based verification: A small example
Introduction to CoCoTB: A Simple Testbench Example
Introduction
CoCoTB (Coroutine-based Co-simulation Test Bench) is an open-source Python library used for verification and testing of hardware designs. It provides a framework for writing testbenches using Python coroutines, allowing easy integration of simulation and test automation. In this blog post, we will explore a small example of a CoCoTB testbench simulated using free simulator icarus and understand the key concepts and features it offers.
Installation of Python, CoCoTB and icarus and its dependencies is not described in this blog post. Detailed installation instructions can be found in the official CoCoTB documentation (https://docs.cocotb.org/) and on many Youtube videos ( e.g. my favorite is: cocotb tutorial Part 0 : Setting the environment)
Directory structure and files
All files are in two directories:
<your starting directory>/hdl
<your starting directory>/tests
<your starting directory>/tests/wrappers
Here is the list of all the files used in this example:
<your starting directory>/hdl/SimpleDUT.v
<your starting directory>/tests/SimpleDUT_test.py
<your starting directory>/tests/Makefile
<your starting directory>/tests/wrappers/SimpleDUT_test.v
Here is a source code of all files explained
SimpleDUT.v (The DUT):
Our example involves a simple DUT with a single input 4-bit signal "din" and an output 4-bit signal "dout". The function of the DUT is simple: "dout" is always equal to "din".
There is a “wrapper” for the DUT (SimpleDUT_test.v) just to add a capability to collect VCD file (SimpleDUT.vcd) to later display waveforms of verification/simulation run:
A Python/CoCoTB testbench integrated with a self-checking test: SimpleDUT_test.py
Testbench/self-checking test functionality is simple:
on every falling edge of the clock we apply an input value to the DUT in a range of values 0-15.
further on every rising edge of the clock we verify that the output of DUT is the same as the input applied.
A waveform detail of the verification run:
Makefile
A commands to execute the verification/simulation run:
cd <your starting directory>/tests/
make SimpleDUT
Simulation log file
rm -rf sim_build
make sim MODULE=SimpleDUT_test TOPLEVEL=SimpleDUT_test
make[1]: Entering directory '<your starting directory>/tests'
rm -f results.xml
make -f Makefile results.xml
make[2]: Entering directory '<your starting directory>/tests'
mkdir -p sim_build
/usr/bin/iverilog -o sim_build/sim.vvp -D COCOTB_SIM=1 -s SimpleDUT_test -f sim_build/cmds.f -g2012 <your starting directory>/tests/../hdl/or_gate.v <your starting directory>/tests/wrappers/or_test.v <your starting directory>/tests/wrappers/ifc_test.v <your starting directory>/tests/../hdl/ifc_or.v <your starting directory>/tests/../hdl/FIFO1.v <your starting directory>/tests/../hdl/FIFO2.v <your starting directory>/tests/../hdl/SimpleDUT.v <your starting directory>/tests/wrappers/SimpleDUT_test.v
rm -f results.xml
MODULE=SimpleDUT_test TESTCASE= TOPLEVEL=SimpleDUT_test TOPLEVEL_LANG=verilog \
/usr/bin/vvp -M <your starting directory>/venv/lib/python3.11/site-packages/cocotb/libs -m libcocotbvpi_icarus sim_build/sim.vvp
-.--ns INFO gpi ..mbed/gpi_embed.cpp:105 in set_program_name_in_venv Using Python virtual environment interpreter at <your starting directory>/venv/bin/python
-.--ns INFO gpi ../gpi/GpiCommon.cpp:101 in gpi_print_registered_impl VPI registered
0.00ns INFO cocotb Running on Icarus Verilog version 11.0 (stable)
0.00ns INFO cocotb Running tests with cocotb v1.8.0 from <your starting directory>/venv/lib/python3.11/site-packages/cocotb
0.00ns INFO cocotb Seeding Python random module with 1687439769
0.00ns INFO cocotb.regression Found test SimpleDUT_test.test_simple_dut
0.00ns INFO cocotb.regression running test_simple_dut (1/1)
0.00ns INFO cocotb.SimpleDUT_test Running stimulus...
<your starting directory>/tests/SimpleDUT_test.py:26: DeprecationWarning: Setting values on handles using the ``dut.handle = value`` syntax is deprecated. Instead use the ``handle.value = value`` syntax
dut.din = i
0.00ns INFO cocotb.SimpleDUT_test Checking output...
155.00ns INFO cocotb.SimpleDUT_test Stimulus complete.
160.00ns INFO cocotb.SimpleDUT_test Output check passed.
160.00ns INFO cocotb.regression test_simple_dut passed
160.00ns INFO cocotb.regression ****************************************************************************************
** TEST STATUS SIM TIME (ns) REAL TIME (s) RATIO (ns/s) **
****************************************************************************************
** SimpleDUT_test.test_simple_dut PASS 160.00 0.00 52059.02 **
****************************************************************************************
** TESTS=1 PASS=1 FAIL=0 SKIP=0 160.00 0.03 5276.97 **
****************************************************************************************
VCD info: dumpfile SimpleDUT.vcd opened for output.
VCD warning: $dumpvars: Unsupported argument type (vpiPackage)
make[2]: Leaving directory '<your starting directory>/tests'
make[1]: Leaving directory '<your starting directory>/tests'
The Test Flow
The test flow is orchestrated using coroutines, which are functions that can be paused and resumed asynchronously. The testbench comprises three coroutines: "stimulus", "check_output", and "test_simple_dut". Let's examine each of them in detail.
The "stimulus" Coroutine:
The "stimulus" coroutine is responsible for driving inputs to the DUT's "din" signal. In each iteration of the loop, it assigns a value to "din" and waits for a falling edge of the clock signal using the "FallingEdge" trigger. This ensures synchronization with the DUT's clock. The stimulus generation continues for 16 iterations, simulating all possible input scenarios.
The "check_output" Coroutine:
The "check_output" coroutine verifies the DUT's output by comparing it with the expected values. It uses the "RisingEdge" trigger to wait for rising edges of the clock signal, ensuring synchronization. Within the loop, it checks if the output "dout" matches the expected value. If not, it raises a "TestFailure" exception, indicating a mismatch.
The "test_simple_dut" Coroutine:
The "test_simple_dut" coroutine serves as the main test function annotated with the "@cocotb.test()" decorator. It sets up the clock, starts the "stimulus" coroutine, and then awaits the completion of the "check_output" coroutine. This ensures that the test execution pauses until the output verification is complete.
Some CoCoTB testbench features/building blocks
For example, you can use await RisingEdge(signal) to wait until the specified signal experiences a rising edge before proceeding to the next step in your test.
RisingEdge: Waits for the rising edge of a signal to occur before proceeding.
Edge: Waits for any edge (rising or falling) of a signal to occur before proceeding.
The Timer class is a trigger that allows you to introduce a delay or wait for a specific amount of simulation time before proceeding to the next step in the testbench.
e.g.
from cocotb.triggers import Event
Event: A trigger that can be used for synchronization between different parts of the testbench. It can be used to signal or wait for events to occur.
The Event trigger is useful for synchronizing different parts of the testbench, allowing one coroutine or process to wait until another process triggers the event.
e.g. The Event trigger basic functionality example
Here are the most common functions/methods associated with the Event class in Cocotb:
Event() - Constructor: Creates an instance of the Event class.
Event.is_set() - Method: Returns a boolean indicating whether the event is currently set (True) or unset (False).
Event.wait() - Method: Suspends the execution of a coroutine until the event is set. If the event is already set, the coroutine continues immediately.
Event.set() - Method: Sets the event, allowing any coroutines waiting on it to proceed.
Event.clear() - Method: Resets the event to an unset state. Any coroutines waiting on the event will be suspended until the event is set again.
e.g. The Event trigger usage of Event.wait(),Event.set(), Event.clear()
Result:
Coroutine: Waiting for event...
Coroutine: Event is set!
Coroutine: Event is set again!
from cocotb.triggers import NextTimeStep
NextTimeStep: Advances to the next simulation time step, allowing you to synchronize with the next delta cycle.
e.g. the my_test coroutine will perform some actions and then wait for the next simulation time step using the NextTimeStep trigger.
This ensures that any changes or updates that occur at the current time step have taken effect before proceeding with the subsequent steps.
from cocotb.triggers import Combine
Combine: Combines multiple triggers into a single trigger.
It waits for all the constituent triggers to occur before proceeding.
e.g. the test function my_test creates two individual triggers: trigger1 that waits for a rising edge of the clk signal and trigger2 that introduces a 100 nanosecond delay using the Timer trigger.
The Combine trigger is then used to combine trigger1 and trigger2 into a single trigger called combined_trigger.
The await statement is used to wait for the combined trigger to occur.
This means the test will proceed only when both the rising edge of clk and the 100 nanosecond delay have happened.
from cocotb.triggers import ReadOnly
ReadOnly: Temporarily releases control to the simulator, allowing other processes to execute before resuming the current process.
It is particularly useful in situations where you want to allow other parts of the testbench to make progress or perform certain actions before continuing with the current process.
e.g. The most generic usage case of ReadOnly
e.g. The more elaborate usage case of ReadOnly
from cocotb.triggers import Lock
Lock: Provides a mechanism to acquire and release locks for synchronization purposes.
It allows you to control access to shared resources and ensure that only one process or coroutine can execute a critical section of code at a time.
e.g. Lock used to synchronize access to a critical section of code
from cocotb.triggers import Join
Join: Combines multiple triggers and waits for any of the constituent triggers to occur before proceeding.
Decorator of functions
@cocotb.test()
The line @cocotb.test() is a decorator in Python that is used to mark a function as a test case for Cocotb
When a function is decorated with @cocotb.test(), it indicates that the function is a test case that should be executed by Cocotb during the simulation
@cocotb.coroutine
In Cocotb, the @cocotb.coroutine decorator is used to mark a function as a coroutine
Coroutines in Cocotb allow you to write asynchronous code that can be paused and resumed based on various triggers or events.
It enables the use of await and other asynchronous features within the function(e.g. cocotb.fork(), cocotb.wait(), cocotb.join(), yield ).
@cocotb.decorators.external
@cocotb.decorators.external: This decorator marks a function as an external function that can be called from within a test.
It is used when you want to invoke functions defined outside the test scope, such as utility functions or interface functions.
Driver
BusDriver
e.g.MyBusDriver is a custom driver derived from BusDriver. It inherits the _driver_send and _driver_recv methods, which define the behavior of the driver during send and receive operations
DeprecationWarning: cocotb.fork has been deprecated in favor of cocotb.start_soon and cocotb.start
cocotb.fork() : current coroutine can continue executing without waiting for the forked coroutine to complete.
When you use cocotb.start_soon(), the coroutine is started, but the execution of the current coroutine will be paused until the started coroutine completes.
This means that the current coroutine will wait for the started coroutine to finish before proceeding.
Conclusion
In this blog post, we explored a small example of a CoCoTB testbench. Also we learned about some CoCoTB features/building blocks included but not limited to: coroutines, triggers, and how to use CoCoTB to drive inputs, verify outputs, and synchronize testbench components. CoCoTB provides a powerful and flexible framework for hardware verification and testing, making it easier to write effective testbenches using Python.
© 2023 ASIC Stoic. All rights reserved.