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
at what points in time in the simulation to call the
_recv()
function, andwhat 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)