Testbench Tools

Buses

Buses are simply defined as collection of signals. The Bus class will automatically bundle any group of signals together that are named similar to dut.<bus_name><separator><signal_name>. For instance,

dut.stream_in_valid
dut.stream_in_data

have a bus name of stream_in, a separator of _, and signal names of valid and data. A list of signal names, or a dictionary mapping attribute names to signal names is also passed into the Bus class. Buses can have values driven onto them, be captured (returning a dictionary), or sampled and stored into a similar object.

stream_in_bus = Bus(dut, "stream_in", ["valid", "data"])  # '_' is the default separator

Driving Buses

Examples and specific bus implementation bus drivers (AMBA, Avalon, XGMII, and others) exist in the Driver class enabling a test to append transactions to perform the serialization of transactions onto a physical interface. Here is an example using the AvalonST bus driver in the endian_swapper example:

from cocotb_bus.drivers.avalon import AvalonST as AvalonSTDriver


class EndianSwapperTB(object):

    def __init__(self, dut, debug=False):
        self.dut = dut
        self.stream_in = AvalonSTDriver(dut, "stream_in", dut.clk)


async def run_test(dut, data_in=None, config_coroutine=None, idle_inserter=None,
                   backpressure_inserter=None):

    cocotb.start_soon(Clock(dut.clk, (5000, "ns")).start())
    tb = EndianSwapperTB(dut)

    await tb.reset()
    dut.stream_out_ready.value = 1

    if idle_inserter is not None:
        tb.stream_in.set_valid_generator(idle_inserter())

    # Send in the packets
    for transaction in data_in():
        await tb.stream_in.send(transaction)

Monitoring Buses

For our testbenches to actually be useful, we have to monitor some of these buses, and not just drive them. That’s where the Monitor class comes in, with pre-built monitors for Avalon and XGMII buses. The Monitor class is a base class which you are expected to derive for your particular purpose.

You must create a _monitor_recv() function which is responsible for determining

  1. at what points in time in the simulation to call the _recv() function, and

  2. what transaction values to pass to be stored in the monitor’s receiving queue.

Monitors are good for both outputs of the DUT for verification, and for the inputs of the DUT, to drive a test model of the DUT to be compared to the actual DUT. For this purpose, input monitors will often have a callback function passed that is a model. This model will often generate expected transactions, which are then compared using the Scoreboard class.

class BitMonitor(Monitor):
    """Observe single input or output of DUT."""

    def __init__(self, name, signal, clock, callback=None, event=None):
        self.name = name
        self.signal = signal
        self.clock = clock
        Monitor.__init__(self, callback, event)

    async def _monitor_recv(self):
        clkedge = RisingEdge(self.clock)

        while True:
            # Capture signal at rising edge of clock
            await clkedge
            vec = self.signal.value
            self._recv(vec)


def input_gen():
    """Generator for input data applied by BitDriver"""
    while True:
        yield random.randint(1,5), random.randint(1,5)


class DFF_TB(object):
    def __init__(self, dut, init_val):

        self.dut = dut

        # Create input driver and output monitor
        self.input_drv = BitDriver(dut.d, dut.c, input_gen())
        self.output_mon = BitMonitor("output", dut.q, dut.c)

        # Create a scoreboard on the outputs
        self.expected_output = [ init_val ]

        # Reconstruct the input transactions from the pins
        # and send them to our 'model'
        self.input_mon = BitMonitor("input", dut.d, dut.c, callback=self.model)

    def model(self, transaction):
        """Model the DUT based on the input transaction."""
        # Do not append an output transaction for the last clock cycle of the
        # simulation, that is, after stop() has been called.
        if not self.stopped:
            self.expected_output.append(transaction)

Tracking Testbench Errors

The Scoreboard class is used to compare the actual outputs to expected outputs. Monitors are added to the scoreboard for the actual outputs, and the expected outputs can be either a simple list, or a function that provides a transaction.

Here is some code from the dff example, similar to the above, with the scoreboard added.

class DFF_TB(object):
    def __init__(self, dut, init_val):
        self.dut = dut

        # Create input driver and output monitor
        self.input_drv = BitDriver(dut.d, dut.c, input_gen())
        self.output_mon = BitMonitor("output", dut.q, dut.c)

        # Create a scoreboard on the outputs
        self.expected_output = [ init_val ]
        self.scoreboard = Scoreboard(dut)
        self.scoreboard.add_interface(self.output_mon, self.expected_output)

        # Reconstruct the input transactions from the pins
        # and send them to our 'model'
        self.input_mon = BitMonitor("input", dut.d, dut.c, callback=self.model)