Skip to content

Commit ff14a59

Browse files
committed
Merge branch 'cabal-oth' into 'devel'
Cabal oth See merge request ndk/ndk-fpga!418
2 parents 7d20943 + 18175c2 commit ff14a59

9 files changed

Lines changed: 594 additions & 2 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Modules.tcl: Components include script
2+
# Copyright (C) 2026 CESNET z. s. p. o.
3+
# Author(s): Jakub Cabal <cabal@cesnet.cz>
4+
#
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
7+
lappend PACKAGES "$OFM_PATH/comp/base/pkg/math_pack.vhd"
8+
lappend PACKAGES "$OFM_PATH/comp/base/pkg/type_pack.vhd"
9+
10+
lappend MOD "$ENTITY_BASE/axis_discard.vhd"
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
-- Copyright (C) 2026 CESNET z. s. p. o.
2+
-- Author(s): Jakub Cabal <cabal@cesnet.cz>
3+
-- SPDX-License-Identifier: BSD-3-Clause
4+
5+
library IEEE;
6+
use IEEE.std_logic_1164.all;
7+
use IEEE.numeric_std.all;
8+
9+
use work.math_pack.all;
10+
use work.type_pack.all;
11+
12+
-- The AXIS_DISCARD component filters (drops) AXI-Stream packets based on a
13+
-- per-packet discard control signal. When RX_AXI_DISCARD is asserted at
14+
-- Start-of-Packet, the entire packet is consumed from the RX interface
15+
-- without being forwarded to the TX interface. When RX_AXI_DISCARD is
16+
-- deasserted at SOP, the packet passes through unchanged.
17+
--
18+
-- The discard decision is sampled only at the first word of each packet
19+
-- (SOP) and remains active for the entire packet duration. Discarded
20+
-- packets are consumed immediately regardless of TX backpressure,
21+
-- ensuring that discarded packets never block the pipeline.
22+
--
23+
-- Guaranteed throughput: 1 word per clock cycle.
24+
--
25+
entity AXIS_DISCARD is
26+
generic (
27+
-- AXI-Stream data bus width in bits; must be a multiple of 8.
28+
AXI_TDATA_WIDTH : natural := 512;
29+
-- AXI-Stream user signal width in bits. Set to 0 to disable TUSER.
30+
AXI_TUSER_WIDTH : natural := 64;
31+
-- Target device.
32+
DEVICE : string := "AGILEX"
33+
);
34+
port (
35+
CLK : in std_logic;
36+
RESET : in std_logic;
37+
38+
-- =====================================================================
39+
-- RX AXI-Stream Interface
40+
-- =====================================================================
41+
RX_AXI_TDATA : in std_logic_vector(AXI_TDATA_WIDTH-1 downto 0);
42+
RX_AXI_TKEEP : in std_logic_vector(AXI_TDATA_WIDTH/8-1 downto 0);
43+
RX_AXI_TUSER : in std_logic_vector(AXI_TUSER_WIDTH-1 downto 0) := (others => '0');
44+
RX_AXI_TLAST : in std_logic;
45+
RX_AXI_TVALID : in std_logic;
46+
RX_AXI_TREADY : out std_logic;
47+
48+
-- =====================================================================
49+
-- Discard control (sampled only at the first word of each packet)
50+
-- When '1' at SOP, the entire packet is dropped.
51+
-- =====================================================================
52+
RX_AXI_DISCARD : in std_logic;
53+
54+
-- =====================================================================
55+
-- TX AXI-Stream Interface
56+
-- =====================================================================
57+
TX_AXI_TDATA : out std_logic_vector(AXI_TDATA_WIDTH-1 downto 0);
58+
TX_AXI_TKEEP : out std_logic_vector(AXI_TDATA_WIDTH/8-1 downto 0);
59+
TX_AXI_TUSER : out std_logic_vector(AXI_TUSER_WIDTH-1 downto 0);
60+
TX_AXI_TLAST : out std_logic;
61+
TX_AXI_TVALID : out std_logic;
62+
TX_AXI_TREADY : in std_logic
63+
);
64+
end entity;
65+
66+
architecture FULL of AXIS_DISCARD is
67+
68+
-- Active discard flag: combinatorial at SOP, registered during packet
69+
signal discard : std_logic;
70+
signal discard_reg : std_logic;
71+
72+
-- Packet tracking
73+
signal in_pkt : std_logic;
74+
signal rx_transfer : std_logic;
75+
76+
begin
77+
78+
-- =====================================================================
79+
-- Discard Flag Muxing
80+
-- At SOP (in_pkt=0): use combinatorial RX_AXI_DISCARD input
81+
-- During packet (in_pkt=1): use registered value captured at SOP
82+
-- =====================================================================
83+
discard <= RX_AXI_DISCARD when in_pkt = '0' else discard_reg;
84+
85+
-- =====================================================================
86+
-- AXI-Stream Handshake
87+
--
88+
-- When discarding: RX_AXI_TREADY = '1' (consume immediately, bypass
89+
-- TX backpressure) and TX_AXI_TVALID = '0' (suppress
90+
-- output).
91+
-- When not discarding: Normal pass-through with backpressure
92+
-- propagation from TX to RX.
93+
-- =====================================================================
94+
RX_AXI_TREADY <= '1' when discard = '1' else TX_AXI_TREADY;
95+
TX_AXI_TVALID <= RX_AXI_TVALID and not discard;
96+
97+
-- Transfer occurs when both VALID and READY are asserted
98+
rx_transfer <= RX_AXI_TVALID and RX_AXI_TREADY;
99+
100+
-- =====================================================================
101+
-- Data Pass-Through (combinatorial, no pipeline)
102+
-- Output data is only visible when TX_AXI_TVALID is asserted (i.e.,
103+
-- when not discarding), so discarded data never appears on TX.
104+
-- =====================================================================
105+
TX_AXI_TDATA <= RX_AXI_TDATA;
106+
TX_AXI_TKEEP <= RX_AXI_TKEEP;
107+
TX_AXI_TLAST <= RX_AXI_TLAST;
108+
109+
-- TUSER pass-through (conditional generate for zero-width case)
110+
tuser_g : if AXI_TUSER_WIDTH > 0 generate
111+
TX_AXI_TUSER <= RX_AXI_TUSER;
112+
else generate
113+
TX_AXI_TUSER <= (others => '0');
114+
end generate;
115+
116+
-- =====================================================================
117+
-- Packet Tracking and Discard Flag Registration
118+
--
119+
-- Tracks whether we are currently inside a packet and captures the
120+
-- discard flag at SOP for the duration of the packet.
121+
-- =====================================================================
122+
process (CLK)
123+
begin
124+
if rising_edge(CLK) then
125+
if (RESET = '1') then
126+
in_pkt <= '0';
127+
discard_reg <= '0';
128+
elsif (rx_transfer = '1') then
129+
if (RX_AXI_TLAST = '1') then
130+
-- EOP: no longer inside a packet
131+
in_pkt <= '0';
132+
elsif (in_pkt = '0') then
133+
-- SOP accept: entering a multi-word packet, capture discard
134+
in_pkt <= '1';
135+
discard_reg <= RX_AXI_DISCARD;
136+
end if;
137+
-- Middle of packet (in_pkt=1, TLAST=0): hold registered values
138+
end if;
139+
end if;
140+
end process;
141+
142+
end architecture;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright (C) 2026 CESNET z. s. p. o.
2+
# Author(s): Jakub Cabal <cabal@cesnet.cz>
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
TOP_LEVEL_ENT=AXIS_DISCARD
7+
TARGET=cocotb
8+
9+
.PHONY: all
10+
all: comp
11+
12+
include ../../../../../build/Makefile
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Copyright (C) 2026 CESNET z. s. p. o.
2+
# Author(s): Jakub Cabal <cabal@cesnet.cz>
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""Cocotb tests for AXIS_DISCARD component."""
7+
8+
import random
9+
from dataclasses import dataclass
10+
from typing import Optional, Tuple
11+
12+
import cocotb
13+
from cocotb.clock import Clock
14+
from cocotb.triggers import ClockCycles
15+
from cocotbext.ofm.base.generators import ItemRateLimiter
16+
from cocotbext.ofm.ver.generators import random_packets
17+
18+
from testbench import Testbench, DiscardInstruction
19+
20+
21+
@dataclass
22+
class BPCfg:
23+
"""Backpressure configuration."""
24+
min_hold: int = 1
25+
max_hold: int = 5
26+
low_prob: float = 0.5
27+
28+
29+
async def _backpressure(signal, clock, cfg: Optional[BPCfg] = None):
30+
"""Apply random backpressure to a ready signal."""
31+
cfg = cfg or BPCfg()
32+
while True:
33+
hold_cycles = random.randint(cfg.min_hold, cfg.max_hold)
34+
if random.random() < cfg.low_prob:
35+
signal.value = 0
36+
else:
37+
signal.value = 1
38+
await ClockCycles(clock, hold_cycles)
39+
40+
41+
async def _run_test(
42+
dut,
43+
pkt_count: int = 10,
44+
pkt_range: Tuple[int, int] = (60, 8000),
45+
discard_prob: float = 0.5,
46+
tx_cfg: Optional[BPCfg] = None,
47+
test_name: str = "",
48+
zero_idles_chance: int = 50,
49+
max_idles: int = 5
50+
):
51+
"""Run test with specified packet count and configuration."""
52+
cocotb.log.info(f"Starting AXIS_DISCARD {test_name} test")
53+
cocotb.start_soon(Clock(dut.CLK, 5, units="ns").start())
54+
55+
tb = Testbench(dut, debug=False)
56+
await tb.reset()
57+
cocotb.log.info("Reset completed")
58+
59+
tb.rx_driver.set_idle_generator(ItemRateLimiter(max_idles=max_idles, zero_idles_chance=zero_idles_chance))
60+
61+
tx_task = None
62+
if tx_cfg:
63+
tx_task = cocotb.start_soon(_backpressure(dut.TX_AXI_TREADY, dut.CLK, tx_cfg))
64+
65+
pkt_iter = random_packets(min_size=pkt_range[0], max_size=pkt_range[1], count=pkt_count)
66+
67+
for i, pkt_data in enumerate(pkt_iter):
68+
discard = 1 if random.random() < discard_prob else 0
69+
70+
discard_instr = DiscardInstruction(discard=discard)
71+
72+
await tb.send_packet_with_discard(
73+
pkt_data=pkt_data,
74+
discard_instr=discard_instr
75+
)
76+
77+
if (i + 1) % 500 == 0:
78+
cocotb.log.info(f"Sent {i + 1}/{pkt_count} packets")
79+
80+
# Wait for all expected packets to be processed with timeout
81+
timeout = 0
82+
last_frame_cnt = 0
83+
while tb.tx_monitor.frame_cnt < tb.pkts_expected and timeout < 1000000:
84+
if tb.tx_monitor.frame_cnt % 200 == 0 and tb.tx_monitor.frame_cnt != last_frame_cnt:
85+
last_frame_cnt = tb.tx_monitor.frame_cnt
86+
cocotb.log.info(f"Frames received: {tb.tx_monitor.frame_cnt}/{tb.pkts_expected}")
87+
await ClockCycles(dut.CLK, 10)
88+
timeout += 1
89+
90+
if tx_task:
91+
tx_task.kill()
92+
93+
cocotb.log.info(f"Test completed: {tb.tx_monitor.frame_cnt}/{tb.pkts_expected} packets "
94+
f"(total sent: {tb.pkts_sent}, discarded: {tb.pkts_sent - tb.pkts_expected})")
95+
96+
if tb.tx_monitor.frame_cnt < tb.pkts_expected:
97+
raise AssertionError(f"Only {tb.tx_monitor.frame_cnt}/{tb.pkts_expected} packets processed")
98+
if tb.scoreboard.errors > 0:
99+
raise AssertionError(f"Test failed with {tb.scoreboard.errors} errors")
100+
101+
102+
@cocotb.test()
103+
async def run_test_random(dut, pkt_count=3000):
104+
"""Test with fully random packet lengths and ~50% discard probability."""
105+
await _run_test(
106+
dut, pkt_count=pkt_count,
107+
pkt_range=(60, 8000),
108+
discard_prob=0.5,
109+
tx_cfg=BPCfg(1, 5, 0.5),
110+
test_name="random",
111+
zero_idles_chance=50,
112+
max_idles=5
113+
)
114+
115+
116+
@cocotb.test()
117+
async def run_test_aggressive_backpressure(dut, pkt_count=3000):
118+
"""Test with aggressive backpressure on TX interface."""
119+
await _run_test(
120+
dut, pkt_count=pkt_count,
121+
pkt_range=(60, 8000),
122+
discard_prob=0.5,
123+
tx_cfg=BPCfg(10, 50, 0.8),
124+
test_name="aggressive_backpressure",
125+
zero_idles_chance=50,
126+
max_idles=5
127+
)
128+
129+
130+
@cocotb.test()
131+
async def run_test_high_discard_rate(dut, pkt_count=3000):
132+
"""Test with high discard probability (90%)."""
133+
await _run_test(
134+
dut, pkt_count=pkt_count,
135+
pkt_range=(60, 8000),
136+
discard_prob=0.9,
137+
tx_cfg=BPCfg(1, 5, 0.5),
138+
test_name="high_discard_rate",
139+
zero_idles_chance=50,
140+
max_idles=5
141+
)
142+
143+
144+
@cocotb.test()
145+
async def run_test_low_discard_rate(dut, pkt_count=3000):
146+
"""Test with low discard probability (10%)."""
147+
await _run_test(
148+
dut, pkt_count=pkt_count,
149+
pkt_range=(60, 8000),
150+
discard_prob=0.1,
151+
tx_cfg=BPCfg(1, 5, 0.5),
152+
test_name="low_discard_rate",
153+
zero_idles_chance=50,
154+
max_idles=5
155+
)
156+
157+
158+
@cocotb.test()
159+
async def run_test_no_discard(dut, pkt_count=2000):
160+
"""Test with no discards - all packets should pass through."""
161+
await _run_test(
162+
dut, pkt_count=pkt_count,
163+
pkt_range=(60, 8000),
164+
discard_prob=0.0,
165+
tx_cfg=BPCfg(1, 5, 0.5),
166+
test_name="no_discard",
167+
zero_idles_chance=50,
168+
max_idles=5
169+
)
170+
171+
172+
@cocotb.test()
173+
async def run_test_all_discard(dut, pkt_count=2000):
174+
"""Test with all packets discarded - no packets should appear on TX."""
175+
await _run_test(
176+
dut, pkt_count=pkt_count,
177+
pkt_range=(60, 8000),
178+
discard_prob=1.0,
179+
tx_cfg=BPCfg(1, 5, 0.5),
180+
test_name="all_discard",
181+
zero_idles_chance=50,
182+
max_idles=5
183+
)
184+
185+
186+
@cocotb.test()
187+
async def run_test_small_packets(dut, pkt_count=3000):
188+
"""Test with small packets from 60 to 75 bytes."""
189+
await _run_test(
190+
dut, pkt_count=pkt_count,
191+
pkt_range=(60, 75),
192+
discard_prob=0.5,
193+
tx_cfg=BPCfg(1, 10, 0.3),
194+
test_name="small_packets",
195+
zero_idles_chance=0,
196+
max_idles=10
197+
)
198+
199+
200+
@cocotb.test()
201+
async def run_test_jumbo_packets(dut, pkt_count=1000):
202+
"""Test with jumbo packets up to 9216 bytes."""
203+
await _run_test(
204+
dut, pkt_count=pkt_count,
205+
pkt_range=(4000, 9216),
206+
discard_prob=0.5,
207+
tx_cfg=BPCfg(1, 5, 0.5),
208+
test_name="jumbo_packets",
209+
zero_idles_chance=50,
210+
max_idles=5
211+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (C) 2026 CESNET z. s. p. o.
2+
# Author(s): Jakub Cabal <cabal@cesnet.cz>
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
view wave
7+
delete wave *
8+
9+
add_wave -group {ALL} -noupdate -hex /axis_discard/*
10+
11+
config wave -signalnamewidth 1

0 commit comments

Comments
 (0)