Skip to content

Commit 198bb2b

Browse files
committed
Merge branch 'schwarz-doc-cocotb_doc' into 'devel'
Dokumentace cocotb/cocotbext See merge request ndk/ndk-fpga!226
2 parents 38ec5c6 + 798996d commit 198bb2b

File tree

16 files changed

+1194
-112
lines changed

16 files changed

+1194
-112
lines changed

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,66 +16,88 @@
1616
from cocotb_bus.scoreboard import Scoreboard
1717
from cocotbext.ofm.utils.throughput_probe import ThroughputProbe, ThroughputProbeMfbInterface
1818

19-
19+
# definition of the class encapsulating components of the test
2020
class testbench():
21+
# dut = device tree to the tested component
2122
def __init__(self, dut, debug=False):
2223
self.dut = dut
24+
# setting up the input driver and connecting it to signals begging with "RX"
2325
self.stream_in = MFBDriver(dut, "RX", dut.CLK)
24-
self.backpressure = BitDriver(dut.TX_DST_RDY, dut.CLK)
26+
# setting up the output monitor and connecting it to signals begging with "TX"
2527
self.stream_out = MFBMonitor(dut, "TX", dut.CLK)
28+
# setting up driver of the DST_RDY so it randomly fluctuates between 0 and 1
29+
self.backpressure = BitDriver(dut.TX_DST_RDY, dut.CLK)
2630

31+
# setting up the probe measuring throughput
2732
self.throughput_probe = ThroughputProbe(ThroughputProbeMfbInterface(self.stream_out), throughput_units="bits")
2833
self.throughput_probe.add_log_interval(0, None)
2934
self.throughput_probe.set_log_period(10)
3035

31-
# Create a scoreboard on the stream_out bus
36+
# counter of sent transactions
3237
self.pkts_sent = 0
38+
# list of the transactions that are expected to be received
3339
self.expected_output = []
40+
# setting up scoreboard which compares received transactions with the expected transactions
3441
self.scoreboard = Scoreboard(dut)
42+
# linking monitor with it's expected output
3543
self.scoreboard.add_interface(self.stream_out, self.expected_output)
3644

45+
# setting up logging level
3746
if debug:
3847
self.stream_in.log.setLevel(cocotb.logging.DEBUG)
3948
self.stream_out.log.setLevel(cocotb.logging.DEBUG)
4049

50+
# method for adding transactions to expected output
4151
def model(self, transaction):
4252
"""Model the DUT based on the input transaction"""
4353
self.expected_output.append(transaction)
4454
self.pkts_sent += 1
4555

56+
# simulation of reset
4657
async def reset(self):
4758
self.dut.RST.value = 1
4859
await ClockCycles(self.dut.CLK, 2)
4960
self.dut.RST.value = 0
5061
await RisingEdge(self.dut.CLK)
5162

5263

64+
# defining a test. Functions with "@cocotb.test()" decorator will be automatically found and run
5365
@cocotb.test()
5466
async def run_test(dut, pkt_count=10000, frame_size_min=60, frame_size_max=512):
5567
# Start clock generator
5668
cocotb.start_soon(Clock(dut.CLK, 5, units="ns").start())
69+
70+
# initialization of the test bench
5771
tb = testbench(dut, debug=False)
72+
73+
# running simulated reset
5874
await tb.reset()
75+
76+
# staring the BitDriver (randomized DST_RDY)
5977
tb.backpressure.start((1, i % 5) for i in itertools.count())
6078

79+
# generating random packets
6180
for transaction in random_packets(frame_size_min, frame_size_max, pkt_count):
81+
# adding generated packet to tb.expected_output
6282
tb.model(transaction)
83+
# logging the generated packet
6384
cocotb.log.debug("generated transaction: " + transaction.hex())
85+
# passing generated packet to the driver to be sent to the bus
6486
tb.stream_in.append(transaction)
6587

