Cocotb is a convenient way to write your hardware verification tests in Python. Let’s see how to use it with the OSS toolchain.

The example used in this article is available on Github, so you can see the complete example there.

Installing Dependencies

Once again I will reuse the distrobox container introduced in the first article of this series as the development system.

First, let’s create a project directory somewhere. Let’s place our HDL in src/hdl and our test sources in test. Then let’s create test/requirements.txt with this content:

cocotb==1.9.2
cocotbext-wishbone

This specifies the python packages we want to use in our testbench. Please note that OSS CAD Suite already uses a release candidate of cocotb 2.0. We want to stick to version 1 for now, so we’ll have to downgrade.

Ideally, we would do all our package installation in a Python venv like this:

python3 -m venv .venv
source .venv/bin/activate

Unfortunately, venv support is currently broken in OSS CAD Suite, see issue 138. Using the system python executable is also not possible, see issue 80.

As venvs are currently broken, we will install the dependencies in the system location. Please note that you might have to clean up $HOME/.local/bin and $HOME/.local/lib/python3.* if you already have those libraries installed there.

cd test
python3 -m pip install -r requirements.txt

The Wishbone Slave

The Wishbone slave code is quite simple and is essentially the code presented in the ZipCPU Blog, with signals renamed to match the NEORV32 style. For your convenience, here is the full code:

