Skip to content

Commit

Permalink
Merge branch 'schwarz-modelling_mi_transactions_with_object' into 'de…
Browse files Browse the repository at this point in the history
…vel'

Modelování, posílání a přijímání transakcí v MI driveru, monitoru a testu přes objekt

See merge request ndk/ndk-fpga!70
  • Loading branch information
jakubcabal committed Jan 2, 2025
2 parents e1e975f + a82696f commit b89fbe1
Show file tree
Hide file tree
Showing 10 changed files with 620 additions and 311 deletions.
127 changes: 72 additions & 55 deletions comp/mi_tools/pipe/cocotb/cocotb_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,48 @@
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, ClockCycles
from drivers import MIMasterDriverTB as MIMasterDriver
from drivers import MISlaveDriverTB as MISlaveDriver
from cocotbext.ofm.mi.monitors import MIMasterMonitor, MISlaveMonitor
from cocotbext.ofm.mi.drivers import MIRequestDriverAgent, MIResponseDriverAgent
from cocotbext.ofm.mi.proxymonitor import MIProxyMonitor
from cocotbext.ofm.mi.monitors import MIMonitor
from cocotbext.ofm.ver.generators import random_packets
from cocotbext.ofm.utils.math import ceildiv
from cocotb_bus.drivers import BitDriver
from cocotb_bus.scoreboard import Scoreboard
from scoreboard import Scoreboard
from cocotbext.ofm.mi.transaction import MiRequestTransaction, MiResponseTransaction, MiTransaction, MiTransactionType
from cocotb.binary import BinaryValue
from cocotbext.ofm.utils.signals import filter_bytes_by_bitmask

import itertools
from random import choice, randint


class testbench():
def __init__(self, dut, debug=False):
self.dut = dut
self.master_stream_in = MIMasterDriver(dut, "IN", dut.CLK)
self.master_stream_out = MIMasterMonitor(dut, "OUT", dut.CLK)
self.slave_stream_in = MISlaveDriver(dut, "OUT", dut.CLK)
self.slave_stream_out = MISlaveMonitor(dut, "IN", dut.CLK)
self.request_stream_in = MIRequestDriverAgent(dut, "IN", dut.CLK)
self.response_stream_out = MIMonitor(dut, "OUT", dut.CLK)
self.response_proxy = MIProxyMonitor(self.response_stream_out, MiTransactionType.Request)
self.response_stream_in = MIResponseDriverAgent(dut, "OUT", dut.CLK)
self.request_stream_out = MIMonitor(dut, "IN", dut.CLK)
self.request_proxy = MIProxyMonitor(self.request_stream_out, MiTransactionType.Response)

self.backpressure = BitDriver(dut.OUT_ARDY, dut.CLK)

# Create a scoreboard on the master_stream_out bus
# Create a scoreboard on the response_stream_out bus
self.pkts_sent = 0
self.expected_output_master = []
self.expected_output_slave = []
self.expected_output = []
self.scoreboard = Scoreboard(dut)
self.scoreboard.add_interface(self.master_stream_out, self.expected_output_master)
self.scoreboard.add_interface(self.slave_stream_out, self.expected_output_slave)
self.scoreboard.add_interface(self.response_proxy, self.expected_output)
self.scoreboard.add_interface(self.request_proxy, self.expected_output)

if debug:
self.master_stream_in.log.setLevel(cocotb.logging.DEBUG)
self.master_stream_out.log.setLevel(cocotb.logging.DEBUG)
self.request_stream_in.log.setLevel(cocotb.logging.DEBUG)
self.response_stream_out.log.setLevel(cocotb.logging.DEBUG)

def model(self, transaction: bytes, testcase: int):
def model(self, test_trans: MiTransaction):
"""Model the DUT based on the input transaction"""
if testcase == 0:
self.expected_output_slave.append(transaction)
else:
self.expected_output_master.append(transaction)

self.expected_output.append(test_trans)
self.pkts_sent += 1

