Skip to content

Commit afc5575

Browse files
committed
Merge branch 'kondys_axi_concatenator' into 'devel'
feat(axis_tools): introduce AXIS_PACKET_CONCATENATOR, a component to merge two frames into one See merge request ndk/ndk-fpga!378
2 parents 21e8b03 + a1698a2 commit afc5575

8 files changed

Lines changed: 818 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Modules.tcl: Components include script
2+
# Copyright (C) 2026 CESNET
3+
# Author(s): Daniel Kondys <kondys@cesnet.cz>
4+
#
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
7+
# Component paths
8+
set PKG_BASE "$OFM_PATH/comp/base/pkg"
9+
set LOGIC_BASE "$OFM_PATH/comp/base/logic"
10+
11+
# Packages
12+
lappend PACKAGES "$PKG_BASE/math_pack.vhd"
13+
lappend PACKAGES "$PKG_BASE/type_pack.vhd"
14+
15+
# Components
16+
lappend COMPONENTS [ list "LAST_ONE" "$LOGIC_BASE/last_one" "FULL" ]
17+
lappend COMPONENTS [ list "BARREL_SHIFTER_GEN" "$LOGIC_BASE/barrel_shifter" "FULL" ]
18+
lappend COMPONENTS [ list "GEN_ENC" "$LOGIC_BASE/enc" "FULL" ]
19+
20+
# Files
21+
lappend MOD "$ENTITY_BASE/axis_packet_concatenator.vhd"

comp/axis_tools/edit/packet_concatenator/axis_packet_concatenator.vhd

