Skip to content

Commit 8c4af13

Browse files
committed
Merge branch 'kondys_feat_cocotb_mvb_transactions' into 'devel'
Cocotb - MVB: implementation of MVB transactions and their usage in drivers and monitors + a new rate limiter (for items) See merge request ndk/ndk-fpga!77
2 parents cf4d634 + c18ee6e commit 8c4af13

File tree

11 files changed

+445
-146
lines changed

11 files changed

+445
-146
lines changed

comp/mvb_tools/storage/fifox/cocotb/cocotb_test.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# cocotb_test.py:
22
# Copyright (C) 2024 CESNET z. s. p. o.
33
# Author(s): Ondřej Schwarz <[email protected]>
4+
# Daniel Kondys <[email protected]>
45
#
56
# SPDX-License-Identifier: BSD-3-Clause
67

@@ -11,18 +12,20 @@
1112
from cocotb.triggers import RisingEdge, ClockCycles
1213
from cocotbext.ofm.mvb.drivers import MVBDriver
1314
from cocotbext.ofm.mvb.monitors import MVBMonitor
14-
from cocotbext.ofm.ver.generators import random_packets
15+
from cocotbext.ofm.ver.generators import random_integers
1516
from cocotb_bus.drivers import BitDriver
1617
from cocotb_bus.scoreboard import Scoreboard
1718
from cocotbext.ofm.utils.throughput_probe import ThroughputProbe, ThroughputProbeMvbInterface
19+
from cocotbext.ofm.base.generators import ItemRateLimiter
20+
from cocotbext.ofm.mvb.transaction import MvbTrClassic
1821

1922

2023
class testbench():
2124
def __init__(self, dut, debug=False):
2225
self.dut = dut
2326
self.stream_in = MVBDriver(dut, "RX", dut.CLK)
2427
self.backpressure = BitDriver(dut.TX_DST_RDY, dut.CLK)
25-
self.stream_out = MVBMonitor(dut, "TX", dut.CLK)
28+
self.stream_out = MVBMonitor(dut, "TX", dut.CLK, tr_type=MvbTrClassic)
2629

2730
self.throughput_probe = ThroughputProbe(ThroughputProbeMvbInterface(self.stream_out), throughput_units="items")
2831
self.throughput_probe.set_log_period(10)
@@ -51,17 +54,25 @@ async def reset(self):
5154

5255

5356
@cocotb.test()
54-
async def run_test(dut, pkt_count=10000, item_width=1):
57+
async def run_test(dut, pkt_count=10000):
5558
# Start clock generator
5659
cocotb.start_soon(Clock(dut.CLK, 5, units="ns").start())
5760
tb = testbench(dut, debug=False)
61+
# Change MVB drivers IdleGenerator to ItemRateLimiter
62+
# Note: the RateLimiter's rate is affected by backpressure (DST_RDY).
63+
# Eventhough it takes into account cycles with DST_RDY=0, the desired rate might not be achievable.
64+
idle_gen_conf = dict(random_idles=True, max_idles=5, zero_idles_chance=50)
65+
tb.stream_in.set_idle_generator(ItemRateLimiter(rate_percentage=30, **idle_gen_conf))
5866
await tb.reset()
5967
tb.backpressure.start((1, i % 5) for i in itertools.count())
6068

61-
for transaction in random_packets(item_width, item_width, pkt_count):
62-
tb.model(transaction)
63-
cocotb.log.debug(f"generated transaction: {transaction.hex()}")
64-
tb.stream_in.append(transaction)
69+
data_width = tb.stream_in.item_widths["data"]
70+
for transaction in random_integers(0, 2**data_width-1, pkt_count):
71+
cocotb.log.debug(f"generated transaction: {hex(transaction)}")
72+
mvb_tr = MvbTrClassic
73+
mvb_tr.data = transaction
74+
tb.model(mvb_tr)
75+
tb.stream_in.append(mvb_tr)
6576