async def reset(self):
Expand All @@ -59,58 +62,72 @@ async def reset(self):
async def run_test(dut, pkt_count: int = 1000, item_width_min: int = 1, item_width_max: int = 32):
# Start clock generator
cocotb.start_soon(Clock(dut.CLK, 5, units='ns').start())
tb = testbench(dut)
tb = testbench(dut, debug=False)
await tb.reset()

tb.backpressure.start((1, i % 5) for i in itertools.count())

for testcase in range(2):
"""two test cases - read = 0, write = 1"""
item_count = 0
trans_cntr = 0

item_count = 0
for transaction in random_packets(item_width_min, item_width_max, pkt_count):
trans_cntr += 1
request_type = choice([MiTransactionType.Request, MiTransactionType.Response])
start_addr = randint(0, 2**(tb.request_stream_in.addr_width*8)-1)

for transaction in random_packets(item_width_min, item_width_max, pkt_count):
left_to_enable = len(transaction)
addr = start_addr
offset_transaction = transaction
byte_enable = BinaryValue(2**len(transaction) - 1)

cycles = (len(transaction) + (tb.master_stream_in.data_width - 1)) // tb.master_stream_in.data_width
start_offset = addr % tb.request_stream_in.data_width
end_offset = -(addr + len(offset_transaction)) % tb.request_stream_in.data_width

item_count += cycles
offset_transaction = bytes(start_offset) + offset_transaction + bytes(end_offset)
byte_enable = BinaryValue(("0" * start_offset) + byte_enable.binstr + ("0" * end_offset))
addr = addr - start_offset

for i in range(cycles):
if left_to_enable > tb.master_stream_in.data_width:
byte_enable = 2**tb.master_stream_in.data_width - 1
"""setting all bytes of byte enable to 1"""
cycles = ceildiv(bus_width=tb.request_stream_in.data_width, transaction_len=len(offset_transaction))

left_to_enable -= tb.master_stream_in.data_width
for i in range(cycles):
data = offset_transaction[i*tb.request_stream_in.data_width:(i+1)*tb.request_stream_in.data_width]

else:
byte_enable = 2**left_to_enable - 1
be_slice = BinaryValue(byte_enable.binstr[i*tb.request_stream_in.data_width:(i+1)*tb.request_stream_in.data_width][::-1], bigEndian=False).integer
enabled_data = filter_bytes_by_bitmask(data, be_slice)

byte_enable = 15 if testcase == 0 else byte_enable
if len(enabled_data) == 0:
continue

tb.model((int.from_bytes(transaction[0:tb.master_stream_in.addr_width], 'little') + i * tb.master_stream_in.addr_width, int.from_bytes(transaction[i * tb.master_stream_in.data_width:(i + 1) * tb.master_stream_in.data_width], 'little'), byte_enable), testcase)
test_trans = MiTransaction()
test_trans.trans_type = request_type
test_trans.addr = addr + i*tb.request_stream_in.addr_width
test_trans.data = int.from_bytes(enabled_data, 'little')
test_trans.be = be_slice
tb.model(test_trans)
item_count += 1

if testcase == 0:
"""read test"""
cocotb.log.debug(f"generated transaction: {transaction.hex()}")
tb.master_stream_in.append(("rd", transaction))
tb.slave_stream_in.append(("rd", transaction))
request_trans = MiRequestTransaction()
request_trans.trans_type = request_type
request_trans.addr = start_addr
request_trans.data = transaction
request_trans.data_len = len(transaction)

else:
"""write test"""
cocotb.log.debug(f"generated transaction: {transaction.hex()}")
tb.master_stream_in.append(("wr", transaction))
tb.slave_stream_in.append(("wr", transaction))
response_trans = MiResponseTransaction()
response_trans.trans_type = request_type
response_trans.data = offset_transaction
response_trans.be = byte_enable.integer

last_num = 0
cocotb.log.debug(f"generated transaction: {transaction.hex()}")
tb.request_stream_in.append(request_trans)
tb.response_stream_in.append(response_trans)

