How to Get started with Cocotb
Introduction
In the fast-paced world of tech, chances are you've stumbled upon Python—a popular programming language. Renowned for its versatility and user-friendly nature, Python finds applications across diverse fields like web development and data science. Nearly every software engineer has likely written a few Python scripts to automate their workflow. A growing number of hardware engineers are making the shift from Perl to Python to fulfill their scripting requirements. This transition reflects Python's rising popularity and its effectiveness in streamlining tasks within hardware development. Many experts believe Python has the potential for a more significant impact in the hardware industry than in software. Python possesses the capability to democratize the RTL Design and Verification domain with its user-friendly nature and extensive library support, actively managed to cater to various needs.
That's precisely why we're eager to launch a series of blogs exploring Python for Design Verification. Join us on this exciting journey as we dive into the potential of Python to transform the verification landscape.
Benefits of Python for Verification in Hardware Design
The design verification space shares many similarities with the testing challenges encountered in the software industry. Our verification methodologies draw significant inspiration from the software world, and we've successfully adapted proven practices from software testing to enhance our processes. Our verification engineers can design Testbenches in SystemVerilog following the best coding practices. However, using SystemVerilog for verification has few challenges:
New graduates often encounter discomfort with the language syntax, leading to a learning curve when utilizing SystemVerilog for verification.
Simulating all verification features of the language reliably requires access to a commercial simulator, presenting a significant challenge.
This is why experts believe Python could facilitate a significant "shift left" in the Verification industry. With an abundance of freely available online resources, individuals can quickly learn Python and utilize open-source simulators to simulate testbenches. With a lot of us having a hands-on design verification engineer we would not only design Testbenches quickly but would be more confident in closing our designs with coverage.
So the question is - how can we use Python for Design Verification. Here's where the python library called "Cocotb" comes into action.
Cocotb
Cocotb is (as shared on their Github page):
cocotb is a coroutine based co-simulation library for writing VHDL and Verilog testbenches in Python.
Simply put, it allows us to design Testbenches using Python (and all of its helpful features) and does all of the dirty work of integrating with simulators in the backend. Leaving the engineers to focus on writing the testcases to hit those tricky corner bugs while the rest is handled by the library. Like I said before we could easily get started it with once we have the following tools installed in our System (I've successfully installed all of this on MacBook Air M1 and as well as on Ubuntu Machine):
Python 3.6+
GNU Make 3+
An HDL simulator
- I've successfully tested it on iverilog and Verilator
Once we have the above tools installed, we could simply do the following to get the cocotb python library:
pip install cocotb
That's it, we could now start experimenting with Cocotb by Verifying a D flip-flop. Let's take a quick look at the RTL:
// _Dff_
module dff (
input logic clk,
input logic d,
output logic q
);
always_ff @(posedge clk) begin
q <= d;
end
endmodule
The above RTL implements a standard D-ff using the d
pin as the input port and the q
pin as the output. This is implemented using the always_ff
block using the non-blocking assignments. Let's try using the Python based testbench to verify this design.
The first thing we need to do in order to test the design is to generate stimulus and the clock. Using cocotb, we could easily use the random
python library and then get the stimulus generated randomly. The clock on the other hand could be generated by using the already available cocotb.clock
python class. Here's a snippet of how it is done:
# test_dff.py
import random
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
@cocotb.test()
async def dff_simple_test(dut):
"""Test that d propagates to q"""
# Set initial input value to prevent it from floating
dut.d.value = 0
clock = Clock(dut.clk, 10, units="us") # Create a 10us period clock on port clk
# Start the clock. Start it low to avoid issues on the first RisingEdge
cocotb.start_soon(clock.start(start_high=False))
# Synchronize with the clock. This will regisiter the initial `d` value
await RisingEdge(dut.clk)
expected_val = 0 # Matches initial input value
for i in range(10):
val = random.randint(0, 1)
Alright, so there are multiple things which have happened above. We've imported bunch of cocotb classes and the python random class and have also created a async def
called dff_simple_test
decorated with @cocotb.test()
. While using cocotb, a simple python function can be treated as a test. In order to do so, we just need to add the @cocotb.test()
decorator for the function. That would allow cocotb to treat that function as a test and would be automatically run when the simulation starts. The next thing to note is that the function takes dut
object as an argument. In cocotb the dut
object basically consists the handle to the top module of the design (or testbench) and could be used to either sample signals from the design or drive signals. That is what is achieved when we drive the d
input pin to a value of 0. Since we have already imported the cocotb.clock
python class, we create the clock with 10us
period. However, this just creates the clock but doesn't start it and hence needs to be called using the following:
clock = Clock(dut.clk, 10, units="us") # Create a 10us period clock on port clk
# Start the clock. Start it low to avoid issues on the first RisingEdge
cocotb.start_soon(clock.start(start_high=False))
Having set the initial value to the d
pin and starting the clock
, we could wait for the rising edge of the clock and then start driving inputs. Since we have already imported the random
library, we use the randint
between 0
and 1
to drive random input values to the d
pin (as the d
pin is only a single bit signal). This is done for 10
clock cycles by using a for
loop. Note that once the input is driven the await
statement is added to allow the clock to tick to the next rising edge of the cycle. This makes sure that the for
loop drives all the input on the rising edge of the clock. Here's the look at the complete testbench code:
# test_dff.py
import random
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
@cocotb.test()
async def dff_simple_test(dut):
"""Test that d propagates to q"""
# Set initial input value to prevent it from floating
dut.d.value = 0
clock = Clock(dut.clk, 10, units="us") # Create a 10us period clock on port clk
# Start the clock. Start it low to avoid issues on the first RisingEdge
cocotb.start_soon(clock.start(start_high=False))
# Synchronize with the clock. This will regisiter the initial `d` value
await RisingEdge(dut.clk)
for i in range(10):
val = random.randint(0, 1)
dut.d.value = val # Assign the random value val to the input port d
await RisingEdge(dut.clk)
# Check the final input on the next clock
await RisingEdge(dut.clk)
Great, we now have the RTL design and the Testbench coded. The next thing is to compile both together and let cocotb simulate the design along with the testbench. For this, cocotb already provides a set of makefiles
which are created for various simulators and can be used for managing the infra on how the design and testbench are simulated. Here's a look at the makefile which would allow us to compile both the testbench and design and simulate it using cocotb:
# Makefile
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = $(shell pwd)/dff.sv
TOPLEVEL = dff
MODULE = test_dff
include $(shell cocotb-config --makefiles)/Makefile.sim
Since most of the setup is already available in the Makefile.sim
file, we just needed to pass the VERILOG_SOURCES
, TOPLEVEL_LANG
, MODULE
and the TOPLEVEL
makefile variables to have the simulation setup. Now, we could just run it by doing the following on the terminal:
make SIM=icarus
That's it - the above would simulate the design using the python testbench. In the next post we would add how can we monitor the RTL outputs and also check whether the output matches the TB expectation. Until then try out the above design and testbench and share your output in the comment section!
Subscribe to my newsletter
Read articles from QuickSilicon directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
QuickSilicon
QuickSilicon
A unique and unparalleled platform to improve your hardware design skills. Learn new concepts, implement the RTL and verify it - all in one streamlined platform.