6688
last_num = 0
89+
# checking if all the expected packets have been received
6790
while (tb.stream_out.frame_cnt < pkt_count):
91+
# logging number of received packets after every 1000 packets
6892
if (tb.stream_out.frame_cnt // 1000) > last_num:
6993
last_num = tb.stream_out.frame_cnt // 1000
7094
cocotb.log.info("Number of transactions processed: %d/%d" % (tb.stream_out.frame_cnt, pkt_count))
95+
# if not all packets have been received yet, waiting 100 cycles so the simulation doesn't stop prematurelly
7196
await ClockCycles(dut.CLK, 100)
7297

73-
await ClockCycles(dut.CLK, 100)
74-
cocotb.log.debug("RX: %d/%d" % (tb.stream_in.frame_cnt, pkt_count))
75-
cocotb.log.debug("TX: %d/%d" % (tb.stream_out.frame_cnt, pkt_count))
76-
cocotb.log.debug("SC: %d/%d" % (tb.pkts_sent, pkt_count))
77-
98+
# logging values measured by throughput probe
7899
tb.throughput_probe.log_max_throughput()
79100
tb.throughput_probe.log_average_throughput()
80101

102+
# displaying result of the test
81103
raise tb.scoreboard.result

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

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Daniel Kondys <kondys@cesnet.cz>
55

66

7+
# importing required modules
78
import itertools
89

910
import cocotb
@@ -18,70 +19,115 @@
1819
from cocotbext.ofm.base.generators import ItemRateLimiter
1920
from cocotbext.ofm.mvb.transaction import MvbTrClassic
2021

21-
22+
# definition of the class encapsulating components of the test
2223
class testbench():
24+
# dut = device tree of the tested component
2325
def __init__(self, dut, debug=False):
2426
self.dut = dut
27+
28+
# setting up the input driver and connecting it to signals beginning with "RX"
2529
self.stream_in = MVBDriver(dut, "RX", dut.CLK)
26-
self.backpressure = BitDriver(dut.TX_DST_RDY, dut.CLK)
30+
31+
# setting up the output monitor and connecting it to signals beginning with "TX"
2732
self.stream_out = MVBMonitor(dut, "TX", dut.CLK, tr_type=MvbTrClassic)
2833

34+
# setting up driver of the DST_RDY so it randomly fluctuates between 0 and 1
35+
self.backpressure = BitDriver(dut.TX_DST_RDY, dut.CLK)
36+
37+
# setting up the probe measuring throughput
2938
self.throughput_probe = ThroughputProbe(ThroughputProbeMvbInterface(self.stream_out), throughput_units="items")
3039
self.throughput_probe.set_log_period(10)
3140
self.throughput_probe.add_log_interval(0, None)
3241

33-
# Create a scoreboard on the stream_out bus
42+
# counter of sent transactions
3443
self.pkts_sent = 0
44+
45+
# list of the transactions that are expected to be received
3546
self.expected_output = []
47+
48+
# setting up a scoreboard which compares received transactions with the expected transactions
3649
self.scoreboard = Scoreboard(dut)
50+
51+
# linking a monitor with its expected output
3752
self.scoreboard.add_interface(self.stream_out, self.expected_output)
3853

54+
# setting up the logging level
3955
if debug:
4056
self.stream_in.log.setLevel(cocotb.logging.DEBUG)
4157
self.stream_out.log.setLevel(cocotb.logging.DEBUG)
4258

59+
# method for adding transactions to the expected output
4360
def model(self, transaction):
4461
"""Model the DUT based on the input transaction"""
4562
self.expected_output.append(transaction)
4663
self.pkts_sent += 1
4764

65+
# method preforming a hardware reset
4866
async def reset(self):
4967
self.dut.RESET.value = 1
5068
await ClockCycles(self.dut.CLK, 10)
5169
self.dut.RESET.value = 0
5270
await RisingEdge(self.dut.CLK)
5371

5472

73+
# defining a test - functions with "@cocotb.test()" decorator will be automatically found and run
5574
@cocotb.test()
5675
async def run_test(dut, pkt_count=10000):
57-
# Start clock generator
76+
# start a clock generator
5877
cocotb.start_soon(Clock(dut.CLK, 5, units="ns").start())
78+
79+
# initialization of the test bench
5980
tb = testbench(dut, debug=False)
60-
# Change MVB drivers IdleGenerator to ItemRateLimiter
61-
# Note: the RateLimiter's rate is affected by backpressure (DST_RDY).
62-
# Eventhough it takes into account cycles with DST_RDY=0, the desired rate might not be achievable.
81+
82+
# change MVB driver's IdleGenerator to ItemRateLimiter
83+
# note: the RateLimiter's rate is affected by backpressure (DST_RDY).
84+
# Even though it takes into account cycles with DST_RDY=0, the desired rate might not be achievable.
6385
idle_gen_conf = dict(random_idles=True, max_idles=5, zero_idles_chance=50)
6486
tb.stream_in.set_idle_generator(ItemRateLimiter(rate_percentage=30, **idle_gen_conf))
87+
88+
# running simulated reset
6589
await tb.reset()
90+
91+
# starting the BitDriver (randomized DST_RDY)
6692
tb.backpressure.start((1, i % 5) for i in itertools.count())
6793

94+
# dynamically getting the width of the data signal that will be set (useful if the width of the signal may change)
6895
data_width = tb.stream_in.item_widths["data"]
96+
97+
# generating MVB items as random integers between the minimum and maximum unsigned value of the item
6998
for transaction in random_integers(0, 2**data_width-1, pkt_count):
99+
100+
# logging the generated transaction
70101
cocotb.log.debug(f"generated transaction: {hex(transaction)}")
102+
103+
# initializing MVB transaction object
71104
mvb_tr = MvbTrClassic()
105+
106+
# setting data of MVB transaction to the generated integer
72107
mvb_tr.data = transaction
108+
109+
# appending the transaction to tb.expected_output
73110
tb.model(mvb_tr)
111+
112+
# passing the transaction to the driver which then writes it to the bus
74113
tb.stream_in.append(mvb_tr)
75114

76115
last_num = 0
77116

117+
# checking if all the expected packets have been received
78118
while (tb.stream_out.item_cnt < pkt_count):
119+
120+
# logging number of received packets after every 1000 packets
79121
if (tb.stream_out.item_cnt // 1000) > last_num:
80122
last_num = tb.stream_out.item_cnt // 1000
81123
cocotb.log.info(f"Number of transactions processed: {tb.stream_out.item_cnt}/{pkt_count}")
124+
125+
# if not all packets have been received yet, waiting 100 cycles so the simulation doesn't stop prematurely
82126
await ClockCycles(dut.CLK, 100)
83127

128+
# logging values measured by throughput probe
84129
tb.throughput_probe.log_max_throughput()
85130
tb.throughput_probe.log_average_throughput()
86131

132+
# displaying result of the test
87133
raise tb.scoreboard.result

comp/mvb_tools/storage/fifox/cocotb/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "cocotb-hash-table-simple-test"
33
version = "0.1.0"
44
dependencies = [
5-
"cocotbext-ofm[nfb] @ ${NDK_FPGA_COCOTBEXT_OFM_URL}",
5+
"cocotbext-ofm @ ${NDK_FPGA_COCOTBEXT_OFM_URL}",
66
"setuptools",
77
]
88

doc/source/basic_cocotb_test.rst

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
=============================================================
2+
Getting Started with Verifications Using cocotb/cocotbext-ndk
3+
=============================================================
4+
5+
In this section, you will learn how to create a basic test for a flow/storage hardware component (such as a pipe,
6+
FIFO, etc.).
7+
8+
To get started, first create a ``cocotb`` folder in the directory where the tested component is located,
9+
and put all the scripts implemented in this tutorial into it.
10+
11+
12+
Creating a Test
13+
===============
14+
15+
As an example, a simple test of an MVB FIFOX component will be used.
16+
It can be found at ``ndk-fpga/comp/mvb_tools/storage/fifox/cocotb/cocotb_test.py``.
17+
18+
.. literalinclude:: ../../comp/mvb_tools/storage/fifox/cocotb/cocotb_test.py
19+
:language: python
20+
:linenos:
21+
:encoding: utf-8
22+
23+
This test can be used as a template for tests of basic `flow` and `storage` components, and can be easily adapted for
24+
most other verifications.
25+
26+
It consists of two basic parts: the testbench class and the test itself.
27+
28+
The testbench is the more reusable of the two and usually looks basically the same, so it can be copied and adapted.
29+
Its purpose is to initialize and encapsulate objects that drive the test. It sets up drivers,
30+
monitors, a scoreboard, expected outputs, and other optional objects, such as a bit driver for ready signals,
31+
adds probes, and so on. It also includes a simulated reset.
32+
33+
The second part is the test part. It can consist of one test (typical for simple components) or multiple tests
34+
(more common for larger designs, such as the whole firmware of a card). Every test must have the ``@cocotb.test()``
35+
decorator and be ``async``.
36+
37+
A test begins with the clock starting, testbench initialization, and a reset.
38+
After the reset, a bit driver is started to test the component's reaction to backpressure (dst_rdy).
39+
Random data is then generated, which can either be done
40+
using ``random_transactions`` or random data that is then inserted into transaction objects (this is the
41+
case in the test above). The generated transaction is then passed to the ``model`` method of the testbench,
42+
which inserts it into the ``expected_output`` list. The generated transaction is also inserted into
43+
the driver's send queue using the ``append`` method, from where it is then written onto the bus.
44+
45+
The data is then read from the bus by a monitor, which should pack it into a transaction of the same type as was
46+
modeled and pass it to the test's scoreboard via a callback. The scoreboard pops the transaction from the
47+
front of the expected output queue that the monitor is connected to and compares this transaction with the transaction
48+
it received from the monitor. If they are not the same, a test failure is raised.
49+
50+
A waiting loop is implemented to ensure that the test doesn't report scoreboard results prematurely before all the transactions have been received.
51+
Otherwise, the scoreboard may receive a different number of transactions than it expected, which will lead to an error.
52+
53+
After all the packets are received, ``tb.scoreboard.result`` is raised, and the test results are shown.
54+
55+
56+
Running the Test
57+
================
58+
59+
To successfully build and run the simulation, it's necessary to implement a couple more files.
60+
Examples of these can again be found in the ``ndk-fpga/comp/mvb_tools/storage/fifox/cocotb/``
61+
folder.
62+
63+
First, it is necessary to implement a ``pyproject.toml`` with all test dependencies listed:
64+
65+
.. literalinclude:: ../../comp/mvb_tools/storage/fifox/cocotb/pyproject.toml
66+
:language: toml
67+
:linenos:
68+
:encoding: utf-8
69+
70+
Then, a ``prepare.sh`` script is required. This script should create a Python virtual environment and use
71+
the ``pyproject.toml`` file created in the previous step to install all the dependencies into the environment.
72+
It usually looks something like this:
73+
74+
.. literalinclude:: ../../comp/mvb_tools/storage/fifox/cocotb/prepare.sh
75+
:language: bash
76+
:linenos:
77+
:encoding: utf-8
78+
79+
Use a special ``cocotb_test_sig.fdo`` file to define the signals that will be displayed in the simulator's waveform.
80+
81+
.. literalinclude:: ../../comp/mvb_tools/storage/fifox/cocotb/cocotb_test_sig.fdo
82+
:language: bash
83+
:linenos:
84+
:encoding: utf-8
85+
86+
Finally, create a ``Makefile`` that will run the simulation:
87+
88+
.. literalinclude:: ../../comp/mvb_tools/storage/fifox/cocotb/Makefile
89+
:language: bash
90+
:linenos:
91+
:encoding: utf-8
92+
93+
.. note:: Don't forget to adjust the values that are component-specific and the relative paths if needed.
94+
95+
You can run the simulation by executing the ``prepare.sh`` script, entering the created virtual environment,
96+
and running the ``Makefile``. All of this can be achieved with this one-liner:
97+
98+
.. code-block:: bash
99+
100+
. ./prepare && make

0 commit comments

Comments
 (0)