stream_out = tb.slave_stream_out if testcase == 0 else tb.master_stream_out
transaction_type = "read" if testcase == 0 else "write"
last_num = 0
stream_out_item_cnt = tb.response_proxy.item_cnt + tb.request_proxy.item_cnt

while stream_out.item_cnt < item_count:
if stream_out.item_cnt // 1000 > last_num:
last_num = stream_out.item_cnt // 1000
cocotb.log.info(f"Number of {transaction_type} transactions processed: {stream_out.item_cnt}/{item_count}")
await ClockCycles(dut.CLK, 100)
while stream_out_item_cnt < item_count:
if stream_out_item_cnt // 1000 > last_num:
last_num = stream_out_item_cnt // 1000
cocotb.log.info(f"Number of transactions processed: {stream_out_item_cnt}/{item_count}")
await ClockCycles(dut.CLK, 100)
stream_out_item_cnt = tb.response_proxy.item_cnt + tb.request_proxy.item_cnt

raise tb.scoreboard.result
71 changes: 71 additions & 0 deletions comp/mi_tools/pipe/cocotb/scoreboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# scoreboard.py: Custom scoreboard for MI Pipe Test
# Copyright (C) 2024 CESNET z. s. p. o.
# Author(s): Ondřej Schwarz <[email protected]>
#
# SPDX-License-Identifier: BSD-3-Clause

from cocotb_bus.scoreboard import Scoreboard as BaseScoreboard
from cocotbext.ofm.mi.transaction import MiTransaction
from cocotb.utils import hexdiffs
from cocotb.result import TestFailure


class Scoreboard(BaseScoreboard):
"""Custom scoreboard for MI Pipe Test"""

def __init__(self, dut, reorder_depth=0, fail_immediately=True):
super().__init__(dut, reorder_depth=reorder_depth, fail_immediately=fail_immediately)

def compare(self, got: MiTransaction, exp: MiTransaction, log, strict_type=True):
"""Custom compare function for MI Pipe Test
Args:
got: object from MiTransaction class passed by the monitor
exp: object from MiTransaction class passed by the test
log: logging object
strict_type: if type of the transaction should be compared
"""

# Type comparison
if strict_type and type(got) != type(exp):
self.errors += 1
log.error("Received transaction type is different than expected")
log.info("Received: %s but expected %s" %
(str(type(got)), str(type(exp))))
if self._imm:
raise TestFailure("Received transaction of wrong type. "
"Set strict_type=False to avoid this.")
return

elif not strict_type:
raise NotImplementedError("Non-strict type not implemented for MI Pipe Test.")

# Comparing modeled and received values
strgot, strexp = [], []

if got.trans_type != exp.trans_type:
return

for i in ["addr", "data", ("be", "byte_enable")]:
item, text = i if isinstance(i, tuple) else (i, i)
if getattr(got, item) != getattr(exp, item):
self.errors += 1
strgot.append(f"{text}: {hex(getattr(got, item))}")
strexp.append(f"{text}: {hex(getattr(exp, item))}")

if self.errors > 0:
log.error("Received transaction differed from expected output")
log.info(f"Expected: {'; '.join(strexp)}")
log.info(f"Received: {'; '.join(strgot)}")

log.warning("Difference:\n%s" % hexdiffs('; '.join(strexp), '; '.join(strgot)))
if self._imm:
raise TestFailure("Received transaction differed from expected "
"transaction")
else:
try:
log.debug("Received expected transaction %d bytes" %
(len(got)))
log.debug(repr(got))
except Exception:
pass
30 changes: 3 additions & 27 deletions comp/mi_tools/test_space/cocotb/cocotb_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, ClockCycles
from cocotbext.ofm.mi.drivers import MIMasterDriver as MIDriver
from cocotbext.ofm.mi.drivers import MIRequestDriver as MIDriver
from cocotbext.ofm.ver.generators import random_packets
from cocotb_bus.scoreboard import Scoreboard