module neosd (
    input clk_i,
    input rstn_i,

    input[31:0] wb_adr_i,
    input[31:0] wb_dat_i,
    input wb_we_i,
    input[3:0] wb_sel_i,
    input wb_stb_i,
    input wb_cyc_i,

    output reg wb_ack_o,
    output wb_err_o,
    output reg[31:0] wb_dat_o
);
    // Wishbone code based on https://zipcpu.com/zipcpu/2017/05/29/simple-wishbone.html
    wire wb_stall_o;

    always @(posedge clk_i) begin
        if (wb_stb_i && wb_we_i && !wb_stall_o) begin
            // Your write logic here, such as
            // memory[i_addr] <= i_data;
        end
    end

    always @(posedge clk_i) begin
        case (wb_adr_i)
            32'h0: wb_dat_o <= 32'h00000000;
            default: wb_dat_o <= 32'h00000000;
        endcase
    end

    always @(posedge clk_i) begin
        if (rstn_i == 1'b0)
            wb_ack_o <= 1'b0;
        else
            wb_ack_o <= (wb_stb_i && !wb_stall_o);
    end

    assign wb_stall_o = 1'b0;
    assign wb_err_o = 1'b0;

endmodule

The Testbench

With the basic module ready, we have to write the testbench. It consists of two parts: Various wrapper files to set up cocotb and the actual testbench in Python.

Wrapper Files

First, we will need a top-level file in test/tb.v wrapping our Wishbone slave:

`default_nettype none
`timescale 1ns / 1ps

module tb ();

    initial begin
        $dumpfile("tb.vcd");
        $dumpvars(0, tb);
        #1;
    end

    reg clk;
    reg rstn;

    reg[31:0] wb_adr_i;
    reg[31:0] wb_dat_i;
    reg wb_we_i;
    reg[3:0] wb_sel_i;
    reg wb_stb_i;
    reg wb_cyc_i;

    wire wb_ack_o;
    wire wb_err_o;
    wire[31:0] wb_dat_o;

    neosd dut (
        .clk_i(clk),
        .rstn_i(rstn),
    
        .wb_adr_i(wb_adr_i),
        .wb_dat_i(wb_dat_i),
        .wb_we_i(wb_we_i),
        .wb_sel_i(wb_sel_i),
        .wb_stb_i(wb_stb_i),
        .wb_cyc_i(wb_cyc_i),
    
        .wb_ack_o(wb_ack_o),
        .wb_err_o(wb_err_o),
        .wb_dat_o(wb_dat_o)
    );

endmodule

The initial begin block ensures to dump all signals to tb.vcd. The remaining module simply instantiates the neosd Wishbone slave. Top-level signals will be driven by the Python testbench.

In addition, we need the Makefile, which I have adapted from the Tiny Tapeout template:

# Makefile
# See https://docs.cocotb.org/en/stable/quickstart.html for more info

# defaults
SIM ?= icarus
TOPLEVEL_LANG ?= verilog
SRC_DIR = $(PWD)/../src/hdl
PROJECT_SOURCES = neosd.v

# RTL simulation:
SIM_BUILD = sim_build/rtl
VERILOG_SOURCES += $(addprefix $(SRC_DIR)/,$(PROJECT_SOURCES))

# Allow sharing configuration between design and testbench via `include`:
COMPILE_ARGS 		+= -I$(SRC_DIR)

# Include the testbench sources:
VERILOG_SOURCES += $(PWD)/tb.v
TOPLEVEL = tb

# MODULE is the basename of the Python test file
MODULE = test

# include cocotb's make rules to take care of the simulator setup
include $(shell cocotb-config --makefiles)/Makefile.sim

For now, we’ll use iverilog as simulator. It is slower than Verilator but has the huge benefit of supporting high-impedance and undefined signal states. This makes it easier to see if you forgot to properly assign signals, or if your reset logic is broken.

Although not strictly required, a .gitignore file is also quite useful.

The Main Python Test

Now for the interesting part: The main testbench. As already indicated by the requirements.txt file, we will make use of the cocotbext-wishbone library.

The testbench file then first contains all required imports:

import cocotb
from cocotb.clock import Clock
from cocotb.triggers import ClockCycles

from cocotbext.wishbone.driver import WishboneMaster
from cocotbext.wishbone.driver import WBOp

Next, we define the test and log a start message:

@cocotb.test()
async def test_project(dut):
    dut._log.info("Start")

Let’s create a 100 MHz clock:

    # Set the clock to 100 MHz
    clock = Clock(dut.clk, 10, units="ns")
    cocotb.start_soon(clock.start())

We can now configure the cocotbext-wishbone driver:

    wbs = WishboneMaster(dut, "", dut.clk,
        width=32,
        timeout=10,
        signals_dict={"cyc":  "wb_cyc_i",
                    "stb":  "wb_stb_i",
                    "we":   "wb_we_i",
                    "adr":  "wb_adr_i",
                    "datwr":"wb_dat_i",
                    "datrd":"wb_dat_o",
                    "ack":  "wb_ack_o" })

I’m not yet sure how to handle the byte write enable and the error signal with this library.

Then perform the reset sequence:

    # Reset
    dut._log.info("Reset")
    dut.rstn.value = 0
    await ClockCycles(dut.clk, 3)
    dut.rstn.value = 1

As the final step, let’s issue two write requests to the slave. Let’s write 0xabcd1234 to address 0 and 0x1bcd1234 to address 1:

    await wbs.send_cycle([WBOp(0, 0xabcd1234), WBOp(1, 0x1bcd1234)])
    await ClockCycles(dut.clk, 3)

We also wait 3 clock cycles in the end to slightly extend the waveforms.

Running the Simulation

with all the setup in place, running the simulation is simple:

cd test
make

This is the beginning of the output you should get:

rm -f results.xml
"make" -f Makefile results.xml
make[1]: Entering directory '/home/jpfau/Dokumente/Projekte/FPGA/neosd/test'
/opt/oss-cad-suite/bin/iverilog -o sim_build/rtl/sim.vvp -D COCOTB_SIM=1 -s tb -g2012 -I/home/jpfau/Dokumente/Projekte/FPGA/neosd/test/../src/hdl -f sim_build/rtl/cmds.f  /home/jpfau/Dokumente/Projekte/FPGA/neosd/test/../src/hdl/neosd.v /home/jpfau/Dokumente/Projekte/FPGA/neosd/test/tb.v
rm -f results.xml
MODULE=test TESTCASE= TOPLEVEL=tb TOPLEVEL_LANG=verilog \
         /opt/oss-cad-suite/bin/vvp -M /opt/oss-cad-suite/lib/python3.11/site-packages/cocotb/libs -m libcocotbvpi_icarus   sim_build/rtl/sim.vvp  
     -.--ns INFO     gpi                                ..mbed/gpi_embed.cpp:108  in set_program_name_in_venv        Using Python virtual environment interpreter at /opt/oss-cad-suite/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 13.0 (devel)
     0.00ns INFO     cocotb                             Running tests with cocotb v1.9.2 from /opt/oss-cad-suite/lib/python3.11/site-packages/cocotb
     0.00ns INFO     cocotb                             Seeding Python random module with 1741120577
     0.00ns INFO     cocotb.regression                  Found test test.test_project
     0.00ns INFO     cocotb.regression                  running test_project (1/1)
     0.00ns INFO     cocotb.tb                          Start
     0.00ns INFO     cocotb.tb                          Reset
VCD info: dumpfile tb.vcd opened for output.
...

You can then open test/tv.vcd in surfer to view the simulation waveform: Simulated Signal Waveforms