6677
last_num = 0
6778

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/sh
2+
3+
ROOT_PATH=../../../../..
4+
5+
PKG_COCOTBEXT_OFM=$ROOT_PATH/python/cocotbext/
6+
7+
# Python virtual environment
8+
python -m venv venv-fifox
9+
source venv-fifox/bin/activate
10+
11+
python -m pip install setuptools
12+
python -m pip install $PKG_COCOTBEXT_OFM
13+
14+
echo ""
15+
echo "Now activate environment with:"
16+
echo "source venv-fifox/bin/activate"

comp/mvb_tools/storage/mvb_hash_table_simple/cocotb/cocotb_test.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from cocotbext.ofm.utils.servicer import Servicer
3030
from cocotbext.ofm.utils.device import get_dtb
3131
from cocotbext.ofm.utils.math import ceildiv
32+
from cocotbext.ofm.mvb.transaction import MvbTrClassic
3233

3334
import itertools
3435
from math import log2
@@ -117,7 +118,7 @@ def load_file(self, path: str, params: dict) -> (dict, list, list, dict):
117118
out_keys.append(mvb_key)
118119
out_data[mvb_key] = data
119120

120-
table.append([h, (mvb_key << (self.stream_out._item_width * 8 + 1)) + ((data << 1) + 1)])
121+
table.append([h, (mvb_key << ((self.stream_out.item_widths["data"] // 8) * 8 + 1)) + ((data << 1) + 1)])
121122

122123
out_config.append(table)
123124

@@ -175,9 +176,9 @@ async def run_test(dut, config_file: str = "test_configs/test_config_1B.yaml", c
175176
cocotb.log.debug(f"TABLE_CAPACITY: {table_capacity}")
176177

177178
"""Asserting that the read configuration match configuration of the drivers connected to the component."""
178-
assert mvb_items == tb.stream_in._items
179-
assert mvb_key_width_bytes == tb.stream_in._item_width
180-
assert data_out_width_bytes == tb.stream_out._item_width
179+
assert mvb_items == tb.stream_in.items
180+
assert mvb_key_width_bytes == tb.stream_in.item_widths["data"] // 8 # FIXME
181+
assert data_out_width_bytes == tb.stream_out.item_widths["data"] // 8 # FIXME
181182
assert hash_width == log2(table_capacity)
182183

183184
"""Loading configuration from a config file"""
@@ -237,19 +238,23 @@ async def run_test(dut, config_file: str = "test_configs/test_config_1B.yaml", c
237238
for transaction in random_packets(item_width, item_width, pkt_count):
238239
int_transaction = int.from_bytes(transaction, "little")
239240

241+
mvb_tr = MvbTrClassic()
240242
if int_transaction in model_keys:
241-
tb.model((model_data[int_transaction].to_bytes(data_out_width_bytes, 'little'), 1))
243+
mvb_tr.data = model_data[int_transaction]
244+
vld = 1
242245
else:
243-
tb.model(((0).to_bytes(data_out_width_bytes, 'little'), 0))
246+
mvb_tr.data = 0
247+
vld = 0
248+
tb.model((mvb_tr, vld))
244249

245250
cocotb.log.info(f"generated transaction: {transaction.hex()}")
246251
tb.stream_in.append(transaction)
247252

248253
last_num = 0
249254

250255
while (tb.stream_out.item_cnt < pkt_count):
251-
if (tb.stream_out.item_cnt // 1000) > last_num:
252-
last_num = tb.stream_out.item_cnt // 1000
256+
if (num := tb.stream_out.item_cnt // 1000) > last_num:
257+
last_num = num
253258
cocotb.log.info(f"Number of random transactions processed: {tb.stream_out.item_cnt}/{pkt_count}")
254259
await ClockCycles(dut.CLK, 100)
255260

comp/mvb_tools/storage/mvb_hash_table_simple/cocotb/monitors.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,35 @@
55
# SPDX-License-Identifier: BSD-3-Clause
66

77
from cocotbext.ofm.mvb.monitors import MVBMonitor
8+
from cocotbext.ofm.mvb.transaction import MvbTrClassic
89

910

1011
class MVB_HASH_TABLE_SIMPLE_Monitor(MVBMonitor):
1112
_signals = ["data", "match", "vld", "src_rdy", "dst_rdy"]
1213

13-
def receive_data(self, data, offset):
14+
def recv_bytes(self, vld):
15+
data_val = self.bus.data.value
16+
data_val.big_endian = False
17+
data_bytes = data_val.buff
18+
1419
match_val = self.bus.match.value
1520
match_val.big_endian = False
1621

1722
self.log.debug(f"MATCH: {match_val}")
1823

19-
if match_val[offset] == 1:
20-
self._recv((data[offset*self._item_width:(offset+1)*self._item_width], 1))
21-
else:
22-
self._recv((self._item_width * b'\x00', 0))
24+
item_bytes = self._item_widths["data"] // 8
25+
for i in range(self._items):
26+
# Mask and shift the Valid signal per each Item
27+
if vld & 1:
28+
if match_val & 1:
29+
# Getting the data slice (Item) from the "bytes" transaction
30+
data_b = data_bytes[i*item_bytes : (i+1)*item_bytes]
31+
# Converting the data slice (Item) to the MvbTrClassic object
32+
mvb_tr = MvbTrClassic.from_bytes(data_b)
33+
self._recv((mvb_tr, 1))
34+
else:
35+
mvb_tr = MvbTrClassic()
36+
mvb_tr.data = 0
37+
self._recv((mvb_tr, 0))
38+
match_val >>= 1
39+
vld >>= 1

comp/mvb_tools/storage/mvb_hash_table_simple/prepare.sh

100755100644
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#!/bin/sh
22

3-
OFM_PATH=../../../../../ofm
3+
ROOT_PATH=../../../../..
44

55
#swbase=../../../swbase/
66
swbase=git+https://github.com/CESNET/ndk-sw.git#subdirectory=
77

88
PKG_PYNFB=${swbase}pynfb/
99
PKG_LIBNFBEXT_PYTHON=${swbase}ext/libnfb_ext_python/
10-
PKG_COCOTBEXT_OFM=$OFM_PATH/python/cocotbext/
10+
PKG_COCOTBEXT=$ROOT_PATH/python/cocotbext/
1111

1212
# Python virtual environment
1313
python -m venv venv-cocotb
@@ -18,9 +18,10 @@ python -m pip install pylibfdt fdt
1818
python -m pip install scapy
1919
python -m pip install colorama
2020
python -m pip install pyyaml
21-
python -m pip install $PKG_PYNFB
22-
python -m pip install $PKG_LIBNFBEXT_PYTHON
23-
python -m pip install $PKG_COCOTBEXT_OFM
21+
python -m pip wheel -w ./cocotbwheels $PKG_PYNFB
22+
python -m pip install --find-links ./cocotbwheels nfb
23+
python -m pip install --find-links ./cocotbwheels $PKG_LIBNFBEXT_PYTHON
24+
python -m pip install $PKG_COCOTBEXT
2425

2526
echo ""
2627
echo "Now activate environment with:"

python/cocotbext/cocotbext/ofm/base/generators.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from random import choices
2+
13
from .transaction import Transaction, IdleTransaction
24

35

@@ -105,3 +107,89 @@ def put(self, transaction, **kwargs):
105107
# decrease current rate with the expected target rate to maintain value near zero
106108
ir -= items * self._target_rate
107109
self._current_rate = 0 if ir < 0 else ir
110+
111+
112+
class ItemRateLimiter(IdleGenerator):
113+
"""
114+
Limit throughput to achieve specified rate by generating IdleTransactions.
115+
116+
Supply the "random_idles" argument to generate IdleTransactions with a grain of randomness.
117+
If unsupplied or False, IdleTransactions are generated quite periodically (depends on dst_rdy).
118+
119+
Expects:
120+
rate_percentage (int): throughput percentage.
121+
E.g., rate_percentage=90 means 90% throughput (10% IdleTransactions).
122+
When set to 0 (default), IdleTransactions are generated at random.
123+
124+
Optional (kwargs):
125+
'random_idles' (bool): Return the number of IdleTransactions with a grain of randomness, yet still trying to preserve the ratio.
126+
If unsupplied or False, IdleTransactions are generated quite periodically (dependant on dst_rdy).
127+
Irrelevant when `rate_percentage` is set to 0 (=IdleTransactions are generated at random).
128+
'max_idles' (int): Maximum number of IdleTransactions that can be returned by the `get` method.
129+
Used only when `rate_percentage` is set to 0 (=IdleTransactions are generated at random).
130+
'zero_idles_chance' (int): Probability of the `get` method returning 0 IdleTransactions compared to all other values.
131+
This is to reduce the number of IdleTransactions.
132+
Expected values are between (inclusive) 0 and 100 (100 results in full-speed).
133+
When `max_idles` is set to 0, `zero_idles_chance` is automatically set to 100.
134+
Used only when `rate_percentage` is set to 0 (=IdleTransactions are generated at random).
135+
"""
136+
137+
def __init__(self, rate_percentage=0, **kwargs):
138+
super().__init__()
139+
140+
self._rate_percentage = rate_percentage
141+
self._idles_random = kwargs.get("random_idles", True)
142+
self._max_idles = kwargs.get("max_idles", 5)
143+
self._zero_idles_chance = kwargs.get("zero_idles_chance", 50)
144+
145+
self._target_rate_ratio = self._rate_percentage / 100
146+
self._idle_rate_ratio = 1 - self._target_rate_ratio
147+
self._idle_transactions = 0
148+
self._total_transactions = 0
149+
150+
def configure(self, **kwargs):
151+
super().configure(**kwargs)
152+
153+
self._rate_percentage = kwargs.get("rate_percentage", self._rate_percentage)
154+
self._idles_random = kwargs.get("random_idles", self._idles_random)
155+
self._max_idles = kwargs.get("max_idles", self._max_idles)
156+
self._zero_idles_chance = kwargs.get("zero_idles_chance", self._zero_idles_chance)
157+
if self._max_idles == 0:
158+
self._zero_idles_chance = 100
159+
160+
def get(self, transaction, **kwargs):
161+
162+
# Return random number of Idle transactions (=random throughput)
163+
if self._rate_percentage == 0:
164+
# The number of Idle transactions is in range (0, self._max_idles)
165+
# with 100-self._zero_idles_chance % chance of returning 0.
166+
idles = [i for i in range(0, self._max_idles)]
167+
wghts = [(100 - self._zero_idles_chance) // max(len(idles), 1)] * len(idles)
168+
return choices(population=[0, *idles], weights=[self._zero_idles_chance, *wghts], k=1)[0]
169+
170+
## Calculate the amount of Idle Transactions to uphold the set ratio (throughput)
171+
# The basic formula:
172+
# self._idle_rate_ratio = self._idle_transactions / self._total_transactions
173+
# When sending (using the put function) non-idle transactions,
174+
# self._total_transactions increments and I need to find value x, which
175+
# represents the number of idle transactions to send to preserve the set ratio.
176+
# Hence:
177+
# self._idle_rate_ratio = (self._idle_transactions + x) / self._total_transactions
178+
# Find the value of x (+truncate) like so:
179+
x = int(self._idle_rate_ratio * self._total_transactions - self._idle_transactions)
180+
if self._idles_random:
181+
if x < 0:
182+
return 0
183+
else:
184+
# This allows to return +1 more than is actually calculated.
185+
return choices(population=range(x+2), weights=None, k=1)[0]
186+
else:
187+
return max(0, x)
188+
189+
def put(self, transaction, **kwargs):
190+
items_count = kwargs.get("items", 1)
191+
192+
if isinstance(transaction, IdleTransaction):
193+
self._idle_transactions += items_count
194+
195+
self._total_transactions += items_count

0 commit comments

Comments
 (0)