From 8d3d703656d3687c16609e8ebd7cff11b2c4d3a8 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Sat, 8 Feb 2025 19:59:11 -0800 Subject: [PATCH] eth: Add MAC control modules Signed-off-by: Alex Forencich --- rtl/eth/taxi_mac_ctrl_rx.sv | 421 +++++++++++++ rtl/eth/taxi_mac_ctrl_tx.sv | 394 ++++++++++++ tb/eth/taxi_mac_ctrl_rx/Makefile | 52 ++ .../taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.py | 592 ++++++++++++++++++ .../taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.sv | 121 ++++ tb/eth/taxi_mac_ctrl_tx/Makefile | 51 ++ .../taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.py | 473 ++++++++++++++ .../taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.sv | 99 +++ 8 files changed, 2203 insertions(+) create mode 100644 rtl/eth/taxi_mac_ctrl_rx.sv create mode 100644 rtl/eth/taxi_mac_ctrl_tx.sv create mode 100644 tb/eth/taxi_mac_ctrl_rx/Makefile create mode 100644 tb/eth/taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.py create mode 100644 tb/eth/taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.sv create mode 100644 tb/eth/taxi_mac_ctrl_tx/Makefile create mode 100644 tb/eth/taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.py create mode 100644 tb/eth/taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.sv diff --git a/rtl/eth/taxi_mac_ctrl_rx.sv b/rtl/eth/taxi_mac_ctrl_rx.sv new file mode 100644 index 0000000..ceae156 --- /dev/null +++ b/rtl/eth/taxi_mac_ctrl_rx.sv @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * MAC control receiver + */ +module taxi_mac_ctrl_rx # +( + parameter ID_W = 8, + parameter DEST_W = 8, + parameter USER_W = 1, + parameter logic USE_READY = 1'b0, + parameter MCF_PARAMS_SIZE = 18 +) +( + input wire logic clk, + input wire logic rst, + + /* + * AXI4-Stream input (sink) + */ + taxi_axis_if.snk s_axis, + + /* + * AXI4-Stream output (source) + */ + taxi_axis_if.src m_axis, + + /* + * MAC control frame interface + */ + output wire logic mcf_valid, + output wire logic [47:0] mcf_eth_dst, + output wire logic [47:0] mcf_eth_src, + output wire logic [15:0] mcf_eth_type, + output wire logic [15:0] mcf_opcode, + output wire logic [MCF_PARAMS_SIZE*8-1:0] mcf_params, + output wire logic [ID_W-1:0] mcf_id, + output wire logic [DEST_W-1:0] mcf_dest, + output wire logic [USER_W-1:0] mcf_user, + + /* + * Configuration + */ + input wire logic [47:0] cfg_mcf_rx_eth_dst_mcast, + input wire logic cfg_mcf_rx_check_eth_dst_mcast, + input wire logic [47:0] cfg_mcf_rx_eth_dst_ucast, + input wire logic cfg_mcf_rx_check_eth_dst_ucast, + input wire logic [47:0] cfg_mcf_rx_eth_src, + input wire logic cfg_mcf_rx_check_eth_src, + input wire logic [15:0] cfg_mcf_rx_eth_type, + input wire logic [15:0] cfg_mcf_rx_opcode_lfc, + input wire logic cfg_mcf_rx_check_opcode_lfc, + input wire logic [15:0] cfg_mcf_rx_opcode_pfc, + input wire logic cfg_mcf_rx_check_opcode_pfc, + input wire logic cfg_mcf_rx_forward, + input wire logic cfg_mcf_rx_enable, + + /* + * Status + */ + output wire logic stat_rx_mcf +); + +// extract parameters +localparam DATA_W = s_axis.DATA_W; +localparam logic KEEP_EN = s_axis.KEEP_EN && m_axis.KEEP_EN; +localparam KEEP_W = s_axis.KEEP_W; +localparam logic STRB_EN = s_axis.STRB_EN && m_axis.STRB_EN; +localparam logic LAST_EN = s_axis.LAST_EN && m_axis.LAST_EN; +localparam logic ID_EN = s_axis.ID_EN && m_axis.ID_EN; +localparam logic DEST_EN = s_axis.DEST_EN && m_axis.DEST_EN; +localparam logic USER_EN = s_axis.USER_EN && m_axis.USER_EN; + +localparam BYTE_LANES = KEEP_EN ? KEEP_W : 1; + +localparam HDR_SIZE = 60; + +localparam CYCLE_CNT = (HDR_SIZE+BYTE_LANES-1)/BYTE_LANES; + +localparam PTR_W = $clog2(CYCLE_CNT+1); + +localparam OFFSET = HDR_SIZE % BYTE_LANES; + +// check configuration +if (BYTE_LANES * 8 != DATA_W) + $fatal(0, "Error: AXI stream interface requires byte (8-bit) granularity (instance %m)"); + +if (MCF_PARAMS_SIZE > 44) + $fatal(0, "Error: Maximum MCF_PARAMS_SIZE is 44 bytes (instance %m)"); + +if (m_axis.DATA_W != DATA_W) + $fatal(0, "Error: Interface DATA_W parameter mismatch (instance %m)"); + +if (KEEP_EN && m_axis.KEEP_W != KEEP_W) + $fatal(0, "Error: Interface KEEP_W parameter mismatch (instance %m)"); + +/* + +MAC control frame + + Field Length + Destination MAC address 6 octets [01:80:C2:00:00:01] + Source MAC address 6 octets + Ethertype 2 octets [0x8808] + Opcode 2 octets + Parameters 0-44 octets + +This module manages the reception of MAC control frames. Incoming frames are +checked based on the ethertype and (optionally) MAC addresses. Matching control +frames are marked by setting tuser[0] on the data output and forwarded through +a separate interface for processing. + +*/ + +logic read_mcf_reg = 1'b1, read_mcf_next; +logic mcf_frame_reg = 1'b0, mcf_frame_next; +logic [PTR_W-1:0] ptr_reg = 0, ptr_next; + +logic s_axis_tready_reg = 1'b0, s_axis_tready_next; + +// internal datapath +logic [DATA_W-1:0] m_axis_tdata_int; +logic [KEEP_W-1:0] m_axis_tkeep_int; +logic m_axis_tvalid_int; +logic m_axis_tready_int_reg = 1'b0; +logic m_axis_tlast_int; +logic [ID_W-1:0] m_axis_tid_int; +logic [DEST_W-1:0] m_axis_tdest_int; +logic [USER_W-1:0] m_axis_tuser_int; +wire m_axis_tready_int_early; + +logic mcf_valid_reg = 0, mcf_valid_next; +logic [47:0] mcf_eth_dst_reg = 0, mcf_eth_dst_next; +logic [47:0] mcf_eth_src_reg = 0, mcf_eth_src_next; +logic [15:0] mcf_eth_type_reg = 0, mcf_eth_type_next; +logic [15:0] mcf_opcode_reg = 0, mcf_opcode_next; +logic [MCF_PARAMS_SIZE*8-1:0] mcf_params_reg = 0, mcf_params_next; +logic [ID_W-1:0] mcf_id_reg = 0, mcf_id_next; +logic [DEST_W-1:0] mcf_dest_reg = 0, mcf_dest_next; +logic [USER_W-1:0] mcf_user_reg = 0, mcf_user_next; + +logic stat_rx_mcf_reg = 1'b0, stat_rx_mcf_next; + +assign s_axis.tready = s_axis_tready_reg; + +assign mcf_valid = mcf_valid_reg; +assign mcf_eth_dst = mcf_eth_dst_reg; +assign mcf_eth_src = mcf_eth_src_reg; +assign mcf_eth_type = mcf_eth_type_reg; +assign mcf_opcode = mcf_opcode_reg; +assign mcf_params = mcf_params_reg; +assign mcf_id = mcf_id_reg; +assign mcf_dest = mcf_dest_reg; +assign mcf_user = mcf_user_reg; + +assign stat_rx_mcf = stat_rx_mcf_reg; + +wire mcf_eth_dst_mcast_match = mcf_eth_dst_next == cfg_mcf_rx_eth_dst_mcast; +wire mcf_eth_dst_ucast_match = mcf_eth_dst_next == cfg_mcf_rx_eth_dst_ucast; +wire mcf_eth_src_match = mcf_eth_src_next == cfg_mcf_rx_eth_src; +wire mcf_eth_type_match = mcf_eth_type_next == cfg_mcf_rx_eth_type; +wire mcf_opcode_lfc_match = mcf_opcode_next == cfg_mcf_rx_opcode_lfc; +wire mcf_opcode_pfc_match = mcf_opcode_next == cfg_mcf_rx_opcode_pfc; + +wire mcf_eth_dst_match = ((mcf_eth_dst_mcast_match && cfg_mcf_rx_check_eth_dst_mcast) || + (mcf_eth_dst_ucast_match && cfg_mcf_rx_check_eth_dst_ucast) || + (!cfg_mcf_rx_check_eth_dst_mcast && !cfg_mcf_rx_check_eth_dst_ucast)); + +wire mcf_opcode_match = ((mcf_opcode_lfc_match && cfg_mcf_rx_check_opcode_lfc) || + (mcf_opcode_pfc_match && cfg_mcf_rx_check_opcode_pfc) || + (!cfg_mcf_rx_check_opcode_lfc && !cfg_mcf_rx_check_opcode_pfc)); + +wire mcf_match = (mcf_eth_dst_match && + (mcf_eth_src_match || !cfg_mcf_rx_check_eth_src) && + mcf_eth_type_match && mcf_opcode_match); + +always_comb begin + read_mcf_next = read_mcf_reg; + mcf_frame_next = mcf_frame_reg; + ptr_next = ptr_reg; + + // pass through data + m_axis_tdata_int = s_axis.tdata; + m_axis_tkeep_int = s_axis.tkeep; + m_axis_tvalid_int = s_axis.tvalid; + m_axis_tlast_int = s_axis.tlast; + m_axis_tid_int = s_axis.tid; + m_axis_tdest_int = s_axis.tdest; + m_axis_tuser_int = USER_W'(s_axis.tuser); + + s_axis_tready_next = m_axis_tready_int_early || !USE_READY; + + mcf_valid_next = 1'b0; + mcf_eth_dst_next = mcf_eth_dst_reg; + mcf_eth_src_next = mcf_eth_src_reg; + mcf_eth_type_next = mcf_eth_type_reg; + mcf_opcode_next = mcf_opcode_reg; + mcf_params_next = mcf_params_reg; + mcf_id_next = mcf_id_reg; + mcf_dest_next = mcf_dest_reg; + mcf_user_next = mcf_user_reg; + + stat_rx_mcf_next = 1'b0; + + if ((s_axis.tready || !USE_READY) && s_axis.tvalid) begin + if (read_mcf_reg) begin + ptr_next = ptr_reg + 1; + + mcf_id_next = s_axis.tid; + mcf_dest_next = s_axis.tdest; + mcf_user_next = s_axis.tuser; + + `define _HEADER_FIELD_(offset, field) \ + if (ptr_reg == PTR_W'(offset/BYTE_LANES)) begin \ + field = s_axis.tdata[(offset%BYTE_LANES)*8 +: 8]; \ + end + + `_HEADER_FIELD_(0, mcf_eth_dst_next[5*8 +: 8]) + `_HEADER_FIELD_(1, mcf_eth_dst_next[4*8 +: 8]) + `_HEADER_FIELD_(2, mcf_eth_dst_next[3*8 +: 8]) + `_HEADER_FIELD_(3, mcf_eth_dst_next[2*8 +: 8]) + `_HEADER_FIELD_(4, mcf_eth_dst_next[1*8 +: 8]) + `_HEADER_FIELD_(5, mcf_eth_dst_next[0*8 +: 8]) + `_HEADER_FIELD_(6, mcf_eth_src_next[5*8 +: 8]) + `_HEADER_FIELD_(7, mcf_eth_src_next[4*8 +: 8]) + `_HEADER_FIELD_(8, mcf_eth_src_next[3*8 +: 8]) + `_HEADER_FIELD_(9, mcf_eth_src_next[2*8 +: 8]) + `_HEADER_FIELD_(10, mcf_eth_src_next[1*8 +: 8]) + `_HEADER_FIELD_(11, mcf_eth_src_next[0*8 +: 8]) + `_HEADER_FIELD_(12, mcf_eth_type_next[1*8 +: 8]) + `_HEADER_FIELD_(13, mcf_eth_type_next[0*8 +: 8]) + `_HEADER_FIELD_(14, mcf_opcode_next[1*8 +: 8]) + `_HEADER_FIELD_(15, mcf_opcode_next[0*8 +: 8]) + + if (ptr_reg == PTR_W'(0/BYTE_LANES)) begin + // ensure params field gets cleared + mcf_params_next = 0; + end + + for (integer k = 0; k < MCF_PARAMS_SIZE; k = k + 1) begin + if (ptr_reg == PTR_W'((16+k)/BYTE_LANES)) begin + mcf_params_next[k*8 +: 8] = s_axis.tdata[((16+k)%BYTE_LANES)*8 +: 8]; + end + end + + if (ptr_reg == PTR_W'(15/BYTE_LANES) && (!KEEP_EN || s_axis.tkeep[13%BYTE_LANES])) begin + // record match at end of opcode field + mcf_frame_next = mcf_match && cfg_mcf_rx_enable; + end + + if (ptr_reg == PTR_W'((HDR_SIZE-1)/BYTE_LANES)) begin + read_mcf_next = 1'b0; + end + + `undef _HEADER_FIELD_ + end + + if (s_axis.tlast) begin + if (s_axis.tuser[0]) begin + // frame marked invalid + end else if (mcf_frame_next) begin + if (!cfg_mcf_rx_forward) begin + // mark frame invalid + m_axis_tuser_int[0] = 1'b1; + end + // transfer out MAC control frame + mcf_valid_next = 1'b1; + stat_rx_mcf_next = 1'b1; + end + + read_mcf_next = 1'b1; + mcf_frame_next = 1'b0; + ptr_next = '0; + end + end +end + +always_ff @(posedge clk) begin + read_mcf_reg <= read_mcf_next; + mcf_frame_reg <= mcf_frame_next; + ptr_reg <= ptr_next; + + s_axis_tready_reg <= s_axis_tready_next; + + mcf_valid_reg <= mcf_valid_next; + mcf_eth_dst_reg <= mcf_eth_dst_next; + mcf_eth_src_reg <= mcf_eth_src_next; + mcf_eth_type_reg <= mcf_eth_type_next; + mcf_opcode_reg <= mcf_opcode_next; + mcf_params_reg <= mcf_params_next; + mcf_id_reg <= mcf_id_next; + mcf_dest_reg <= mcf_dest_next; + mcf_user_reg <= mcf_user_next; + + stat_rx_mcf_reg <= stat_rx_mcf_next; + + if (rst) begin + read_mcf_reg <= 1'b1; + mcf_frame_reg <= 1'b0; + ptr_reg <= '0; + s_axis_tready_reg <= 1'b0; + mcf_valid_reg <= 1'b0; + stat_rx_mcf_reg <= 1'b0; + end +end + +// output datapath logic +reg [DATA_W-1:0] m_axis_tdata_reg = '0; +reg [KEEP_W-1:0] m_axis_tkeep_reg = '0; +reg m_axis_tvalid_reg = 1'b0, m_axis_tvalid_next; +reg m_axis_tlast_reg = 1'b0; +reg [ID_W-1:0] m_axis_tid_reg = '0; +reg [DEST_W-1:0] m_axis_tdest_reg = '0; +reg [USER_W-1:0] m_axis_tuser_reg = '0; + +reg [DATA_W-1:0] temp_m_axis_tdata_reg = '0; +reg [KEEP_W-1:0] temp_m_axis_tkeep_reg = '0; +reg temp_m_axis_tvalid_reg = 1'b0, temp_m_axis_tvalid_next; +reg temp_m_axis_tlast_reg = 1'b0; +reg [ID_W-1:0] temp_m_axis_tid_reg = '0; +reg [DEST_W-1:0] temp_m_axis_tdest_reg = '0; +reg [USER_W-1:0] temp_m_axis_tuser_reg = '0; + +// datapath control +reg store_axis_int_to_output; +reg store_axis_int_to_temp; +reg store_axis_temp_to_output; + +assign m_axis.tdata = m_axis_tdata_reg; +assign m_axis.tkeep = KEEP_EN ? m_axis_tkeep_reg : '1; +assign m_axis.tstrb = m_axis.tkeep; +assign m_axis.tvalid = m_axis_tvalid_reg; +assign m_axis.tlast = m_axis_tlast_reg; +assign m_axis.tid = ID_EN ? m_axis_tid_reg : '0; +assign m_axis.tdest = DEST_EN ? m_axis_tdest_reg : '0; +assign m_axis.tuser = USER_EN ? m_axis_tuser_reg : '0; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +assign m_axis_tready_int_early = m_axis.tready || !USE_READY || (!temp_m_axis_tvalid_reg && (!m_axis_tvalid_reg || !m_axis_tvalid_int)); + +always_comb begin + // transfer sink ready state to source + m_axis_tvalid_next = m_axis_tvalid_reg; + temp_m_axis_tvalid_next = temp_m_axis_tvalid_reg; + + store_axis_int_to_output = 1'b0; + store_axis_int_to_temp = 1'b0; + store_axis_temp_to_output = 1'b0; + + if (m_axis_tready_int_reg) begin + // input is ready + if (m_axis.tready || !USE_READY || !m_axis_tvalid_reg) begin + // output is ready or currently not valid, transfer data to output + m_axis_tvalid_next = m_axis_tvalid_int; + store_axis_int_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_m_axis_tvalid_next = m_axis_tvalid_int; + store_axis_int_to_temp = 1'b1; + end + end else if (m_axis.tready || !USE_READY) begin + // input is not ready, but output is ready + m_axis_tvalid_next = temp_m_axis_tvalid_reg; + temp_m_axis_tvalid_next = 1'b0; + store_axis_temp_to_output = 1'b1; + end +end + +always_ff @(posedge clk) begin + m_axis_tvalid_reg <= m_axis_tvalid_next; + m_axis_tready_int_reg <= m_axis_tready_int_early; + temp_m_axis_tvalid_reg <= temp_m_axis_tvalid_next; + + // datapath + if (store_axis_int_to_output) begin + m_axis_tdata_reg <= m_axis_tdata_int; + m_axis_tkeep_reg <= m_axis_tkeep_int; + m_axis_tlast_reg <= m_axis_tlast_int; + m_axis_tid_reg <= m_axis_tid_int; + m_axis_tdest_reg <= m_axis_tdest_int; + m_axis_tuser_reg <= m_axis_tuser_int; + end else if (store_axis_temp_to_output) begin + m_axis_tdata_reg <= temp_m_axis_tdata_reg; + m_axis_tkeep_reg <= temp_m_axis_tkeep_reg; + m_axis_tlast_reg <= temp_m_axis_tlast_reg; + m_axis_tid_reg <= temp_m_axis_tid_reg; + m_axis_tdest_reg <= temp_m_axis_tdest_reg; + m_axis_tuser_reg <= temp_m_axis_tuser_reg; + end + + if (store_axis_int_to_temp) begin + temp_m_axis_tdata_reg <= m_axis_tdata_int; + temp_m_axis_tkeep_reg <= m_axis_tkeep_int; + temp_m_axis_tlast_reg <= m_axis_tlast_int; + temp_m_axis_tid_reg <= m_axis_tid_int; + temp_m_axis_tdest_reg <= m_axis_tdest_int; + temp_m_axis_tuser_reg <= m_axis_tuser_int; + end + + if (rst) begin + m_axis_tvalid_reg <= 1'b0; + m_axis_tready_int_reg <= 1'b0; + temp_m_axis_tvalid_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/rtl/eth/taxi_mac_ctrl_tx.sv b/rtl/eth/taxi_mac_ctrl_tx.sv new file mode 100644 index 0000000..b30afa3 --- /dev/null +++ b/rtl/eth/taxi_mac_ctrl_tx.sv @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * MAC control transmitter + */ +module taxi_mac_ctrl_tx # +( + parameter ID_W = 8, + parameter DEST_W = 8, + parameter USER_W = 1, + parameter MCF_PARAMS_SIZE = 18 +) +( + input wire logic clk, + input wire logic rst, + + /* + * AXI4-Stream input (sink) + */ + taxi_axis_if.snk s_axis, + + /* + * AXI4-Stream output (source) + */ + taxi_axis_if.src m_axis, + + /* + * MAC control frame interface + */ + input wire logic mcf_valid, + output wire logic mcf_ready, + input wire logic [47:0] mcf_eth_dst, + input wire logic [47:0] mcf_eth_src, + input wire logic [15:0] mcf_eth_type, + input wire logic [15:0] mcf_opcode, + input wire logic [MCF_PARAMS_SIZE*8-1:0] mcf_params, + input wire logic [ID_W-1:0] mcf_id, + input wire logic [DEST_W-1:0] mcf_dest, + input wire logic [USER_W-1:0] mcf_user, + + /* + * Pause interface + */ + input wire logic tx_pause_req, + output wire logic tx_pause_ack, + + /* + * Status + */ + output wire logic stat_tx_mcf +); + +// extract parameters +localparam DATA_W = s_axis.DATA_W; +localparam logic KEEP_EN = s_axis.KEEP_EN && m_axis.KEEP_EN; +localparam KEEP_W = s_axis.KEEP_W; +localparam logic STRB_EN = s_axis.STRB_EN && m_axis.STRB_EN; +localparam logic LAST_EN = s_axis.LAST_EN && m_axis.LAST_EN; +localparam logic ID_EN = s_axis.ID_EN && m_axis.ID_EN; +localparam logic DEST_EN = s_axis.DEST_EN && m_axis.DEST_EN; +localparam logic USER_EN = s_axis.USER_EN && m_axis.USER_EN; + +localparam BYTE_LANES = KEEP_EN ? KEEP_W : 1; + +localparam HDR_SIZE = 60; + +localparam CYCLE_CNT = (HDR_SIZE+BYTE_LANES-1)/BYTE_LANES; + +localparam PTR_W = $clog2(CYCLE_CNT+1); + +localparam OFFSET = HDR_SIZE % BYTE_LANES; + +// check configuration +if (BYTE_LANES * 8 != DATA_W) + $fatal(0, "Error: AXI stream interface requires byte (8-bit) granularity (instance %m)"); + +if (MCF_PARAMS_SIZE > 44) + $fatal(0, "Error: Maximum MCF_PARAMS_SIZE is 44 bytes (instance %m)"); + +if (m_axis.DATA_W != DATA_W) + $fatal(0, "Error: Interface DATA_W parameter mismatch (instance %m)"); + +if (KEEP_EN && m_axis.KEEP_W != KEEP_W) + $fatal(0, "Error: Interface KEEP_W parameter mismatch (instance %m)"); + +/* + +MAC control frame + + Field Length + Destination MAC address 6 octets [01:80:C2:00:00:01] + Source MAC address 6 octets + Ethertype 2 octets [0x8808] + Opcode 2 octets + Parameters 0-44 octets + +This module manages the transmission of MAC control frames. Control frames +are accepted in parallel, serialized, and merged at a higher priority with +data traffic. + +*/ + +logic send_data_reg = 1'b0, send_data_next; +logic send_mcf_reg = 1'b0, send_mcf_next; +logic [PTR_W-1:0] ptr_reg = 0, ptr_next; + +logic s_axis_tready_reg = 1'b0, s_axis_tready_next; +logic mcf_ready_reg = 1'b0, mcf_ready_next; +logic tx_pause_ack_reg = 1'b0, tx_pause_ack_next; +logic stat_tx_mcf_reg = 1'b0, stat_tx_mcf_next; + +// internal datapath +logic [DATA_W-1:0] m_axis_tdata_int; +logic [KEEP_W-1:0] m_axis_tkeep_int; +logic m_axis_tvalid_int; +logic m_axis_tready_int_reg = 1'b0; +logic m_axis_tlast_int; +logic [ID_W-1:0] m_axis_tid_int; +logic [DEST_W-1:0] m_axis_tdest_int; +logic [USER_W-1:0] m_axis_tuser_int; +wire m_axis_tready_int_early; + +assign s_axis.tready = s_axis_tready_reg; +assign mcf_ready = mcf_ready_reg; +assign tx_pause_ack = tx_pause_ack_reg; +assign stat_tx_mcf = stat_tx_mcf_reg; + +always_comb begin + send_data_next = send_data_reg; + send_mcf_next = send_mcf_reg; + ptr_next = ptr_reg; + + s_axis_tready_next = 1'b0; + mcf_ready_next = 1'b0; + tx_pause_ack_next = tx_pause_ack_reg; + stat_tx_mcf_next = 1'b0; + + m_axis_tdata_int = 0; + m_axis_tkeep_int = 0; + m_axis_tvalid_int = 1'b0; + m_axis_tlast_int = 1'b0; + m_axis_tid_int = 0; + m_axis_tdest_int = 0; + m_axis_tuser_int = 0; + + if (!send_data_reg && !send_mcf_reg) begin + m_axis_tdata_int = s_axis.tdata; + m_axis_tkeep_int = s_axis.tkeep; + m_axis_tvalid_int = 1'b0; + m_axis_tlast_int = s_axis.tlast; + m_axis_tid_int = s_axis.tid; + m_axis_tdest_int = s_axis.tdest; + m_axis_tuser_int = USER_W'(s_axis.tuser); + s_axis_tready_next = m_axis_tready_int_early && !tx_pause_req; + tx_pause_ack_next = tx_pause_req; + if (s_axis.tvalid && s_axis.tready) begin + s_axis_tready_next = m_axis_tready_int_early; + tx_pause_ack_next = 1'b0; + m_axis_tvalid_int = 1'b1; + if (s_axis.tlast) begin + s_axis_tready_next = m_axis_tready_int_early && !mcf_valid && !mcf_ready; + send_data_next = 1'b0; + end else begin + send_data_next = 1'b1; + end + end else if (mcf_valid) begin + s_axis_tready_next = 1'b0; + ptr_next = 0; + send_mcf_next = 1'b1; + mcf_ready_next = (CYCLE_CNT == 1) && m_axis_tready_int_early; + end + end + + if (send_data_reg) begin + m_axis_tdata_int = s_axis.tdata; + m_axis_tkeep_int = s_axis.tkeep; + m_axis_tvalid_int = 1'b0; + m_axis_tlast_int = s_axis.tlast; + m_axis_tid_int = s_axis.tid; + m_axis_tdest_int = s_axis.tdest; + m_axis_tuser_int = USER_W'(s_axis.tuser); + s_axis_tready_next = m_axis_tready_int_early; + if (s_axis.tvalid && s_axis.tready) begin + m_axis_tvalid_int = 1'b1; + if (s_axis.tlast) begin + s_axis_tready_next = m_axis_tready_int_early && !tx_pause_req; + send_data_next = 1'b0; + if (mcf_valid) begin + s_axis_tready_next = 1'b0; + ptr_next = 0; + send_mcf_next = 1'b1; + mcf_ready_next = (CYCLE_CNT == 1) && m_axis_tready_int_early; + end + end else begin + send_data_next = 1'b1; + end + end + end + + if (send_mcf_reg) begin + mcf_ready_next = (CYCLE_CNT == 1 || ptr_reg == PTR_W'(CYCLE_CNT-1)) && m_axis_tready_int_early; + if (m_axis_tready_int_reg) begin + ptr_next = ptr_reg + 1; + + m_axis_tvalid_int = 1'b1; + m_axis_tid_int = mcf_id; + m_axis_tdest_int = mcf_dest; + m_axis_tuser_int = mcf_user; + + `define _HEADER_FIELD_(offset, field) \ + if (ptr_reg == PTR_W'(offset/BYTE_LANES)) begin \ + m_axis_tdata_int[(offset%BYTE_LANES)*8 +: 8] = field; \ + m_axis_tkeep_int[offset%BYTE_LANES] = 1'b1; \ + end + + `_HEADER_FIELD_(0, mcf_eth_dst[5*8 +: 8]) + `_HEADER_FIELD_(1, mcf_eth_dst[4*8 +: 8]) + `_HEADER_FIELD_(2, mcf_eth_dst[3*8 +: 8]) + `_HEADER_FIELD_(3, mcf_eth_dst[2*8 +: 8]) + `_HEADER_FIELD_(4, mcf_eth_dst[1*8 +: 8]) + `_HEADER_FIELD_(5, mcf_eth_dst[0*8 +: 8]) + `_HEADER_FIELD_(6, mcf_eth_src[5*8 +: 8]) + `_HEADER_FIELD_(7, mcf_eth_src[4*8 +: 8]) + `_HEADER_FIELD_(8, mcf_eth_src[3*8 +: 8]) + `_HEADER_FIELD_(9, mcf_eth_src[2*8 +: 8]) + `_HEADER_FIELD_(10, mcf_eth_src[1*8 +: 8]) + `_HEADER_FIELD_(11, mcf_eth_src[0*8 +: 8]) + `_HEADER_FIELD_(12, mcf_eth_type[1*8 +: 8]) + `_HEADER_FIELD_(13, mcf_eth_type[0*8 +: 8]) + `_HEADER_FIELD_(14, mcf_opcode[1*8 +: 8]) + `_HEADER_FIELD_(15, mcf_opcode[0*8 +: 8]) + + for (integer k = 0; k < HDR_SIZE-16; k = k + 1) begin + if (ptr_reg == PTR_W'((16+k)/BYTE_LANES)) begin + if (k < MCF_PARAMS_SIZE) begin + m_axis_tdata_int[((16+k)%BYTE_LANES)*8 +: 8] = mcf_params[k*8 +: 8]; + end else begin + m_axis_tdata_int[((16+k)%BYTE_LANES)*8 +: 8] = 0; + end + m_axis_tkeep_int[(16+k)%BYTE_LANES] = 1'b1; + end + end + + if (ptr_reg == PTR_W'((HDR_SIZE-1)/BYTE_LANES)) begin + s_axis_tready_next = m_axis_tready_int_early && !tx_pause_req; + mcf_ready_next = 1'b0; + m_axis_tlast_int = 1'b1; + send_mcf_next = 1'b0; + stat_tx_mcf_next = 1'b1; + end else begin + mcf_ready_next = (ptr_next == PTR_W'(CYCLE_CNT-1)) && m_axis_tready_int_early; + end + + `undef _HEADER_FIELD_ + end + end +end + +always_ff @(posedge clk) begin + send_data_reg <= send_data_next; + send_mcf_reg <= send_mcf_next; + ptr_reg <= ptr_next; + + s_axis_tready_reg <= s_axis_tready_next; + mcf_ready_reg <= mcf_ready_next; + tx_pause_ack_reg <= tx_pause_ack_next; + stat_tx_mcf_reg <= stat_tx_mcf_next; + + if (rst) begin + send_data_reg <= 1'b0; + send_mcf_reg <= 1'b0; + ptr_reg <= 0; + s_axis_tready_reg <= 1'b0; + mcf_ready_reg <= 1'b0; + tx_pause_ack_reg <= 1'b0; + stat_tx_mcf_reg <= 1'b0; + end +end + +// output datapath logic +reg [DATA_W-1:0] m_axis_tdata_reg = '0; +reg [KEEP_W-1:0] m_axis_tkeep_reg = '0; +reg m_axis_tvalid_reg = 1'b0, m_axis_tvalid_next; +reg m_axis_tlast_reg = 1'b0; +reg [ID_W-1:0] m_axis_tid_reg = '0; +reg [DEST_W-1:0] m_axis_tdest_reg = '0; +reg [USER_W-1:0] m_axis_tuser_reg = '0; + +reg [DATA_W-1:0] temp_m_axis_tdata_reg = '0; +reg [KEEP_W-1:0] temp_m_axis_tkeep_reg = '0; +reg temp_m_axis_tvalid_reg = 1'b0, temp_m_axis_tvalid_next; +reg temp_m_axis_tlast_reg = 1'b0; +reg [ID_W-1:0] temp_m_axis_tid_reg = '0; +reg [DEST_W-1:0] temp_m_axis_tdest_reg = '0; +reg [USER_W-1:0] temp_m_axis_tuser_reg = '0; + +// datapath control +reg store_axis_int_to_output; +reg store_axis_int_to_temp; +reg store_axis_temp_to_output; + +assign m_axis.tdata = m_axis_tdata_reg; +assign m_axis.tkeep = KEEP_EN ? m_axis_tkeep_reg : '1; +assign m_axis.tstrb = m_axis.tkeep; +assign m_axis.tvalid = m_axis_tvalid_reg; +assign m_axis.tlast = m_axis_tlast_reg; +assign m_axis.tid = ID_EN ? m_axis_tid_reg : '0; +assign m_axis.tdest = DEST_EN ? m_axis_tdest_reg : '0; +assign m_axis.tuser = USER_EN ? m_axis_tuser_reg : '0; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +assign m_axis_tready_int_early = m_axis.tready || (!temp_m_axis_tvalid_reg && (!m_axis_tvalid_reg || !m_axis_tvalid_int)); + +always_comb begin + // transfer sink ready state to source + m_axis_tvalid_next = m_axis_tvalid_reg; + temp_m_axis_tvalid_next = temp_m_axis_tvalid_reg; + + store_axis_int_to_output = 1'b0; + store_axis_int_to_temp = 1'b0; + store_axis_temp_to_output = 1'b0; + + if (m_axis_tready_int_reg) begin + // input is ready + if (m_axis.tready || !m_axis_tvalid_reg) begin + // output is ready or currently not valid, transfer data to output + m_axis_tvalid_next = m_axis_tvalid_int; + store_axis_int_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_m_axis_tvalid_next = m_axis_tvalid_int; + store_axis_int_to_temp = 1'b1; + end + end else if (m_axis.tready) begin + // input is not ready, but output is ready + m_axis_tvalid_next = temp_m_axis_tvalid_reg; + temp_m_axis_tvalid_next = 1'b0; + store_axis_temp_to_output = 1'b1; + end +end + +always_ff @(posedge clk) begin + m_axis_tvalid_reg <= m_axis_tvalid_next; + m_axis_tready_int_reg <= m_axis_tready_int_early; + temp_m_axis_tvalid_reg <= temp_m_axis_tvalid_next; + + // datapath + if (store_axis_int_to_output) begin + m_axis_tdata_reg <= m_axis_tdata_int; + m_axis_tkeep_reg <= m_axis_tkeep_int; + m_axis_tlast_reg <= m_axis_tlast_int; + m_axis_tid_reg <= m_axis_tid_int; + m_axis_tdest_reg <= m_axis_tdest_int; + m_axis_tuser_reg <= m_axis_tuser_int; + end else if (store_axis_temp_to_output) begin + m_axis_tdata_reg <= temp_m_axis_tdata_reg; + m_axis_tkeep_reg <= temp_m_axis_tkeep_reg; + m_axis_tlast_reg <= temp_m_axis_tlast_reg; + m_axis_tid_reg <= temp_m_axis_tid_reg; + m_axis_tdest_reg <= temp_m_axis_tdest_reg; + m_axis_tuser_reg <= temp_m_axis_tuser_reg; + end + + if (store_axis_int_to_temp) begin + temp_m_axis_tdata_reg <= m_axis_tdata_int; + temp_m_axis_tkeep_reg <= m_axis_tkeep_int; + temp_m_axis_tlast_reg <= m_axis_tlast_int; + temp_m_axis_tid_reg <= m_axis_tid_int; + temp_m_axis_tdest_reg <= m_axis_tdest_int; + temp_m_axis_tuser_reg <= m_axis_tuser_int; + end + + if (rst) begin + m_axis_tvalid_reg <= 1'b0; + m_axis_tready_int_reg <= 1'b0; + temp_m_axis_tvalid_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/tb/eth/taxi_mac_ctrl_rx/Makefile b/tb/eth/taxi_mac_ctrl_rx/Makefile new file mode 100644 index 0000000..cc987ee --- /dev/null +++ b/tb/eth/taxi_mac_ctrl_rx/Makefile @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2023-2025 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = taxi_mac_ctrl_rx +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = test_$(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv +VERILOG_SOURCES += ../../../rtl/eth/$(DUT).sv +VERILOG_SOURCES += ../../../rtl/axis/taxi_axis_if.sv + +# handle file list files +process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1))) +process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f)) +uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1)) +VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES))) + +# module parameters +export PARAM_DATA_W := 8 +export PARAM_ID_W := 8 +export PARAM_DEST_W := 8 +export PARAM_USER_W := 1 +export PARAM_USE_READY := 1 +export PARAM_MCF_PARAMS_SIZE := 18 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) +else ifeq ($(SIM), verilator) + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + VERILATOR_TRACE = 1 + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/tb/eth/taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.py b/tb/eth/taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.py new file mode 100644 index 0000000..4d6e278 --- /dev/null +++ b/tb/eth/taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.py @@ -0,0 +1,592 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os +import random + +from scapy.layers.l2 import Ether + +import pytest +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamFrame +from cocotbext.axi.stream import define_stream + + +McfBus, McfTransaction, McfSource, McfSink, McfMonitor = define_stream("Mcf", + signals=["valid", "eth_dst", "eth_src", "eth_type", "opcode", "params"], + optional_signals=["ready", "id", "dest", "user"] +) + + +class TB: + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.clk, 8, units="ns").start()) + + self.source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis), dut.clk, dut.rst) + self.sink = AxiStreamSink(AxiStreamBus.from_entity(dut.m_axis), dut.clk, dut.rst) + self.mcf_sink = McfSink(McfBus.from_prefix(dut, "mcf"), dut.clk, dut.rst) + + dut.cfg_mcf_rx_eth_dst_mcast.setimmediatevalue(0) + dut.cfg_mcf_rx_check_eth_dst_mcast.setimmediatevalue(0) + dut.cfg_mcf_rx_eth_dst_ucast.setimmediatevalue(0) + dut.cfg_mcf_rx_check_eth_dst_ucast.setimmediatevalue(0) + dut.cfg_mcf_rx_eth_src.setimmediatevalue(0) + dut.cfg_mcf_rx_check_eth_src.setimmediatevalue(0) + dut.cfg_mcf_rx_eth_type.setimmediatevalue(0) + dut.cfg_mcf_rx_opcode_lfc.setimmediatevalue(0) + dut.cfg_mcf_rx_check_opcode_lfc.setimmediatevalue(0) + dut.cfg_mcf_rx_opcode_pfc.setimmediatevalue(0) + dut.cfg_mcf_rx_check_opcode_pfc.setimmediatevalue(0) + + dut.cfg_mcf_rx_forward.setimmediatevalue(0) + dut.cfg_mcf_rx_enable.setimmediatevalue(0) + + def set_idle_generator(self, generator=None): + if generator: + self.source.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + self.sink.set_pause_generator(generator()) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + async def send(self, pkt): + await self.source.send(bytes(pkt)) + + async def recv(self): + rx_frame = await self.sink.recv() + + assert not rx_frame.tuser + + return Ether(bytes(rx_frame)) + + async def recv_mcf(self): + rx_frame = await self.mcf_sink.recv() + + data = bytearray() + data.extend(rx_frame.eth_dst.integer.to_bytes(6, 'big')) + data.extend(rx_frame.eth_src.integer.to_bytes(6, 'big')) + data.extend(rx_frame.eth_type.integer.to_bytes(2, 'big')) + data.extend(rx_frame.opcode.integer.to_bytes(2, 'big')) + data.extend(rx_frame.params.integer.to_bytes(44, 'little')) + + return Ether(data) + + +async def run_test_data(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_frames = [] + + for test_data in [payload_data(x) for x in payload_lengths()]: + test_frame = AxiStreamFrame(test_data) + test_frame.tid = cur_id + test_frame.tdest = cur_id | (0 << src_shift) + + test_frames.append(test_frame) + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for test_frame in test_frames: + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_mcf(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.cfg_mcf_rx_eth_dst_mcast.value = 0x0180C2000001 + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + dut.cfg_mcf_rx_eth_dst_ucast.value = 0xDAD1D2D3D4D5 + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + dut.cfg_mcf_rx_eth_src.value = 0x5A5152535455 + dut.cfg_mcf_rx_check_eth_src.value = 0 + dut.cfg_mcf_rx_eth_type.value = 0x8808 + dut.cfg_mcf_rx_opcode_lfc.value = 0x0001 + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_opcode_pfc.value = 0x0101 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + dut.cfg_mcf_rx_forward.value = 0 + dut.cfg_mcf_rx_enable.value = 1 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_pkts = [] + + for payload in [payload_data(x) for x in payload_lengths()]: + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + + test_frame = AxiStreamFrame(bytes(test_pkt)) + test_frame.tid = cur_id + test_frame.tdest = cur_id | (1 << src_shift) + + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for cur_id, test_pkt in test_pkts: + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == bytes(test_pkt) + assert rx_frame.tid == cur_id + assert rx_frame.tdest == cur_id | (1 << src_shift) + assert rx_frame.tuser + + rx_pkt = await tb.recv_mcf() + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + # check prefix as padding may be different + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_tuser_assert(dut): + + tb = TB(dut) + + byte_lanes = tb.source.byte_lanes + + await tb.reset() + + dut.cfg_mcf_rx_eth_dst_mcast.value = 0x0180C2000001 + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + dut.cfg_mcf_rx_eth_dst_ucast.value = 0xDAD1D2D3D4D5 + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + dut.cfg_mcf_rx_eth_src.value = 0x5A5152535455 + dut.cfg_mcf_rx_check_eth_src.value = 0 + dut.cfg_mcf_rx_eth_type.value = 0x8808 + dut.cfg_mcf_rx_opcode_lfc.value = 0x0001 + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_opcode_pfc.value = 0x0101 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + dut.cfg_mcf_rx_forward.value = 0 + dut.cfg_mcf_rx_enable.value = 1 + + # data + payload = bytearray(itertools.islice(itertools.cycle(range(256)), byte_lanes*16)) + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8000) + test_pkt = eth / payload + test_frame = AxiStreamFrame(bytes(test_pkt), tuser=1) + await tb.source.send(test_frame) + + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tuser + + # MAC control + payload = bytearray(itertools.islice(itertools.cycle(range(256)), 18)) + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + test_frame = AxiStreamFrame(bytes(test_pkt), tuser=1) + await tb.source.send(test_frame) + + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tuser + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_mcf_filter(dut): + + tb = TB(dut) + + await tb.reset() + + dut.cfg_mcf_rx_eth_dst_mcast.value = 0x0180C2000001 + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + dut.cfg_mcf_rx_eth_dst_ucast.value = 0xDAD1D2D3D4D5 + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + dut.cfg_mcf_rx_eth_src.value = 0x5A5152535455 + dut.cfg_mcf_rx_check_eth_src.value = 0 + dut.cfg_mcf_rx_eth_type.value = 0x8808 + dut.cfg_mcf_rx_opcode_lfc.value = 0x0001 + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_opcode_pfc.value = 0x0101 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + dut.cfg_mcf_rx_forward.value = 0 + dut.cfg_mcf_rx_enable.value = 1 + + async def check(tb, pkt, should_match): + await tb.source.send(bytes(pkt)) + + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == bytes(pkt) + + if should_match: + assert rx_frame.tuser + + rx_pkt = await tb.recv_mcf() + + assert bytes(rx_pkt).find(bytes(pkt)) == 0 + else: + assert not rx_frame.tuser + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + payload = bytearray(itertools.islice(itertools.cycle(range(256)), 18)) + + # Multicast destination address + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 1 + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + + # Unicast destination address + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 1 + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + + # Source address + dut.cfg_mcf_rx_check_eth_src.value = 1 + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:AA:AA:AA', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + dut.cfg_mcf_rx_check_eth_src.value = 0 + + # Ethertype + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8880) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + # Opcode + dut.cfg_mcf_rx_check_opcode_lfc.value = 1 + dut.cfg_mcf_rx_check_opcode_pfc.value = 1 + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x01' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x01\x01' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = tb.source.byte_lanes + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.cfg_mcf_rx_eth_dst_mcast.value = 0x0180C2000001 + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + dut.cfg_mcf_rx_eth_dst_ucast.value = 0xDAD1D2D3D4D5 + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + dut.cfg_mcf_rx_eth_src.value = 0x5A5152535455 + dut.cfg_mcf_rx_check_eth_src.value = 0 + dut.cfg_mcf_rx_eth_type.value = 0x8808 + dut.cfg_mcf_rx_opcode_lfc.value = 0x0001 + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_opcode_pfc.value = 0x0101 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + dut.cfg_mcf_rx_forward.value = 0 + dut.cfg_mcf_rx_enable.value = 1 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_pkts = [] + + for k in range(256): + if random.randrange(8) != 0: + length = random.randint(1, byte_lanes*16) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8000) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + dest = cur_id | (0 << src_shift) + else: + length = random.randint(1, 18) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + dest = cur_id | (1 << src_shift) + + test_frame = AxiStreamFrame(bytes(test_pkt)) + test_frame.tid = cur_id + test_frame.tdest = dest + + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for cur_id, test_pkt in test_pkts: + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == bytes(test_pkt) + assert rx_frame.tid == cur_id + assert (rx_frame.tdest & count_mask) == cur_id + + if rx_frame.tdest >> src_shift: + assert rx_frame.tuser + + rx_pkt = await tb.recv_mcf() + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + # check prefix as padding may be different + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + else: + assert not rx_frame.tuser + + for k in range(1000): + await RisingEdge(dut.clk) + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +def size_list(): + return list(range(1, 128)) + [512, 1514, 9214] + [60]*10 + + +def mcf_size_list(): + return list(range(1, 19)) + + +def incrementing_payload(length): + return bytes(itertools.islice(itertools.cycle(range(256)), length)) + + +if cocotb.SIM_NAME: + + factory = TestFactory(run_test_data) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_test_mcf) + factory.add_option("payload_lengths", [mcf_size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_test_tuser_assert) + factory.generate_tests() + + factory = TestFactory(run_test_mcf_filter) + factory.generate_tests() + + factory = TestFactory(run_stress_test) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.abspath(os.path.dirname(__file__)) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', '..', 'rtl')) + + +def process_f_files(files): + lst = {} + for f in files: + if f[-2:].lower() == '.f': + with open(f, 'r') as fp: + l = fp.read().split() + for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]): + lst[os.path.basename(f)] = f + else: + lst[os.path.basename(f)] = f + return list(lst.values()) + + +@pytest.mark.parametrize("data_w", [8, 16, 32, 64, 128, 256, 512]) +def test_taxi_mac_ctrl_rx(request, data_w): + dut = "taxi_mac_ctrl_rx" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = module + + verilog_sources = [ + os.path.join(tests_dir, f"{toplevel}.sv"), + os.path.join(rtl_dir, "eth", f"{dut}.sv"), + os.path.join(rtl_dir, "axis", "taxi_axis_if.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['DATA_W'] = data_w + parameters['ID_W'] = 8 + parameters['DEST_W'] = 8 + parameters['USER_W'] = 1 + parameters['USE_READY'] = 1 + parameters['MCF_PARAMS_SIZE'] = 18 + + extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()} + + sim_build = os.path.join(tests_dir, "sim_build", + request.node.name.replace('[', '-').replace(']', '')) + + cocotb_test.simulator.run( + simulator="verilator", + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + ) diff --git a/tb/eth/taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.sv b/tb/eth/taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.sv new file mode 100644 index 0000000..b42be74 --- /dev/null +++ b/tb/eth/taxi_mac_ctrl_rx/test_taxi_mac_ctrl_rx.sv @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * MAC control receiver testbench + */ +module test_taxi_mac_ctrl_rx # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 64, + parameter ID_W = 8, + parameter DEST_W = 8, + parameter USER_W = 1, + parameter logic USE_READY = 1'b0, + parameter MCF_PARAMS_SIZE = 18 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +logic clk; +logic rst; + +taxi_axis_if #(.DATA_W(DATA_W), .ID_EN(1), .ID_W(ID_W), .DEST_EN(1), .DEST_W(DEST_W), .USER_EN(1), .USER_W(USER_W)) s_axis(), m_axis(); + +logic mcf_valid; +logic [47:0] mcf_eth_dst; +logic [47:0] mcf_eth_src; +logic [15:0] mcf_eth_type; +logic [15:0] mcf_opcode; +logic [MCF_PARAMS_SIZE*8-1:0] mcf_params; +logic [ID_W-1:0] mcf_id; +logic [DEST_W-1:0] mcf_dest; +logic [USER_W-1:0] mcf_user; + +logic [47:0] cfg_mcf_rx_eth_dst_mcast; +logic cfg_mcf_rx_check_eth_dst_mcast; +logic [47:0] cfg_mcf_rx_eth_dst_ucast; +logic cfg_mcf_rx_check_eth_dst_ucast; +logic [47:0] cfg_mcf_rx_eth_src; +logic cfg_mcf_rx_check_eth_src; +logic [15:0] cfg_mcf_rx_eth_type; +logic [15:0] cfg_mcf_rx_opcode_lfc; +logic cfg_mcf_rx_check_opcode_lfc; +logic [15:0] cfg_mcf_rx_opcode_pfc; +logic cfg_mcf_rx_check_opcode_pfc; +logic cfg_mcf_rx_forward; +logic cfg_mcf_rx_enable; + +logic stat_rx_mcf; + +taxi_mac_ctrl_rx #( + .ID_W(ID_W), + .DEST_W(DEST_W), + .USER_W(USER_W), + .USE_READY(USE_READY), + .MCF_PARAMS_SIZE(MCF_PARAMS_SIZE) +) +uut ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis(s_axis), + + /* + * AXI4-Stream output (source) + */ + .m_axis(m_axis), + + /* + * MAC control frame interface + */ + .mcf_valid(mcf_valid), + .mcf_eth_dst(mcf_eth_dst), + .mcf_eth_src(mcf_eth_src), + .mcf_eth_type(mcf_eth_type), + .mcf_opcode(mcf_opcode), + .mcf_params(mcf_params), + .mcf_id(mcf_id), + .mcf_dest(mcf_dest), + .mcf_user(mcf_user), + + /* + * Configuration + */ + .cfg_mcf_rx_eth_dst_mcast(cfg_mcf_rx_eth_dst_mcast), + .cfg_mcf_rx_check_eth_dst_mcast(cfg_mcf_rx_check_eth_dst_mcast), + .cfg_mcf_rx_eth_dst_ucast(cfg_mcf_rx_eth_dst_ucast), + .cfg_mcf_rx_check_eth_dst_ucast(cfg_mcf_rx_check_eth_dst_ucast), + .cfg_mcf_rx_eth_src(cfg_mcf_rx_eth_src), + .cfg_mcf_rx_check_eth_src(cfg_mcf_rx_check_eth_src), + .cfg_mcf_rx_eth_type(cfg_mcf_rx_eth_type), + .cfg_mcf_rx_opcode_lfc(cfg_mcf_rx_opcode_lfc), + .cfg_mcf_rx_check_opcode_lfc(cfg_mcf_rx_check_opcode_lfc), + .cfg_mcf_rx_opcode_pfc(cfg_mcf_rx_opcode_pfc), + .cfg_mcf_rx_check_opcode_pfc(cfg_mcf_rx_check_opcode_pfc), + .cfg_mcf_rx_forward(cfg_mcf_rx_forward), + .cfg_mcf_rx_enable(cfg_mcf_rx_enable), + + /* + * Status + */ + .stat_rx_mcf(stat_rx_mcf) +); + +endmodule + +`resetall diff --git a/tb/eth/taxi_mac_ctrl_tx/Makefile b/tb/eth/taxi_mac_ctrl_tx/Makefile new file mode 100644 index 0000000..0f07ca9 --- /dev/null +++ b/tb/eth/taxi_mac_ctrl_tx/Makefile @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2023-2025 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = taxi_mac_ctrl_tx +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = test_$(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv +VERILOG_SOURCES += ../../../rtl/eth/$(DUT).sv +VERILOG_SOURCES += ../../../rtl/axis/taxi_axis_if.sv + +# handle file list files +process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1))) +process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f)) +uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1)) +VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES))) + +# module parameters +export PARAM_DATA_W := 8 +export PARAM_ID_W := 8 +export PARAM_DEST_W := 8 +export PARAM_USER_W := 1 +export PARAM_MCF_PARAMS_SIZE := 18 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) +else ifeq ($(SIM), verilator) + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + VERILATOR_TRACE = 1 + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/tb/eth/taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.py b/tb/eth/taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.py new file mode 100644 index 0000000..43e9880 --- /dev/null +++ b/tb/eth/taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2023-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os +import random + +from scapy.layers.l2 import Ether +from scapy.utils import mac2str + +import pytest +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Event +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamFrame +from cocotbext.axi.stream import define_stream + + +McfBus, McfTransaction, McfSource, McfSink, McfMonitor = define_stream("Mcf", + signals=["valid", "eth_dst", "eth_src", "eth_type", "opcode", "params"], + optional_signals=["ready", "id", "dest", "user"] +) + + +class TB: + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.clk, 8, units="ns").start()) + + self.source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis), dut.clk, dut.rst) + self.sink = AxiStreamSink(AxiStreamBus.from_entity(dut.m_axis), dut.clk, dut.rst) + self.mcf_source = McfSource(McfBus.from_prefix(dut, "mcf"), dut.clk, dut.rst) + + dut.tx_pause_req.setimmediatevalue(0) + + def set_idle_generator(self, generator=None): + if generator: + self.source.set_pause_generator(generator()) + self.mcf_source.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + self.sink.set_pause_generator(generator()) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + async def send(self, pkt): + await self.source.send(bytes(pkt)) + + async def send_mcf(self, pkt): + mcf = McfTransaction() + mcf.eth_dst = int.from_bytes(mac2str(pkt[Ether].dst), 'big') + mcf.eth_src = int.from_bytes(mac2str(pkt[Ether].src), 'big') + mcf.eth_type = pkt[Ether].type + mcf.opcode = int.from_bytes(bytes(pkt[Ether].payload)[0:2], 'big') + mcf.params = int.from_bytes(bytes(pkt[Ether].payload)[2:], 'little') + + await self.mcf_source.send(mcf) + + async def recv(self): + rx_frame = await self.sink.recv() + + assert not rx_frame.tuser + + return Ether(bytes(rx_frame)) + + +async def run_test_data(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.tx_pause_req.value = 0 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_frames = [] + + for test_data in [payload_data(x) for x in payload_lengths()]: + test_frame = AxiStreamFrame(test_data) + test_frame.tid = cur_id | (0 << src_shift) + test_frame.tdest = cur_id + + test_frames.append(test_frame) + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for test_frame in test_frames: + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_mcf(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + await tb.reset() + + dut.tx_pause_req.value = 0 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_pkts = [] + + opcode = 1 + + for payload in [payload_data(x) for x in payload_lengths()]: + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (opcode.to_bytes(2, 'big') + payload) + test_pkts.append(test_pkt.copy()) + + await tb.send_mcf(test_pkt) + + opcode += 1 + + for test_pkt in test_pkts: + rx_pkt = await tb.recv() + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + # check prefix as frame gets zero-padded + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_tuser_assert(dut): + + tb = TB(dut) + + await tb.reset() + + dut.tx_pause_req.value = 0 + + test_data = bytearray(itertools.islice(itertools.cycle(range(256)), 32)) + test_frame = AxiStreamFrame(test_data, tuser=1) + await tb.source.send(test_frame) + + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tuser + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_arb_test(dut): + + tb = TB(dut) + + byte_lanes = tb.source.byte_lanes + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.tx_pause_req.value = 0 + + test_pkts = [] + test_frames = [] + + for k in range(4): + length = byte_lanes*16 + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8000) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + + test_frame = AxiStreamFrame(bytes(test_pkt), tx_complete=Event()) + test_frame.tid = cur_id | (0 << src_shift) + test_frame.tdest = cur_id + test_frames.append(test_frame) + + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + length = random.randint(1, 18) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + + # start transmit in the middle of frame 2 + await test_frames[1].tx_complete.wait() + for j in range(8): + await RisingEdge(dut.clk) + await tb.send_mcf(test_pkt) + await FallingEdge(dut.mcf_valid) + + cur_id = (cur_id + 1) % max_count + + for k in [0, 1, 2, 4, 3]: + rx_frame = await tb.sink.recv() + + rx_pkt = Ether(bytes(rx_frame)) + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + cur_id, test_pkt = test_pkts[k] + + if rx_pkt.type == 0x8808: + # check prefix as frame gets zero-padded + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + assert rx_frame.tid == 0 + assert rx_frame.tdest == 0 + else: + assert bytes(rx_pkt) == bytes(test_pkt) + assert rx_frame.tid == cur_id | (0 << src_shift) + assert rx_frame.tdest == cur_id + + assert not rx_frame.tuser + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = tb.source.byte_lanes + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.tx_pause_req.value = 0 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_pkts = [list() for x in range(2)] + + for k in range(256): + length = random.randint(1, byte_lanes*16) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8000) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts[0].append((cur_id, test_pkt.copy())) + + test_frame = AxiStreamFrame(bytes(test_pkt)) + test_frame.tid = cur_id | (0 << src_shift) + test_frame.tdest = cur_id + + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for k in range(16): + length = random.randint(1, 18) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts[1].append((cur_id, test_pkt.copy())) + + for c in range(random.randint(8, 64)): + await RisingEdge(dut.clk) + await tb.send_mcf(test_pkt) + await FallingEdge(dut.mcf_valid) + + cur_id = (cur_id + 1) % max_count + + while any(test_pkts): + rx_frame = await tb.sink.recv() + + rx_pkt = Ether(bytes(rx_frame)) + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + test_pkt = None + + if rx_pkt.type == 0x8808: + cur_id, test_pkt = test_pkts[1].pop(0) + # check prefix as frame gets zero-padded + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + assert rx_frame.tid == 0 + assert rx_frame.tdest == 0 + else: + cur_id, test_pkt = test_pkts[0].pop(0) + assert bytes(rx_pkt) == bytes(test_pkt) + assert rx_frame.tid == cur_id | (0 << src_shift) + assert rx_frame.tdest == cur_id + + assert not rx_frame.tuser + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +def size_list(): + return list(range(1, 128)) + [512, 1514, 9214] + [60]*10 + + +def mcf_size_list(): + return list(range(1, 19)) + + +def incrementing_payload(length): + return bytes(itertools.islice(itertools.cycle(range(256)), length)) + + +if cocotb.SIM_NAME: + + factory = TestFactory(run_test_data) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_test_mcf) + factory.add_option("payload_lengths", [mcf_size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_test_tuser_assert) + factory.generate_tests() + + factory = TestFactory(run_arb_test) + factory.generate_tests() + + factory = TestFactory(run_stress_test) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.abspath(os.path.dirname(__file__)) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', '..', 'rtl')) + + +def process_f_files(files): + lst = {} + for f in files: + if f[-2:].lower() == '.f': + with open(f, 'r') as fp: + l = fp.read().split() + for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]): + lst[os.path.basename(f)] = f + else: + lst[os.path.basename(f)] = f + return list(lst.values()) + + +@pytest.mark.parametrize("data_w", [8, 16, 32, 64, 128, 256, 512]) +def test_taxi_mac_ctrl_tx(request, data_w): + dut = "taxi_mac_ctrl_tx" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = module + + verilog_sources = [ + os.path.join(tests_dir, f"{toplevel}.sv"), + os.path.join(rtl_dir, "eth", f"{dut}.sv"), + os.path.join(rtl_dir, "axis", "taxi_axis_if.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['DATA_W'] = data_w + parameters['ID_W'] = 8 + parameters['DEST_W'] = 8 + parameters['USER_W'] = 1 + parameters['MCF_PARAMS_SIZE'] = 18 + + extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()} + + sim_build = os.path.join(tests_dir, "sim_build", + request.node.name.replace('[', '-').replace(']', '')) + + cocotb_test.simulator.run( + simulator="verilator", + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + ) diff --git a/tb/eth/taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.sv b/tb/eth/taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.sv new file mode 100644 index 0000000..e4c4b9d --- /dev/null +++ b/tb/eth/taxi_mac_ctrl_tx/test_taxi_mac_ctrl_tx.sv @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * MAC control transmitter testbench + */ +module test_taxi_mac_ctrl_tx # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 64, + parameter ID_W = 8, + parameter DEST_W = 8, + parameter USER_W = 1, + parameter MCF_PARAMS_SIZE = 18 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +logic clk; +logic rst; + +taxi_axis_if #(.DATA_W(DATA_W), .ID_EN(1), .ID_W(ID_W), .DEST_EN(1), .DEST_W(DEST_W), .USER_EN(1), .USER_W(USER_W)) s_axis(), m_axis(); + +logic mcf_valid; +logic mcf_ready; +logic [47:0] mcf_eth_dst; +logic [47:0] mcf_eth_src; +logic [15:0] mcf_eth_type; +logic [15:0] mcf_opcode; +logic [MCF_PARAMS_SIZE*8-1:0] mcf_params; +logic [ID_W-1:0] mcf_id; +logic [DEST_W-1:0] mcf_dest; +logic [USER_W-1:0] mcf_user; + +logic tx_pause_req; +logic tx_pause_ack; + +logic stat_tx_mcf; + +taxi_mac_ctrl_tx #( + .ID_W(ID_W), + .DEST_W(DEST_W), + .USER_W(USER_W), + .MCF_PARAMS_SIZE(MCF_PARAMS_SIZE) +) +uut ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis(s_axis), + + /* + * AXI4-Stream output (source) + */ + .m_axis(m_axis), + + /* + * MAC control frame interface + */ + .mcf_valid(mcf_valid), + .mcf_ready(mcf_ready), + .mcf_eth_dst(mcf_eth_dst), + .mcf_eth_src(mcf_eth_src), + .mcf_eth_type(mcf_eth_type), + .mcf_opcode(mcf_opcode), + .mcf_params(mcf_params), + .mcf_id(mcf_id), + .mcf_dest(mcf_dest), + .mcf_user(mcf_user), + + /* + * Pause interface + */ + .tx_pause_req(tx_pause_req), + .tx_pause_ack(tx_pause_ack), + + /* + * Status + */ + .stat_tx_mcf(stat_tx_mcf) +); + +endmodule + +`resetall