Lines changed: 490 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Makefile: Makefile to compile module
2+
# Copyright (C) 2026 CESNET z. s. p. o.
3+
# Author(s): Daniel Kondys <kondys@cesnet.cz>
4+
#
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
7+
TOP_LEVEL_ENT=AXIS_PACKET_CONCATENATOR
8+
TARGET=cocotb
9+
10+
.PHONY: all
11+
all: comp
12+
13+
include ../../../../../build/Makefile
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright (C) 2026 CESNET z. s. p. o.
2+
# Author(s): Daniel Kondys <kondys@cesnet.cz>
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
import itertools
7+
8+
import cocotb
9+
from cocotb.clock import Clock
10+
from cocotb.triggers import ClockCycles
11+
12+
from cocotbext.ofm.axi4stream.transaction import Axi4StreamTransaction
13+
from cocotbext.ofm.base.generators import ItemRateLimiter
14+
from cocotbext.ofm.ver.generators import random_transactions
15+
16+
from testbench import Testbench
17+
18+
19+
@cocotb.test()
20+
async def run_base_test(dut, min_size=40, max_size=200, pkt_count=10000):
21+
"""Base test with random gaps and backpressure, random packet sizes min_size-max_size."""
22+
cocotb.log.info("Starting AXIS_PACKET_CONCATENATOR base test")
23+
24+
# Start clock generator
25+
cocotb.start_soon(Clock(dut.CLK, 5, units="ns").start())
26+
27+
tb = Testbench(dut, debug=False)
28+
29+
# Set up rate limiters for both input streams
30+
idle_gen_conf = dict(random_idles=True, max_idles=5, zero_idles_chance=50)
31+
tb.rx0_drv.set_idle_generator(ItemRateLimiter(rate_percentage=20, **idle_gen_conf))
32+
tb.rx1_drv.set_idle_generator(ItemRateLimiter(rate_percentage=90, **idle_gen_conf))
33+
34+
await tb.reset()
35+
36+
# Start backpressure
37+
tb.backpressure.start((1, i % 3) for i in itertools.count())
38+
39+
# Generate and send transactions using random_transactions generator
40+
rx0_gen = random_transactions(Axi4StreamTransaction, tb.rx0_drv, "TDATA", min_size, max_size, pkt_count)
41+
rx1_gen = random_transactions(Axi4StreamTransaction, tb.rx1_drv, "TDATA", 20, 70, pkt_count)
42+
43+
for i, (rx0_tr, rx1_tr) in enumerate(zip(rx0_gen, rx1_gen)):
44+
cocotb.log.debug(f"Generated packets iteration #{i}: RX0={len(rx0_tr.TDATA)}B, RX1={len(rx1_tr.TDATA)}B")
45+
46+
# Model the expected output
47+
tb.model(rx0_tr, rx1_tr)
48+
49+
# Send to DUT
50+
tb.rx0_drv.append(rx0_tr)
51+
tb.rx1_drv.append(rx1_tr)
52+
53+
await ClockCycles(dut.CLK, 10)
54+
55+
# Wait for all transactions to be received
56+
last_num = 0
57+
while tb.tx_mon.frame_cnt < pkt_count:
58+
if (tb.tx_mon.frame_cnt // 1000) > last_num:
59+
last_num = tb.tx_mon.frame_cnt // 1000
60+
cocotb.log.info(f"Number of transactions processed: {tb.tx_mon.frame_cnt}/{pkt_count}")
61+
await ClockCycles(dut.CLK, 100)
62+
63+
cocotb.log.info(f"Test completed: {tb.tx_mon.frame_cnt}/{pkt_count} packets processed")
64+
raise tb.scoreboard.result
65+
66+
67+
@cocotb.test()
68+
async def run_full_speed_test(dut, min_size=40, max_size=500, pkt_count=10000):
69+
"""Full speed test without gaps and backpressure, random packet sizes min_size-max_size."""
70+
cocotb.log.info("Starting AXIS_PACKET_CONCATENATOR full speed test")
71+
72+
# Start clock generator
73+
cocotb.start_soon(Clock(dut.CLK, 5, units="ns").start())
74+
75+
tb = Testbench(dut, debug=False)
76+
77+
# No idle generators - full speed on input
78+
# No backpressure - full speed on output
79+
dut.TX_AXIS_TREADY.value = 1
80+
81+
await tb.reset()
82+
83+
# Generate and send transactions using random_transactions generator
84+
rx0_gen = random_transactions(Axi4StreamTransaction, tb.rx0_drv, "TDATA", min_size, max_size, pkt_count)
85+
rx1_gen = random_transactions(Axi4StreamTransaction, tb.rx1_drv, "TDATA", 1, 20, pkt_count)
86+
87+
for i, (rx0_tr, rx1_tr) in enumerate(zip(rx0_gen, rx1_gen)):
88+
cocotb.log.debug(f"Generated packets iteration #{i}: RX0={len(rx0_tr.TDATA)}B, RX1={len(rx1_tr.TDATA)}B")
89+
90+
# Model the expected output
91+
tb.model(rx0_tr, rx1_tr)
92+
93+
# Send to DUT
94+
tb.rx0_drv.append(rx0_tr)
95+
tb.rx1_drv.append(rx1_tr)
96+
97+
await ClockCycles(dut.CLK, 10)
98+
99+
# Wait for all transactions to be received
100+
last_num = 0
101+
while tb.tx_mon.frame_cnt < pkt_count:
102+
if (tb.tx_mon.frame_cnt // 1000) > last_num:
103+
last_num = tb.tx_mon.frame_cnt // 1000
104+
cocotb.log.info(f"Number of transactions processed: {tb.tx_mon.frame_cnt}/{pkt_count}")
105+
await ClockCycles(dut.CLK, 100)
106+
107+
cocotb.log.info(f"Test completed: {tb.tx_mon.frame_cnt}/{pkt_count} packets processed")
108+
raise tb.scoreboard.result
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# cocotb_test_sig.fdo : Include file with signals
2+
# Copyright (C) 2026 CESNET z. s. p. o.
3+
# Author(s): Daniel Kondys <kondys@cesnet.cz>
4+
#
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
7+
view wave
8+
delete wave *
9+
10+
add_wave -group {Top} -noupdate -hex /axis_frame_concatenator/*
11+
12+
config wave -signalnamewidth 1
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[project]
2+
name = "cocotb-packet-concatenator"
3+
version = "0.1.0"
4+
dependencies = [
5+
"cocotbext-ofm[nfb] @ ${NDK_FPGA_COCOTBEXT_OFM_URL}",
6+
"setuptools",
7+
]
8+
9+
[build-system]
10+
requires = ["pdm-backend"]
11+
build-backend = "pdm.backend"
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Copyright (C) 2026 CESNET z. s. p. o.
2+
# Author(s): Daniel Kondys <kondys@cesnet.cz>
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""Testbench for AXIS_PACKET_CONCATENATOR component."""
7+
8+
import cocotb
9+
from cocotb.triggers import RisingEdge, ClockCycles
10+
from cocotb_bus.drivers import BitDriver
11+
from cocotb_bus.scoreboard import Scoreboard
12+
13+
from cocotbext.ofm.axi4stream.drivers import Axi4StreamMaster
14+
from cocotbext.ofm.axi4stream.monitors import Axi4Stream
15+
from cocotbext.ofm.axi4stream.transaction import Axi4StreamTransaction
16+
17+
18+
def _format_bytes(data: bytes, bytes_per_line: int = 32) -> str:
19+
"""Format bytes as a readable string with hex values separated by spaces.
20+
21+
Args:
22+
data: The bytes data to format.
23+
bytes_per_line: Number of bytes to display per line.
24+
25+
Returns:
26+
A formatted string with hex values separated by spaces.
27+
"""
28+
if not isinstance(data, bytes):
29+
return str(data)
30+
31+
lines = []
32+
for i in range(0, len(data), bytes_per_line):
33+
chunk = data[i:i + bytes_per_line]
34+
# Format each byte as two-digit hex with space separator
35+
hex_bytes = ' '.join(f'{b:02x}' for b in chunk)
36+
# Add offset at the beginning of each line
37+
lines.append(f" 0x{i:04x}: {hex_bytes}")
38+
39+
return '\n'.join(lines) if lines else " (empty)"
40+
41+
42+
def _format_transaction(transaction, label: str) -> str:
43+
"""Format a transaction for display.
44+
45+
Args:
46+
transaction: The transaction to format (expected or received).
47+
label: Label to display (e.g., "MODEL" or "DUT").
48+
49+
Returns:
50+
A formatted string representation of the transaction.
51+
"""
52+
# Get TDATA from transaction
53+
if hasattr(transaction, 'TDATA'):
54+
data = transaction.TDATA
55+
else:
56+
data = str(transaction)
57+
58+
if isinstance(data, bytes):
59+
header = f"{label} ({len(data)} bytes):"
60+
body = _format_bytes(data, bytes_per_line=32)
61+
return f"{header}\n{body}"
62+
else:
63+
return f"{label}:\n {data}"
64+
65+
66+
def _compare_transactions(expected, actual, transaction_num: int = 0) -> tuple:
67+
"""Compare two transactions and return detailed mismatch output.
68+
69+
Args:
70+
expected: The expected transaction from model.
71+
actual: The actual transaction from DUT.
72+
transaction_num: Transaction number for display.
73+
74+
Returns:
75+
Tuple of (match: bool, message: str).
76+
"""
77+
match = expected == actual
78+
79+
if match:
80+
return True, ""
81+
82+
lines = []
83+
lines.append("")
84+
lines.append("=" * 70)
85+
lines.append(f"Transaction #{transaction_num}: MISMATCH DETECTED")
86+
lines.append("=" * 70)
87+
88+
msg = "\n".join(lines)
89+
90+
# Print expected (model) transaction
91+
msg += "\n" + _format_transaction(expected, "MODEL (Expected)")
92+
msg += "\n" + "-" * 70 + "\n"
93+
# Print received (DUT) transaction
94+
msg += _format_transaction(actual, "DUT (Received)")
95+
msg += "\n" + "=" * 70
96+
97+
return False, msg
98+
99+
100+
class Testbench:
101+
"""Testbench for AXIS_PACKET_CONCATENATOR component."""
102+
103+
def __init__(self, dut, debug=False):
104+
self.dut = dut
105+
self.rx0_drv = Axi4StreamMaster(dut, "RX0_AXIS", dut.CLK)
106+
self.rx1_drv = Axi4StreamMaster(dut, "RX1_AXIS", dut.CLK)
107+
self.tx_mon = Axi4Stream(dut, "TX_AXIS", dut.CLK, trans_type=Axi4StreamTransaction)
108+
self.backpressure = BitDriver(dut.TX_AXIS_TREADY, dut.CLK)
109+
110+
self.expected_output = []
111+
self.scoreboard = Scoreboard(dut)
112+
self.compared = 0
113+
114+
def compare_wrapper(actual):
115+
"""Compare actual output with expected, providing detailed mismatch info."""
116+
if not self.expected_output:
117+
cocotb.log.error("Received unexpected packet")
118+
return
119+
expected = self.expected_output.pop(0)
120+
self.compared += 1
121+
match, msg = _compare_transactions(expected, actual, self.compared)
122+
if not match:
123+
cocotb.log.error(msg)
124+
self.scoreboard.errors += 1
125+
raise AssertionError(f"Transaction mismatch detected:\n{msg}")
126+
# Log success for each transaction
127+
cocotb.log.debug(f"Transaction #{self.compared}: OK ({len(actual.TDATA)} bytes)")
128+
return match
129+
130+
self.scoreboard.add_interface(self.tx_mon, self.expected_output, compare_fn=compare_wrapper)
131+
132+
if debug:
133+
self.rx0_drv.log.setLevel(cocotb.logging.DEBUG)
134+
self.rx1_drv.log.setLevel(cocotb.logging.DEBUG)
135+
self.tx_mon.log.setLevel(cocotb.logging.DEBUG)
136+
137+
def model(self, rx0_tr: Axi4StreamTransaction, rx1_tr: Axi4StreamTransaction):
138+
"""Model of the DUT - concatenates two packets"""
139+
concatenated = rx0_tr.TDATA + rx1_tr.TDATA
140+
tx_tr = Axi4StreamTransaction()
141+
tx_tr.TDATA = concatenated
142+
self.expected_output.append(tx_tr)
143+
144+
async def reset(self):
145+
self.dut.RESET.value = 1
146+
await ClockCycles(self.dut.CLK, 10)
147+
self.dut.RESET.value = 0
148+
await RisingEdge(self.dut.CLK)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Makefile: Makefile to compile module
2+
# Copyright (C) 2024 CESNET
3+
# Author(s): Jakub Cabal <cabal@cesnet.cz>
4+
#
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
7+
TOP_LEVEL_ENT=AXIS_PACKET_CONCATENATOR
8+
9+
SYNTH=quartus
10+
export DEVICE=AGILEX
11+
12+
.PHONY: all
13+
all: comp
14+
15+
include ../../../../../build/Makefile

0 commit comments

Comments
 (0)