Expand Down Expand Up @@ -38,43 +38,19 @@ async def reset(self):


@cocotb.test()
async def run_test(dut, pkt_count=1000, item_width_min=1, item_width_max=32):
async def run_test(dut, pkt_count=5000, item_width_min=1, item_width_max=32):
# Start clock generator
cocotb.start_soon(Clock(dut.CLK, 5, units='ns').start())
tb = testbench(dut)
await tb.reset()

cocotb.log.info("\nREAD32 AND WRITE32 TEST\n")

for transaction in random_packets(4, 4, pkt_count):
cocotb.log.debug(f"generated transaction: {transaction.hex()}")
await tb.stream_in.write32(int.from_bytes(transaction, 'little'), transaction)
output = await tb.stream_in.read32(int.from_bytes(transaction, 'little'))
cocotb.log.debug(f"recieved transaction: {output.hex()}")

assert output == transaction

cocotb.log.info("DONE")

cocotb.log.info("\nREAD AND WRITE TEST\n")

for transaction in random_packets(item_width_min, item_width_max, pkt_count):
cocotb.log.debug(f"generated transaction: {transaction.hex()}")
await tb.stream_in.write(int.from_bytes(transaction[0:4], 'little'), transaction)
output = await tb.stream_in.read(int.from_bytes(transaction[0:4], 'little'), len(transaction))
cocotb.log.debug(f"recieved transaction: {output.hex()}")

assert output == transaction

cocotb.log.info("DONE")

cocotb.log.info("\nREAD64 AND WRITE64 TEST\n")

for transaction in random_packets(8, 8, pkt_count):
cocotb.log.debug(f"generated transaction: {transaction.hex()}")
await tb.stream_in.write64(int.from_bytes(transaction[0:4], 'little'), transaction)
output = await tb.stream_in.read64(int.from_bytes(transaction[0:4], 'little'))
cocotb.log.debug(f"recieved transaction: {output.hex()}")
cocotb.log.debug(f"received transaction: {output.hex()}")

assert output == transaction

Expand Down
62 changes: 62 additions & 0 deletions python/cocotbext/cocotbext/ofm/base/proxymonitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# proxymonitor.py: ProxyMonitor
# Copyright (C) 2024 CESNET z. s. p. o.
# Author(s): Ondřej Schwarz <[email protected]>
#
# SPDX-License-Identifier: BSD-3-Clause

from cocotb_bus.monitors import BusMonitor
from abc import ABC, abstractmethod


class ProxyMonitor(BusMonitor, ABC):
"""
Generic Proxy Monitor used for redirecting traffic from cocotb BusMonitor
and running it through a filter. It automatically connects to the _recv
function of the passed BusMonitor.
Attributes:
_item_cnt(int): number of items which successfully passed through the filter.
"""

def __init__(self, monitor: BusMonitor):
"""
Args:
monitor: BusMonitor which this class is to be the proxy for.
"""
super().__init__(monitor.entity, monitor.name, monitor.clock)
monitor.add_callback(self.monitor_callback)
self._item_cnt = 0

@property
def item_cnt(self):
return self._item_cnt

def monitor_callback(self, *args, **kwargs) -> None:
"""
Callback connected to the passed BusMonitor's _recv function.
The received transaction is run through a filter and then
passed on through the _recv function.
"""
filtered_transaction = self._filter_transaction(*args, **kwargs)

if filtered_transaction is None:
return

self._recv(filtered_transaction)
self._item_cnt += 1

@abstractmethod
def _filter_transaction(self, transaction, *args, **kwargs) -> any:
"""
Filter function used in function monitor_callback. It is ment to be redefined
for specific usecases.
The placeholder filter implemented here lets all the transactions through.
"""
return transaction

async def _monitor_recv(self):
"""
This function must be implemented in every BusMonitor child, however, here it has
no use. However, it may be redifined for specific usecases.
"""
pass
Loading

0 comments on commit b89fbe1

Please sign in to comment.