From a375eb342db7de6ef450d2607eda6f6867e76f87 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Fri, 7 Feb 2025 18:03:06 -0800 Subject: [PATCH] eth: Add AXI stream 32-bit XGMII Ethernet frame transmitter module and testbench Signed-off-by: Alex Forencich --- rtl/eth/taxi_axis_xgmii_tx_32.sv | 543 ++++++++++++++++++ tb/eth/taxi_axis_xgmii_tx_32/Makefile | 56 ++ .../test_taxi_axis_xgmii_tx_32.py | 347 +++++++++++ .../test_taxi_axis_xgmii_tx_32.sv | 99 ++++ 4 files changed, 1045 insertions(+) create mode 100644 rtl/eth/taxi_axis_xgmii_tx_32.sv create mode 100644 tb/eth/taxi_axis_xgmii_tx_32/Makefile create mode 100644 tb/eth/taxi_axis_xgmii_tx_32/test_taxi_axis_xgmii_tx_32.py create mode 100644 tb/eth/taxi_axis_xgmii_tx_32/test_taxi_axis_xgmii_tx_32.sv diff --git a/rtl/eth/taxi_axis_xgmii_tx_32.sv b/rtl/eth/taxi_axis_xgmii_tx_32.sv new file mode 100644 index 0000000..4d44f24 --- /dev/null +++ b/rtl/eth/taxi_axis_xgmii_tx_32.sv @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2015-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream XGMII frame transmitter (AXI in, XGMII out) + */ +module taxi_axis_xgmii_tx_32 # +( + parameter DATA_W = 32, + parameter CTRL_W = (DATA_W/8), + parameter logic PADDING_EN = 1'b1, + parameter logic DIC_EN = 1'b1, + parameter MIN_FRAME_LEN = 64, + parameter logic PTP_TS_EN = 1'b0, + parameter PTP_TS_W = 96, + parameter logic TX_CPL_CTRL_IN_TUSER = 1'b1 +) +( + input wire logic clk, + input wire logic rst, + + /* + * Transmit interface (AXI stream) + */ + taxi_axis_if.snk s_axis_tx, + taxi_axis_if.src m_axis_tx_cpl, + + /* + * XGMII output + */ + output wire logic [DATA_W-1:0] xgmii_txd, + output wire logic [CTRL_W-1:0] xgmii_txc, + + /* + * PTP + */ + input wire logic [PTP_TS_W-1:0] ptp_ts, + + /* + * Configuration + */ + input wire logic [7:0] cfg_ifg, + input wire logic cfg_tx_enable, + + /* + * Status + */ + output wire logic start_packet, + output wire logic error_underflow +); + +// extract parameters +localparam KEEP_W = DATA_W/8; +localparam USER_W = TX_CPL_CTRL_IN_TUSER ? 2 : 1; +localparam TX_TAG_W = s_axis_tx.ID_W; + +localparam EMPTY_W = $clog2(KEEP_W); +localparam MIN_LEN_W = $clog2(MIN_FRAME_LEN-4-CTRL_W+1); + +// check configuration +if (DATA_W != 32) + $fatal(0, "Error: Interface width must be 32 (instance %m)"); + +if (KEEP_W*8 != DATA_W || CTRL_W*8 != DATA_W) + $fatal(0, "Error: Interface requires byte (8-bit) granularity (instance %m)"); + +if (s_axis_tx.DATA_W != DATA_W) + $fatal(0, "Error: Interface DATA_W parameter mismatch (instance %m)"); + +if (s_axis_tx.USER_W != USER_W) + $fatal(0, "Error: Interface USER_W parameter mismatch (instance %m)"); + +localparam [7:0] + ETH_PRE = 8'h55, + ETH_SFD = 8'hD5; + +localparam [7:0] + XGMII_IDLE = 8'h07, + XGMII_START = 8'hfb, + XGMII_TERM = 8'hfd, + XGMII_ERROR = 8'hfe; + +localparam [3:0] + STATE_IDLE = 4'd0, + STATE_PREAMBLE = 4'd1, + STATE_PAYLOAD = 4'd2, + STATE_PAD = 4'd3, + STATE_FCS_1 = 4'd4, + STATE_FCS_2 = 4'd5, + STATE_FCS_3 = 4'd6, + STATE_ERR = 4'd7, + STATE_IFG = 4'd8; + +logic [3:0] state_reg = STATE_IDLE, state_next; + +// datapath control signals +logic reset_crc; +logic update_crc; + +logic [DATA_W-1:0] s_tdata_reg = '0, s_tdata_next; +logic [EMPTY_W-1:0] s_empty_reg = '0, s_empty_next; + +logic [DATA_W-1:0] fcs_output_txd_0; +logic [DATA_W-1:0] fcs_output_txd_1; +logic [CTRL_W-1:0] fcs_output_txc_0; +logic [CTRL_W-1:0] fcs_output_txc_1; + +logic [7:0] ifg_offset; + +logic extra_cycle; + +logic frame_reg = 1'b0, frame_next; +logic frame_error_reg = 1'b0, frame_error_next; +logic [MIN_LEN_W-1:0] frame_min_count_reg = '0, frame_min_count_next; + +logic [7:0] ifg_count_reg = 8'd0, ifg_count_next; +logic [1:0] deficit_idle_count_reg = 2'd0, deficit_idle_count_next; + +logic s_axis_tx_tready_reg = 1'b0, s_axis_tx_tready_next; + +logic [PTP_TS_W-1:0] m_axis_tx_cpl_ts_reg = '0, m_axis_tx_cpl_ts_next; +logic [TX_TAG_W-1:0] m_axis_tx_cpl_tag_reg = '0, m_axis_tx_cpl_tag_next; +logic m_axis_tx_cpl_valid_reg = 1'b0, m_axis_tx_cpl_valid_next; + +logic [31:0] crc_state_reg[3:0]; +wire [31:0] crc_state_next[3:0]; + +logic [DATA_W-1:0] xgmii_txd_reg = {CTRL_W{XGMII_IDLE}}, xgmii_txd_next; +logic [CTRL_W-1:0] xgmii_txc_reg = {CTRL_W{1'b1}}, xgmii_txc_next; + +logic start_packet_reg = 1'b0, start_packet_next; +logic error_underflow_reg = 1'b0, error_underflow_next; + +assign s_axis_tx.tready = s_axis_tx_tready_reg; + +assign xgmii_txd = xgmii_txd_reg; +assign xgmii_txc = xgmii_txc_reg; + +assign m_axis_tx_cpl.tdata = PTP_TS_EN ? m_axis_tx_cpl_ts_reg : '0; +assign m_axis_tx_cpl.tkeep = 1'b1; +assign m_axis_tx_cpl.tstrb = m_axis_tx_cpl.tkeep; +assign m_axis_tx_cpl.tvalid = m_axis_tx_cpl_valid_reg; +assign m_axis_tx_cpl.tlast = 1'b1; +assign m_axis_tx_cpl.tid = m_axis_tx_cpl_tag_reg; +assign m_axis_tx_cpl.tdest = '0; +assign m_axis_tx_cpl.tuser = '0; + +assign start_packet = start_packet_reg; +assign error_underflow = error_underflow_reg; + +for (genvar n = 0; n < 4; n = n + 1) begin : crc + + taxi_lfsr #( + .LFSR_W(32), + .LFSR_POLY(32'h4c11db7), + .LFSR_GALOIS(1), + .LFSR_FEED_FORWARD(0), + .REVERSE(1), + .DATA_W(8*(n+1)) + ) + eth_crc ( + .data_in(s_tdata_reg[0 +: 8*(n+1)]), + .state_in(crc_state_reg[3]), + .data_out(), + .state_out(crc_state_next[n]) + ); + +end + +function [1:0] keep2empty; + input [3:0] k; + casez (k) + 4'bzzz0: keep2empty = 2'd3; + 4'bzz01: keep2empty = 2'd3; + 4'bz011: keep2empty = 2'd2; + 4'b0111: keep2empty = 2'd1; + 4'b1111: keep2empty = 2'd0; + endcase +endfunction + +// Mask input data +wire [DATA_W-1:0] s_axis_tx_tdata_masked; + +for (genvar n = 0; n < CTRL_W; n = n + 1) begin + assign s_axis_tx_tdata_masked[n*8 +: 8] = s_axis_tx.tkeep[n] ? s_axis_tx.tdata[n*8 +: 8] : 8'd0; +end + +// FCS cycle calculation +always_comb begin + casez (s_empty_reg) + 2'd3: begin + fcs_output_txd_0 = {~crc_state_next[0][23:0], s_tdata_reg[7:0]}; + fcs_output_txd_1 = {{2{XGMII_IDLE}}, XGMII_TERM, ~crc_state_reg[0][31:24]}; + fcs_output_txc_0 = 4'b0000; + fcs_output_txc_1 = 4'b1110; + ifg_offset = 8'd3; + extra_cycle = 1'b0; + end + 2'd2: begin + fcs_output_txd_0 = {~crc_state_next[1][15:0], s_tdata_reg[15:0]}; + fcs_output_txd_1 = {XGMII_IDLE, XGMII_TERM, ~crc_state_reg[1][31:16]}; + fcs_output_txc_0 = 4'b0000; + fcs_output_txc_1 = 4'b1100; + ifg_offset = 8'd2; + extra_cycle = 1'b0; + end + 2'd1: begin + fcs_output_txd_0 = {~crc_state_next[2][7:0], s_tdata_reg[23:0]}; + fcs_output_txd_1 = {XGMII_TERM, ~crc_state_reg[2][31:8]}; + fcs_output_txc_0 = 4'b0000; + fcs_output_txc_1 = 4'b1000; + ifg_offset = 8'd1; + extra_cycle = 1'b0; + end + 2'd0: begin + fcs_output_txd_0 = s_tdata_reg; + fcs_output_txd_1 = ~crc_state_reg[3]; + fcs_output_txc_0 = 4'b0000; + fcs_output_txc_1 = 4'b0000; + ifg_offset = 8'd4; + extra_cycle = 1'b1; + end + endcase +end + +always_comb begin + state_next = STATE_IDLE; + + reset_crc = 1'b0; + update_crc = 1'b0; + + frame_next = frame_reg; + frame_error_next = frame_error_reg; + frame_min_count_next = frame_min_count_reg; + + ifg_count_next = ifg_count_reg; + deficit_idle_count_next = deficit_idle_count_reg; + + s_axis_tx_tready_next = 1'b0; + + s_tdata_next = s_tdata_reg; + s_empty_next = s_empty_reg; + + m_axis_tx_cpl_ts_next = m_axis_tx_cpl_ts_reg; + m_axis_tx_cpl_tag_next = m_axis_tx_cpl_tag_reg; + m_axis_tx_cpl_valid_next = 1'b0; + + if (start_packet_reg) begin + if (PTP_TS_EN) begin + m_axis_tx_cpl_ts_next = ptp_ts; + end + m_axis_tx_cpl_tag_next = s_axis_tx.tid; + if (TX_CPL_CTRL_IN_TUSER) begin + m_axis_tx_cpl_valid_next = (s_axis_tx.tuser >> 1) != 0; + end else begin + m_axis_tx_cpl_valid_next = 1'b1; + end + end + + // XGMII idle + xgmii_txd_next = {CTRL_W{XGMII_IDLE}}; + xgmii_txc_next = {CTRL_W{1'b1}}; + + start_packet_next = 1'b0; + error_underflow_next = 1'b0; + + if (s_axis_tx.tvalid && s_axis_tx.tready) begin + frame_next = !s_axis_tx.tlast; + end + + case (state_reg) + STATE_IDLE: begin + // idle state - wait for data + frame_error_next = 1'b0; + frame_min_count_next = MIN_LEN_W'(MIN_FRAME_LEN-4-CTRL_W); + reset_crc = 1'b1; + + // XGMII idle + xgmii_txd_next = {CTRL_W{XGMII_IDLE}}; + xgmii_txc_next = {CTRL_W{1'b1}}; + + s_tdata_next = s_axis_tx_tdata_masked; + s_empty_next = keep2empty(s_axis_tx.tkeep); + + if (s_axis_tx.tvalid && cfg_tx_enable) begin + // XGMII start and preamble + xgmii_txd_next = {{3{ETH_PRE}}, XGMII_START}; + xgmii_txc_next = 4'b0001; + s_axis_tx_tready_next = 1'b1; + state_next = STATE_PREAMBLE; + end else begin + ifg_count_next = 8'd0; + deficit_idle_count_next = 2'd0; + state_next = STATE_IDLE; + end + end + STATE_PREAMBLE: begin + // send preamble + reset_crc = 1'b1; + + s_tdata_next = s_axis_tx_tdata_masked; + s_empty_next = keep2empty(s_axis_tx.tkeep); + + xgmii_txd_next = {ETH_SFD, {3{ETH_PRE}}}; + xgmii_txc_next = {CTRL_W{1'b0}}; + + s_axis_tx_tready_next = 1'b1; + start_packet_next = 1'b1; + state_next = STATE_PAYLOAD; + end + STATE_PAYLOAD: begin + // transfer payload + update_crc = 1'b1; + s_axis_tx_tready_next = 1'b1; + + if (frame_min_count_reg > MIN_LEN_W'(CTRL_W)) begin + frame_min_count_next = MIN_LEN_W'(frame_min_count_reg - CTRL_W); + end else begin + frame_min_count_next = 0; + end + + xgmii_txd_next = s_tdata_reg; + xgmii_txc_next = {CTRL_W{1'b0}}; + + s_tdata_next = s_axis_tx_tdata_masked; + s_empty_next = keep2empty(s_axis_tx.tkeep); + + if (!s_axis_tx.tvalid || s_axis_tx.tlast) begin + s_axis_tx_tready_next = frame_next; // drop frame + frame_error_next = !s_axis_tx.tvalid || s_axis_tx.tuser[0]; + error_underflow_next = !s_axis_tx.tvalid; + + if (PADDING_EN && frame_min_count_reg != 0) begin + if (frame_min_count_reg > MIN_LEN_W'(CTRL_W)) begin + s_empty_next = 0; + state_next = STATE_PAD; + end else begin + if (keep2empty(s_axis_tx.tkeep) > 2'(CTRL_W-frame_min_count_reg)) begin + s_empty_next = 2'(CTRL_W-frame_min_count_reg); + end + state_next = STATE_FCS_1; + end + end else begin + state_next = STATE_FCS_1; + end + end else begin + state_next = STATE_PAYLOAD; + end + end + STATE_PAD: begin + // pad frame to MIN_FRAME_LEN + s_axis_tx_tready_next = frame_next; // drop frame + + xgmii_txd_next = s_tdata_reg; + xgmii_txc_next = {CTRL_W{1'b0}}; + + s_tdata_next = 32'd0; + s_empty_next = 0; + + update_crc = 1'b1; + + if (frame_min_count_reg > MIN_LEN_W'(CTRL_W)) begin + frame_min_count_next = MIN_LEN_W'(frame_min_count_reg - CTRL_W); + state_next = STATE_PAD; + end else begin + frame_min_count_next = 0; + s_empty_next = 2'(CTRL_W-frame_min_count_reg); + state_next = STATE_FCS_1; + end + end + STATE_FCS_1: begin + // last cycle + s_axis_tx_tready_next = frame_next; // drop frame + + xgmii_txd_next = fcs_output_txd_0; + xgmii_txc_next = fcs_output_txc_0; + + update_crc = 1'b1; + + ifg_count_next = (cfg_ifg > 8'd12 ? cfg_ifg : 8'd12) - ifg_offset + 8'(deficit_idle_count_reg); + if (frame_error_reg) begin + state_next = STATE_ERR; + end else begin + state_next = STATE_FCS_2; + end + end + STATE_FCS_2: begin + // last cycle + s_axis_tx_tready_next = frame_next; // drop frame + + xgmii_txd_next = fcs_output_txd_1; + xgmii_txc_next = fcs_output_txc_1; + + if (extra_cycle) begin + state_next = STATE_FCS_3; + end else begin + state_next = STATE_IFG; + end + end + STATE_FCS_3: begin + // last cycle + s_axis_tx_tready_next = frame_next; // drop frame + + xgmii_txd_next = {{3{XGMII_IDLE}}, XGMII_TERM}; + xgmii_txc_next = {CTRL_W{1'b1}}; + + if (DIC_EN) begin + if (ifg_count_next > 8'd3) begin + state_next = STATE_IFG; + end else begin + deficit_idle_count_next = 2'(ifg_count_next); + ifg_count_next = 8'd0; + s_axis_tx_tready_next = 1'b1; + state_next = STATE_IDLE; + end + end else begin + if (ifg_count_next > 8'd0) begin + state_next = STATE_IFG; + end else begin + state_next = STATE_IDLE; + end + end + end + STATE_ERR: begin + // terminate packet with error + s_axis_tx_tready_next = frame_next; // drop frame + + // XGMII error + xgmii_txd_next = {XGMII_TERM, {3{XGMII_ERROR}}}; + xgmii_txc_next = {CTRL_W{1'b1}}; + + ifg_count_next = cfg_ifg > 8'd12 ? cfg_ifg : 8'd12; + + state_next = STATE_IFG; + end + STATE_IFG: begin + // send IFG + s_axis_tx_tready_next = frame_next; // drop frame + + // XGMII idle + xgmii_txd_next = {CTRL_W{XGMII_IDLE}}; + xgmii_txc_next = {CTRL_W{1'b1}}; + + if (ifg_count_reg > 8'd4) begin + ifg_count_next = ifg_count_reg - 8'd4; + end else begin + ifg_count_next = 8'd0; + end + + if (DIC_EN) begin + if (ifg_count_next > 8'd3 || frame_reg) begin + state_next = STATE_IFG; + end else begin + deficit_idle_count_next = 2'(ifg_count_next); + ifg_count_next = 8'd0; + state_next = STATE_IDLE; + end + end else begin + if (ifg_count_next > 8'd0 || frame_reg) begin + state_next = STATE_IFG; + end else begin + state_next = STATE_IDLE; + end + end + end + default: begin + // invalid state, return to idle + state_next = STATE_IDLE; + end + endcase +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + + frame_reg <= frame_next; + frame_error_reg <= frame_error_next; + frame_min_count_reg <= frame_min_count_next; + + ifg_count_reg <= ifg_count_next; + deficit_idle_count_reg <= deficit_idle_count_next; + + s_tdata_reg <= s_tdata_next; + s_empty_reg <= s_empty_next; + + s_axis_tx_tready_reg <= s_axis_tx_tready_next; + + m_axis_tx_cpl_ts_reg <= m_axis_tx_cpl_ts_next; + m_axis_tx_cpl_tag_reg <= m_axis_tx_cpl_tag_next; + m_axis_tx_cpl_valid_reg <= m_axis_tx_cpl_valid_next; + + for (integer i = 0; i < 3; i = i + 1) begin + crc_state_reg[i] <= crc_state_next[i]; + end + + if (update_crc) begin + crc_state_reg[3] <= crc_state_next[3]; + end + + if (reset_crc) begin + crc_state_reg[3] <= '1; + end + + xgmii_txd_reg <= xgmii_txd_next; + xgmii_txc_reg <= xgmii_txc_next; + + start_packet_reg <= start_packet_next; + error_underflow_reg <= error_underflow_next; + + if (rst) begin + state_reg <= STATE_IDLE; + + frame_reg <= 1'b0; + + ifg_count_reg <= 8'd0; + deficit_idle_count_reg <= 2'd0; + + s_axis_tx_tready_reg <= 1'b0; + + m_axis_tx_cpl_valid_reg <= 1'b0; + + xgmii_txd_reg <= {CTRL_W{XGMII_IDLE}}; + xgmii_txc_reg <= {CTRL_W{1'b1}}; + + start_packet_reg <= 1'b0; + error_underflow_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/tb/eth/taxi_axis_xgmii_tx_32/Makefile b/tb/eth/taxi_axis_xgmii_tx_32/Makefile new file mode 100644 index 0000000..03b55bf --- /dev/null +++ b/tb/eth/taxi_axis_xgmii_tx_32/Makefile @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2020-2025 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = taxi_axis_xgmii_tx_32 +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/lfsr/taxi_lfsr.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 := 32 +export PARAM_CTRL_W := $(shell expr $(PARAM_DATA_W) / 8 ) +export PARAM_PADDING_EN := 1 +export PARAM_DIC_EN := 1 +export PARAM_MIN_FRAME_LEN := 64 +export PARAM_PTP_TS_EN := 1 +export PARAM_PTP_TS_W := 96 +export PARAM_TX_TAG_W := 16 +export PARAM_TX_CPL_CTRL_IN_TUSER := 1 + +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_axis_xgmii_tx_32/test_taxi_axis_xgmii_tx_32.py b/tb/eth/taxi_axis_xgmii_tx_32/test_taxi_axis_xgmii_tx_32.py new file mode 100644 index 0000000..296571d --- /dev/null +++ b/tb/eth/taxi_axis_xgmii_tx_32/test_taxi_axis_xgmii_tx_32.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2020-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os + +import cocotb_test.simulator +import pytest + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.utils import get_time_from_sim_steps +from cocotb.regression import TestFactory + +from cocotbext.eth import XgmiiSink, PtpClockSimTime +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamFrame + + +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, 3.2, units="ns").start()) + + self.source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis_tx), dut.clk, dut.rst) + self.sink = XgmiiSink(dut.xgmii_txd, dut.xgmii_txc, dut.clk, dut.rst) + + self.ptp_clock = PtpClockSimTime(ts_tod=dut.ptp_ts, clock=dut.clk) + self.tx_cpl_sink = AxiStreamSink(AxiStreamBus.from_entity(dut.m_axis_tx_cpl), dut.clk, dut.rst) + + dut.cfg_ifg.setimmediatevalue(0) + dut.cfg_tx_enable.setimmediatevalue(0) + + 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 run_test(dut, payload_lengths=None, payload_data=None, ifg=12): + + tb = TB(dut) + + tb.dut.cfg_ifg.value = ifg + tb.dut.cfg_tx_enable.value = 1 + + await tb.reset() + + test_frames = [payload_data(x) for x in payload_lengths()] + + for test_data in test_frames: + await tb.source.send(AxiStreamFrame(test_data, tid=0, tuser=2)) + + for test_data in test_frames: + rx_frame = await tb.sink.recv() + tx_cpl = await tb.tx_cpl_sink.recv() + + ptp_ts_ns = int(tx_cpl.tdata[0]) / 2**16 + + rx_frame_sfd_ns = get_time_from_sim_steps(rx_frame.sim_time_sfd, "ns") + + tb.log.info("TX frame PTP TS: %f ns", ptp_ts_ns) + tb.log.info("RX frame SFD sim time: %f ns", rx_frame_sfd_ns) + tb.log.info("Difference: %f ns", abs(rx_frame_sfd_ns - ptp_ts_ns)) + + assert rx_frame.get_payload() == test_data + assert rx_frame.check_fcs() + assert rx_frame.ctrl is None + assert abs(rx_frame_sfd_ns - ptp_ts_ns - 3.2) < 0.01 + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_alignment(dut, payload_data=None, ifg=12): + + enable_dic = int(dut.DIC_EN.value) + + tb = TB(dut) + + byte_width = tb.source.width // 8 + + tb.dut.cfg_ifg.value = ifg + tb.dut.cfg_tx_enable.value = 1 + + await tb.reset() + + for length in range(60, 92): + + for k in range(10): + await RisingEdge(dut.clk) + + test_frames = [payload_data(length) for k in range(10)] + start_lane = [] + + for test_data in test_frames: + await tb.source.send(AxiStreamFrame(test_data, tid=0, tuser=2)) + + for test_data in test_frames: + rx_frame = await tb.sink.recv() + tx_cpl = await tb.tx_cpl_sink.recv() + + ptp_ts_ns = int(tx_cpl.tdata[0]) / 2**16 + + rx_frame_sfd_ns = get_time_from_sim_steps(rx_frame.sim_time_sfd, "ns") + + tb.log.info("TX frame PTP TS: %f ns", ptp_ts_ns) + tb.log.info("RX frame SFD sim time: %f ns", rx_frame_sfd_ns) + tb.log.info("Difference: %f ns", abs(rx_frame_sfd_ns - ptp_ts_ns)) + + assert rx_frame.get_payload() == test_data + assert rx_frame.check_fcs() + assert rx_frame.ctrl is None + assert abs(rx_frame_sfd_ns - ptp_ts_ns - 3.2) < 0.01 + + start_lane.append(rx_frame.start_lane) + + tb.log.info("length: %d", length) + tb.log.info("start_lane: %s", start_lane) + + start_lane_ref = [] + + # compute expected starting lanes + lane = 0 + deficit_idle_count = 0 + + for test_data in test_frames: + if ifg == 0: + lane = 0 + + start_lane_ref.append(lane) + lane = (lane + len(test_data)+4+ifg) % byte_width + + if enable_dic: + offset = lane % 4 + if deficit_idle_count+offset >= 4: + offset += 4 + lane = (lane - offset) % byte_width + deficit_idle_count = (deficit_idle_count + offset) % 4 + else: + offset = lane % 4 + if offset > 0: + offset += 4 + lane = (lane - offset) % byte_width + + tb.log.info("start_lane_ref: %s", start_lane_ref) + + assert start_lane_ref == start_lane + + await RisingEdge(dut.clk) + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_underrun(dut, ifg=12): + + tb = TB(dut) + + tb.dut.cfg_ifg.value = ifg + tb.dut.cfg_tx_enable.value = 1 + + await tb.reset() + + test_data = bytes(x for x in range(60)) + + for k in range(3): + test_frame = AxiStreamFrame(test_data) + await tb.source.send(test_frame) + + for k in range(32): + await RisingEdge(dut.clk) + + tb.source.pause = True + + for k in range(4): + await RisingEdge(dut.clk) + + tb.source.pause = False + + for k in range(3): + rx_frame = await tb.sink.recv() + + if k == 1: + assert rx_frame.data[-1] == 0xFE + assert rx_frame.ctrl[-1] == 1 + else: + assert rx_frame.get_payload() == test_data + assert rx_frame.check_fcs() + assert rx_frame.ctrl is None + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_error(dut, ifg=12): + + tb = TB(dut) + + tb.dut.cfg_ifg.value = ifg + tb.dut.cfg_tx_enable.value = 1 + + await tb.reset() + + test_data = bytes(x for x in range(60)) + + for k in range(3): + test_frame = AxiStreamFrame(test_data) + if k == 1: + test_frame.tuser = 1 + await tb.source.send(test_frame) + + for k in range(3): + rx_frame = await tb.sink.recv() + + if k == 1: + assert rx_frame.data[-1] == 0xFE + assert rx_frame.ctrl[-1] == 1 + else: + assert rx_frame.get_payload() == test_data + assert rx_frame.check_fcs() + assert rx_frame.ctrl is None + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def size_list(): + return list(range(60, 128)) + [512, 1514, 9214] + [60]*10 + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +def cycle_en(): + return itertools.cycle([0, 0, 0, 1]) + + +if cocotb.SIM_NAME: + + factory = TestFactory(run_test) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("ifg", [12]) + factory.generate_tests() + + factory = TestFactory(run_test_alignment) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("ifg", [12]) + factory.generate_tests() + + for test in [run_test_underrun, run_test_error]: + + factory = TestFactory(test) + factory.add_option("ifg", [12]) + 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("dic_en", [1, 0]) +def test_taxi_axis_xgmii_tx_32(request, dic_en): + dut = "taxi_axis_xgmii_tx_32" + 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, "lfsr", "taxi_lfsr.sv"), + os.path.join(rtl_dir, "axis", "taxi_axis_if.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['DATA_W'] = 32 + parameters['CTRL_W'] = parameters['DATA_W'] // 8 + parameters['PADDING_EN'] = 1 + parameters['DIC_EN'] = dic_en + parameters['MIN_FRAME_LEN'] = 64 + parameters['PTP_TS_EN'] = 1 + parameters['PTP_TS_W'] = 96 + parameters['TX_TAG_W'] = 16 + parameters['TX_CPL_CTRL_IN_TUSER'] = 1 + + 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_axis_xgmii_tx_32/test_taxi_axis_xgmii_tx_32.sv b/tb/eth/taxi_axis_xgmii_tx_32/test_taxi_axis_xgmii_tx_32.sv new file mode 100644 index 0000000..6f93bca --- /dev/null +++ b/tb/eth/taxi_axis_xgmii_tx_32/test_taxi_axis_xgmii_tx_32.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 + +/* + * AXI4-Stream XGMII frame transmitter testbench + */ +module test_taxi_axis_xgmii_tx_32 # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 32, + parameter CTRL_W = (DATA_W/8), + parameter logic PADDING_EN = 1'b1, + parameter logic DIC_EN = 1'b1, + parameter MIN_FRAME_LEN = 64, + parameter logic PTP_TS_EN = 1'b0, + parameter PTP_TS_W = 96, + parameter TX_TAG_W = 16, + parameter logic TX_CPL_CTRL_IN_TUSER = 1'b0 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +localparam USER_W = TX_CPL_CTRL_IN_TUSER ? 2 : 1; + +logic clk; +logic rst; + +taxi_axis_if #(.DATA_W(DATA_W), .USER_W(USER_W), .ID_EN(1), .ID_W(TX_TAG_W)) s_axis_tx(); +taxi_axis_if #(.DATA_W(PTP_TS_W), .KEEP_W(1), .ID_EN(1), .ID_W(TX_TAG_W)) m_axis_tx_cpl(); + +logic [DATA_W-1:0] xgmii_txd; +logic [CTRL_W-1:0] xgmii_txc; + +logic [PTP_TS_W-1:0] ptp_ts; + +logic [7:0] cfg_ifg; +logic cfg_tx_enable; + +logic start_packet; +logic error_underflow; + +taxi_axis_xgmii_tx_32 #( + .DATA_W(DATA_W), + .CTRL_W(CTRL_W), + .PADDING_EN(PADDING_EN), + .DIC_EN(DIC_EN), + .MIN_FRAME_LEN(MIN_FRAME_LEN), + .PTP_TS_EN(PTP_TS_EN), + .PTP_TS_W(PTP_TS_W), + .TX_CPL_CTRL_IN_TUSER(TX_CPL_CTRL_IN_TUSER) +) +uut ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis_tx(s_axis_tx), + .m_axis_tx_cpl(m_axis_tx_cpl), + + /* + * XGMII output + */ + .xgmii_txd(xgmii_txd), + .xgmii_txc(xgmii_txc), + + /* + * PTP + */ + .ptp_ts(ptp_ts), + + /* + * Configuration + */ + .cfg_ifg(cfg_ifg), + .cfg_tx_enable(cfg_tx_enable), + + /* + * Status + */ + .start_packet(start_packet), + .error_underflow(error_underflow) +); + +endmodule + +`resetall