From c914adf9f188dea63076f3474871a257a01c7bff Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Fri, 7 Feb 2025 18:02:48 -0800 Subject: [PATCH] eth: Add AXI stream GMII Ethernet frame receiver module and testbench Signed-off-by: Alex Forencich --- rtl/eth/taxi_axis_gmii_tx.sv | 445 ++++++++++++++++++ tb/eth/taxi_axis_gmii_tx/Makefile | 54 +++ .../test_taxi_axis_gmii_tx.py | 299 ++++++++++++ .../test_taxi_axis_gmii_tx.sv | 106 +++++ 4 files changed, 904 insertions(+) create mode 100644 rtl/eth/taxi_axis_gmii_tx.sv create mode 100644 tb/eth/taxi_axis_gmii_tx/Makefile create mode 100644 tb/eth/taxi_axis_gmii_tx/test_taxi_axis_gmii_tx.py create mode 100644 tb/eth/taxi_axis_gmii_tx/test_taxi_axis_gmii_tx.sv diff --git a/rtl/eth/taxi_axis_gmii_tx.sv b/rtl/eth/taxi_axis_gmii_tx.sv new file mode 100644 index 0000000..ccc88af --- /dev/null +++ b/rtl/eth/taxi_axis_gmii_tx.sv @@ -0,0 +1,445 @@ +// 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 GMII frame transmitter (AXI in, GMII out) + */ +module taxi_axis_gmii_tx # +( + parameter DATA_W = 8, + parameter logic PADDING_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, + + /* + * GMII output + */ + output wire logic [DATA_W-1:0] gmii_txd, + output wire logic gmii_tx_en, + output wire logic gmii_tx_er, + + /* + * PTP + */ + input wire logic [PTP_TS_W-1:0] ptp_ts, + + /* + * Control + */ + input wire logic clk_enable, + input wire logic mii_select, + + /* + * 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 +); + +localparam USER_W = TX_CPL_CTRL_IN_TUSER ? 2 : 1; +localparam TX_TAG_W = s_axis_tx.ID_W; + +localparam MIN_LEN_W = $clog2(MIN_FRAME_LEN-4-1+1); + +// check configuration +if (DATA_W != 8) + $fatal(0, "Error: Interface width must be 8 (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 [2:0] + STATE_IDLE = 3'd0, + STATE_PREAMBLE = 3'd1, + STATE_PAYLOAD = 3'd2, + STATE_LAST = 3'd3, + STATE_PAD = 3'd4, + STATE_FCS = 3'd5, + STATE_IFG = 3'd6; + +logic [2:0] state_reg = STATE_IDLE, state_next; + +// datapath control signals +logic reset_crc; +logic update_crc; + +logic [7:0] s_tdata_reg = 8'd0, s_tdata_next; + +logic mii_odd_reg = 1'b0, mii_odd_next; +logic [3:0] mii_msn_reg = 4'b0, mii_msn_next; + +logic frame_reg = 1'b0, frame_next; +logic frame_error_reg = 1'b0, frame_error_next; +logic [7:0] frame_ptr_reg = '0, frame_ptr_next; +logic [MIN_LEN_W-1:0] frame_min_count_reg = '0, frame_min_count_next; + +logic [7:0] gmii_txd_reg = 8'd0, gmii_txd_next; +logic gmii_tx_en_reg = 1'b0, gmii_tx_en_next; +logic gmii_tx_er_reg = 1'b0, gmii_tx_er_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 start_packet_int_reg = 1'b0, start_packet_int_next; +logic start_packet_reg = 1'b0, start_packet_next; +logic error_underflow_reg = 1'b0, error_underflow_next; + +logic [31:0] crc_state = '1; +wire [31:0] crc_next; + +assign s_axis_tx.tready = s_axis_tx_tready_reg; + +assign gmii_txd = gmii_txd_reg; +assign gmii_tx_en = gmii_tx_en_reg; +assign gmii_tx_er = gmii_tx_er_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; + +taxi_lfsr #( + .LFSR_W(32), + .LFSR_POLY(32'h4c11db7), + .LFSR_GALOIS(1), + .LFSR_FEED_FORWARD(0), + .REVERSE(1), + .DATA_W(8) +) +eth_crc_8 ( + .data_in(s_tdata_reg), + .state_in(crc_state), + .data_out(), + .state_out(crc_next) +); + +always_comb begin + state_next = STATE_IDLE; + + reset_crc = 1'b0; + update_crc = 1'b0; + + mii_odd_next = mii_odd_reg; + mii_msn_next = mii_msn_reg; + + frame_next = frame_reg; + frame_error_next = frame_error_reg; + frame_ptr_next = frame_ptr_reg; + frame_min_count_next = frame_min_count_reg; + + s_axis_tx_tready_next = 1'b0; + + s_tdata_next = s_tdata_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 + m_axis_tx_cpl_ts_next = ptp_ts; + 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 + + gmii_txd_next = '0; + gmii_tx_en_next = 1'b0; + gmii_tx_er_next = 1'b0; + + start_packet_int_next = start_packet_int_reg; + 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 + + if (!clk_enable) begin + // clock disabled - hold state and outputs + gmii_txd_next = gmii_txd_reg; + gmii_tx_en_next = gmii_tx_en_reg; + gmii_tx_er_next = gmii_tx_er_reg; + state_next = state_reg; + end else if (mii_select && mii_odd_reg) begin + // MII odd cycle - hold state, output MSN + mii_odd_next = 1'b0; + gmii_txd_next = {4'd0, mii_msn_reg}; + gmii_tx_en_next = gmii_tx_en_reg; + gmii_tx_er_next = gmii_tx_er_reg; + state_next = state_reg; + if (start_packet_int_reg) begin + start_packet_int_next = 1'b0; + start_packet_next = 1'b1; + end + end else begin + case (state_reg) + STATE_IDLE: begin + // idle state - wait for packet + reset_crc = 1'b1; + + mii_odd_next = 1'b0; + frame_ptr_next = 1; + + frame_error_next = 1'b0; + frame_min_count_next = MIN_LEN_W'(MIN_FRAME_LEN-4-1); + + if (s_axis_tx.tvalid && cfg_tx_enable) begin + mii_odd_next = 1'b1; + gmii_txd_next = ETH_PRE; + gmii_tx_en_next = 1'b1; + state_next = STATE_PREAMBLE; + end else begin + state_next = STATE_IDLE; + end + end + STATE_PREAMBLE: begin + // send preamble + reset_crc = 1'b1; + + mii_odd_next = 1'b1; + frame_ptr_next = frame_ptr_reg + 1; + + gmii_txd_next = ETH_PRE; + gmii_tx_en_next = 1'b1; + + if (frame_ptr_reg == 6) begin + s_axis_tx_tready_next = 1'b1; + s_tdata_next = s_axis_tx.tdata; + state_next = STATE_PREAMBLE; + end else if (frame_ptr_reg == 7) begin + // end of preamble; start payload + frame_ptr_next = '0; + if (s_axis_tx_tready_reg) begin + s_axis_tx_tready_next = 1'b1; + s_tdata_next = s_axis_tx.tdata; + end + gmii_txd_next = ETH_SFD; + if (mii_select) begin + start_packet_int_next = 1'b1; + end else begin + start_packet_next = 1'b1; + end + state_next = STATE_PAYLOAD; + end else begin + state_next = STATE_PREAMBLE; + end + end + STATE_PAYLOAD: begin + // send payload + + update_crc = 1'b1; + s_axis_tx_tready_next = 1'b1; + + mii_odd_next = 1'b1; + + if (frame_min_count_reg != 0) begin + frame_min_count_next = frame_min_count_reg - 1; + end + + gmii_txd_next = s_tdata_reg; + gmii_tx_en_next = 1'b1; + + s_tdata_next = s_axis_tx.tdata; + + 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; + + state_next = STATE_LAST; + end else begin + state_next = STATE_PAYLOAD; + end + end + STATE_LAST: begin + // last payload word + + update_crc = 1'b1; + s_axis_tx_tready_next = frame_next; // drop frame + + mii_odd_next = 1'b1; + + gmii_txd_next = s_tdata_reg; + gmii_tx_en_next = 1'b1; + + if (PADDING_EN && frame_min_count_reg != 0) begin + frame_min_count_next = frame_min_count_reg - 1; + s_tdata_next = 8'd0; + state_next = STATE_PAD; + end else begin + frame_ptr_next = '0; + state_next = STATE_FCS; + end + end + STATE_PAD: begin + // send padding + s_axis_tx_tready_next = frame_next; // drop frame + + update_crc = 1'b1; + mii_odd_next = 1'b1; + + gmii_txd_next = 8'd0; + gmii_tx_en_next = 1'b1; + + s_tdata_next = 8'd0; + + if (frame_min_count_reg != 0) begin + frame_min_count_next = frame_min_count_reg - 1; + state_next = STATE_PAD; + end else begin + frame_ptr_next = '0; + state_next = STATE_FCS; + end + end + STATE_FCS: begin + // send FCS + s_axis_tx_tready_next = frame_next; // drop frame + + mii_odd_next = 1'b1; + frame_ptr_next = frame_ptr_reg + 1; + + case (frame_ptr_reg[1:0]) + 2'd0: gmii_txd_next = ~crc_state[7:0]; + 2'd1: gmii_txd_next = ~crc_state[15:8]; + 2'd2: gmii_txd_next = ~crc_state[23:16]; + 2'd3: gmii_txd_next = ~crc_state[31:24]; + endcase + gmii_tx_en_next = 1'b1; + gmii_tx_er_next = frame_error_reg; + + if (frame_ptr_reg < 3) begin + state_next = STATE_FCS; + end else begin + frame_ptr_next = '0; + state_next = STATE_IFG; + end + end + STATE_IFG: begin + // send IFG + s_axis_tx_tready_next = frame_next; // drop frame + + mii_odd_next = 1'b1; + frame_ptr_next = frame_ptr_reg + 1; + + if (frame_ptr_reg < cfg_ifg-1 || frame_reg) begin + state_next = STATE_IFG; + end else begin + state_next = STATE_IDLE; + end + end + default: begin + // invalid state, return to idle + state_next = STATE_IDLE; + end + endcase + + if (mii_select) begin + mii_msn_next = gmii_txd_next[7:4]; + gmii_txd_next[7:4] = 4'd0; + end + end +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + + frame_reg <= frame_next; + frame_error_reg <= frame_error_next; + frame_ptr_reg <= frame_ptr_next; + frame_min_count_reg <= frame_min_count_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; + + mii_odd_reg <= mii_odd_next; + mii_msn_reg <= mii_msn_next; + + s_tdata_reg <= s_tdata_next; + + s_axis_tx_tready_reg <= s_axis_tx_tready_next; + + gmii_txd_reg <= gmii_txd_next; + gmii_tx_en_reg <= gmii_tx_en_next; + gmii_tx_er_reg <= gmii_tx_er_next; + + if (reset_crc) begin + crc_state <= '1; + end else if (update_crc) begin + crc_state <= crc_next; + end + + start_packet_int_reg <= start_packet_int_next; + start_packet_reg <= start_packet_next; + error_underflow_reg <= error_underflow_next; + + if (rst) begin + state_reg <= STATE_IDLE; + + frame_reg <= 1'b0; + + s_axis_tx_tready_reg <= 1'b0; + + m_axis_tx_cpl_valid_reg <= 1'b0; + + gmii_tx_en_reg <= 1'b0; + gmii_tx_er_reg <= 1'b0; + + start_packet_int_reg <= 1'b0; + start_packet_reg <= 1'b0; + error_underflow_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/tb/eth/taxi_axis_gmii_tx/Makefile b/tb/eth/taxi_axis_gmii_tx/Makefile new file mode 100644 index 0000000..5a0b33a --- /dev/null +++ b/tb/eth/taxi_axis_gmii_tx/Makefile @@ -0,0 +1,54 @@ +# 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_gmii_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/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 := 8 +export PARAM_PADDING_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_gmii_tx/test_taxi_axis_gmii_tx.py b/tb/eth/taxi_axis_gmii_tx/test_taxi_axis_gmii_tx.py new file mode 100644 index 0000000..311da95 --- /dev/null +++ b/tb/eth/taxi_axis_gmii_tx/test_taxi_axis_gmii_tx.py @@ -0,0 +1,299 @@ +#!/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 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 GmiiSink, 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) + + self._enable_generator = None + self._enable_cr = None + + cocotb.start_soon(Clock(dut.clk, 8, units="ns").start()) + + self.source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis_tx), dut.clk, dut.rst) + self.sink = GmiiSink(dut.gmii_txd, dut.gmii_tx_er, dut.gmii_tx_en, + dut.clk, dut.rst, dut.clk_enable, dut.mii_select) + + 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.clk_enable.setimmediatevalue(1) + dut.mii_select.setimmediatevalue(0) + 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) + + def set_enable_generator(self, generator=None): + if self._enable_cr is not None: + self._enable_cr.kill() + self._enable_cr = None + + self._enable_generator = generator + + if self._enable_generator is not None: + self._enable_cr = cocotb.start_soon(self._run_enable()) + + def clear_enable_generator(self): + self.set_enable_generator(None) + + async def _run_enable(self): + for val in self._enable_generator: + self.dut.clk_enable.value = val + await RisingEdge(self.dut.clk) + + +async def run_test(dut, payload_lengths=None, payload_data=None, ifg=12, enable_gen=None, mii_sel=False): + + tb = TB(dut) + + tb.dut.cfg_ifg.value = ifg + tb.dut.cfg_tx_enable.value = 1 + tb.dut.mii_select.value = mii_sel + + if enable_gen is not None: + tb.set_enable_generator(enable_gen()) + + 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.error is None + assert abs(rx_frame_sfd_ns - ptp_ts_ns - (32 if enable_gen else 8)) < 0.01 + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_underrun(dut, ifg=12, enable_gen=None, mii_sel=False): + + tb = TB(dut) + + tb.dut.cfg_ifg.value = ifg + tb.dut.cfg_tx_enable.value = 1 + tb.dut.mii_select.value = mii_sel + + if enable_gen is not None: + tb.set_enable_generator(enable_gen()) + + 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(200 if mii_sel else 100): + while True: + await RisingEdge(dut.clk) + if dut.clk_enable.value.integer: + break + + tb.source.pause = True + + for k in range(10): + while True: + await RisingEdge(dut.clk) + if dut.clk_enable.value.integer: + break + + tb.source.pause = False + + for k in range(3): + rx_frame = await tb.sink.recv() + + if k == 1: + assert rx_frame.error[-1] == 1 + else: + assert rx_frame.get_payload() == test_data + assert rx_frame.check_fcs() + assert rx_frame.error is None + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_error(dut, ifg=12, enable_gen=None, mii_sel=False): + + tb = TB(dut) + + tb.dut.cfg_ifg.value = ifg + tb.dut.cfg_tx_enable.value = 1 + tb.dut.mii_select.value = mii_sel + + if enable_gen is not None: + tb.set_enable_generator(enable_gen()) + + 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.error[-1] == 1 + else: + assert rx_frame.get_payload() == test_data + assert rx_frame.check_fcs() + assert rx_frame.error is None + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def size_list(): + return list(range(60, 128)) + [512, 1514] + [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.add_option("enable_gen", [None, cycle_en]) + factory.add_option("mii_sel", [False, True]) + factory.generate_tests() + + for test in [run_test_underrun, run_test_error]: + + factory = TestFactory(test) + factory.add_option("ifg", [12]) + factory.add_option("enable_gen", [None, cycle_en]) + factory.add_option("mii_sel", [False, True]) + 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()) + + +def test_taxi_axis_gmii_tx(request): + dut = "taxi_axis_gmii_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, "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'] = 8 + parameters['PADDING_EN'] = 1 + 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_gmii_tx/test_taxi_axis_gmii_tx.sv b/tb/eth/taxi_axis_gmii_tx/test_taxi_axis_gmii_tx.sv new file mode 100644 index 0000000..bc46429 --- /dev/null +++ b/tb/eth/taxi_axis_gmii_tx/test_taxi_axis_gmii_tx.sv @@ -0,0 +1,106 @@ +// 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 GMII frame transmitter testbench + */ +module test_taxi_axis_gmii_tx # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 8, + parameter logic PADDING_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] gmii_txd; +logic gmii_tx_en; +logic gmii_tx_er; + +logic [PTP_TS_W-1:0] ptp_ts; + +logic clk_enable; +logic mii_select; + +logic [7:0] cfg_ifg; +logic cfg_tx_enable; + +logic start_packet; +logic error_underflow; + +taxi_axis_gmii_tx #( + .DATA_W(DATA_W), + .PADDING_EN(PADDING_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), + + /* + * GMII output + */ + .gmii_txd(gmii_txd), + .gmii_tx_en(gmii_tx_en), + .gmii_tx_er(gmii_tx_er), + + /* + * PTP + */ + .ptp_ts(ptp_ts), + + /* + * Control + */ + .clk_enable(clk_enable), + .mii_select(mii_select), + + /* + * Configuration + */ + .cfg_ifg(cfg_ifg), + .cfg_tx_enable(cfg_tx_enable), + + /* + * Status + */ + .start_packet(start_packet), + .error_underflow(error_underflow) +); + +endmodule + +`resetall