How to Get started with Cocotb

7 min read

Cover Image for 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.

💡
You could learn and practice design problems with QuickSilicon's Hands-On RTL Design course. The course covers different problems from Beginner to Advance Levels.

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.

Python for Design Verification

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!