diff --git a/src/eth/rtl/taxi_axis_basex_rx_16.sv b/src/eth/rtl/taxi_axis_basex_rx_16.sv new file mode 100644 index 0000000..21b4083 --- /dev/null +++ b/src/eth/rtl/taxi_axis_basex_rx_16.sv @@ -0,0 +1,662 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream 1000BASE-X frame receiver (1000BASE-X in, AXI out) + */ +module taxi_axis_basex_rx_16 # +( + parameter DATA_W = 16, + parameter CTRL_W = 2, + parameter logic GBX_IF_EN = 1'b0, + parameter logic PTP_TS_EN = 1'b0, + parameter logic PTP_TS_FMT_TOD = 1'b1, + parameter PTP_TS_W = 96 +) +( + input wire logic clk, + input wire logic rst, + + /* + * 1000BASE-X encoded input + */ + input wire logic [DATA_W-1:0] encoded_rx_data, + input wire logic [CTRL_W-1:0] encoded_rx_data_k, + input wire logic encoded_rx_data_valid, + + /* + * Receive interface (AXI stream) + */ + taxi_axis_if.src m_axis_rx, + + /* + * PTP + */ + input wire logic [PTP_TS_W-1:0] ptp_ts, + + /* + * Configuration + */ + input wire logic [15:0] cfg_rx_max_pkt_len = 16'd1518-1, + input wire logic cfg_rx_enable, + + /* + * Status + */ + output wire logic [1:0] rx_start_packet, + output wire logic [1:0] stat_rx_byte, + output wire logic [15:0] stat_rx_pkt_len, + output wire logic stat_rx_pkt_fragment, + output wire logic stat_rx_pkt_jabber, + output wire logic stat_rx_pkt_ucast, + output wire logic stat_rx_pkt_mcast, + output wire logic stat_rx_pkt_bcast, + output wire logic stat_rx_pkt_vlan, + output wire logic stat_rx_pkt_good, + output wire logic stat_rx_pkt_bad, + output wire logic stat_rx_err_oversize, + output wire logic stat_rx_err_bad_fcs, + output wire logic stat_rx_err_bad_block, + output wire logic stat_rx_err_framing, + output wire logic stat_rx_err_preamble +); + +// extract parameters +localparam KEEP_W = DATA_W/8; +localparam USER_W = (PTP_TS_EN ? PTP_TS_W : 0) + 1; + +// check configuration +if (DATA_W != 16) + $fatal(0, "Error: Interface width must be 16 (instance %m)"); + +if (KEEP_W*8 != DATA_W) + $fatal(0, "Error: Interface requires byte (8-bit) granularity (instance %m)"); + +if (CTRL_W != 2) + $fatal(0, "Error: CTRL_W must be 2 (instance %m)"); + +if (m_axis_rx.DATA_W != DATA_W) + $fatal(0, "Error: Interface DATA_W parameter mismatch (instance %m)"); + +if (m_axis_rx.USER_W != USER_W) + $fatal(0, "Error: Interface USER_W parameter mismatch (instance %m)"); + +typedef enum logic [7:0] { + ETH_PRE = 8'h55, + ETH_SFD = 8'hD5 +} eth_pre_t; + +function [7:0] D(input [4:0] edcba, input [2:0] hgf); + D = {hgf, edcba}; +endfunction + +function [7:0] K(input [4:0] edcba, input [2:0] hgf); + K = {hgf, edcba}; +endfunction + +localparam logic [15:0] CTRL_C1 = {D(21,5), K(28,5)}; +localparam logic [15:0] CTRL_C2 = {D(2,2), K(28,5)}; +localparam logic [15:0] CTRL_I1 = {D(5,6), K(28,5)}; +localparam logic [15:0] CTRL_I2 = {D(16,2), K(28,5)}; +localparam logic [7:0] CTRL_R = K(23,7); +localparam logic [7:0] CTRL_S = K(27,7); +localparam logic [7:0] CTRL_T = K(29,7); +localparam logic [7:0] CTRL_V = K(30,7); +localparam logic [15:0] CTRL_L1 = {D(6,5), K(28,5)}; +localparam logic [15:0] CTRL_L2 = {D(26,4), K(28,5)}; + +typedef enum logic [2:0] { + STATE_IDLE, + STATE_PREAMBLE, + STATE_PIPE, + STATE_PAYLOAD, + STATE_LAST +} state_t; + +state_t state_reg = STATE_IDLE, state_next; + +// datapath control signals +logic reset_crc; + +logic [DATA_W-1:0] input_data_d0_reg = '0; +logic [DATA_W-1:0] input_data_d1_reg = '0; +logic [DATA_W-1:0] input_data_d2_reg = '0; + +logic input_start_d0_reg = 1'b0; + +logic frame_oversize_reg = 1'b0, frame_oversize_next; +logic pre_ok_reg = 1'b0, pre_ok_next; +logic [2:0] hdr_ptr_reg = '0, hdr_ptr_next; +logic is_mcast_reg = 1'b0, is_mcast_next; +logic is_bcast_reg = 1'b0, is_bcast_next; +logic is_8021q_reg = 1'b0, is_8021q_next; +logic [15:0] frame_len_reg = '0, frame_len_next; +logic [14:0] frame_len_lim_cyc_reg = '0, frame_len_lim_cyc_next; +logic frame_len_lim_last_reg = '0, frame_len_lim_last_next; +logic frame_len_lim_check_reg = '0, frame_len_lim_check_next; + +logic [DATA_W-1:0] m_axis_rx_tdata_reg = '0, m_axis_rx_tdata_next; +logic [KEEP_W-1:0] m_axis_rx_tkeep_reg = '0, m_axis_rx_tkeep_next; +logic m_axis_rx_tvalid_reg = 1'b0, m_axis_rx_tvalid_next; +logic m_axis_rx_tlast_reg = 1'b0, m_axis_rx_tlast_next; +logic m_axis_rx_tuser_reg = 1'b0, m_axis_rx_tuser_next; + +logic [1:0] start_packet_reg = '0, start_packet_next; +logic frame_reg = 1'b0; + +logic [1:0] stat_rx_byte_reg = '0, stat_rx_byte_next; +logic [15:0] stat_rx_pkt_len_reg = '0, stat_rx_pkt_len_next; +logic stat_rx_pkt_fragment_reg = 1'b0, stat_rx_pkt_fragment_next; +logic stat_rx_pkt_jabber_reg = 1'b0, stat_rx_pkt_jabber_next; +logic stat_rx_pkt_ucast_reg = 1'b0, stat_rx_pkt_ucast_next; +logic stat_rx_pkt_mcast_reg = 1'b0, stat_rx_pkt_mcast_next; +logic stat_rx_pkt_bcast_reg = 1'b0, stat_rx_pkt_bcast_next; +logic stat_rx_pkt_vlan_reg = 1'b0, stat_rx_pkt_vlan_next; +logic stat_rx_pkt_good_reg = 1'b0, stat_rx_pkt_good_next; +logic stat_rx_pkt_bad_reg = 1'b0, stat_rx_pkt_bad_next; +logic stat_rx_err_oversize_reg = 1'b0, stat_rx_err_oversize_next; +logic stat_rx_err_bad_fcs_reg = 1'b0, stat_rx_err_bad_fcs_next; +logic stat_rx_err_bad_block_reg = 1'b0; +logic stat_rx_err_framing_reg = 1'b0, stat_rx_err_framing_next; +logic stat_rx_err_preamble_reg = 1'b0, stat_rx_err_preamble_next; + +logic [PTP_TS_W-1:0] ptp_ts_out_reg = '0, ptp_ts_out_next; + +logic [31:0] crc_state_reg = '1; + +wire [31:0] crc_state; + +wire [1:0] crc_valid; +logic [1:0] crc_valid_reg = '0; + +assign crc_valid[1] = crc_state == ~32'h2144df1c; +assign crc_valid[0] = crc_state == ~32'hc622f71d; + +logic [4+16-1:0] last_ts_reg = '0; +logic [4+16-1:0] ts_inc_reg = '0; + +assign m_axis_rx.tdata = m_axis_rx_tdata_reg; +assign m_axis_rx.tkeep = m_axis_rx_tkeep_reg; +assign m_axis_rx.tstrb = m_axis_rx.tkeep; +assign m_axis_rx.tvalid = m_axis_rx_tvalid_reg; +assign m_axis_rx.tlast = m_axis_rx_tlast_reg; +assign m_axis_rx.tid = '0; +assign m_axis_rx.tdest = '0; +assign m_axis_rx.tuser[0] = m_axis_rx_tuser_reg; +if (PTP_TS_EN) begin + assign m_axis_rx.tuser[1 +: PTP_TS_W] = ptp_ts_out_reg; +end + +assign rx_start_packet = start_packet_reg; + +assign stat_rx_byte = stat_rx_byte_reg; +assign stat_rx_pkt_len = stat_rx_pkt_len_reg; +assign stat_rx_pkt_fragment = stat_rx_pkt_fragment_reg; +assign stat_rx_pkt_jabber = stat_rx_pkt_jabber_reg; +assign stat_rx_pkt_ucast = stat_rx_pkt_ucast_reg; +assign stat_rx_pkt_mcast = stat_rx_pkt_mcast_reg; +assign stat_rx_pkt_bcast = stat_rx_pkt_bcast_reg; +assign stat_rx_pkt_vlan = stat_rx_pkt_vlan_reg; +assign stat_rx_pkt_good = stat_rx_pkt_good_reg; +assign stat_rx_pkt_bad = stat_rx_pkt_bad_reg; +assign stat_rx_err_oversize = stat_rx_err_oversize_reg; +assign stat_rx_err_bad_fcs = stat_rx_err_bad_fcs_reg; +assign stat_rx_err_bad_block = stat_rx_err_bad_block_reg; +assign stat_rx_err_framing = stat_rx_err_framing_reg; +assign stat_rx_err_preamble = stat_rx_err_preamble_reg; + +// Mask input data +wire [DATA_W-1:0] encoded_rx_data_masked; +wire [CTRL_W-1:0] encoded_rx_data_term; + +for (genvar n = 0; n < CTRL_W; n = n + 1) begin + assign encoded_rx_data_masked[n*8 +: 8] = (n > 0 && encoded_rx_data_k[n]) ? 8'd0 : encoded_rx_data[n*8 +: 8]; + assign encoded_rx_data_term[n] = encoded_rx_data_k[n] && (encoded_rx_data[n*8 +: 8] == CTRL_T); +end + +taxi_lfsr #( + .LFSR_W(32), + .LFSR_POLY(32'h4c11db7), + .LFSR_GALOIS(1), + .LFSR_FEED_FORWARD(0), + .REVERSE(1), + .DATA_W(DATA_W), + .DATA_IN_EN(1'b1), + .DATA_OUT_EN(1'b0) +) +eth_crc ( + .data_in(input_data_d0_reg), + .state_in(crc_state_reg), + .data_out(), + .state_out(crc_state) +); + +always_comb begin + state_next = STATE_IDLE; + + reset_crc = 1'b0; + + frame_oversize_next = frame_oversize_reg; + pre_ok_next = pre_ok_reg; + hdr_ptr_next = hdr_ptr_reg; + is_mcast_next = is_mcast_reg; + is_bcast_next = is_bcast_reg; + is_8021q_next = is_8021q_reg; + frame_len_next = frame_len_reg; + frame_len_lim_cyc_next = frame_len_lim_cyc_reg; + frame_len_lim_last_next = frame_len_lim_last_reg; + frame_len_lim_check_next = frame_len_lim_check_reg; + + m_axis_rx_tdata_next = input_data_d2_reg; + m_axis_rx_tkeep_next = {KEEP_W{1'b1}}; + m_axis_rx_tvalid_next = 1'b0; + m_axis_rx_tlast_next = 1'b0; + m_axis_rx_tuser_next = 1'b0; + + ptp_ts_out_next = ptp_ts_out_reg; + + start_packet_next = '0; + + stat_rx_byte_next = '0; + stat_rx_pkt_len_next = '0; + stat_rx_pkt_fragment_next = 1'b0; + stat_rx_pkt_jabber_next = 1'b0; + stat_rx_pkt_ucast_next = 1'b0; + stat_rx_pkt_mcast_next = 1'b0; + stat_rx_pkt_bcast_next = 1'b0; + stat_rx_pkt_vlan_next = 1'b0; + stat_rx_pkt_good_next = 1'b0; + stat_rx_pkt_bad_next = 1'b0; + stat_rx_err_oversize_next = 1'b0; + stat_rx_err_bad_fcs_next = 1'b0; + stat_rx_err_framing_next = 1'b0; + stat_rx_err_preamble_next = 1'b0; + + if (GBX_IF_EN && !encoded_rx_data_valid) begin + // data from gearbox not valid - hold state + state_next = state_reg; + end else begin + // counter to measure frame length + if (&frame_len_reg[15:1] == 0) begin + casez (encoded_rx_data_k) + 2'b00: frame_len_next = frame_len_reg + 16'(KEEP_W); + 2'b10: frame_len_next = frame_len_reg + 1; + default: frame_len_next = frame_len_reg + 0; + endcase + end else begin + frame_len_next = '1; + end + + // counter for max frame length enforcement + if (frame_len_lim_cyc_reg != 0) begin + frame_len_lim_cyc_next = frame_len_lim_cyc_reg - 1; + end else begin + frame_len_lim_cyc_next = '0; + end + + if (frame_len_lim_last_reg == 0) begin + if (frame_len_lim_cyc_reg == 1) begin + frame_len_lim_check_next = 1'b1; + end + end else begin + if (frame_len_lim_cyc_reg == 2) begin + frame_len_lim_check_next = 1'b1; + end + end + + // address and ethertype checks + if (&hdr_ptr_reg == 0) begin + hdr_ptr_next = hdr_ptr_reg + 1; + end + + case (hdr_ptr_reg) + 3'd0: begin + is_mcast_next = input_data_d2_reg[0]; + is_bcast_next = &input_data_d2_reg; + end + 3'd1: is_bcast_next = is_bcast_reg && &input_data_d2_reg; + 3'd2: is_bcast_next = is_bcast_reg && &input_data_d2_reg; + 3'd6: is_8021q_next = {input_data_d2_reg[7:0], input_data_d2_reg[15:8]} == 16'h8100; + default: begin + // do nothing + end + endcase + + case (state_reg) + STATE_IDLE: begin + // idle state - wait for packet + reset_crc = 1'b1; + + frame_oversize_next = 1'b0; + frame_len_next = 16'(KEEP_W); + frame_len_lim_cyc_next = cfg_rx_max_pkt_len[15:1]; + frame_len_lim_last_next = cfg_rx_max_pkt_len[0] + 1; + frame_len_lim_check_next = 1'b0; + hdr_ptr_next = 0; + + pre_ok_next = 1'b1; + + if (input_start_d0_reg && encoded_rx_data_k == 0) begin + state_next = STATE_PREAMBLE; + end else begin + state_next = STATE_IDLE; + end + end + STATE_PREAMBLE: begin + // drop preamble + reset_crc = 1'b1; + + frame_oversize_next = 1'b0; + frame_len_next = 16'(KEEP_W); + frame_len_lim_cyc_next = cfg_rx_max_pkt_len[15:1]; + frame_len_lim_last_next = cfg_rx_max_pkt_len[0] + 1; + frame_len_lim_check_next = 1'b0; + hdr_ptr_next = 0; + + ptp_ts_out_next = ptp_ts; + + if (encoded_rx_data_k != 0) begin + // control character + stat_rx_err_framing_next = 1'b1; + state_next = STATE_IDLE; + end else begin + // data + if (input_data_d0_reg == {2{ETH_PRE}}) begin + // normal preamble + state_next = STATE_PREAMBLE; + end else if (input_data_d0_reg == {ETH_SFD, ETH_PRE}) begin + // start in lane 0 + start_packet_next[0] = 1'b1; + if (cfg_rx_enable) begin + stat_rx_byte_next = 2'd2; + state_next = STATE_PIPE; + end else begin + state_next = STATE_IDLE; + end + end else if (input_data_d0_reg[0 +: 8] == ETH_SFD) begin + // start in lane 1 (truncated preamble) + // TODO!! + start_packet_next[1] = 1'b1; + if (cfg_rx_enable) begin + stat_rx_byte_next = 2'd1; + state_next = STATE_PIPE; + end else begin + state_next = STATE_IDLE; + end + end else begin + // abnormal preamble + pre_ok_next = 1'b0; + state_next = STATE_PREAMBLE; + end + end + end + STATE_PIPE: begin + // wait for pipeline to fill + + hdr_ptr_next = 0; + + if (encoded_rx_data_k != 0) begin + // control or error characters in packet + stat_rx_err_framing_next = 1'b1; + state_next = STATE_IDLE; + end else if (input_data_d2_reg == 16'h5555) begin + // preamble continues + stat_rx_byte_next = 2'(KEEP_W); + state_next = STATE_PIPE; + end else if (input_data_d2_reg == 16'hD555) begin + // start in lane 0 + stat_rx_byte_next = 2'(KEEP_W); + state_next = STATE_PAYLOAD; + end else begin + // invalid preamble + stat_rx_err_framing_next = 1'b1; + state_next = STATE_IDLE; + end + end + STATE_PAYLOAD: begin + // read payload + m_axis_rx_tdata_next = input_data_d2_reg; + m_axis_rx_tkeep_next = {KEEP_W{1'b1}}; + m_axis_rx_tvalid_next = 1'b1; + m_axis_rx_tlast_next = 1'b0; + m_axis_rx_tuser_next = 1'b0; + + if (encoded_rx_data_k[0]) begin + stat_rx_byte_next = 2'd0; + end else if (encoded_rx_data_k[1]) begin + stat_rx_byte_next = 2'd1; + if (frame_len_lim_check_reg) begin + if (frame_len_lim_last_reg < 1) begin + frame_oversize_next = 1'b1; + end + end + end else begin + stat_rx_byte_next = 2'(KEEP_W); + if (frame_len_lim_check_reg) begin + // at the limit but this isn't a termination character + frame_oversize_next = 1'b1; + end + end + + // if (encoded_rx_data_k && encoded_rx_data != CTRL_T) begin + // frame_error_next = 1'b1; + // stat_rx_err_framing_next = 1'b1; + // end + + // if (framing_error_reg) begin + // // control or error characters in packet + // m_axis_rx_tlast_next = 1'b1; + // m_axis_rx_tuser_next = 1'b1; + // stat_rx_pkt_bad_next = 1'b1; + // stat_rx_pkt_len_next = frame_len_next; + // stat_rx_pkt_ucast_next = !is_mcast_reg; + // stat_rx_pkt_mcast_next = is_mcast_reg && !is_bcast_reg; + // stat_rx_pkt_bcast_next = is_bcast_reg; + // stat_rx_pkt_vlan_next = is_8021q_reg; + // stat_rx_err_oversize_next = frame_oversize_next; + // stat_rx_err_framing_next = 1'b1; + // stat_rx_err_preamble_next = !pre_ok_reg; + // stat_rx_pkt_fragment_next = frame_len_next[15:6] == 0; + // stat_rx_pkt_jabber_next = frame_oversize_next; + // reset_crc = 1'b1; + // state_next = STATE_IDLE; + // end else if (term_first_cycle_reg) begin + if (encoded_rx_data_k[0]) begin + // end this cycle + m_axis_rx_tkeep_next = 2'b11; + m_axis_rx_tlast_next = 1'b1; + if (encoded_rx_data[0 +: 8] != CTRL_T) begin + // not a termination character + m_axis_rx_tuser_next = 1'b1; + stat_rx_err_framing_next = 1'b1; + stat_rx_pkt_fragment_next = frame_len_next[15:6] == 0; + stat_rx_pkt_jabber_next = frame_oversize_next; + stat_rx_pkt_bad_next = 1'b1; + end else if (crc_valid[1]) begin + // CRC valid + if (frame_oversize_next) begin + // too long + m_axis_rx_tuser_next = 1'b1; + stat_rx_pkt_bad_next = 1'b1; + end else begin + // length OK + m_axis_rx_tuser_next = 1'b0; + stat_rx_pkt_good_next = 1'b1; + end + end else begin + m_axis_rx_tuser_next = 1'b1; + stat_rx_pkt_fragment_next = frame_len_next[15:6] == 0; + stat_rx_pkt_jabber_next = frame_oversize_next; + stat_rx_pkt_bad_next = 1'b1; + stat_rx_err_bad_fcs_next = 1'b1; + end + stat_rx_pkt_len_next = frame_len_next; + stat_rx_pkt_ucast_next = !is_mcast_reg; + stat_rx_pkt_mcast_next = is_mcast_reg && !is_bcast_reg; + stat_rx_pkt_bcast_next = is_bcast_reg; + stat_rx_pkt_vlan_next = is_8021q_reg; + stat_rx_err_oversize_next = frame_oversize_next; + stat_rx_err_preamble_next = !pre_ok_reg; + reset_crc = 1'b1; + state_next = STATE_IDLE; + end else if (encoded_rx_data_k[1]) begin + // need extra cycle + // TODO check term char + state_next = STATE_LAST; + end else begin + state_next = STATE_PAYLOAD; + end + end + STATE_LAST: begin + // last cycle of packet + m_axis_rx_tdata_next = input_data_d2_reg; + m_axis_rx_tkeep_next = 2'b01; + m_axis_rx_tvalid_next = 1'b1; + m_axis_rx_tlast_next = 1'b1; + m_axis_rx_tuser_next = 1'b0; + + reset_crc = 1'b1; + + if (crc_valid[0]) begin + // CRC valid + if (frame_oversize_reg) begin + // too long + m_axis_rx_tuser_next = 1'b1; + stat_rx_pkt_bad_next = 1'b1; + end else begin + // length OK + m_axis_rx_tuser_next = 1'b0; + stat_rx_pkt_good_next = 1'b1; + end + end else begin + m_axis_rx_tuser_next = 1'b1; + stat_rx_pkt_fragment_next = frame_len_reg[15:6] == 0; + stat_rx_pkt_jabber_next = frame_oversize_reg; + stat_rx_pkt_bad_next = 1'b1; + stat_rx_err_bad_fcs_next = 1'b1; + end + + stat_rx_pkt_len_next = frame_len_reg; + stat_rx_pkt_ucast_next = !is_mcast_reg; + stat_rx_pkt_mcast_next = is_mcast_reg && !is_bcast_reg; + stat_rx_pkt_bcast_next = is_bcast_reg; + stat_rx_pkt_vlan_next = is_8021q_reg; + stat_rx_err_oversize_next = frame_oversize_reg; + stat_rx_err_preamble_next = !pre_ok_reg; + + state_next = STATE_IDLE; + end + default: begin + // invalid state, return to idle + state_next = STATE_IDLE; + end + endcase + end +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + + frame_oversize_reg <= frame_oversize_next; + pre_ok_reg <= pre_ok_next; + hdr_ptr_reg <= hdr_ptr_next; + is_mcast_reg <= is_mcast_next; + is_bcast_reg <= is_bcast_next; + is_8021q_reg <= is_8021q_next; + frame_len_reg <= frame_len_next; + frame_len_lim_cyc_reg <= frame_len_lim_cyc_next; + frame_len_lim_last_reg <= frame_len_lim_last_next; + frame_len_lim_check_reg <= frame_len_lim_check_next; + + m_axis_rx_tdata_reg <= m_axis_rx_tdata_next; + m_axis_rx_tkeep_reg <= m_axis_rx_tkeep_next; + m_axis_rx_tvalid_reg <= m_axis_rx_tvalid_next; + m_axis_rx_tlast_reg <= m_axis_rx_tlast_next; + m_axis_rx_tuser_reg <= m_axis_rx_tuser_next; + + ptp_ts_out_reg <= ptp_ts_out_next; + + start_packet_reg <= start_packet_next; + + stat_rx_byte_reg <= stat_rx_byte_next; + stat_rx_pkt_len_reg <= stat_rx_pkt_len_next; + stat_rx_pkt_fragment_reg <= stat_rx_pkt_fragment_next; + stat_rx_pkt_jabber_reg <= stat_rx_pkt_jabber_next; + stat_rx_pkt_ucast_reg <= stat_rx_pkt_ucast_next; + stat_rx_pkt_mcast_reg <= stat_rx_pkt_mcast_next; + stat_rx_pkt_bcast_reg <= stat_rx_pkt_bcast_next; + stat_rx_pkt_vlan_reg <= stat_rx_pkt_vlan_next; + stat_rx_pkt_good_reg <= stat_rx_pkt_good_next; + stat_rx_pkt_bad_reg <= stat_rx_pkt_bad_next; + stat_rx_err_oversize_reg <= stat_rx_err_oversize_next; + stat_rx_err_bad_fcs_reg <= stat_rx_err_bad_fcs_next; + stat_rx_err_bad_block_reg <= 1'b0; + stat_rx_err_framing_reg <= stat_rx_err_framing_next; + stat_rx_err_preamble_reg <= stat_rx_err_preamble_next; + + if (!GBX_IF_EN || encoded_rx_data_valid) begin + input_data_d0_reg <= encoded_rx_data_masked; + input_data_d1_reg <= input_data_d0_reg; + input_data_d2_reg <= input_data_d1_reg; + + input_start_d0_reg <= 1'b0; + + // start control character detection + if (encoded_rx_data_k[0] && encoded_rx_data[7:0] == CTRL_S) begin + input_start_d0_reg <= 1'b1; + end + + if (reset_crc) begin + crc_state_reg <= '1; + end else begin + crc_state_reg <= crc_state; + end + + crc_valid_reg <= crc_valid; + end + + last_ts_reg <= (4+16)'(ptp_ts); + ts_inc_reg <= (4+16)'(ptp_ts) - last_ts_reg; + + if (rst) begin + state_reg <= STATE_IDLE; + + m_axis_rx_tvalid_reg <= 1'b0; + + start_packet_reg <= '0; + frame_reg <= 1'b0; + + stat_rx_byte_reg <= '0; + stat_rx_pkt_len_reg <= '0; + stat_rx_pkt_fragment_reg <= 1'b0; + stat_rx_pkt_jabber_reg <= 1'b0; + stat_rx_pkt_ucast_reg <= 1'b0; + stat_rx_pkt_mcast_reg <= 1'b0; + stat_rx_pkt_bcast_reg <= 1'b0; + stat_rx_pkt_vlan_reg <= 1'b0; + stat_rx_pkt_good_reg <= 1'b0; + stat_rx_pkt_bad_reg <= 1'b0; + stat_rx_err_oversize_reg <= 1'b0; + stat_rx_err_bad_fcs_reg <= 1'b0; + stat_rx_err_bad_block_reg <= 1'b0; + stat_rx_err_framing_reg <= 1'b0; + stat_rx_err_preamble_reg <= 1'b0; + + input_start_d0_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/eth/rtl/taxi_axis_basex_rx_8.sv b/src/eth/rtl/taxi_axis_basex_rx_8.sv new file mode 100644 index 0000000..f4e485a --- /dev/null +++ b/src/eth/rtl/taxi_axis_basex_rx_8.sv @@ -0,0 +1,538 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream 1000BASE-X frame receiver (1000BASE-X in, AXI out) + */ +module taxi_axis_basex_rx_8 # +( + parameter DATA_W = 8, + parameter CTRL_W = 1, + parameter logic GBX_IF_EN = 1'b0, + parameter logic PTP_TS_EN = 1'b0, + parameter PTP_TS_W = 96 +) +( + input wire logic clk, + input wire logic rst, + + /* + * 1000BASE-X encoded input + */ + input wire logic [DATA_W-1:0] encoded_rx_data, + input wire logic [CTRL_W-1:0] encoded_rx_data_k, + input wire logic encoded_rx_data_valid, + + /* + * Receive interface (AXI stream) + */ + taxi_axis_if.src m_axis_rx, + + /* + * PTP + */ + input wire logic [PTP_TS_W-1:0] ptp_ts, + + /* + * Configuration + */ + input wire logic [15:0] cfg_rx_max_pkt_len = 16'd1518-1, + input wire logic cfg_rx_enable, + + /* + * Status + */ + output wire logic rx_start_packet, + output wire logic stat_rx_byte, + output wire logic [15:0] stat_rx_pkt_len, + output wire logic stat_rx_pkt_fragment, + output wire logic stat_rx_pkt_jabber, + output wire logic stat_rx_pkt_ucast, + output wire logic stat_rx_pkt_mcast, + output wire logic stat_rx_pkt_bcast, + output wire logic stat_rx_pkt_vlan, + output wire logic stat_rx_pkt_good, + output wire logic stat_rx_pkt_bad, + output wire logic stat_rx_err_oversize, + output wire logic stat_rx_err_bad_fcs, + output wire logic stat_rx_err_bad_block, + output wire logic stat_rx_err_framing, + output wire logic stat_rx_err_preamble +); + +localparam USER_W = (PTP_TS_EN ? PTP_TS_W : 0) + 1; + +// check configuration +if (DATA_W != 8) + $fatal(0, "Error: Interface width must be 8 (instance %m)"); + +if (m_axis_rx.DATA_W != DATA_W) + $fatal(0, "Error: Interface DATA_W parameter mismatch (instance %m)"); + +if (m_axis_rx.USER_W != USER_W) + $fatal(0, "Error: Interface USER_W parameter mismatch (instance %m)"); + +typedef enum logic [7:0] { + ETH_PRE = 8'h55, + ETH_SFD = 8'hD5 +} eth_pre_t; + +function [7:0] D(input [4:0] edcba, input [2:0] hgf); + D = {hgf, edcba}; +endfunction + +function [7:0] K(input [4:0] edcba, input [2:0] hgf); + K = {hgf, edcba}; +endfunction + +localparam logic [15:0] CTRL_C1 = {D(21,5), K(28,5)}; +localparam logic [15:0] CTRL_C2 = {D(2,2), K(28,5)}; +localparam logic [15:0] CTRL_I1 = {D(5,6), K(28,5)}; +localparam logic [15:0] CTRL_I2 = {D(16,2), K(28,5)}; +localparam logic [7:0] CTRL_R = K(23,7); +localparam logic [7:0] CTRL_S = K(27,7); +localparam logic [7:0] CTRL_T = K(29,7); +localparam logic [7:0] CTRL_V = K(30,7); +localparam logic [15:0] CTRL_L1 = {D(6,5), K(28,5)}; +localparam logic [15:0] CTRL_L2 = {D(26,4), K(28,5)}; + +typedef enum logic [1:0] { + STATE_IDLE, + STATE_PREAMBLE, + STATE_PIPE, + STATE_PAYLOAD +} state_t; + +state_t state_reg = STATE_IDLE, state_next; + +// datapath control signals +logic reset_crc; +logic update_crc; + +logic in_frame_reg = 1'b0; + +logic [DATA_W-1:0] encoded_rx_data_d0_reg = '0; +logic [DATA_W-1:0] encoded_rx_data_d1_reg = '0; +logic [DATA_W-1:0] encoded_rx_data_d2_reg = '0; +logic [DATA_W-1:0] encoded_rx_data_d3_reg = '0; +logic [DATA_W-1:0] encoded_rx_data_d4_reg = '0; + +logic encoded_rx_data_k_d0_reg = 1'b0; + +logic frame_error_reg = 1'b0, frame_error_next; +logic pre_ok_reg = 1'b0, pre_ok_next; +logic [3:0] hdr_ptr_reg = '0, hdr_ptr_next; +logic is_mcast_reg = 1'b0, is_mcast_next; +logic is_bcast_reg = 1'b0, is_bcast_next; +logic is_8021q_reg = 1'b0, is_8021q_next; +logic [15:0] frame_len_reg = '0, frame_len_next; +logic [15:0] frame_len_lim_reg = '0, frame_len_lim_next; +logic frame_len_lim_check_reg = '0, frame_len_lim_check_next; +logic odd_reg = 1'b0, odd_next; + +logic [DATA_W-1:0] m_axis_rx_tdata_reg = '0, m_axis_rx_tdata_next; +logic m_axis_rx_tvalid_reg = 1'b0, m_axis_rx_tvalid_next; +logic m_axis_rx_tlast_reg = 1'b0, m_axis_rx_tlast_next; +logic m_axis_rx_tuser_reg = 1'b0, m_axis_rx_tuser_next; + +logic start_packet_int_reg = 1'b0; +logic start_packet_reg = 1'b0; + +logic stat_rx_byte_reg = 1'b0, stat_rx_byte_next; +logic [15:0] stat_rx_pkt_len_reg = '0, stat_rx_pkt_len_next; +logic stat_rx_pkt_fragment_reg = 1'b0, stat_rx_pkt_fragment_next; +logic stat_rx_pkt_jabber_reg = 1'b0, stat_rx_pkt_jabber_next; +logic stat_rx_pkt_ucast_reg = 1'b0, stat_rx_pkt_ucast_next; +logic stat_rx_pkt_mcast_reg = 1'b0, stat_rx_pkt_mcast_next; +logic stat_rx_pkt_bcast_reg = 1'b0, stat_rx_pkt_bcast_next; +logic stat_rx_pkt_vlan_reg = 1'b0, stat_rx_pkt_vlan_next; +logic stat_rx_pkt_good_reg = 1'b0, stat_rx_pkt_good_next; +logic stat_rx_pkt_bad_reg = 1'b0, stat_rx_pkt_bad_next; +logic stat_rx_err_oversize_reg = 1'b0, stat_rx_err_oversize_next; +logic stat_rx_err_bad_fcs_reg = 1'b0, stat_rx_err_bad_fcs_next; +logic stat_rx_err_bad_block_reg = 1'b0, stat_rx_err_bad_block_next; +logic stat_rx_err_framing_reg = 1'b0, stat_rx_err_framing_next; +logic stat_rx_err_preamble_reg = 1'b0, stat_rx_err_preamble_next; + +logic [PTP_TS_W-1:0] ptp_ts_out_reg = '0; + +logic [31:0] crc_state_reg = '1; +wire [31:0] crc_state; + +assign m_axis_rx.tdata = m_axis_rx_tdata_reg; +assign m_axis_rx.tkeep = 1'b1; +assign m_axis_rx.tstrb = m_axis_rx.tkeep; +assign m_axis_rx.tvalid = m_axis_rx_tvalid_reg; +assign m_axis_rx.tlast = m_axis_rx_tlast_reg; +assign m_axis_rx.tid = '0; +assign m_axis_rx.tdest = '0; +assign m_axis_rx.tuser[0] = m_axis_rx_tuser_reg; +if (PTP_TS_EN) begin + assign m_axis_rx.tuser[1 +: PTP_TS_W] = ptp_ts_out_reg; +end + +assign rx_start_packet = start_packet_reg; + +assign stat_rx_byte = stat_rx_byte_reg; +assign stat_rx_pkt_len = stat_rx_pkt_len_reg; +assign stat_rx_pkt_fragment = stat_rx_pkt_fragment_reg; +assign stat_rx_pkt_jabber = stat_rx_pkt_jabber_reg; +assign stat_rx_pkt_ucast = stat_rx_pkt_ucast_reg; +assign stat_rx_pkt_mcast = stat_rx_pkt_mcast_reg; +assign stat_rx_pkt_bcast = stat_rx_pkt_bcast_reg; +assign stat_rx_pkt_vlan = stat_rx_pkt_vlan_reg; +assign stat_rx_pkt_good = stat_rx_pkt_good_reg; +assign stat_rx_pkt_bad = stat_rx_pkt_bad_reg; +assign stat_rx_err_oversize = stat_rx_err_oversize_reg; +assign stat_rx_err_bad_fcs = stat_rx_err_bad_fcs_reg; +assign stat_rx_err_bad_block = stat_rx_err_bad_block_reg; +assign stat_rx_err_framing = stat_rx_err_framing_reg; +assign stat_rx_err_preamble = stat_rx_err_preamble_reg; + +taxi_lfsr #( + .LFSR_W(32), + .LFSR_POLY(32'h4c11db7), + .LFSR_GALOIS(1), + .LFSR_FEED_FORWARD(0), + .REVERSE(1), + .DATA_W(DATA_W), + .DATA_IN_EN(1'b1), + .DATA_OUT_EN(1'b0) +) +eth_crc_8 ( + .data_in(encoded_rx_data_d0_reg), + .state_in(crc_state_reg), + .data_out(), + .state_out(crc_state) +); + +wire crc_valid = crc_state == ~32'h2144df1c; + +always_comb begin + state_next = STATE_IDLE; + + reset_crc = 1'b0; + update_crc = 1'b0; + + frame_error_next = frame_error_reg; + pre_ok_next = pre_ok_reg; + hdr_ptr_next = hdr_ptr_reg; + is_mcast_next = is_mcast_reg; + is_bcast_next = is_bcast_reg; + is_8021q_next = is_8021q_reg; + frame_len_next = frame_len_reg; + frame_len_lim_next = frame_len_lim_reg; + frame_len_lim_check_next = frame_len_lim_check_reg; + odd_next = odd_reg; + + m_axis_rx_tdata_next = '0; + m_axis_rx_tvalid_next = 1'b0; + m_axis_rx_tlast_next = 1'b0; + m_axis_rx_tuser_next = 1'b0; + + stat_rx_byte_next = 1'b0; + stat_rx_pkt_len_next = '0; + stat_rx_pkt_fragment_next = 1'b0; + stat_rx_pkt_jabber_next = 1'b0; + stat_rx_pkt_ucast_next = 1'b0; + stat_rx_pkt_mcast_next = 1'b0; + stat_rx_pkt_bcast_next = 1'b0; + stat_rx_pkt_vlan_next = 1'b0; + stat_rx_pkt_good_next = 1'b0; + stat_rx_pkt_bad_next = 1'b0; + stat_rx_err_oversize_next = 1'b0; + stat_rx_err_bad_fcs_next = 1'b0; + stat_rx_err_bad_block_next = 1'b0; + stat_rx_err_framing_next = 1'b0; + stat_rx_err_preamble_next = 1'b0; + + if (GBX_IF_EN && !encoded_rx_data_valid) begin + // data from gearbox not valid - hold state + state_next = state_reg; + end else begin + + odd_next = !odd_reg; + + // counter to measure frame length + if (&frame_len_reg == 0) begin + frame_len_next = frame_len_reg + 1; + end + + // counter for max frame length enforcement + if (frame_len_lim_reg != 0) begin + frame_len_lim_next = frame_len_lim_reg - 1; + end else begin + frame_len_lim_check_next = 1'b1; + end + + // address and ethertype checks + if (&hdr_ptr_reg == 0) begin + hdr_ptr_next = hdr_ptr_reg + 1; + end + + case (hdr_ptr_reg) + 4'd0: begin + is_mcast_next = encoded_rx_data_d4_reg[0]; + is_bcast_next = encoded_rx_data_d4_reg == 8'hff; + end + 4'd1: is_bcast_next = is_bcast_reg && encoded_rx_data_d4_reg == 8'hff; + 4'd2: is_bcast_next = is_bcast_reg && encoded_rx_data_d4_reg == 8'hff; + 4'd3: is_bcast_next = is_bcast_reg && encoded_rx_data_d4_reg == 8'hff; + 4'd4: is_bcast_next = is_bcast_reg && encoded_rx_data_d4_reg == 8'hff; + 4'd5: is_bcast_next = is_bcast_reg && encoded_rx_data_d4_reg == 8'hff; + 4'd12: is_8021q_next = encoded_rx_data_d4_reg == 8'h81; + 4'd13: is_8021q_next = is_8021q_reg && encoded_rx_data_d4_reg == 8'h00; + default: begin + // do nothing + end + endcase + + case (state_reg) + STATE_IDLE: begin + // idle state - wait for packet + reset_crc = 1'b1; + frame_error_next = 1'b0; + frame_len_next = 1; + frame_len_lim_next = cfg_rx_max_pkt_len; + frame_len_lim_check_next = 1'b0; + hdr_ptr_next = 0; + is_mcast_next = 1'b0; + is_bcast_next = 1'b0; + is_8021q_next = 1'b0; + + pre_ok_next = 1'b1; + + if (encoded_rx_data_k_d0_reg && encoded_rx_data_d0_reg == CTRL_S && !encoded_rx_data_k) begin + state_next = STATE_PREAMBLE; + end else begin + state_next = STATE_IDLE; + end + end + STATE_PREAMBLE: begin + // idle state - wait for packet + reset_crc = 1'b1; + frame_error_next = 1'b0; + frame_len_next = 1; + frame_len_lim_next = cfg_rx_max_pkt_len; + frame_len_lim_check_next = 1'b0; + hdr_ptr_next = 0; + is_mcast_next = 1'b0; + is_bcast_next = 1'b0; + is_8021q_next = 1'b0; + + if (encoded_rx_data_k) begin + // control character + stat_rx_err_framing_next = 1'b1; + state_next = STATE_IDLE; + end else begin + // data + if (encoded_rx_data_d0_reg == ETH_PRE) begin + // normal preamble + state_next = STATE_PREAMBLE; + end else if (encoded_rx_data_d0_reg == ETH_SFD) begin + // start + if (cfg_rx_enable) begin + stat_rx_byte_next = 1'b1; + state_next = STATE_PIPE; + end else begin + state_next = STATE_IDLE; + end + end else begin + // abnormal preamble + pre_ok_next = 1'b0; + state_next = STATE_PREAMBLE; + end + end + end + STATE_PIPE: begin + // wait for FCS pipeline to fill + update_crc = 1'b1; + hdr_ptr_next = 0; + is_mcast_next = 1'b0; + is_bcast_next = 1'b0; + is_8021q_next = 1'b0; + + stat_rx_byte_next = encoded_rx_data_k == 0; + + if (encoded_rx_data_k) begin + // control character + frame_error_next = 1'b1; + stat_rx_err_framing_next = 1'b1; + state_next = STATE_IDLE; + end else if (encoded_rx_data_d4_reg == ETH_SFD) begin + state_next = STATE_PAYLOAD; + end else begin + state_next = STATE_PIPE; + end + end + STATE_PAYLOAD: begin + // read payload + update_crc = 1'b1; + + m_axis_rx_tdata_next = encoded_rx_data_d4_reg; + m_axis_rx_tvalid_next = 1'b1; + + stat_rx_byte_next = encoded_rx_data_k == 0; + + if (encoded_rx_data_k && encoded_rx_data != CTRL_T) begin + frame_error_next = 1'b1; + stat_rx_err_framing_next = 1'b1; + end + + if (encoded_rx_data_k) begin + // end of packet + m_axis_rx_tlast_next = 1'b1; + stat_rx_pkt_len_next = frame_len_reg; + stat_rx_pkt_ucast_next = !is_mcast_reg; + stat_rx_pkt_mcast_next = is_mcast_reg && !is_bcast_reg; + stat_rx_pkt_bcast_next = is_bcast_reg; + stat_rx_pkt_vlan_next = is_8021q_reg; + stat_rx_err_oversize_next = frame_len_lim_check_reg; + stat_rx_err_preamble_next = !pre_ok_reg; + if (frame_error_next) begin + // error + m_axis_rx_tuser_next = 1'b1; + stat_rx_pkt_fragment_next = frame_len_reg[15:6] == 0; + stat_rx_pkt_jabber_next = frame_len_lim_check_reg; + stat_rx_pkt_bad_next = 1'b1; + end else if (crc_valid) begin + // FCS good + if (frame_len_lim_check_reg) begin + // too long + m_axis_rx_tuser_next = 1'b1; + stat_rx_pkt_bad_next = 1'b1; + end else begin + // length OK + m_axis_rx_tuser_next = 1'b0; + stat_rx_pkt_good_next = 1'b1; + end + end else begin + // FCS bad + m_axis_rx_tuser_next = 1'b1; + stat_rx_pkt_fragment_next = frame_len_reg[15:6] == 0; + stat_rx_pkt_jabber_next = frame_len_lim_check_reg; + stat_rx_pkt_bad_next = 1'b1; + stat_rx_err_bad_fcs_next = 1'b1; + end + reset_crc = 1'b1; + state_next = STATE_IDLE; + end else begin + state_next = STATE_PAYLOAD; + end + end + default: begin + // invalid state, return to idle + state_next = STATE_IDLE; + end + endcase + end +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + + frame_error_reg <= frame_error_next; + pre_ok_reg <= pre_ok_next; + hdr_ptr_reg <= hdr_ptr_next; + is_mcast_reg <= is_mcast_next; + is_bcast_reg <= is_bcast_next; + is_8021q_reg <= is_8021q_next; + frame_len_reg <= frame_len_next; + frame_len_lim_reg <= frame_len_lim_next; + frame_len_lim_check_reg <= frame_len_lim_check_next; + odd_reg <= odd_next; + + m_axis_rx_tdata_reg <= m_axis_rx_tdata_next; + m_axis_rx_tvalid_reg <= m_axis_rx_tvalid_next; + m_axis_rx_tlast_reg <= m_axis_rx_tlast_next; + m_axis_rx_tuser_reg <= m_axis_rx_tuser_next; + + start_packet_int_reg <= 1'b0; + start_packet_reg <= 1'b0; + + if (start_packet_int_reg) begin + ptp_ts_out_reg <= ptp_ts; + start_packet_reg <= 1'b1; + end + + if (!GBX_IF_EN || encoded_rx_data_valid) begin + if (in_frame_reg) begin + in_frame_reg <= encoded_rx_data_k == 0; + end else if (encoded_rx_data_k == 0 && encoded_rx_data == ETH_SFD) begin + in_frame_reg <= 1'b1; + start_packet_int_reg <= 1'b1; + end + + encoded_rx_data_d0_reg <= encoded_rx_data; + encoded_rx_data_d1_reg <= encoded_rx_data_d0_reg; + encoded_rx_data_d2_reg <= encoded_rx_data_d1_reg; + encoded_rx_data_d3_reg <= encoded_rx_data_d2_reg; + encoded_rx_data_d4_reg <= encoded_rx_data_d3_reg; + + encoded_rx_data_k_d0_reg <= encoded_rx_data_k; + + if (reset_crc) begin + crc_state_reg <= '1; + end else if (update_crc) begin + crc_state_reg <= crc_state; + end + end + + stat_rx_byte_reg <= stat_rx_byte_next; + stat_rx_pkt_len_reg <= stat_rx_pkt_len_next; + stat_rx_pkt_fragment_reg <= stat_rx_pkt_fragment_next; + stat_rx_pkt_jabber_reg <= stat_rx_pkt_jabber_next; + stat_rx_pkt_ucast_reg <= stat_rx_pkt_ucast_next; + stat_rx_pkt_mcast_reg <= stat_rx_pkt_mcast_next; + stat_rx_pkt_bcast_reg <= stat_rx_pkt_bcast_next; + stat_rx_pkt_vlan_reg <= stat_rx_pkt_vlan_next; + stat_rx_pkt_good_reg <= stat_rx_pkt_good_next; + stat_rx_pkt_bad_reg <= stat_rx_pkt_bad_next; + stat_rx_err_oversize_reg <= stat_rx_err_oversize_next; + stat_rx_err_bad_fcs_reg <= stat_rx_err_bad_fcs_next; + stat_rx_err_bad_block_reg <= stat_rx_err_bad_block_next; + stat_rx_err_framing_reg <= stat_rx_err_framing_next; + stat_rx_err_preamble_reg <= stat_rx_err_preamble_next; + + if (rst) begin + state_reg <= STATE_IDLE; + + m_axis_rx_tvalid_reg <= 1'b0; + + start_packet_int_reg <= 1'b0; + start_packet_reg <= 1'b0; + + stat_rx_byte_reg <= 1'b0; + stat_rx_pkt_len_reg <= '0; + stat_rx_pkt_fragment_reg <= 1'b0; + stat_rx_pkt_jabber_reg <= 1'b0; + stat_rx_pkt_ucast_reg <= 1'b0; + stat_rx_pkt_mcast_reg <= 1'b0; + stat_rx_pkt_bcast_reg <= 1'b0; + stat_rx_pkt_vlan_reg <= 1'b0; + stat_rx_pkt_good_reg <= 1'b0; + stat_rx_pkt_bad_reg <= 1'b0; + stat_rx_err_oversize_reg <= 1'b0; + stat_rx_err_bad_fcs_reg <= 1'b0; + stat_rx_err_bad_block_reg <= 1'b0; + stat_rx_err_framing_reg <= 1'b0; + stat_rx_err_preamble_reg <= 1'b0; + + in_frame_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/eth/rtl/taxi_axis_basex_tx_16.sv b/src/eth/rtl/taxi_axis_basex_tx_16.sv new file mode 100644 index 0000000..524800d --- /dev/null +++ b/src/eth/rtl/taxi_axis_basex_tx_16.sv @@ -0,0 +1,857 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream 1000BASE-X frame transmitter (AXI in, 1000BASE-X out) + */ +module taxi_axis_basex_tx_16 # +( + parameter DATA_W = 16, + parameter CTRL_W = 2, + parameter logic GBX_IF_EN = 1'b0, + parameter GBX_CNT = 1, + parameter logic DIC_EN = 1'b1, + 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, + + /* + * 1000BASE-X encoded interface + */ + output wire logic [DATA_W-1:0] encoded_tx_data, + output wire logic [CTRL_W-1:0] encoded_tx_data_k, + output wire logic [CTRL_W-1:0] encoded_tx_data_dm, + output wire logic [CTRL_W-1:0] encoded_tx_data_dv, + output wire logic encoded_tx_data_valid, + input wire logic [GBX_CNT-1:0] tx_gbx_req_sync = '0, + input wire logic tx_gbx_req_stall = '0, + output wire logic [GBX_CNT-1:0] tx_gbx_sync, + + /* + * PTP + */ + input wire logic [PTP_TS_W-1:0] ptp_ts, + + /* + * Configuration + */ + input wire logic [15:0] cfg_tx_max_pkt_len = 16'd1518-1, + input wire logic [7:0] cfg_tx_ifg = 8'd12, + input wire logic cfg_tx_enable, + + /* + * Status + */ + output wire logic [1:0] tx_start_packet, + output wire logic [1:0] stat_tx_byte, + output wire logic [15:0] stat_tx_pkt_len, + output wire logic stat_tx_pkt_ucast, + output wire logic stat_tx_pkt_mcast, + output wire logic stat_tx_pkt_bcast, + output wire logic stat_tx_pkt_vlan, + output wire logic stat_tx_pkt_good, + output wire logic stat_tx_pkt_bad, + output wire logic stat_tx_err_oversize, + output wire logic stat_tx_err_user, + output wire logic stat_tx_err_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); + +// check configuration +if (DATA_W != 16) + $fatal(0, "Error: Interface width must be 16 (instance %m)"); + +if (KEEP_W*8 != DATA_W) + $fatal(0, "Error: Interface requires byte (8-bit) granularity (instance %m)"); + +if (CTRL_W != 2) + $fatal(0, "Error: CTRL_W must be 2 (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)"); + +typedef enum logic [7:0] { + ETH_PRE = 8'h55, + ETH_SFD = 8'hD5 +} eth_pre_t; + +function [7:0] D(input [4:0] edcba, input [2:0] hgf); + D = {hgf, edcba}; +endfunction + +function [7:0] K(input [4:0] edcba, input [2:0] hgf); + K = {hgf, edcba}; +endfunction + +localparam logic [15:0] CTRL_C1 = {D(21,5), K(28,5)}; +localparam logic [15:0] CTRL_C2 = {D(2,2), K(28,5)}; +localparam logic [15:0] CTRL_I1 = {D(5,6), K(28,5)}; +localparam logic [15:0] CTRL_I2 = {D(16,2), K(28,5)}; +localparam logic [7:0] CTRL_R = K(23,7); +localparam logic [7:0] CTRL_S = K(27,7); +localparam logic [7:0] CTRL_T = K(29,7); +localparam logic [7:0] CTRL_V = K(30,7); +localparam logic [15:0] CTRL_L1 = {D(6,5), K(28,5)}; +localparam logic [15:0] CTRL_L2 = {D(26,4), K(28,5)}; + +function logic rd_flip_3b4b(input [2:0] hgf); + case (hgf) + 3'b000: rd_flip_3b4b = 1'b1; + 3'b001: rd_flip_3b4b = 1'b0; + 3'b010: rd_flip_3b4b = 1'b0; + 3'b011: rd_flip_3b4b = 1'b0; + 3'b100: rd_flip_3b4b = 1'b1; + 3'b101: rd_flip_3b4b = 1'b0; + 3'b110: rd_flip_3b4b = 1'b0; + 3'b111: rd_flip_3b4b = 1'b1; + endcase +endfunction + +function logic rd_flip_5b6b(input [4:0] edcba, input k); + case (edcba) + 5'b00000: rd_flip_5b6b = 1'b1; + 5'b00001: rd_flip_5b6b = 1'b1; + 5'b00010: rd_flip_5b6b = 1'b1; + 5'b00011: rd_flip_5b6b = 1'b0; + 5'b00100: rd_flip_5b6b = 1'b1; + 5'b00101: rd_flip_5b6b = 1'b0; + 5'b00110: rd_flip_5b6b = 1'b0; + 5'b00111: rd_flip_5b6b = 1'b0; + 5'b01000: rd_flip_5b6b = 1'b1; + 5'b01001: rd_flip_5b6b = 1'b0; + 5'b01010: rd_flip_5b6b = 1'b0; + 5'b01011: rd_flip_5b6b = 1'b0; + 5'b01100: rd_flip_5b6b = 1'b0; + 5'b01101: rd_flip_5b6b = 1'b0; + 5'b01110: rd_flip_5b6b = 1'b0; + 5'b01111: rd_flip_5b6b = 1'b1; + 5'b10000: rd_flip_5b6b = 1'b1; + 5'b10001: rd_flip_5b6b = 1'b0; + 5'b10010: rd_flip_5b6b = 1'b0; + 5'b10011: rd_flip_5b6b = 1'b0; + 5'b10100: rd_flip_5b6b = 1'b0; + 5'b10101: rd_flip_5b6b = 1'b0; + 5'b10110: rd_flip_5b6b = 1'b0; + 5'b10111: rd_flip_5b6b = 1'b1; + 5'b11000: rd_flip_5b6b = 1'b1; + 5'b11001: rd_flip_5b6b = 1'b0; + 5'b11010: rd_flip_5b6b = 1'b0; + 5'b11011: rd_flip_5b6b = 1'b1; + 5'b11100: rd_flip_5b6b = k; // K28 + 5'b11101: rd_flip_5b6b = 1'b1; + 5'b11110: rd_flip_5b6b = 1'b1; + 5'b11111: rd_flip_5b6b = 1'b1; + endcase +endfunction + +function logic rd_flip_8b10b(input [7:0] d, input k); + rd_flip_8b10b = rd_flip_5b6b(d[4:0], k) ^ rd_flip_3b4b(d[7:5]); +endfunction + +typedef enum logic [3:0] { + STATE_IDLE, + STATE_PREAMBLE, + STATE_PAYLOAD, + STATE_FCS_1, + STATE_FCS_2, + STATE_FCS_3, + STATE_TR, + STATE_RR, + STATE_ERR, + STATE_IFG +} state_t; + +state_t 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 frame_reg = 1'b0, frame_next; +logic frame_error_reg = 1'b0, frame_error_next; +logic frame_oversize_reg = 1'b0, frame_oversize_next; +logic [2:0] hdr_ptr_reg = '0, hdr_ptr_next; +logic is_mcast_reg = 1'b0, is_mcast_next; +logic is_bcast_reg = 1'b0, is_bcast_next; +logic is_8021q_reg = 1'b0, is_8021q_next; +logic [15:0] frame_len_reg = '0, frame_len_next; +logic [14:0] frame_len_lim_cyc_reg = '0, frame_len_lim_cyc_next; +logic frame_len_lim_last_reg = '0, frame_len_lim_last_next; +logic frame_len_lim_check_reg = '0, frame_len_lim_check_next; +logic [1:0] pre_cnt_reg = '0, pre_cnt_next; +logic [7:0] ifg_cnt_reg = '0, ifg_cnt_next; +logic deficit_idle_cnt_reg = 1'd0, deficit_idle_cnt_next; +logic rd_reg = 1'b0, rd_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 [DATA_W-1:0] encoded_tx_data_reg = '0, encoded_tx_data_next; +logic [CTRL_W-1:0] encoded_tx_data_k_reg = '0, encoded_tx_data_k_next; +logic [CTRL_W-1:0] encoded_tx_data_dm_reg = '0, encoded_tx_data_dm_next; +logic [CTRL_W-1:0] encoded_tx_data_dv_reg = '0, encoded_tx_data_dv_next; +logic encoded_tx_data_valid_reg = 1'b0; +logic [GBX_CNT-1:0] tx_gbx_sync_reg = '0; + +logic start_packet_int_reg = 1'b0, start_packet_int_next; +logic start_packet_reg = 1'b0, start_packet_next; + +logic [1:0] stat_tx_byte_reg = '0, stat_tx_byte_next; +logic [15:0] stat_tx_pkt_len_reg = '0, stat_tx_pkt_len_next; +logic stat_tx_pkt_ucast_reg = 1'b0, stat_tx_pkt_ucast_next; +logic stat_tx_pkt_mcast_reg = 1'b0, stat_tx_pkt_mcast_next; +logic stat_tx_pkt_bcast_reg = 1'b0, stat_tx_pkt_bcast_next; +logic stat_tx_pkt_vlan_reg = 1'b0, stat_tx_pkt_vlan_next; +logic stat_tx_pkt_good_reg = 1'b0, stat_tx_pkt_good_next; +logic stat_tx_pkt_bad_reg = 1'b0, stat_tx_pkt_bad_next; +logic stat_tx_err_oversize_reg = 1'b0, stat_tx_err_oversize_next; +logic stat_tx_err_user_reg = 1'b0, stat_tx_err_user_next; +logic stat_tx_err_underflow_reg = 1'b0, stat_tx_err_underflow_next; + +assign s_axis_tx.tready = s_axis_tx_tready_reg && (!GBX_IF_EN || !tx_gbx_req_stall); + +assign encoded_tx_data = encoded_tx_data_reg; +assign encoded_tx_data_k = encoded_tx_data_k_reg; +assign encoded_tx_data_dm = encoded_tx_data_dm_reg; +assign encoded_tx_data_dv = encoded_tx_data_dv_reg; +assign encoded_tx_data_valid = GBX_IF_EN ? encoded_tx_data_valid_reg : 1'b1; +assign tx_gbx_sync = GBX_IF_EN ? tx_gbx_sync_reg : '0; + +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 tx_start_packet = {1'b0, start_packet_reg}; + +assign stat_tx_byte = stat_tx_byte_reg; +assign stat_tx_pkt_len = stat_tx_pkt_len_reg; +assign stat_tx_pkt_ucast = stat_tx_pkt_ucast_reg; +assign stat_tx_pkt_mcast = stat_tx_pkt_mcast_reg; +assign stat_tx_pkt_bcast = stat_tx_pkt_bcast_reg; +assign stat_tx_pkt_vlan = stat_tx_pkt_vlan_reg; +assign stat_tx_pkt_good = stat_tx_pkt_good_reg; +assign stat_tx_pkt_bad = stat_tx_pkt_bad_reg; +assign stat_tx_err_oversize = stat_tx_err_oversize_reg; +assign stat_tx_err_user = stat_tx_err_user_reg; +assign stat_tx_err_underflow = stat_tx_err_underflow_reg; + +logic [DATA_W+24-1:0] crc_data_reg = '0, crc_data_next; +logic [31:0] crc_state_reg = '0; +wire [31:0] crc_state; + +taxi_lfsr #( + .LFSR_W(32), + .LFSR_POLY(32'h4c11db7), + .LFSR_GALOIS(1), + .LFSR_FEED_FORWARD(0), + .REVERSE(1), + .DATA_W(DATA_W+24), + .DATA_IN_EN(1'b1), + .DATA_OUT_EN(1'b0), + .STATE_SHIFT_PRE(0), + .STATE_SHIFT_POST(-24) +) +eth_crc ( + .data_in(crc_data_reg), + .state_in('0), + .data_out(), + .state_out(crc_state) +); + +function [0:0] keep2empty(input [1:0] k); + casez (k) + 2'bz0: keep2empty = 1'd1; + 2'b01: keep2empty = 1'd1; + 2'b11: keep2empty = 1'd0; + endcase +endfunction + +// FCS cycle calculation +logic [DATA_W-1:0] fcs_output_data_0; +logic [DATA_W-1:0] fcs_output_data_1; +logic [DATA_W-1:0] fcs_output_data_2; +logic [CTRL_W-1:0] fcs_output_data_k_0; +logic [CTRL_W-1:0] fcs_output_data_k_1; +logic [CTRL_W-1:0] fcs_output_data_k_2; +logic [7:0] ifg_offset; +logic extra_cycle; + +always_comb begin + casez (s_empty_reg) + 1'd1: begin + fcs_output_data_0 = {~crc_state[7:0], s_tdata_reg[7:0]}; + fcs_output_data_1 = ~crc_state_reg[23:8]; + fcs_output_data_2 = {CTRL_T, ~crc_state_reg[31:24]}; + fcs_output_data_k_0 = 2'b00; + fcs_output_data_k_1 = 2'b00; + fcs_output_data_k_2 = 2'b10; + ifg_offset = 8'd1; + extra_cycle = 1'b0; + end + 1'd0: begin + fcs_output_data_0 = s_tdata_reg; + fcs_output_data_1 = ~crc_state_reg[15:0]; + fcs_output_data_2 = ~crc_state_reg[31:16]; + fcs_output_data_k_0 = 2'b00; + fcs_output_data_k_1 = 2'b00; + fcs_output_data_k_2 = 2'b00; + ifg_offset = 8'd0; + 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_oversize_next = frame_oversize_reg; + hdr_ptr_next = hdr_ptr_reg; + is_mcast_next = is_mcast_reg; + is_bcast_next = is_bcast_reg; + is_8021q_next = is_8021q_reg; + frame_len_next = frame_len_reg; + frame_len_lim_cyc_next = frame_len_lim_cyc_reg; + frame_len_lim_last_next = frame_len_lim_last_reg; + frame_len_lim_check_next = frame_len_lim_check_reg; + pre_cnt_next = pre_cnt_reg; + ifg_cnt_next = ifg_cnt_reg; + deficit_idle_cnt_next = deficit_idle_cnt_reg; + rd_next = rd_reg; + + s_axis_tx_tready_next = 1'b0; + + s_tdata_next = s_tdata_reg; + s_empty_next = s_empty_reg; + + crc_data_next = crc_data_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 + 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 + + encoded_tx_data_next = '0; + encoded_tx_data_k_next = '0; + encoded_tx_data_dm_next = '0; + encoded_tx_data_dv_next = '0; + + start_packet_int_next = start_packet_int_reg; + start_packet_next = 1'b0; + + stat_tx_byte_next = '0; + stat_tx_pkt_len_next = '0; + stat_tx_pkt_ucast_next = 1'b0; + stat_tx_pkt_mcast_next = 1'b0; + stat_tx_pkt_bcast_next = 1'b0; + stat_tx_pkt_vlan_next = 1'b0; + stat_tx_pkt_good_next = 1'b0; + stat_tx_pkt_bad_next = 1'b0; + stat_tx_err_oversize_next = 1'b0; + stat_tx_err_user_next = 1'b0; + stat_tx_err_underflow_next = 1'b0; + + if (s_axis_tx.tvalid && s_axis_tx.tready) begin + frame_next = !s_axis_tx.tlast; + end + + if (GBX_IF_EN && tx_gbx_req_stall) begin + // gearbox stall - hold state + state_next = state_reg; + s_axis_tx_tready_next = s_axis_tx_tready_reg; + end else begin + // counter to measure frame length + if (&frame_len_reg[15:1] == 0) begin + frame_len_next = frame_len_reg + 16'(KEEP_W); + end else begin + frame_len_next = '1; + end + + // counter for max frame length enforcement + if (frame_len_lim_cyc_reg != 0) begin + frame_len_lim_cyc_next = frame_len_lim_cyc_reg - 1; + end else begin + frame_len_lim_cyc_next = '0; + end + + if (frame_len_lim_cyc_reg == 4) begin + frame_len_lim_check_next = 1'b1; + end + + // address and ethertype checks + if (&hdr_ptr_reg == 0) begin + hdr_ptr_next = hdr_ptr_reg + 1; + end + + case (hdr_ptr_reg) + 3'd0: begin + is_mcast_next = s_tdata_reg[0]; + is_bcast_next = &s_tdata_reg; + end + 3'd1: is_bcast_next = is_bcast_reg && &s_tdata_reg; + 3'd2: is_bcast_next = is_bcast_reg && &s_tdata_reg; + 3'd6: is_8021q_next = {s_tdata_reg[7:0], s_tdata_reg[15:8]} == 16'h8100; + default: begin + // do nothing + end + endcase + + if (pre_cnt_reg != 0) begin + pre_cnt_next = pre_cnt_reg - 1; + end + + if (ifg_cnt_reg[7:1] != 0) begin + ifg_cnt_next = ifg_cnt_reg - 8'(KEEP_W); + end else begin + ifg_cnt_next = '0; + end + + // FCS + casez (s_axis_tx.tkeep) + 2'b11: crc_data_next = {24'd0, s_axis_tx.tdata} ^ {8'd0, crc_state}; + default: crc_data_next = {24'd0, s_axis_tx.tdata[7:0], 8'd0} ^ {crc_state, 8'd0}; + endcase + + case (state_reg) + STATE_IDLE: begin + // idle state - wait for data + reset_crc = 1'b1; + + frame_error_next = 1'b0; + frame_oversize_next = 1'b0; + hdr_ptr_next = 0; + frame_len_next = 0; + {frame_len_lim_cyc_next, frame_len_lim_last_next} = cfg_tx_max_pkt_len; + frame_len_lim_check_next = 1'b0; + pre_cnt_next = 2'd2; + + encoded_tx_data_next = rd_reg ? CTRL_I1 : CTRL_I2; + encoded_tx_data_k_next = 2'b01; + encoded_tx_data_dm_next = 2'b01; + encoded_tx_data_dv_next = {1'b0, rd_reg}; + + s_tdata_next = s_axis_tx.tdata; + s_empty_next = keep2empty(s_axis_tx.tkeep); + + m_axis_tx_cpl_tag_next = s_axis_tx.tid; + + if (s_axis_tx.tvalid && cfg_tx_enable) begin + // Preamble and SFD + encoded_tx_data_next = {{1{ETH_PRE}}, CTRL_S}; + encoded_tx_data_k_next = 2'b01; + state_next = STATE_PREAMBLE; + end else begin + ifg_cnt_next = 8'd0; + deficit_idle_cnt_next = 1'd0; + state_next = STATE_IDLE; + end + end + STATE_PREAMBLE: begin + // send preamble + reset_crc = 1'b1; + + hdr_ptr_next = 0; + frame_len_next = 0; + {frame_len_lim_cyc_next, frame_len_lim_last_next} = cfg_tx_max_pkt_len; + frame_len_lim_check_next = 1'b0; + + s_tdata_next = s_axis_tx.tdata; + s_empty_next = keep2empty(s_axis_tx.tkeep); + + crc_data_next = {24'd0, s_axis_tx.tdata} ^ {8'd0, 32'hffffffff}; + + encoded_tx_data_next = {2{ETH_PRE}}; + encoded_tx_data_k_next = 2'b00; + + start_packet_int_next = 1'b0; + + if (pre_cnt_reg == 1) begin + s_axis_tx_tready_next = 1'b1; + s_tdata_next = s_axis_tx.tdata; + state_next = STATE_PREAMBLE; + end else if (pre_cnt_reg == 0) begin + // end of preamble; start payload + if (s_axis_tx_tready_reg) begin + s_axis_tx_tready_next = 1'b1; + s_tdata_next = s_axis_tx.tdata; + end + encoded_tx_data_next = {ETH_SFD, {1{ETH_PRE}}}; + encoded_tx_data_k_next = 2'b00; + start_packet_int_next = 1'b1; + state_next = STATE_PAYLOAD; + end else begin + state_next = STATE_PREAMBLE; + end + end + STATE_PAYLOAD: begin + // transfer payload + + update_crc = 1'b1; + s_axis_tx_tready_next = 1'b1; + + encoded_tx_data_next = s_tdata_reg; + encoded_tx_data_k_next = 2'b00; + + s_tdata_next = s_axis_tx.tdata; + s_empty_next = keep2empty(s_axis_tx.tkeep); + + stat_tx_byte_next = 2'(KEEP_W); + + start_packet_next = start_packet_int_reg; + start_packet_int_next = 1'b0; + + if (s_axis_tx.tvalid && s_axis_tx.tlast) begin + if (frame_len_lim_check_reg) begin + if (frame_len_lim_last_reg < 1'(1-keep2empty(s_axis_tx.tkeep))) begin + frame_oversize_next = 1'b1; + end + end + end else begin + if (frame_len_lim_check_reg) begin + // at the limit but the frame doesn't end in this cycle + frame_oversize_next = 1'b1; + end + end + + if (!s_axis_tx.tvalid || s_axis_tx.tlast || frame_oversize_next) begin + s_axis_tx_tready_next = frame_next; // drop frame + frame_error_next = !s_axis_tx.tvalid || s_axis_tx.tuser[0] || frame_oversize_next; + stat_tx_err_user_next = s_axis_tx.tuser[0]; + stat_tx_err_underflow_next = !s_axis_tx.tvalid; + + state_next = STATE_FCS_1; + end else begin + state_next = STATE_PAYLOAD; + end + end + STATE_FCS_1: begin + // FCS + + update_crc = 1'b1; + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = fcs_output_data_0; + encoded_tx_data_k_next = fcs_output_data_k_0; + + stat_tx_byte_next = 2'(KEEP_W); + + if (frame_error_reg) begin + state_next = STATE_ERR; + end else begin + state_next = STATE_FCS_2; + end + end + STATE_FCS_2: begin + // FCS + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = fcs_output_data_1; + encoded_tx_data_k_next = fcs_output_data_k_1; + + stat_tx_byte_next = 2'(KEEP_W); + + if (frame_error_reg) begin + state_next = STATE_ERR; + end else begin + state_next = STATE_FCS_3; + end + end + STATE_FCS_3: begin + // FCS + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = fcs_output_data_2; + encoded_tx_data_k_next = fcs_output_data_k_2; + + stat_tx_byte_next = 2-s_empty_reg; + frame_len_next = frame_len_reg + 16'(2-s_empty_reg); + + ifg_cnt_next = (cfg_tx_ifg > 8'd2 ? cfg_tx_ifg : 8'd2) - ifg_offset + 8'(deficit_idle_cnt_reg); + + if (extra_cycle) begin + state_next = STATE_TR; + end else begin + stat_tx_pkt_len_next = frame_len_next; + stat_tx_pkt_good_next = !frame_error_reg; + stat_tx_pkt_bad_next = frame_error_reg; + stat_tx_pkt_ucast_next = !is_mcast_reg; + stat_tx_pkt_mcast_next = is_mcast_reg && !is_bcast_reg; + stat_tx_pkt_bcast_next = is_bcast_reg; + stat_tx_pkt_vlan_next = is_8021q_reg; + stat_tx_err_oversize_next = frame_oversize_reg; + state_next = STATE_RR; + end + end + STATE_ERR: begin + // terminate packet with error + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = {CTRL_T, CTRL_V}; + encoded_tx_data_k_next = 2'b11; + + ifg_cnt_next = cfg_tx_ifg; + + stat_tx_pkt_len_next = frame_len_reg; + stat_tx_pkt_good_next = !frame_error_reg; + stat_tx_pkt_bad_next = frame_error_reg; + stat_tx_pkt_ucast_next = !is_mcast_reg; + stat_tx_pkt_mcast_next = is_mcast_reg && !is_bcast_reg; + stat_tx_pkt_bcast_next = is_bcast_reg; + stat_tx_pkt_vlan_next = is_8021q_reg; + stat_tx_err_oversize_next = frame_oversize_reg; + + state_next = STATE_RR; + end + STATE_TR: begin + // last cycle + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = {CTRL_R, CTRL_T}; + encoded_tx_data_k_next = 2'b11; + + stat_tx_pkt_len_next = frame_len_reg; + stat_tx_pkt_good_next = !frame_error_reg; + stat_tx_pkt_bad_next = frame_error_reg; + stat_tx_pkt_ucast_next = !is_mcast_reg; + stat_tx_pkt_mcast_next = is_mcast_reg && !is_bcast_reg; + stat_tx_pkt_bcast_next = is_bcast_reg; + stat_tx_pkt_vlan_next = is_8021q_reg; + stat_tx_err_oversize_next = frame_oversize_reg; + + if (DIC_EN) begin + if (ifg_cnt_next > 8'd1) begin + state_next = STATE_IFG; + end else begin + deficit_idle_cnt_next = 1'(ifg_cnt_next); + ifg_cnt_next = 8'd0; + state_next = STATE_IDLE; + end + end else begin + if (ifg_cnt_next > 8'd0) begin + state_next = STATE_IFG; + end else begin + state_next = STATE_IDLE; + end + end + end + STATE_RR: begin + // FCS + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = {CTRL_R, CTRL_R}; + encoded_tx_data_k_next = 2'b11; + + if (DIC_EN) begin + if (ifg_cnt_next > 8'd1) begin + state_next = STATE_IFG; + end else begin + deficit_idle_cnt_next = 1'(ifg_cnt_next); + ifg_cnt_next = 8'd0; + state_next = STATE_IDLE; + end + end else begin + if (ifg_cnt_next > 8'd0) begin + state_next = STATE_IFG; + end else begin + state_next = STATE_IDLE; + end + end + end + STATE_IFG: begin + // send IFG + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = rd_reg ? CTRL_I1 : CTRL_I2; + encoded_tx_data_k_next = 2'b01; + encoded_tx_data_dm_next = 2'b01; + encoded_tx_data_dv_next = {1'b0, rd_reg}; + + if (DIC_EN) begin + if (ifg_cnt_next > 8'd1 || frame_reg) begin + state_next = STATE_IFG; + end else begin + deficit_idle_cnt_next = 1'(ifg_cnt_next); + ifg_cnt_next = 8'd0; + state_next = STATE_IDLE; + end + end else begin + if (ifg_cnt_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 + + // update running disparity + rd_next = rd_reg; + for (integer k = 0; k < CTRL_W; k = k + 1) begin + rd_next = rd_next ^ rd_flip_8b10b(encoded_tx_data_next[k*8 +: 8], encoded_tx_data_k_next[k]); + end + end +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + + frame_reg <= frame_next; + frame_error_reg <= frame_error_next; + frame_oversize_reg <= frame_oversize_next; + hdr_ptr_reg <= hdr_ptr_next; + is_mcast_reg <= is_mcast_next; + is_bcast_reg <= is_bcast_next; + is_8021q_reg <= is_8021q_next; + frame_len_reg <= frame_len_next; + frame_len_lim_cyc_reg <= frame_len_lim_cyc_next; + frame_len_lim_last_reg <= frame_len_lim_last_next; + frame_len_lim_check_reg <= frame_len_lim_check_next; + pre_cnt_reg <= pre_cnt_next; + ifg_cnt_reg <= ifg_cnt_next; + deficit_idle_cnt_reg <= deficit_idle_cnt_next; + rd_reg <= rd_next; + + s_tdata_reg <= s_tdata_next; + s_empty_reg <= s_empty_next; + + crc_data_reg <= crc_data_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; + + start_packet_int_reg <= start_packet_int_next; + start_packet_reg <= start_packet_next; + + stat_tx_byte_reg <= stat_tx_byte_next; + stat_tx_pkt_len_reg <= stat_tx_pkt_len_next; + stat_tx_pkt_ucast_reg <= stat_tx_pkt_ucast_next; + stat_tx_pkt_mcast_reg <= stat_tx_pkt_mcast_next; + stat_tx_pkt_bcast_reg <= stat_tx_pkt_bcast_next; + stat_tx_pkt_vlan_reg <= stat_tx_pkt_vlan_next; + stat_tx_pkt_good_reg <= stat_tx_pkt_good_next; + stat_tx_pkt_bad_reg <= stat_tx_pkt_bad_next; + stat_tx_err_oversize_reg <= stat_tx_err_oversize_next; + stat_tx_err_user_reg <= stat_tx_err_user_next; + stat_tx_err_underflow_reg <= stat_tx_err_underflow_next; + + if (GBX_IF_EN && tx_gbx_req_stall) begin + // gearbox stall + encoded_tx_data_valid_reg <= 1'b0; + end else begin + encoded_tx_data_reg <= encoded_tx_data_next; + encoded_tx_data_k_reg <= encoded_tx_data_k_next; + encoded_tx_data_dm_reg <= encoded_tx_data_dm_next; + encoded_tx_data_dv_reg <= encoded_tx_data_dv_next; + encoded_tx_data_valid_reg <= 1'b1; + + encoded_tx_data_valid_reg <= 1'b1; + + if (reset_crc) begin + crc_state_reg <= '1; + end else if (update_crc) begin + crc_state_reg <= crc_state; + end + end + + tx_gbx_sync_reg <= tx_gbx_req_sync; + + if (rst) begin + state_reg <= STATE_IDLE; + + frame_reg <= 1'b0; + deficit_idle_cnt_reg <= 1'd0; + rd_reg <= 1'b0; + + s_axis_tx_tready_reg <= 1'b0; + + m_axis_tx_cpl_valid_reg <= 1'b0; + + encoded_tx_data_reg <= '0; + encoded_tx_data_k_reg <= '0; + encoded_tx_data_dm_reg <= '0; + encoded_tx_data_dv_reg <= '0; + encoded_tx_data_valid_reg <= 1'b0; + tx_gbx_sync_reg <= '0; + + start_packet_int_reg <= 1'b0; + start_packet_reg <= 1'b0; + + stat_tx_byte_reg <= '0; + stat_tx_pkt_len_reg <= '0; + stat_tx_pkt_ucast_reg <= 1'b0; + stat_tx_pkt_mcast_reg <= 1'b0; + stat_tx_pkt_bcast_reg <= 1'b0; + stat_tx_pkt_vlan_reg <= 1'b0; + stat_tx_pkt_good_reg <= 1'b0; + stat_tx_pkt_bad_reg <= 1'b0; + stat_tx_err_oversize_reg <= 1'b0; + stat_tx_err_user_reg <= 1'b0; + stat_tx_err_underflow_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/eth/rtl/taxi_axis_basex_tx_8.sv b/src/eth/rtl/taxi_axis_basex_tx_8.sv new file mode 100644 index 0000000..1351ab9 --- /dev/null +++ b/src/eth/rtl/taxi_axis_basex_tx_8.sv @@ -0,0 +1,754 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream 1000BASE-X frame transmitter (AXI in, 1000BASE-X out) + */ +module taxi_axis_basex_tx_8 # +( + parameter DATA_W = 8, + parameter CTRL_W = 1, + parameter logic PADDING_EN = 1'b1, + parameter MIN_FRAME_LEN = 64, + parameter logic GBX_IF_EN = 1'b0, + parameter GBX_CNT = 1, + parameter logic DIC_EN = 1'b1, + 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, + + /* + * 1000BASE-X encoded interface + */ + output wire logic [DATA_W-1:0] encoded_tx_data, + output wire logic [CTRL_W-1:0] encoded_tx_data_k, + output wire logic [CTRL_W-1:0] encoded_tx_data_dm, + output wire logic [CTRL_W-1:0] encoded_tx_data_dv, + output wire logic encoded_tx_data_valid, + input wire logic [GBX_CNT-1:0] tx_gbx_req_sync = '0, + input wire logic tx_gbx_req_stall = '0, + output wire logic [GBX_CNT-1:0] tx_gbx_sync, + + /* + * PTP + */ + input wire logic [PTP_TS_W-1:0] ptp_ts, + + /* + * Configuration + */ + input wire logic [15:0] cfg_tx_max_pkt_len = 16'd1518-1, + input wire logic [7:0] cfg_tx_ifg = 8'd12, + input wire logic cfg_tx_enable, + + /* + * Status + */ + output wire logic tx_start_packet, + output wire logic stat_tx_byte, + output wire logic [15:0] stat_tx_pkt_len, + output wire logic stat_tx_pkt_ucast, + output wire logic stat_tx_pkt_mcast, + output wire logic stat_tx_pkt_bcast, + output wire logic stat_tx_pkt_vlan, + output wire logic stat_tx_pkt_good, + output wire logic stat_tx_pkt_bad, + output wire logic stat_tx_err_oversize, + output wire logic stat_tx_err_user, + output wire logic stat_tx_err_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)"); + +typedef enum logic [7:0] { + ETH_PRE = 8'h55, + ETH_SFD = 8'hD5 +} eth_pre_t; + +function [7:0] D(input [4:0] edcba, input [2:0] hgf); + D = {hgf, edcba}; +endfunction + +function [7:0] K(input [4:0] edcba, input [2:0] hgf); + K = {hgf, edcba}; +endfunction + +localparam logic [15:0] CTRL_C1 = {D(21,5), K(28,5)}; +localparam logic [15:0] CTRL_C2 = {D(2,2), K(28,5)}; +localparam logic [15:0] CTRL_I1 = {D(5,6), K(28,5)}; +localparam logic [15:0] CTRL_I2 = {D(16,2), K(28,5)}; +localparam logic [7:0] CTRL_R = K(23,7); +localparam logic [7:0] CTRL_S = K(27,7); +localparam logic [7:0] CTRL_T = K(29,7); +localparam logic [7:0] CTRL_V = K(30,7); +localparam logic [15:0] CTRL_L1 = {D(6,5), K(28,5)}; +localparam logic [15:0] CTRL_L2 = {D(26,4), K(28,5)}; + +function logic rd_flip_3b4b(input [2:0] hgf); + case (hgf) + 3'b000: rd_flip_3b4b = 1'b1; + 3'b001: rd_flip_3b4b = 1'b0; + 3'b010: rd_flip_3b4b = 1'b0; + 3'b011: rd_flip_3b4b = 1'b0; + 3'b100: rd_flip_3b4b = 1'b1; + 3'b101: rd_flip_3b4b = 1'b0; + 3'b110: rd_flip_3b4b = 1'b0; + 3'b111: rd_flip_3b4b = 1'b1; + endcase +endfunction + +function logic rd_flip_5b6b(input [4:0] edcba, input k); + case (edcba) + 5'b00000: rd_flip_5b6b = 1'b1; + 5'b00001: rd_flip_5b6b = 1'b1; + 5'b00010: rd_flip_5b6b = 1'b1; + 5'b00011: rd_flip_5b6b = 1'b0; + 5'b00100: rd_flip_5b6b = 1'b1; + 5'b00101: rd_flip_5b6b = 1'b0; + 5'b00110: rd_flip_5b6b = 1'b0; + 5'b00111: rd_flip_5b6b = 1'b0; + 5'b01000: rd_flip_5b6b = 1'b1; + 5'b01001: rd_flip_5b6b = 1'b0; + 5'b01010: rd_flip_5b6b = 1'b0; + 5'b01011: rd_flip_5b6b = 1'b0; + 5'b01100: rd_flip_5b6b = 1'b0; + 5'b01101: rd_flip_5b6b = 1'b0; + 5'b01110: rd_flip_5b6b = 1'b0; + 5'b01111: rd_flip_5b6b = 1'b1; + 5'b10000: rd_flip_5b6b = 1'b1; + 5'b10001: rd_flip_5b6b = 1'b0; + 5'b10010: rd_flip_5b6b = 1'b0; + 5'b10011: rd_flip_5b6b = 1'b0; + 5'b10100: rd_flip_5b6b = 1'b0; + 5'b10101: rd_flip_5b6b = 1'b0; + 5'b10110: rd_flip_5b6b = 1'b0; + 5'b10111: rd_flip_5b6b = 1'b1; + 5'b11000: rd_flip_5b6b = 1'b1; + 5'b11001: rd_flip_5b6b = 1'b0; + 5'b11010: rd_flip_5b6b = 1'b0; + 5'b11011: rd_flip_5b6b = 1'b1; + 5'b11100: rd_flip_5b6b = k; // K28 + 5'b11101: rd_flip_5b6b = 1'b1; + 5'b11110: rd_flip_5b6b = 1'b1; + 5'b11111: rd_flip_5b6b = 1'b1; + endcase +endfunction + +function logic rd_flip_8b10b(input [7:0] d, input k); + rd_flip_8b10b = rd_flip_5b6b(d[4:0], k) ^ rd_flip_3b4b(d[7:5]); +endfunction + +typedef enum logic [3:0] { + STATE_IDLE, + STATE_PREAMBLE, + STATE_PAYLOAD, + STATE_LAST, + STATE_PAD, + STATE_FCS, + STATE_T, + STATE_R, + STATE_IFG +} state_t; + +state_t 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 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 [3:0] hdr_ptr_reg = '0, hdr_ptr_next; +logic is_mcast_reg = 1'b0, is_mcast_next; +logic is_bcast_reg = 1'b0, is_bcast_next; +logic is_8021q_reg = 1'b0, is_8021q_next; +logic [15:0] frame_len_reg = '0, frame_len_next; +logic [15:0] frame_len_lim_reg = '0, frame_len_lim_next; +logic [1:0] fcs_ptr_reg = '0, fcs_ptr_next; +logic [2:0] pre_cnt_reg = '0, pre_cnt_next; +logic [7:0] ifg_cnt_reg = '0, ifg_cnt_next; +logic deficit_idle_cnt_reg = 1'd0, deficit_idle_cnt_next; +logic odd_reg = 1'b0, odd_next; +logic rd_reg = 1'b0, rd_next; + +logic [DATA_W-1:0] encoded_tx_data_reg = '0, encoded_tx_data_next; +logic [CTRL_W-1:0] encoded_tx_data_k_reg = '0, encoded_tx_data_k_next; +logic [CTRL_W-1:0] encoded_tx_data_dm_reg = '0, encoded_tx_data_dm_next; +logic [CTRL_W-1:0] encoded_tx_data_dv_reg = '0, encoded_tx_data_dv_next; +logic encoded_tx_data_valid_reg = 1'b0; +logic [GBX_CNT-1:0] tx_gbx_sync_reg = '0; + +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 stat_tx_byte_reg = 1'b0, stat_tx_byte_next; +logic [15:0] stat_tx_pkt_len_reg = '0, stat_tx_pkt_len_next; +logic stat_tx_pkt_ucast_reg = 1'b0, stat_tx_pkt_ucast_next; +logic stat_tx_pkt_mcast_reg = 1'b0, stat_tx_pkt_mcast_next; +logic stat_tx_pkt_bcast_reg = 1'b0, stat_tx_pkt_bcast_next; +logic stat_tx_pkt_vlan_reg = 1'b0, stat_tx_pkt_vlan_next; +logic stat_tx_pkt_good_reg = 1'b0, stat_tx_pkt_good_next; +logic stat_tx_pkt_bad_reg = 1'b0, stat_tx_pkt_bad_next; +logic stat_tx_err_oversize_reg = 1'b0, stat_tx_err_oversize_next; +logic stat_tx_err_user_reg = 1'b0, stat_tx_err_user_next; +logic stat_tx_err_underflow_reg = 1'b0, stat_tx_err_underflow_next; + +logic [31:0] crc_state_reg = '1; +wire [31:0] crc_state; + +assign s_axis_tx.tready = s_axis_tx_tready_reg && (!GBX_IF_EN || !tx_gbx_req_stall); + +assign encoded_tx_data = encoded_tx_data_reg; +assign encoded_tx_data_k = encoded_tx_data_k_reg; +assign encoded_tx_data_dm = encoded_tx_data_dm_reg; +assign encoded_tx_data_dv = encoded_tx_data_dv_reg; +assign encoded_tx_data_valid = GBX_IF_EN ? encoded_tx_data_valid_reg : 1'b1; +assign tx_gbx_sync = GBX_IF_EN ? tx_gbx_sync_reg : '0; + +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 tx_start_packet = start_packet_reg; +assign stat_tx_byte = stat_tx_byte_reg; +assign stat_tx_pkt_len = stat_tx_pkt_len_reg; +assign stat_tx_pkt_ucast = stat_tx_pkt_ucast_reg; +assign stat_tx_pkt_mcast = stat_tx_pkt_mcast_reg; +assign stat_tx_pkt_bcast = stat_tx_pkt_bcast_reg; +assign stat_tx_pkt_vlan = stat_tx_pkt_vlan_reg; +assign stat_tx_pkt_good = stat_tx_pkt_good_reg; +assign stat_tx_pkt_bad = stat_tx_pkt_bad_reg; +assign stat_tx_err_oversize = stat_tx_err_oversize_reg; +assign stat_tx_err_user = stat_tx_err_user_reg; +assign stat_tx_err_underflow = stat_tx_err_underflow_reg; + +taxi_lfsr #( + .LFSR_W(32), + .LFSR_POLY(32'h4c11db7), + .LFSR_GALOIS(1), + .LFSR_FEED_FORWARD(0), + .REVERSE(1), + .DATA_W(DATA_W), + .DATA_IN_EN(1'b1), + .DATA_OUT_EN(1'b0) +) +eth_crc_8 ( + .data_in(s_tdata_reg), + .state_in(crc_state_reg), + .data_out(), + .state_out(crc_state) +); + +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; + hdr_ptr_next = hdr_ptr_reg; + is_mcast_next = is_mcast_reg; + is_bcast_next = is_bcast_reg; + is_8021q_next = is_8021q_reg; + frame_len_next = frame_len_reg; + frame_len_lim_next = frame_len_lim_reg; + fcs_ptr_next = fcs_ptr_reg; + pre_cnt_next = pre_cnt_reg; + ifg_cnt_next = ifg_cnt_reg; + deficit_idle_cnt_next = deficit_idle_cnt_reg; + odd_next = odd_reg; + rd_next = rd_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; + 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 + + encoded_tx_data_next = '0; + encoded_tx_data_k_next = 1'b0; + encoded_tx_data_dm_next = 1'b0; + encoded_tx_data_dv_next = 1'b0; + + start_packet_int_next = start_packet_int_reg; + start_packet_next = 1'b0; + + stat_tx_byte_next = 1'b0; + stat_tx_pkt_len_next = '0; + stat_tx_pkt_ucast_next = 1'b0; + stat_tx_pkt_mcast_next = 1'b0; + stat_tx_pkt_bcast_next = 1'b0; + stat_tx_pkt_vlan_next = 1'b0; + stat_tx_pkt_good_next = 1'b0; + stat_tx_pkt_bad_next = 1'b0; + stat_tx_err_oversize_next = 1'b0; + stat_tx_err_user_next = 1'b0; + stat_tx_err_underflow_next = 1'b0; + + if (s_axis_tx.tvalid && s_axis_tx.tready) begin + frame_next = !s_axis_tx.tlast; + end + + if (GBX_IF_EN && tx_gbx_req_stall) begin + // gearbox stall - hold state + state_next = state_reg; + s_axis_tx_tready_next = s_axis_tx_tready_reg; + end else begin + odd_next = !odd_reg; + + // counter for min frame length enforcement + if (frame_min_count_reg != 0) begin + frame_min_count_next = frame_min_count_reg - 1; + end + + // counter to measure frame length + if (&frame_len_reg == 0) begin + frame_len_next = frame_len_reg + 1; + end + + // counter for max frame length enforcement + if (frame_len_lim_reg != 0) begin + frame_len_lim_next = frame_len_lim_reg - 1; + end + + // address and ethertype checks + if (&hdr_ptr_reg == 0) begin + hdr_ptr_next = hdr_ptr_reg + 1; + end + + case (hdr_ptr_reg) + 4'd0: begin + is_mcast_next = s_tdata_reg[0]; + is_bcast_next = s_tdata_reg == 8'hff; + end + 4'd1: is_bcast_next = is_bcast_reg && s_tdata_reg == 8'hff; + 4'd2: is_bcast_next = is_bcast_reg && s_tdata_reg == 8'hff; + 4'd3: is_bcast_next = is_bcast_reg && s_tdata_reg == 8'hff; + 4'd4: is_bcast_next = is_bcast_reg && s_tdata_reg == 8'hff; + 4'd5: is_bcast_next = is_bcast_reg && s_tdata_reg == 8'hff; + 4'd12: is_8021q_next = s_tdata_reg == 8'h81; + 4'd13: is_8021q_next = is_8021q_reg && s_tdata_reg == 8'h00; + default: begin + // do nothing + end + endcase + + if (&fcs_ptr_reg == 0) begin + fcs_ptr_next = fcs_ptr_reg + 1; + end + + if (pre_cnt_reg != 0) begin + pre_cnt_next = pre_cnt_reg - 1; + end + + if (ifg_cnt_reg != 0) begin + ifg_cnt_next = ifg_cnt_reg - 1; + end + + case (state_reg) + STATE_IDLE: begin + // idle state - wait for packet + reset_crc = 1'b1; + + hdr_ptr_next = 0; + frame_len_next = 1; + frame_len_lim_next = cfg_tx_max_pkt_len; + pre_cnt_next = 3'd6; + + frame_error_next = 1'b0; + frame_min_count_next = MIN_LEN_W'(MIN_FRAME_LEN-4-1); + + m_axis_tx_cpl_tag_next = s_axis_tx.tid; + + // generate idles + if (!odd_reg) begin + encoded_tx_data_next = K(28,5); + encoded_tx_data_k_next = 1'b1; + encoded_tx_data_dm_next = 1'b1; + encoded_tx_data_dv_next = rd_reg; + end else begin + encoded_tx_data_next = rd_reg ? D(16,2) : D(5,6); + encoded_tx_data_k_next = 1'b0; + encoded_tx_data_dm_next = 1'b1; + encoded_tx_data_dv_next = rd_reg; + end + + if (!odd_reg && s_axis_tx.tvalid && cfg_tx_enable) begin + encoded_tx_data_next = CTRL_S; + encoded_tx_data_k_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; + + hdr_ptr_next = 0; + frame_len_next = 1; + frame_len_lim_next = cfg_tx_max_pkt_len; + + frame_error_next = 1'b0; + frame_min_count_next = MIN_LEN_W'(MIN_FRAME_LEN-4-1); + + encoded_tx_data_next = ETH_PRE; + encoded_tx_data_k_next = 1'b0; + + start_packet_int_next = 1'b0; + + if (pre_cnt_reg == 1) begin + s_axis_tx_tready_next = 1'b1; + s_tdata_next = s_axis_tx.tdata; + state_next = STATE_PREAMBLE; + end else if (pre_cnt_reg == 0) begin + // end of preamble; start payload + if (s_axis_tx_tready_reg) begin + s_axis_tx_tready_next = 1'b1; + s_tdata_next = s_axis_tx.tdata; + end + encoded_tx_data_next = ETH_SFD; + encoded_tx_data_k_next = 1'b0; + start_packet_int_next = 1'b1; + 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; + + encoded_tx_data_next = s_tdata_reg; + encoded_tx_data_k_next = 1'b0; + + s_tdata_next = s_axis_tx.tdata; + + stat_tx_byte_next = 1'b1; + start_packet_next = start_packet_int_reg; + start_packet_int_next = 1'b0; + + if (!s_axis_tx.tvalid || s_axis_tx.tlast || frame_len_lim_reg < 6) begin + s_axis_tx_tready_next = frame_next; // drop frame + stat_tx_err_oversize_next = !s_axis_tx.tlast && frame_len_lim_reg < 6; + frame_error_next = !s_axis_tx.tvalid || s_axis_tx.tuser[0] || stat_tx_err_oversize_next; + stat_tx_err_user_next = s_axis_tx.tuser[0]; + stat_tx_err_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 + + fcs_ptr_next = 2'd0; + + encoded_tx_data_next = s_tdata_reg; + encoded_tx_data_k_next = 1'b0; + + if (frame_error_reg) begin + encoded_tx_data_next = CTRL_V; + encoded_tx_data_k_next = 1'b1; + end + + s_tdata_next = 8'd0; + + stat_tx_byte_next = 1'b1; + + if (PADDING_EN && frame_min_count_reg != 0) begin + state_next = STATE_PAD; + end else begin + state_next = STATE_FCS; + end + end + STATE_PAD: begin + // send padding + s_axis_tx_tready_next = frame_next; // drop frame + + update_crc = 1'b1; + fcs_ptr_next = 2'd0; + + encoded_tx_data_next = s_tdata_reg; + encoded_tx_data_k_next = 1'b0; + + if (frame_error_reg) begin + encoded_tx_data_next = CTRL_V; + encoded_tx_data_k_next = 1'b1; + end + + s_tdata_next = 8'd0; + + stat_tx_byte_next = 1'b1; + + if (frame_min_count_reg != 0) begin + state_next = STATE_PAD; + end else begin + state_next = STATE_FCS; + end + end + STATE_FCS: begin + // send FCS + s_axis_tx_tready_next = frame_next; // drop frame + + ifg_cnt_next = cfg_tx_ifg + 8'(deficit_idle_cnt_reg); + + case (fcs_ptr_reg) + 2'd0: encoded_tx_data_next = ~crc_state_reg[7:0]; + 2'd1: encoded_tx_data_next = ~crc_state_reg[15:8]; + 2'd2: encoded_tx_data_next = ~crc_state_reg[23:16]; + 2'd3: encoded_tx_data_next = ~crc_state_reg[31:24]; + endcase + encoded_tx_data_k_next = 1'b0; + + if (frame_error_reg) begin + encoded_tx_data_next = CTRL_V; + encoded_tx_data_k_next = 1'b1; + end + + stat_tx_byte_next = 1'b1; + + if (&fcs_ptr_reg == 0) begin + state_next = STATE_FCS; + end else begin + stat_tx_pkt_len_next = frame_len_reg; + stat_tx_pkt_good_next = !frame_error_reg; + stat_tx_pkt_bad_next = frame_error_reg; + stat_tx_pkt_ucast_next = !is_mcast_reg; + stat_tx_pkt_mcast_next = is_mcast_reg && !is_bcast_reg; + stat_tx_pkt_bcast_next = is_bcast_reg; + stat_tx_pkt_vlan_next = is_8021q_reg; + state_next = STATE_T; + end + end + STATE_T: begin + // send T + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = CTRL_T; + encoded_tx_data_k_next = 1'b1; + + state_next = STATE_R; + end + STATE_R: begin + // send R + s_axis_tx_tready_next = frame_next; // drop frame + + encoded_tx_data_next = CTRL_R; + encoded_tx_data_k_next = 1'b1; + + if (odd_reg) begin + state_next = STATE_IFG; + end else begin + state_next = STATE_R; + end + end + STATE_IFG: begin + // send IFG + s_axis_tx_tready_next = frame_next; // drop frame + + // generate idles + if (!odd_reg) begin + encoded_tx_data_next = K(28,5); + encoded_tx_data_k_next = 1'b1; + encoded_tx_data_dm_next = 1'b1; + encoded_tx_data_dv_next = rd_reg; + end else begin + encoded_tx_data_next = rd_reg ? D(16,2) : D(5,6); + encoded_tx_data_k_next = 1'b0; + encoded_tx_data_dm_next = 1'b1; + encoded_tx_data_dv_next = rd_reg; + end + + if (DIC_EN) begin + if (ifg_cnt_next > 8'd1 || !odd_reg || frame_reg) begin + state_next = STATE_IFG; + end else begin + deficit_idle_cnt_next = 1'(ifg_cnt_next); + ifg_cnt_next = 8'd0; + state_next = STATE_IDLE; + end + end else begin + if (ifg_cnt_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 + + // update running disparity + rd_next = rd_reg; + for (integer k = 0; k < CTRL_W; k = k + 1) begin + rd_next = rd_next ^ rd_flip_8b10b(encoded_tx_data_next[k*8 +: 8], encoded_tx_data_k_next[k]); + end + end +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; + hdr_ptr_reg <= hdr_ptr_next; + is_mcast_reg <= is_mcast_next; + is_bcast_reg <= is_bcast_next; + is_8021q_reg <= is_8021q_next; + frame_len_reg <= frame_len_next; + frame_len_lim_reg <= frame_len_lim_next; + fcs_ptr_reg <= fcs_ptr_next; + pre_cnt_reg <= pre_cnt_next; + ifg_cnt_reg <= ifg_cnt_next; + deficit_idle_cnt_reg <= deficit_idle_cnt_next; + odd_reg <= odd_next; + rd_reg <= rd_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; + + s_tdata_reg <= s_tdata_next; + + s_axis_tx_tready_reg <= s_axis_tx_tready_next; + + start_packet_int_reg <= start_packet_int_next; + start_packet_reg <= start_packet_next; + stat_tx_byte_reg <= stat_tx_byte_next; + stat_tx_pkt_len_reg <= stat_tx_pkt_len_next; + stat_tx_pkt_ucast_reg <= stat_tx_pkt_ucast_next; + stat_tx_pkt_mcast_reg <= stat_tx_pkt_mcast_next; + stat_tx_pkt_bcast_reg <= stat_tx_pkt_bcast_next; + stat_tx_pkt_vlan_reg <= stat_tx_pkt_vlan_next; + stat_tx_pkt_good_reg <= stat_tx_pkt_good_next; + stat_tx_pkt_bad_reg <= stat_tx_pkt_bad_next; + stat_tx_err_oversize_reg <= stat_tx_err_oversize_next; + stat_tx_err_user_reg <= stat_tx_err_user_next; + stat_tx_err_underflow_reg <= stat_tx_err_underflow_next; + + if (GBX_IF_EN && tx_gbx_req_stall) begin + // gearbox stall + encoded_tx_data_valid_reg <= 1'b0; + end else begin + encoded_tx_data_reg <= encoded_tx_data_next; + encoded_tx_data_k_reg <= encoded_tx_data_k_next; + encoded_tx_data_dm_reg <= encoded_tx_data_dm_next; + encoded_tx_data_dv_reg <= encoded_tx_data_dv_next; + encoded_tx_data_valid_reg <= 1'b1; + + if (reset_crc) begin + crc_state_reg <= '1; + end else if (update_crc) begin + crc_state_reg <= crc_state; + end + end + + tx_gbx_sync_reg <= tx_gbx_req_sync; + + if (rst) begin + state_reg <= STATE_IDLE; + + frame_reg <= 1'b0; + odd_reg <= 1'b0; + rd_reg <= 1'b0; + + s_axis_tx_tready_reg <= 1'b0; + + m_axis_tx_cpl_valid_reg <= 1'b0; + + encoded_tx_data_valid_reg <= 1'b0; + encoded_tx_data_k_reg <= 1'b0; + encoded_tx_data_dm_reg <= 1'b0; + encoded_tx_data_dv_reg <= 1'b0; + + start_packet_int_reg <= 1'b0; + start_packet_reg <= 1'b0; + stat_tx_byte_reg <= 1'b0; + stat_tx_pkt_len_reg <= '0; + stat_tx_pkt_ucast_reg <= 1'b0; + stat_tx_pkt_mcast_reg <= 1'b0; + stat_tx_pkt_bcast_reg <= 1'b0; + stat_tx_pkt_vlan_reg <= 1'b0; + stat_tx_pkt_good_reg <= 1'b0; + stat_tx_pkt_bad_reg <= 1'b0; + stat_tx_err_oversize_reg <= 1'b0; + stat_tx_err_user_reg <= 1'b0; + stat_tx_err_underflow_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/eth/tb/basex.py b/src/eth/tb/basex.py new file mode 100644 index 0000000..29009fa --- /dev/null +++ b/src/eth/tb/basex.py @@ -0,0 +1,760 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import logging + +import cocotb +from cocotb.queue import Queue, QueueFull +from cocotb.triggers import RisingEdge, Timer, First, Event +from cocotb.utils import get_sim_time + +# from cocotbext.eth.constants import (EthPre, XgmiiCtrl, BaseRCtrl, BaseRO, +# BaseRSync, BaseRBlockType, xgmii_ctrl_to_baser_mapping, +# baser_ctrl_to_xgmii_mapping, block_type_term_lane_mapping) +from cocotbext.eth.constants import EthPre, XgmiiCtrl +from cocotbext.eth import GmiiFrame + + +def rd_flip_3b4b(hgf): + if hgf == 0b000: + return 1 + if hgf == 0b001: + return 0 + if hgf == 0b010: + return 0 + if hgf == 0b011: + return 0 + if hgf == 0b100: + return 1 + if hgf == 0b101: + return 0 + if hgf == 0b110: + return 0 + if hgf == 0b111: + return 1 + +def rd_flip_5b6b(edcba, k): + if edcba == 0b00000: + return 1 + if edcba == 0b00001: + return 1 + if edcba == 0b00010: + return 1 + if edcba == 0b00011: + return 0 + if edcba == 0b00100: + return 1 + if edcba == 0b00101: + return 0 + if edcba == 0b00110: + return 0 + if edcba == 0b00111: + return 0 + if edcba == 0b01000: + return 1 + if edcba == 0b01001: + return 0 + if edcba == 0b01010: + return 0 + if edcba == 0b01011: + return 0 + if edcba == 0b01100: + return 0 + if edcba == 0b01101: + return 0 + if edcba == 0b01110: + return 0 + if edcba == 0b01111: + return 1 + if edcba == 0b10000: + return 1 + if edcba == 0b10001: + return 0 + if edcba == 0b10010: + return 0 + if edcba == 0b10011: + return 0 + if edcba == 0b10100: + return 0 + if edcba == 0b10101: + return 0 + if edcba == 0b10110: + return 0 + if edcba == 0b10111: + return 1 + if edcba == 0b11000: + return 1 + if edcba == 0b11001: + return 0 + if edcba == 0b11010: + return 0 + if edcba == 0b11011: + return 1 + if edcba == 0b11100: + return k # K28 + if edcba == 0b11101: + return 1 + if edcba == 0b11110: + return 1 + if edcba == 0b11111: + return 1 + +def rd_flip_8b10b(d, k): + return rd_flip_5b6b(d & 0x1f, k) ^ rd_flip_3b4b(d >> 5) + + +class BaseXSerdesSource(): + + def __init__(self, data, clock, data_k=None, enable=None, slip=None, data_valid=None, + gbx_sync=None, enc_8b10b=True, reverse=False, gbx_cfg=None, *args, **kwargs): + + self.log = logging.getLogger(f"cocotb.{data._path}") + self.data = data + self.clock = clock + self.data_k = data_k + self.enable = enable + self.slip = slip + self.data_valid = data_valid + self.gbx_sync = gbx_sync + self.enc_8b10b = enc_8b10b + self.reverse = reverse + + self.log.info("BASE-X serdes source") + self.log.info("Copyright (c) 2026 FPGA Ninja, LLC") + self.log.info("https://github.com/fpganinja/taxi") + + super().__init__(*args, **kwargs) + + self.active = False + self.queue = Queue() + self.dequeue_event = Event() + self.current_frame = None + self.idle_event = Event() + self.idle_event.set() + + self.enable_dic = True + self.ifg = 12 + + self.bit_offset = 0 + + self.gbx_seq = 0 + self.gbx_seq_len = None + self.gbx_seq_stall = None + self.gbx_in_bits = 10 + self.gbx_out_bits = 10 + self.gbx_bit_cnt = 0 + + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + self.queue_occupancy_limit_bytes = -1 + self.queue_occupancy_limit_frames = -1 + + self.width = len(self.data) + self.byte_size = 8 + self.byte_lanes = self.width // self.byte_size + + self.data_mask = (2**self.width)-1 + + assert self.byte_lanes in [1, 2, 4, 8] + assert self.width == self.byte_lanes * self.byte_size + + self.log.info("BASE-X serdes source model configuration") + self.log.info(" Byte size: %d bits", self.byte_size) + self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes) + self.log.info(" Enable 8b10b encoder: %s", self.enc_8b10b) + self.log.info(" Bit reverse: %s", self.reverse) + + if gbx_cfg: + self.set_gbx_cfg(*gbx_cfg) + + self.data.setimmediatevalue(0) + if self.data_k is not None: + self.data_k.setimmediatevalue(0) + if self.data_valid is not None: + self.data_valid.setimmediatevalue(0) + if self.gbx_sync is not None: + self.gbx_sync.setimmediatevalue(0) + + self._run_cr = cocotb.start_soon(self._run()) + + def set_gbx_cfg(self, seq_len=None, seq_stall=None): + self.log.info("Set gearbox configuration") + + if seq_len is None: + self.log.info("Gearbox disabled") + self.gbx_bit_cnt = 0 + self.gbx_seq_len = None + self.gbx_seq_stall = None + self.gbx_in_bits = 10 + self.gbx_out_bits = 10 + self.gbx_seq = 0 + + seq_stall = sorted(list(set(seq_stall))) + + for x in seq_stall: + assert 0 <= x < seq_len + + self.log.info(" Sequence length: %d cycles", seq_len) + self.log.info(" Stall cycles: %s", seq_stall) + + out_bits = 10 + in_cycles = seq_len + out_cycles = in_cycles - len(seq_stall) + in_bits = (out_bits * out_cycles) // in_cycles + + self.log.info(" Input: %d bits (%d cycles)", in_bits, in_cycles) + self.log.info(" Output: %d bits (%d cycles)", out_bits, out_cycles) + self.log.info(" Gearbox ratio: %d:%d", in_bits, out_bits) + + assert in_cycles*in_bits == out_cycles*out_bits + + self.gbx_seq = 0 + self.gbx_seq_len = seq_len + self.gbx_seq_stall = set(seq_stall) + self.gbx_in_bits = in_bits + self.gbx_out_bits = out_bits + self.gbx_bit_cnt = 0 + + for k in range(self.gbx_seq_len): + self.gbx_bit_cnt += in_bits + if k in self.gbx_seq_stall: + continue + self.gbx_bit_cnt = max(self.gbx_bit_cnt - out_bits, 0) + + async def send(self, frame): + while self.full(): + self.dequeue_event.clear() + await self.dequeue_event.wait() + frame = GmiiFrame(frame) + await self.queue.put(frame) + self.idle_event.clear() + self.queue_occupancy_bytes += len(frame) + self.queue_occupancy_frames += 1 + + def send_nowait(self, frame): + if self.full(): + raise QueueFull() + frame = GmiiFrame(frame) + self.queue.put_nowait(frame) + self.idle_event.clear() + self.queue_occupancy_bytes += len(frame) + self.queue_occupancy_frames += 1 + + def count(self): + return self.queue.qsize() + + def empty(self): + return self.queue.empty() + + def full(self): + if self.queue_occupancy_limit_bytes > 0 and self.queue_occupancy_bytes > self.queue_occupancy_limit_bytes: + return True + elif self.queue_occupancy_limit_frames > 0 and self.queue_occupancy_frames > self.queue_occupancy_limit_frames: + return True + else: + return False + + def idle(self): + return self.empty() and not self.active + + def clear(self): + while not self.queue.empty(): + frame = self.queue.get_nowait() + frame.sim_time_end = None + frame.handle_tx_complete() + self.dequeue_event.set() + self.idle_event.set() + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + async def wait(self): + await self.idle_event.wait() + + async def _run(self): + frame = None + frame_offset = 0 + rd = False + odd = False + carrier_extend = False + in_pre = False + ifg_cnt = 0 + deficit_idle_cnt = 0 + last_d = 0 + self.active = False + + clk_period = 0 + last_clk = 0 + gbx_delay = 0 + + data = 0 + data_k = 0 + + while True: + await RisingEdge(self.clock) + + sim_time = get_sim_time() + if last_clk: + clk_period = sim_time - last_clk + last_clk = sim_time + + # clock enable + if self.enable is not None and not self.enable.value: + continue + + # gearbox sequence + if self.gbx_seq_len: + self.gbx_seq = (self.gbx_seq + 1) % self.gbx_seq_len + + if self.gbx_sync is not None: + self.gbx_sync.value = (self.gbx_seq == 0) + + self.gbx_bit_cnt += self.gbx_in_bits + + # stall cycle + if self.gbx_seq in self.gbx_seq_stall: + self.data.value = 0 + if self.data_k is not None: + self.data_k.value = 0 + if self.data_valid is not None: + self.data_valid.value = 0 + continue + + self.gbx_bit_cnt = max(self.gbx_bit_cnt - self.gbx_out_bits, 0) + gbx_delay = (self.gbx_bit_cnt * clk_period) // self.gbx_in_bits + else: + self.gbx_seq = 0 + self.gbx_bit_cnt = 0 + gbx_delay = 0 + + if self.gbx_sync is not None: + self.gbx_sync.value = 0 + + data = 0 + data_k = 0 + + for k in range(self.byte_lanes): + # idle + if not odd: + d_val = 0xBC # /K28.5/ + k_val = True + elif rd: + # /I1/ + d_val = 0xC5 # /D5.6/ + k_val = False + else: + # /I2/ + d_val = 0x50 # /D16.2/ + k_val = False + + if carrier_extend: + # /R/ + d_val = 0xF7 # (/K23.7/) + k_val = True + + if odd: + carrier_extend = False + + if frame is None: + if ifg_cnt > 1 or (not self.enable_dic and ifg_cnt > 0) or odd: + # in IFG + pass + + else: + # eligible to start + if not self.queue.empty(): + # send frame + frame = self.queue.get_nowait() + self.dequeue_event.set() + self.queue_occupancy_bytes -= len(frame) + self.queue_occupancy_frames -= 1 + self.current_frame = frame + frame.sim_time_start = sim_time + (clk_period // self.byte_lanes * k) - gbx_delay + frame.sim_time_sfd = None + frame.sim_time_end = None + self.log.info("TX frame: %s", frame) + frame.normalize() + assert frame.data[0] == EthPre.PRE + + if self.enable_dic: + deficit_idle_cnt = ifg_cnt + ifg_cnt = 0 + self.active = True + frame_offset = 0 + in_pre = True + else: + # nothing to send + self.active = False + self.idle_event.set() + + if frame is None: + # idle + if ifg_cnt > 0: + ifg_cnt -= 1 + elif deficit_idle_cnt > 0: + deficit_idle_cnt -= 1 + + if frame is not None: + if frame_offset == 0: + # /S/ + d_val = XgmiiCtrl.START # /K27.7/ + k_val = True + elif frame_offset >= len(frame.data): + # /T/ + d_val = XgmiiCtrl.TERM # /K29.7/ + k_val = True + carrier_extend = True + + ifg_cnt = max(self.ifg + deficit_idle_cnt - 1, 0) + frame.sim_time_end = sim_time + (clk_period // self.byte_lanes * k) - gbx_delay + frame.handle_tx_complete() + frame = None + self.current_frame = None + else: + d = frame.data[frame_offset] + if frame.sim_time_sfd is None and not in_pre: + frame.sim_time_sfd = sim_time + (clk_period // self.byte_lanes * k) - gbx_delay + if d == EthPre.SFD: + in_pre = False + if frame.error[frame_offset]: + # /V/ + d_val = XgmiiCtrl.ERROR # /K30.7/ + k_val = True + else: + d_val = d # /Dx.y/ + k_val = False + frame_offset += 1 + + if self.enc_8b10b: + # 8b/10b encode + self.log.error("8b/10b decoder not yet implemented") + data |= (d_val << (k*8)) + data_k |= (k_val << k) + else: + data |= (d_val << (k*8)) + data_k |= (k_val << k) + + odd = not odd + rd = rd ^ rd_flip_8b10b(d_val, k_val) + + # if self.slip is not None and self.slip.value: + # self.bit_offset += 1 + + # self.bit_offset = max(0, self.bit_offset) % 10 + + # if self.bit_offset != 0: + # d = data + + # out_d = ((last_d | d << 66) >> 66-self.bit_offset) & 0xffffffffffffffff + + # last_d = d + + # data = out_d + + data_out = data + data_k_out = data_k + + if self.reverse: + # bit reverse + data_out = sum(1 << (self.width-1-i) for i in range(self.width) if (data_out >> i) & 1) + data_k_out = sum(1 << (1-i) for i in range(self.byte_lanes) if (data_k_out >> i) & 1) + + self.data.value = data_out & self.data_mask + if self.data_k is not None: + self.data_k.value = data_k_out + if self.data_valid is not None: + self.data_valid.value = 1 + + +class BaseXSerdesSink: + + def __init__(self, data, clock, data_k=None, enable=None, data_valid=None, + gbx_req_sync=None, gbx_req_stall=None, gbx_sync=None, + dec_8b10b=True, reverse=False, gbx_cfg=None, *args, **kwargs): + + self.log = logging.getLogger(f"cocotb.{data._path}") + self.data = data + self.clock = clock + self.data_k = data_k + self.enable = enable + self.data_valid = data_valid + self.gbx_req_sync = gbx_req_sync + self.gbx_req_stall = gbx_req_stall + self.gbx_sync = gbx_sync + self.dec_8b10b = dec_8b10b + self.reverse = reverse + + self.log.info("BASE-X serdes sink") + self.log.info("Copyright (c) 2026 FPGA Ninja, LLC") + self.log.info("https://github.com/fpganinja/taxi") + + super().__init__(*args, **kwargs) + + self.active = False + self.queue = Queue() + self.active_event = Event() + + self.gbx_seq = 0 + self.gbx_seq_gen = 0 + self.gbx_seq_len = None + self.gbx_seq_stall = None + self.gbx_in_bits = 10 + self.gbx_out_bits = 10 + self.gbx_bit_cnt = 0 + + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + self.width = len(self.data) + self.byte_size = 8 + self.byte_lanes = self.width // self.byte_size + + assert self.byte_lanes in [1, 2, 4, 8] + assert self.width == self.byte_lanes * self.byte_size + + self.log.info("BASE-X serdes sink model configuration") + self.log.info(" Byte size: %d bits", self.byte_size) + self.log.info(" Data width: %d bits (%d bytes)", self.width, self.byte_lanes) + self.log.info(" Enable 8b10b decoder: %s", self.dec_8b10b) + self.log.info(" Bit reverse: %s", self.reverse) + + if gbx_cfg: + self.set_gbx_cfg(*gbx_cfg) + + if self.gbx_req_sync is not None: + self.gbx_req_sync.setimmediatevalue(0) + if self.gbx_req_stall is not None: + self.gbx_req_stall.setimmediatevalue(0) + + self._run_cr = cocotb.start_soon(self._run()) + + def set_gbx_cfg(self, seq_len=None, seq_stall=None): + self.log.info("Set gearbox configuration") + + if seq_len is None: + self.log.info("Gearbox disabled") + self.gbx_seq_len = None + self.gbx_seq_stall = None + + seq_stall = sorted(list(set(seq_stall))) + + for x in seq_stall: + assert 0 <= x < seq_len + + self.log.info(" Sequence length: %d cycles", seq_len) + self.log.info(" Stall cycles: %s", seq_stall) + + in_bits = 10 + out_cycles = seq_len + in_cycles = out_cycles - len(seq_stall) + out_bits = (in_bits * in_cycles) // out_cycles + + self.log.info(" Input: %d bits (%d cycles)", in_bits, in_cycles) + self.log.info(" Output: %d bits (%d cycles)", out_bits, out_cycles) + self.log.info(" Gearbox ratio: %d:%d", in_bits, out_bits) + + assert in_cycles*in_bits == out_cycles*out_bits + + self.gbx_seq = 0 + self.gbx_seq_gen = 0 + self.gbx_seq_len = seq_len + self.gbx_seq_stall = set(seq_stall) + self.gbx_in_bits = in_bits + self.gbx_out_bits = out_bits + self.gbx_bit_cnt = 0 + + for k in range(self.gbx_seq_len): + self.gbx_bit_cnt = max(self.gbx_bit_cnt - out_bits, 0) + if k in self.gbx_seq_stall: + continue + self.gbx_bit_cnt += in_bits + + def _recv(self, frame, compact=True): + if self.queue.empty(): + self.active_event.clear() + self.queue_occupancy_bytes -= len(frame) + self.queue_occupancy_frames -= 1 + if compact: + frame.compact() + return frame + + async def recv(self, compact=True): + frame = await self.queue.get() + return self._recv(frame, compact) + + def recv_nowait(self, compact=True): + frame = self.queue.get_nowait() + return self._recv(frame, compact) + + def count(self): + return self.queue.qsize() + + def empty(self): + return self.queue.empty() + + def idle(self): + return not self.active + + def clear(self): + while not self.queue.empty(): + self.queue.get_nowait() + self.active_event.clear() + self.queue_occupancy_bytes = 0 + self.queue_occupancy_frames = 0 + + async def wait(self, timeout=0, timeout_unit=None): + if not self.empty(): + return + if timeout: + await First(self.active_event.wait(), Timer(timeout, timeout_unit)) + else: + await self.active_event.wait() + + async def _run(self): + frame = None + rd = False + odd = False + in_pre = False + self.active = False + + clk_period = 0 + last_clk = 0 + gbx_delay = 0 + sync_bad = True + + data = 0 + data_k = 0 + + while True: + await RisingEdge(self.clock) + + sim_time = get_sim_time() + if last_clk: + clk_period = sim_time - last_clk + last_clk = sim_time + + # clock enable + if self.enable is not None and not self.enable.value: + continue + + # gearbox sequence + if self.gbx_seq_len: + # generation + self.gbx_seq_gen = (self.gbx_seq_gen + 1) % self.gbx_seq_len + + if self.gbx_req_sync is not None: + self.gbx_req_sync.value = (self.gbx_seq_gen == 0) + + # stall cycle + if self.gbx_req_stall is not None: + self.gbx_req_stall.value = (self.gbx_seq_gen in self.gbx_seq_stall) + + # sync + self.gbx_seq = (self.gbx_seq + 1) % self.gbx_seq_len + + if self.gbx_sync is not None: + if int(self.gbx_sync.value): + self.gbx_seq = 0 + + self.gbx_bit_cnt = max(self.gbx_bit_cnt - self.gbx_out_bits, 0) + + if self.gbx_seq in self.gbx_seq_stall: + continue + + self.gbx_bit_cnt += self.gbx_in_bits + gbx_delay = (self.gbx_bit_cnt * clk_period) // self.gbx_out_bits + else: + self.gbx_seq = 0 + self.gbx_seq_gen = 0 + self.gbx_bit_cnt = 0 + gbx_delay = 0 + + if self.gbx_sync is not None: + self.gbx_sync.value = 1 + + if self.data_valid is not None: + if not int(self.data_valid.value): + # stall + if self.gbx_seq_len and not sync_bad: + sync_bad = True + self.log.warning("Data not valid outside of gearbox stall cycle") + continue + + sync_bad = False + + data_in = int(self.data.value) + data_k_in = 0 + if self.data_k is not None: + data_k_in = int(self.data_k.value) + + if self.reverse: + # bit reverse + data_in = sum(1 << (self.width-1-i) for i in range(self.width) if (data_in >> i) & 1) + data_k_in = sum(1 << (1-i) for i in range(self.byte_lanes) if (data_k_in >> i) & 1) + + data = data_in + data_k = data_k_in + + for k in range(self.byte_lanes): + if self.dec_8b10b: + # 8b/10b decode + self.log.error("8b/10b decoder not yet implemented") + d_val = (data >> (k*8)) & 0xff + k_val = bool((data_k >> k) & 0x1) + else: + d_val = (data >> (k*8)) & 0xff + k_val = bool((data_k >> k) & 0x1) + + # 1000BASE-X decoding + + if frame is None: + if k_val: + if d_val == 0xBC: + # K28.5 + # sync + odd = False + elif d_val == XgmiiCtrl.START: + # start + if not odd: + frame = GmiiFrame(bytearray([EthPre.PRE]), [False]) + frame.sim_time_start = sim_time + (clk_period // self.byte_lanes * k) + gbx_delay + in_pre = True + else: + self.log.warning("Ignoring unaligned start") + else: + if k_val: + # got a control character; terminate frame reception + if d_val != XgmiiCtrl.TERM: + # store control character if it's not a termination + frame.data.append(d_val) + frame.error.append(True) + + frame.compact() + frame.sim_time_end = sim_time + (clk_period // self.byte_lanes * k) + gbx_delay + self.log.info("RX frame: %s", frame) + + self.queue_occupancy_bytes += len(frame) + self.queue_occupancy_frames += 1 + + self.queue.put_nowait(frame) + self.active_event.set() + + frame = None + else: + if frame.sim_time_sfd is None and not in_pre: + frame.sim_time_sfd = sim_time + (clk_period // self.byte_lanes * k) + gbx_delay + if d_val == EthPre.SFD: + in_pre = False + + frame.data.append(d_val) + frame.error.append(False) + + odd = not odd diff --git a/src/eth/tb/taxi_axis_basex_rx_16/Makefile b/src/eth/tb/taxi_axis_basex_rx_16/Makefile new file mode 100644 index 0000000..ef75f83 --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_rx_16/Makefile @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2026 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +RTL_DIR = ../../rtl +LIB_DIR = ../../lib +TAXI_SRC_DIR = $(LIB_DIR)/taxi/src + +DUT = taxi_axis_basex_rx_16 +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = test_$(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv +VERILOG_SOURCES += $(RTL_DIR)/$(DUT).sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/lfsr/rtl/taxi_lfsr.sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/axis/rtl/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 := 16 +export PARAM_GBX_IF_EN := 0 +export PARAM_PTP_TS_EN := 1 +export PARAM_PTP_TS_FMT_TOD := 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/src/eth/tb/taxi_axis_basex_rx_16/basex.py b/src/eth/tb/taxi_axis_basex_rx_16/basex.py new file mode 120000 index 0000000..306569f --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_rx_16/basex.py @@ -0,0 +1 @@ +../basex.py \ No newline at end of file diff --git a/src/eth/tb/taxi_axis_basex_rx_16/test_taxi_axis_basex_rx_16.py b/src/eth/tb/taxi_axis_basex_rx_16/test_taxi_axis_basex_rx_16.py new file mode 100644 index 0000000..a3c4f5c --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_rx_16/test_taxi_axis_basex_rx_16.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os +import sys + +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 GmiiFrame, PtpClockSimTime +from cocotbext.axi import AxiStreamBus, AxiStreamSink + +try: + from basex import BaseXSerdesSource +except ImportError: + # attempt import from current directory + sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + try: + from basex import BaseXSerdesSource + finally: + del sys.path[0] + + +class TB: + def __init__(self, dut, gbx_cfg=None): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + if gbx_cfg: + self.clk_period = 16 + else: + self.clk_period = 16 + + cocotb.start_soon(Clock(dut.clk, self.clk_period, units="ns").start()) + + self.source = BaseXSerdesSource( + data=dut.encoded_rx_data, + data_k=dut.encoded_rx_data_k, + data_valid=dut.encoded_rx_data_valid, + clock=dut.clk, + enc_8b10b=False, + gbx_cfg=gbx_cfg + ) + self.sink = AxiStreamSink(AxiStreamBus.from_entity(dut.m_axis_rx), dut.clk, dut.rst) + + self.ptp_clock = PtpClockSimTime(ts_tod=dut.ptp_ts, clock=dut.clk) + + dut.cfg_rx_max_pkt_len.setimmediatevalue(0) + dut.cfg_rx_enable.setimmediatevalue(0) + + self.stats = {} + self.stats["stat_rx_byte"] = 0 + self.stats["stat_rx_pkt_len"] = 0 + self.stats["stat_rx_pkt_fragment"] = 0 + self.stats["stat_rx_pkt_jabber"] = 0 + self.stats["stat_rx_pkt_ucast"] = 0 + self.stats["stat_rx_pkt_mcast"] = 0 + self.stats["stat_rx_pkt_bcast"] = 0 + self.stats["stat_rx_pkt_vlan"] = 0 + self.stats["stat_rx_pkt_good"] = 0 + self.stats["stat_rx_pkt_bad"] = 0 + self.stats["stat_rx_err_oversize"] = 0 + self.stats["stat_rx_err_bad_fcs"] = 0 + self.stats["stat_rx_err_bad_block"] = 0 + self.stats["stat_rx_err_framing"] = 0 + self.stats["stat_rx_err_preamble"] = 0 + + cocotb.start_soon(self._run_stats_counters()) + + 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) + + self.stats_reset() + + def stats_reset(self): + for stat in self.stats: + self.stats[stat] = 0 + + async def _run_stats_counters(self): + while True: + await RisingEdge(self.dut.clk) + for stat in self.stats: + self.stats[stat] += int(getattr(self.dut, stat).value) + + +async def run_test(dut, gbx_cfg=None, payload_lengths=None, payload_data=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.source.ifg = ifg + tb.dut.cfg_rx_max_pkt_len.value = 9218-1 + tb.dut.cfg_rx_enable.value = 1 + + await tb.reset() + + test_frames = [payload_data(x) for x in payload_lengths()] + tx_frames = [] + + total_bytes = 0 + total_pkts = 0 + + for test_data in test_frames: + test_frame = GmiiFrame.from_payload(test_data, tx_complete=tx_frames.append) + await tb.source.send(test_frame) + total_bytes += len(test_data)+4 + total_pkts += 1 + + for test_data in test_frames: + rx_frame = await tb.sink.recv() + tx_frame = tx_frames.pop(0) + + frame_error = rx_frame.tuser & 1 + ptp_ts = rx_frame.tuser >> 1 + ptp_ts_ns = ptp_ts / 2**16 + + tx_frame_sfd_ns = get_time_from_sim_steps(tx_frame.sim_time_sfd, "ns") + + tb.log.info("RX frame PTP TS: %f ns", ptp_ts_ns) + tb.log.info("TX frame SFD sim time: %f ns", tx_frame_sfd_ns) + tb.log.info("Difference: %f ns", abs(ptp_ts_ns - tx_frame_sfd_ns)) + + assert rx_frame.tdata == test_data + assert frame_error == 0 + if gbx_cfg is None: + assert abs(ptp_ts_ns - tx_frame_sfd_ns - tb.clk_period*0) < 0.01 + + assert tb.sink.empty() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_rx_byte"] == total_bytes + assert tb.stats["stat_rx_pkt_len"] == total_bytes + assert tb.stats["stat_rx_pkt_fragment"] == 0 + assert tb.stats["stat_rx_pkt_jabber"] == 0 + assert tb.stats["stat_rx_pkt_ucast"] == total_pkts + assert tb.stats["stat_rx_pkt_mcast"] == 0 + assert tb.stats["stat_rx_pkt_bcast"] == 0 + assert tb.stats["stat_rx_pkt_vlan"] == 0 + assert tb.stats["stat_rx_pkt_good"] == total_pkts + assert tb.stats["stat_rx_pkt_bad"] == 0 + assert tb.stats["stat_rx_err_oversize"] == 0 + assert tb.stats["stat_rx_err_bad_fcs"] == 0 + assert tb.stats["stat_rx_err_bad_block"] == 0 + assert tb.stats["stat_rx_err_framing"] == 0 + assert tb.stats["stat_rx_err_preamble"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +async def run_test_oversize(dut, gbx_cfg=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.source.ifg = ifg + tb.dut.cfg_rx_max_pkt_len.value = 1518-1 + tb.dut.cfg_rx_enable.value = 1 + + await tb.reset() + + for max_len in range(128-4-8, 128-4+9): + + tb.stats_reset() + + total_bytes = 0 + total_pkts = 0 + good_bytes = 0 + oversz_pkts = 0 + oversz_bytes_in = 0 + oversz_bytes_out = 0 + + for test_pkt_len in range(max_len-8, max_len+9): + + tb.log.info("max len %d (without FCS), test len %d (without FCS)", max_len, test_pkt_len) + + tb.dut.cfg_rx_max_pkt_len.value = max_len+4-1 + + test_data_1 = bytes(x for x in range(60)) + test_data_2 = bytes(x for x in range(test_pkt_len)) + + for k in range(3): + if k == 1: + test_data = test_data_2 + else: + test_data = test_data_1 + test_frame = GmiiFrame.from_payload(test_data) + await tb.source.send(test_frame) + total_bytes += len(test_data)+4 + total_pkts += 1 + if len(test_data) > max_len: + oversz_pkts += 1 + oversz_bytes_in += len(test_data)+4 + oversz_bytes_out += max_len + else: + good_bytes += len(test_data)+4 + + for k in range(3): + rx_frame = await tb.sink.recv() + + if k == 1: + if test_pkt_len > max_len: + frame_error = rx_frame.tuser[-1] & 1 + assert frame_error + else: + frame_error = rx_frame.tuser & 1 + assert rx_frame.tdata == test_data_2 + assert frame_error == 0 + else: + frame_error = rx_frame.tuser & 1 + assert rx_frame.tdata == test_data_1 + assert frame_error == 0 + + assert tb.sink.empty() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_rx_byte"] >= good_bytes+oversz_bytes_out + assert tb.stats["stat_rx_byte"] <= good_bytes+oversz_bytes_in + assert tb.stats["stat_rx_pkt_len"] >= good_bytes+oversz_bytes_out + assert tb.stats["stat_rx_pkt_len"] <= good_bytes+oversz_bytes_in + assert tb.stats["stat_rx_pkt_fragment"] == 0 + assert tb.stats["stat_rx_pkt_jabber"] == 0 + assert tb.stats["stat_rx_pkt_ucast"] == total_pkts + assert tb.stats["stat_rx_pkt_mcast"] == 0 + assert tb.stats["stat_rx_pkt_bcast"] == 0 + assert tb.stats["stat_rx_pkt_vlan"] == 0 + assert tb.stats["stat_rx_pkt_good"] == total_pkts-oversz_pkts + assert tb.stats["stat_rx_pkt_bad"] == oversz_pkts + assert tb.stats["stat_rx_err_oversize"] == oversz_pkts + assert tb.stats["stat_rx_err_bad_fcs"] == 0 + assert tb.stats["stat_rx_err_bad_block"] == 0 + assert tb.stats["stat_rx_err_framing"] == 0 + assert tb.stats["stat_rx_err_preamble"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +def size_list(): + return list(range(60, 128)) + [512, 1514, 9214] + [60]*10 + [i for i in range(64, 73) for k in range(8)] + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +def cycle_en(): + return itertools.cycle([0, 0, 0, 1]) + + +if getattr(cocotb, 'top', None) is not None: + + gbx_cfgs = [None] + + if cocotb.top.GBX_IF_EN.value: + gbx_cfgs.append((5, [4])) + + factory = TestFactory(run_test) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + # factory.add_option("ifg", list(range(0, 13))) + factory.add_option("gbx_cfg", gbx_cfgs) + factory.generate_tests() + + factory = TestFactory(run_test_oversize) + # factory.add_option("ifg", list(range(0, 13))) + factory.add_option("gbx_cfg", gbx_cfgs) + 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')) +lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib')) +taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src')) + + +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("gbx_en", [1, 0]) +def test_taxi_axis_basex_rx_16(request, gbx_en): + dut = "taxi_axis_basex_rx_16" + 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, f"{dut}.sv"), + os.path.join(taxi_src_dir, "lfsr", "rtl", "taxi_lfsr.sv"), + os.path.join(taxi_src_dir, "axis", "rtl", "taxi_axis_if.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['DATA_W'] = 16 + parameters['GBX_IF_EN'] = gbx_en + parameters['PTP_TS_EN'] = 1 + parameters['PTP_TS_FMT_TOD'] = 1 + parameters['PTP_TS_W'] = 96 if parameters['PTP_TS_FMT_TOD'] else 64 + + 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/src/eth/tb/taxi_axis_basex_rx_16/test_taxi_axis_basex_rx_16.sv b/src/eth/tb/taxi_axis_basex_rx_16/test_taxi_axis_basex_rx_16.sv new file mode 100644 index 0000000..726d196 --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_rx_16/test_taxi_axis_basex_rx_16.sv @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream 1000BASE-X frame receiver testbench + */ +module test_taxi_axis_basex_rx_16 # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 16, + parameter CTRL_W = DATA_W / 8, + parameter logic GBX_IF_EN = 1'b0, + parameter logic PTP_TS_EN = 1'b0, + parameter logic PTP_TS_FMT_TOD = 1'b1, + parameter PTP_TS_W = PTP_TS_FMT_TOD ? 96 : 64 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +localparam USER_W = (PTP_TS_EN ? PTP_TS_W : 0) + 1; + +logic clk; +logic rst; + +logic [DATA_W-1:0] encoded_rx_data; +logic [CTRL_W-1:0] encoded_rx_data_k; +logic encoded_rx_data_valid; + +taxi_axis_if #(.DATA_W(DATA_W), .USER_EN(1), .USER_W(USER_W)) m_axis_rx(); + +logic [PTP_TS_W-1:0] ptp_ts; + +logic [15:0] cfg_rx_max_pkt_len; +logic cfg_rx_enable; + +logic [1:0] rx_start_packet; +logic [1:0] stat_rx_byte; +logic [15:0] stat_rx_pkt_len; +logic stat_rx_pkt_fragment; +logic stat_rx_pkt_jabber; +logic stat_rx_pkt_ucast; +logic stat_rx_pkt_mcast; +logic stat_rx_pkt_bcast; +logic stat_rx_pkt_vlan; +logic stat_rx_pkt_good; +logic stat_rx_pkt_bad; +logic stat_rx_err_oversize; +logic stat_rx_err_bad_fcs; +logic stat_rx_err_bad_block; +logic stat_rx_err_framing; +logic stat_rx_err_preamble; + +taxi_axis_basex_rx_16 #( + .DATA_W(DATA_W), + .CTRL_W(CTRL_W), + .GBX_IF_EN(GBX_IF_EN), + .PTP_TS_EN(PTP_TS_EN), + .PTP_TS_W(PTP_TS_W) +) +uut ( + .clk(clk), + .rst(rst), + + /* + * 1000BASE-X encoded input + */ + .encoded_rx_data(encoded_rx_data), + .encoded_rx_data_k(encoded_rx_data_k), + .encoded_rx_data_valid(encoded_rx_data_valid), + + /* + * AXI4-Stream output (source) + */ + .m_axis_rx(m_axis_rx), + + /* + * PTP + */ + .ptp_ts(ptp_ts), + + /* + * Configuration + */ + .cfg_rx_max_pkt_len(cfg_rx_max_pkt_len), + .cfg_rx_enable(cfg_rx_enable), + + /* + * Status + */ + .rx_start_packet(rx_start_packet), + .stat_rx_byte(stat_rx_byte), + .stat_rx_pkt_len(stat_rx_pkt_len), + .stat_rx_pkt_fragment(stat_rx_pkt_fragment), + .stat_rx_pkt_jabber(stat_rx_pkt_jabber), + .stat_rx_pkt_ucast(stat_rx_pkt_ucast), + .stat_rx_pkt_mcast(stat_rx_pkt_mcast), + .stat_rx_pkt_bcast(stat_rx_pkt_bcast), + .stat_rx_pkt_vlan(stat_rx_pkt_vlan), + .stat_rx_pkt_good(stat_rx_pkt_good), + .stat_rx_pkt_bad(stat_rx_pkt_bad), + .stat_rx_err_oversize(stat_rx_err_oversize), + .stat_rx_err_bad_fcs(stat_rx_err_bad_fcs), + .stat_rx_err_bad_block(stat_rx_err_bad_block), + .stat_rx_err_framing(stat_rx_err_framing), + .stat_rx_err_preamble(stat_rx_err_preamble) +); + +endmodule + +`resetall diff --git a/src/eth/tb/taxi_axis_basex_rx_8/Makefile b/src/eth/tb/taxi_axis_basex_rx_8/Makefile new file mode 100644 index 0000000..029ba7a --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_rx_8/Makefile @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2026 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +RTL_DIR = ../../rtl +LIB_DIR = ../../lib +TAXI_SRC_DIR = $(LIB_DIR)/taxi/src + +DUT = taxi_axis_basex_rx_8 +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = test_$(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv +VERILOG_SOURCES += $(RTL_DIR)/$(DUT).sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/lfsr/rtl/taxi_lfsr.sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/axis/rtl/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_GBX_IF_EN := 0 +export PARAM_PTP_TS_EN := 1 +export PARAM_PTP_TS_FMT_TOD := 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/src/eth/tb/taxi_axis_basex_rx_8/basex.py b/src/eth/tb/taxi_axis_basex_rx_8/basex.py new file mode 120000 index 0000000..306569f --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_rx_8/basex.py @@ -0,0 +1 @@ +../basex.py \ No newline at end of file diff --git a/src/eth/tb/taxi_axis_basex_rx_8/test_taxi_axis_basex_rx_8.py b/src/eth/tb/taxi_axis_basex_rx_8/test_taxi_axis_basex_rx_8.py new file mode 100644 index 0000000..383736d --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_rx_8/test_taxi_axis_basex_rx_8.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os +import sys + +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 GmiiFrame, PtpClockSimTime +from cocotbext.axi import AxiStreamBus, AxiStreamSink + +try: + from basex import BaseXSerdesSource +except ImportError: + # attempt import from current directory + sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + try: + from basex import BaseXSerdesSource + finally: + del sys.path[0] + + +class TB: + def __init__(self, dut, gbx_cfg=None): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + if gbx_cfg: + self.clk_period = 8 + else: + self.clk_period = 8 + + cocotb.start_soon(Clock(dut.clk, self.clk_period, units="ns").start()) + + self.source = BaseXSerdesSource( + data=dut.encoded_rx_data, + data_k=dut.encoded_rx_data_k, + data_valid=dut.encoded_rx_data_valid, + clock=dut.clk, + enc_8b10b=False, + gbx_cfg=gbx_cfg + ) + self.sink = AxiStreamSink(AxiStreamBus.from_entity(dut.m_axis_rx), dut.clk, dut.rst) + + self.ptp_clock = PtpClockSimTime(ts_tod=dut.ptp_ts, clock=dut.clk) + + dut.cfg_rx_max_pkt_len.setimmediatevalue(0) + dut.cfg_rx_enable.setimmediatevalue(0) + + self.stats = {} + self.stats["stat_rx_byte"] = 0 + self.stats["stat_rx_pkt_len"] = 0 + self.stats["stat_rx_pkt_fragment"] = 0 + self.stats["stat_rx_pkt_jabber"] = 0 + self.stats["stat_rx_pkt_ucast"] = 0 + self.stats["stat_rx_pkt_mcast"] = 0 + self.stats["stat_rx_pkt_bcast"] = 0 + self.stats["stat_rx_pkt_vlan"] = 0 + self.stats["stat_rx_pkt_good"] = 0 + self.stats["stat_rx_pkt_bad"] = 0 + self.stats["stat_rx_err_oversize"] = 0 + self.stats["stat_rx_err_bad_fcs"] = 0 + self.stats["stat_rx_err_bad_block"] = 0 + self.stats["stat_rx_err_framing"] = 0 + self.stats["stat_rx_err_preamble"] = 0 + + cocotb.start_soon(self._run_stats_counters()) + + 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) + + self.stats_reset() + + def stats_reset(self): + for stat in self.stats: + self.stats[stat] = 0 + + async def _run_stats_counters(self): + while True: + await RisingEdge(self.dut.clk) + for stat in self.stats: + self.stats[stat] += int(getattr(self.dut, stat).value) + + +async def run_test(dut, gbx_cfg=None, payload_lengths=None, payload_data=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.source.ifg = ifg + tb.dut.cfg_rx_max_pkt_len.value = 9218-1 + tb.dut.cfg_rx_enable.value = 1 + + await tb.reset() + + test_frames = [payload_data(x) for x in payload_lengths()] + tx_frames = [] + + total_bytes = 0 + total_pkts = 0 + + for test_data in test_frames: + test_frame = GmiiFrame.from_payload(test_data, tx_complete=tx_frames.append) + await tb.source.send(test_frame) + total_bytes += max(len(test_data), 60)+4 + total_pkts += 1 + + for test_data in test_frames: + rx_frame = await tb.sink.recv() + tx_frame = tx_frames.pop(0) + + frame_error = rx_frame.tuser & 1 + ptp_ts = rx_frame.tuser >> 1 + ptp_ts_ns = ptp_ts / 2**16 + + tx_frame_sfd_ns = get_time_from_sim_steps(tx_frame.sim_time_sfd, "ns") + + tb.log.info("RX frame PTP TS: %f ns", ptp_ts_ns) + tb.log.info("TX frame SFD sim time: %f ns", tx_frame_sfd_ns) + tb.log.info("Difference: %f ns", abs(ptp_ts_ns - tx_frame_sfd_ns)) + + assert rx_frame.tdata == test_data + assert frame_error == 0 + if gbx_cfg is None: + assert abs(ptp_ts_ns - tx_frame_sfd_ns - tb.clk_period*0) < 0.01 + + assert tb.sink.empty() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_rx_byte"] == total_bytes + assert tb.stats["stat_rx_pkt_len"] == total_bytes + assert tb.stats["stat_rx_pkt_fragment"] == 0 + assert tb.stats["stat_rx_pkt_jabber"] == 0 + assert tb.stats["stat_rx_pkt_ucast"] == total_pkts + assert tb.stats["stat_rx_pkt_mcast"] == 0 + assert tb.stats["stat_rx_pkt_bcast"] == 0 + assert tb.stats["stat_rx_pkt_vlan"] == 0 + assert tb.stats["stat_rx_pkt_good"] == total_pkts + assert tb.stats["stat_rx_pkt_bad"] == 0 + assert tb.stats["stat_rx_err_oversize"] == 0 + assert tb.stats["stat_rx_err_bad_fcs"] == 0 + assert tb.stats["stat_rx_err_bad_block"] == 0 + assert tb.stats["stat_rx_err_framing"] == 0 + assert tb.stats["stat_rx_err_preamble"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +async def run_test_oversize(dut, gbx_cfg=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.source.ifg = ifg + tb.dut.cfg_rx_max_pkt_len.value = 1518-1 + tb.dut.cfg_rx_enable.value = 1 + + await tb.reset() + + for max_len in range(128-4-8, 128-4+9): + + tb.stats_reset() + + total_bytes = 0 + total_pkts = 0 + good_bytes = 0 + oversz_pkts = 0 + oversz_bytes_in = 0 + oversz_bytes_out = 0 + + for test_pkt_len in range(max_len-8, max_len+9): + + tb.log.info("max len %d (without FCS), test len %d (without FCS)", max_len, test_pkt_len) + + tb.dut.cfg_rx_max_pkt_len.value = max_len+4-1 + + test_data_1 = bytes(x for x in range(60)) + test_data_2 = bytes(x for x in range(test_pkt_len)) + + for k in range(3): + if k == 1: + test_data = test_data_2 + else: + test_data = test_data_1 + test_frame = GmiiFrame.from_payload(test_data) + await tb.source.send(test_frame) + total_bytes += max(len(test_data), 60)+4 + total_pkts += 1 + if len(test_data) > max_len: + oversz_pkts += 1 + oversz_bytes_in += len(test_data)+4 + oversz_bytes_out += max_len + else: + good_bytes += len(test_data)+4 + + for k in range(3): + rx_frame = await tb.sink.recv() + + if k == 1: + if test_pkt_len > max_len: + frame_error = rx_frame.tuser[-1] & 1 + assert frame_error + else: + frame_error = rx_frame.tuser & 1 + assert rx_frame.tdata == test_data_2 + assert frame_error == 0 + else: + frame_error = rx_frame.tuser & 1 + assert rx_frame.tdata == test_data_1 + assert frame_error == 0 + + assert tb.sink.empty() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_rx_byte"] >= good_bytes+oversz_bytes_out + assert tb.stats["stat_rx_byte"] <= good_bytes+oversz_bytes_in + assert tb.stats["stat_rx_pkt_len"] >= good_bytes+oversz_bytes_out + assert tb.stats["stat_rx_pkt_len"] <= good_bytes+oversz_bytes_in + assert tb.stats["stat_rx_pkt_fragment"] == 0 + assert tb.stats["stat_rx_pkt_jabber"] == 0 + assert tb.stats["stat_rx_pkt_ucast"] == total_pkts + assert tb.stats["stat_rx_pkt_mcast"] == 0 + assert tb.stats["stat_rx_pkt_bcast"] == 0 + assert tb.stats["stat_rx_pkt_vlan"] == 0 + assert tb.stats["stat_rx_pkt_good"] == total_pkts-oversz_pkts + assert tb.stats["stat_rx_pkt_bad"] == oversz_pkts + assert tb.stats["stat_rx_err_oversize"] == oversz_pkts + assert tb.stats["stat_rx_err_bad_fcs"] == 0 + assert tb.stats["stat_rx_err_bad_block"] == 0 + assert tb.stats["stat_rx_err_framing"] == 0 + assert tb.stats["stat_rx_err_preamble"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +def size_list(): + return list(range(60, 128)) + [512, 1514, 9214] + [60]*10 + [i for i in range(64, 73) for k in range(8)] + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +def cycle_en(): + return itertools.cycle([0, 0, 0, 1]) + + +if getattr(cocotb, 'top', None) is not None: + + gbx_cfgs = [None] + + if cocotb.top.GBX_IF_EN.value: + gbx_cfgs.append((5, [4])) + + factory = TestFactory(run_test) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + # factory.add_option("ifg", list(range(0, 13))) + factory.add_option("gbx_cfg", gbx_cfgs) + factory.generate_tests() + + factory = TestFactory(run_test_oversize) + # factory.add_option("ifg", list(range(0, 13))) + factory.add_option("gbx_cfg", gbx_cfgs) + 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')) +lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib')) +taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src')) + + +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("gbx_en", [1, 0]) +def test_taxi_axis_basex_rx_8(request, gbx_en): + dut = "taxi_axis_basex_rx_8" + 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, f"{dut}.sv"), + os.path.join(taxi_src_dir, "lfsr", "rtl", "taxi_lfsr.sv"), + os.path.join(taxi_src_dir, "axis", "rtl", "taxi_axis_if.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['DATA_W'] = 8 + parameters['GBX_IF_EN'] = gbx_en + parameters['PTP_TS_EN'] = 1 + parameters['PTP_TS_FMT_TOD'] = 1 + parameters['PTP_TS_W'] = 96 if parameters['PTP_TS_FMT_TOD'] else 64 + + 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/src/eth/tb/taxi_axis_basex_rx_8/test_taxi_axis_basex_rx_8.sv b/src/eth/tb/taxi_axis_basex_rx_8/test_taxi_axis_basex_rx_8.sv new file mode 100644 index 0000000..bd273dd --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_rx_8/test_taxi_axis_basex_rx_8.sv @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream 1000BASE-X frame receiver testbench + */ +module test_taxi_axis_basex_rx_8 # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 8, + parameter CTRL_W = DATA_W / 8, + parameter logic GBX_IF_EN = 1'b0, + parameter logic PTP_TS_EN = 1'b0, + parameter logic PTP_TS_FMT_TOD = 1'b1, + parameter PTP_TS_W = PTP_TS_FMT_TOD ? 96 : 64 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +localparam USER_W = (PTP_TS_EN ? PTP_TS_W : 0) + 1; + +logic clk; +logic rst; + +logic [DATA_W-1:0] encoded_rx_data; +logic [CTRL_W-1:0] encoded_rx_data_k; +logic encoded_rx_data_valid; + +taxi_axis_if #(.DATA_W(DATA_W), .USER_EN(1), .USER_W(USER_W)) m_axis_rx(); + +logic [PTP_TS_W-1:0] ptp_ts; + +logic [15:0] cfg_rx_max_pkt_len; +logic cfg_rx_enable; + +logic rx_start_packet; +logic stat_rx_byte; +logic [15:0] stat_rx_pkt_len; +logic stat_rx_pkt_fragment; +logic stat_rx_pkt_jabber; +logic stat_rx_pkt_ucast; +logic stat_rx_pkt_mcast; +logic stat_rx_pkt_bcast; +logic stat_rx_pkt_vlan; +logic stat_rx_pkt_good; +logic stat_rx_pkt_bad; +logic stat_rx_err_oversize; +logic stat_rx_err_bad_fcs; +logic stat_rx_err_bad_block; +logic stat_rx_err_framing; +logic stat_rx_err_preamble; + +taxi_axis_basex_rx_8 #( + .DATA_W(DATA_W), + .CTRL_W(CTRL_W), + .GBX_IF_EN(GBX_IF_EN), + .PTP_TS_EN(PTP_TS_EN), + .PTP_TS_W(PTP_TS_W) +) +uut ( + .clk(clk), + .rst(rst), + + /* + * 1000BASE-X encoded input + */ + .encoded_rx_data(encoded_rx_data), + .encoded_rx_data_k(encoded_rx_data_k), + .encoded_rx_data_valid(encoded_rx_data_valid), + + /* + * AXI4-Stream output (source) + */ + .m_axis_rx(m_axis_rx), + + /* + * PTP + */ + .ptp_ts(ptp_ts), + + /* + * Configuration + */ + .cfg_rx_max_pkt_len(cfg_rx_max_pkt_len), + .cfg_rx_enable(cfg_rx_enable), + + /* + * Status + */ + .rx_start_packet(rx_start_packet), + .stat_rx_byte(stat_rx_byte), + .stat_rx_pkt_len(stat_rx_pkt_len), + .stat_rx_pkt_fragment(stat_rx_pkt_fragment), + .stat_rx_pkt_jabber(stat_rx_pkt_jabber), + .stat_rx_pkt_ucast(stat_rx_pkt_ucast), + .stat_rx_pkt_mcast(stat_rx_pkt_mcast), + .stat_rx_pkt_bcast(stat_rx_pkt_bcast), + .stat_rx_pkt_vlan(stat_rx_pkt_vlan), + .stat_rx_pkt_good(stat_rx_pkt_good), + .stat_rx_pkt_bad(stat_rx_pkt_bad), + .stat_rx_err_oversize(stat_rx_err_oversize), + .stat_rx_err_bad_fcs(stat_rx_err_bad_fcs), + .stat_rx_err_bad_block(stat_rx_err_bad_block), + .stat_rx_err_framing(stat_rx_err_framing), + .stat_rx_err_preamble(stat_rx_err_preamble) +); + +endmodule + +`resetall diff --git a/src/eth/tb/taxi_axis_basex_tx_16/Makefile b/src/eth/tb/taxi_axis_basex_tx_16/Makefile new file mode 100644 index 0000000..6228254 --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_tx_16/Makefile @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2026 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +RTL_DIR = ../../rtl +LIB_DIR = ../../lib +TAXI_SRC_DIR = $(LIB_DIR)/taxi/src + +DUT = taxi_axis_basex_tx_16 +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = test_$(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv +VERILOG_SOURCES += $(RTL_DIR)/$(DUT).sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/lfsr/rtl/taxi_lfsr.sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/axis/rtl/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 := 16 +export PARAM_GBX_IF_EN := 0 +export PARAM_GBX_CNT := 1 +export PARAM_DIC_EN := 1 +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/src/eth/tb/taxi_axis_basex_tx_16/basex.py b/src/eth/tb/taxi_axis_basex_tx_16/basex.py new file mode 120000 index 0000000..306569f --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_tx_16/basex.py @@ -0,0 +1 @@ +../basex.py \ No newline at end of file diff --git a/src/eth/tb/taxi_axis_basex_tx_16/test_taxi_axis_basex_tx_16.py b/src/eth/tb/taxi_axis_basex_tx_16/test_taxi_axis_basex_tx_16.py new file mode 100644 index 0000000..f5453d7 --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_tx_16/test_taxi_axis_basex_tx_16.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os +import sys + +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 PtpClockSimTime +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamFrame + +try: + from basex import BaseXSerdesSink +except ImportError: + # attempt import from current directory + sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + try: + from basex import BaseXSerdesSink + finally: + del sys.path[0] + + +class TB: + def __init__(self, dut, gbx_cfg=None): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + if gbx_cfg: + self.clk_period = 16 + else: + self.clk_period = 16 + + cocotb.start_soon(Clock(dut.clk, self.clk_period, units="ns").start()) + + self.source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis_tx), dut.clk, dut.rst) + self.sink = BaseXSerdesSink( + data=dut.encoded_tx_data, + data_k=dut.encoded_tx_data_k, + data_valid=dut.encoded_tx_data_valid, + gbx_req_sync=dut.tx_gbx_req_sync, + gbx_req_stall=dut.tx_gbx_req_stall, + gbx_sync=dut.tx_gbx_sync, + clock=dut.clk, + dec_8b10b=False, + gbx_cfg=gbx_cfg + ) + + 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_tx_max_pkt_len.setimmediatevalue(0) + dut.cfg_tx_ifg.setimmediatevalue(0) + dut.cfg_tx_enable.setimmediatevalue(0) + + self.stats = {} + self.stats["stat_tx_byte"] = 0 + self.stats["stat_tx_pkt_len"] = 0 + self.stats["stat_tx_pkt_ucast"] = 0 + self.stats["stat_tx_pkt_mcast"] = 0 + self.stats["stat_tx_pkt_bcast"] = 0 + self.stats["stat_tx_pkt_vlan"] = 0 + self.stats["stat_tx_pkt_good"] = 0 + self.stats["stat_tx_pkt_bad"] = 0 + self.stats["stat_tx_err_oversize"] = 0 + self.stats["stat_tx_err_user"] = 0 + self.stats["stat_tx_err_underflow"] = 0 + + cocotb.start_soon(self._run_stats_counters()) + + 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) + + self.stats_reset() + + def stats_reset(self): + for stat in self.stats: + self.stats[stat] = 0 + + async def _run_stats_counters(self): + while True: + await RisingEdge(self.dut.clk) + for stat in self.stats: + self.stats[stat] += int(getattr(self.dut, stat).value) + + +async def run_test(dut, gbx_cfg=None, payload_lengths=None, payload_data=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.dut.cfg_tx_max_pkt_len.value = 9218-1 + tb.dut.cfg_tx_ifg.value = ifg + + await tb.reset() + + for k in range(100): + await RisingEdge(dut.clk) + + tb.dut.cfg_tx_enable.value = 1 + + test_frames = [payload_data(x) for x in payload_lengths()] + + total_bytes = 0 + total_pkts = 0 + + for test_data in test_frames: + await tb.source.send(AxiStreamFrame(test_data, tid=0, tuser=0)) + total_bytes += len(test_data)+4 + total_pkts += 1 + + 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 + if gbx_cfg is None: + assert abs(rx_frame_sfd_ns - ptp_ts_ns - tb.clk_period*1) < 0.01 + + assert tb.sink.empty() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_tx_byte"] == total_bytes + assert tb.stats["stat_tx_pkt_len"] == total_bytes + assert tb.stats["stat_tx_pkt_ucast"] == total_pkts + assert tb.stats["stat_tx_pkt_mcast"] == 0 + assert tb.stats["stat_tx_pkt_bcast"] == 0 + assert tb.stats["stat_tx_pkt_vlan"] == 0 + assert tb.stats["stat_tx_pkt_good"] == total_pkts + assert tb.stats["stat_tx_pkt_bad"] == 0 + assert tb.stats["stat_tx_err_oversize"] == 0 + assert tb.stats["stat_tx_err_user"] == 0 + assert tb.stats["stat_tx_err_underflow"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +async def run_test_underrun(dut, gbx_cfg=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.dut.cfg_tx_max_pkt_len.value = 9218-1 + tb.dut.cfg_tx_ifg.value = ifg + + await tb.reset() + + for k in range(100): + await RisingEdge(dut.clk) + + tb.dut.cfg_tx_enable.value = 1 + + 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(64): + 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.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() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_tx_byte"] > 64*2 + 8 + assert tb.stats["stat_tx_pkt_len"] > 64*2 + 8 + assert tb.stats["stat_tx_pkt_ucast"] == 3 + assert tb.stats["stat_tx_pkt_mcast"] == 0 + assert tb.stats["stat_tx_pkt_bcast"] == 0 + assert tb.stats["stat_tx_pkt_vlan"] == 0 + assert tb.stats["stat_tx_pkt_good"] == 2 + assert tb.stats["stat_tx_pkt_bad"] == 1 + assert tb.stats["stat_tx_err_oversize"] == 0 + assert tb.stats["stat_tx_err_user"] == 0 + assert tb.stats["stat_tx_err_underflow"] == 1 + + for k in range(10): + await RisingEdge(dut.clk) + + +async def run_test_error(dut, gbx_cfg=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.dut.cfg_tx_max_pkt_len.value = 9218-1 + tb.dut.cfg_tx_ifg.value = ifg + + await tb.reset() + + for k in range(100): + await RisingEdge(dut.clk) + + tb.dut.cfg_tx_enable.value = 1 + + 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.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() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_tx_byte"] > 64*2 + 32 + assert tb.stats["stat_tx_pkt_len"] > 64*2 + 32 + assert tb.stats["stat_tx_pkt_ucast"] == 3 + assert tb.stats["stat_tx_pkt_mcast"] == 0 + assert tb.stats["stat_tx_pkt_bcast"] == 0 + assert tb.stats["stat_tx_pkt_vlan"] == 0 + assert tb.stats["stat_tx_pkt_good"] == 2 + assert tb.stats["stat_tx_pkt_bad"] == 1 + assert tb.stats["stat_tx_err_oversize"] == 0 + assert tb.stats["stat_tx_err_user"] == 1 + assert tb.stats["stat_tx_err_underflow"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +async def run_test_oversize(dut, gbx_cfg=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.dut.cfg_tx_max_pkt_len.value = 1518-1 + tb.dut.cfg_tx_ifg.value = ifg + + await tb.reset() + + for k in range(100): + await RisingEdge(dut.clk) + + tb.dut.cfg_tx_enable.value = 1 + + for max_len in range(128-4-8, 128-4+9): + + tb.stats_reset() + + total_bytes = 0 + total_pkts = 0 + good_bytes = 0 + oversz_pkts = 0 + oversz_bytes_in = 0 + oversz_bytes_out = 0 + + for test_pkt_len in range(max_len-8, max_len+9): + + tb.log.info("max len %d (without FCS), test len %d (without FCS)", max_len, test_pkt_len) + + tb.dut.cfg_tx_max_pkt_len.value = max_len+4-1 + + test_data_1 = bytes(x for x in range(60)) + test_data_2 = bytes(x for x in range(test_pkt_len)) + + for k in range(3): + if k == 1: + test_data = test_data_2 + else: + test_data = test_data_1 + test_frame = AxiStreamFrame(test_data) + await tb.source.send(test_frame) + total_bytes += max(len(test_data), 60)+4 + total_pkts += 1 + if len(test_data) > max_len: + oversz_pkts += 1 + oversz_bytes_in += len(test_data)+4 + oversz_bytes_out += max_len + else: + good_bytes += len(test_data)+4 + + for k in range(3): + rx_frame = await tb.sink.recv() + + if k == 1: + if test_pkt_len > max_len: + assert rx_frame.data[-1] == 0xFE + assert rx_frame.error[-1] == 1 + else: + assert rx_frame.get_payload() == test_data_2 + assert rx_frame.check_fcs() + assert rx_frame.error is None + else: + assert rx_frame.get_payload() == test_data_1 + assert rx_frame.check_fcs() + assert rx_frame.error is None + + assert tb.sink.empty() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_tx_byte"] >= good_bytes+oversz_bytes_out-8*oversz_pkts + assert tb.stats["stat_tx_byte"] <= good_bytes+oversz_bytes_in + assert tb.stats["stat_tx_pkt_len"] >= good_bytes+oversz_bytes_out-8*oversz_pkts + assert tb.stats["stat_tx_pkt_len"] <= good_bytes+oversz_bytes_in + assert tb.stats["stat_tx_pkt_ucast"] == total_pkts + assert tb.stats["stat_tx_pkt_mcast"] == 0 + assert tb.stats["stat_tx_pkt_bcast"] == 0 + assert tb.stats["stat_tx_pkt_vlan"] == 0 + assert tb.stats["stat_tx_pkt_good"] == total_pkts - oversz_pkts + assert tb.stats["stat_tx_pkt_bad"] == oversz_pkts + assert tb.stats["stat_tx_err_oversize"] == oversz_pkts + assert tb.stats["stat_tx_err_user"] == 0 + assert tb.stats["stat_tx_err_underflow"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +def size_list(): + return list(range(16, 128)) + [512, 1514, 9214] + [60]*10 + [i for i in range(64, 73) for k in range(8)] + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +def cycle_en(): + return itertools.cycle([0, 0, 0, 1]) + + +if getattr(cocotb, 'top', None) is not None: + + gbx_cfgs = [None] + + if cocotb.top.GBX_IF_EN.value: + gbx_cfgs.append((5, [4])) + + 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("gbx_cfg", gbx_cfgs) + factory.generate_tests() + + for test in [ + run_test_underrun, + run_test_error, + run_test_oversize + ]: + + factory = TestFactory(test) + factory.add_option("ifg", [12]) + factory.add_option("gbx_cfg", gbx_cfgs) + 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')) +lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib')) +taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src')) + + +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]) +@pytest.mark.parametrize("gbx_en", [1, 0]) +def test_taxi_axis_basex_tx_16(request, gbx_en, dic_en): + dut = "taxi_axis_basex_tx_16" + 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, f"{dut}.sv"), + os.path.join(taxi_src_dir, "lfsr", "rtl", "taxi_lfsr.sv"), + os.path.join(taxi_src_dir, "axis", "rtl", "taxi_axis_if.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['DATA_W'] = 16 + parameters['CTRL_W'] = parameters['DATA_W'] // 8 + parameters['GBX_IF_EN'] = gbx_en + parameters['GBX_CNT'] = 1 + parameters['DIC_EN'] = dic_en + 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/src/eth/tb/taxi_axis_basex_tx_16/test_taxi_axis_basex_tx_16.sv b/src/eth/tb/taxi_axis_basex_tx_16/test_taxi_axis_basex_tx_16.sv new file mode 100644 index 0000000..fe080a3 --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_tx_16/test_taxi_axis_basex_tx_16.sv @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream 10000BASE-X frame transmitter testbench + */ +module test_taxi_axis_basex_tx_16 # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 16, + parameter CTRL_W = DATA_W / 8, + parameter logic GBX_IF_EN = 1'b0, + parameter GBX_CNT = 1, + parameter logic DIC_EN = 1'b1, + 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_EN(1), .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] encoded_tx_data; +logic [CTRL_W-1:0] encoded_tx_data_k; +logic [CTRL_W-1:0] encoded_tx_data_dm; +logic [CTRL_W-1:0] encoded_tx_data_dv; +logic encoded_tx_data_valid; +logic [GBX_CNT-1:0] tx_gbx_req_sync; +logic tx_gbx_req_stall; +logic [GBX_CNT-1:0] tx_gbx_sync; + +logic [PTP_TS_W-1:0] ptp_ts; + +logic [15:0] cfg_tx_max_pkt_len; +logic [7:0] cfg_tx_ifg; +logic cfg_tx_enable; + +logic [1:0] tx_start_packet; +logic [1:0] stat_tx_byte; +logic [15:0] stat_tx_pkt_len; +logic stat_tx_pkt_ucast; +logic stat_tx_pkt_mcast; +logic stat_tx_pkt_bcast; +logic stat_tx_pkt_vlan; +logic stat_tx_pkt_good; +logic stat_tx_pkt_bad; +logic stat_tx_err_oversize; +logic stat_tx_err_user; +logic stat_tx_err_underflow; + +taxi_axis_basex_tx_16 #( + .DATA_W(DATA_W), + .CTRL_W(CTRL_W), + .GBX_IF_EN(GBX_IF_EN), + .GBX_CNT(GBX_CNT), + .DIC_EN(DIC_EN), + .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), + + /* + * 10000BASE-X encoded interface + */ + .encoded_tx_data(encoded_tx_data), + .encoded_tx_data_k(encoded_tx_data_k), + .encoded_tx_data_dm(encoded_tx_data_dm), + .encoded_tx_data_dv(encoded_tx_data_dv), + .encoded_tx_data_valid(encoded_tx_data_valid), + .tx_gbx_req_sync(tx_gbx_req_sync), + .tx_gbx_req_stall(tx_gbx_req_stall), + .tx_gbx_sync(tx_gbx_sync), + + /* + * PTP + */ + .ptp_ts(ptp_ts), + + /* + * Configuration + */ + .cfg_tx_max_pkt_len(cfg_tx_max_pkt_len), + .cfg_tx_ifg(cfg_tx_ifg), + .cfg_tx_enable(cfg_tx_enable), + + /* + * Status + */ + .tx_start_packet(tx_start_packet), + .stat_tx_byte(stat_tx_byte), + .stat_tx_pkt_len(stat_tx_pkt_len), + .stat_tx_pkt_ucast(stat_tx_pkt_ucast), + .stat_tx_pkt_mcast(stat_tx_pkt_mcast), + .stat_tx_pkt_bcast(stat_tx_pkt_bcast), + .stat_tx_pkt_vlan(stat_tx_pkt_vlan), + .stat_tx_pkt_good(stat_tx_pkt_good), + .stat_tx_pkt_bad(stat_tx_pkt_bad), + .stat_tx_err_oversize(stat_tx_err_oversize), + .stat_tx_err_user(stat_tx_err_user), + .stat_tx_err_underflow(stat_tx_err_underflow) +); + +endmodule + +`resetall diff --git a/src/eth/tb/taxi_axis_basex_tx_8/Makefile b/src/eth/tb/taxi_axis_basex_tx_8/Makefile new file mode 100644 index 0000000..5ca8c40 --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_tx_8/Makefile @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2026 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +RTL_DIR = ../../rtl +LIB_DIR = ../../lib +TAXI_SRC_DIR = $(LIB_DIR)/taxi/src + +DUT = taxi_axis_basex_tx_8 +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = test_$(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv +VERILOG_SOURCES += $(RTL_DIR)/$(DUT).sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/lfsr/rtl/taxi_lfsr.sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/axis/rtl/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_GBX_IF_EN := 0 +export PARAM_GBX_CNT := 1 +export PARAM_DIC_EN := 1 +export PARAM_PTP_TS_EN := 1 +export PARAM_PTP_TS_FMT_TOD := 1 +export PARAM_PTP_TS_W := $(if $(filter-out 1,$(PARAM_PTP_TS_FMT_TOD)),64,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/src/eth/tb/taxi_axis_basex_tx_8/basex.py b/src/eth/tb/taxi_axis_basex_tx_8/basex.py new file mode 120000 index 0000000..306569f --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_tx_8/basex.py @@ -0,0 +1 @@ +../basex.py \ No newline at end of file diff --git a/src/eth/tb/taxi_axis_basex_tx_8/test_taxi_axis_basex_tx_8.py b/src/eth/tb/taxi_axis_basex_tx_8/test_taxi_axis_basex_tx_8.py new file mode 100644 index 0000000..c112aa6 --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_tx_8/test_taxi_axis_basex_tx_8.py @@ -0,0 +1,485 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os +import sys + +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 PtpClockSimTime +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamFrame + +try: + from basex import BaseXSerdesSink +except ImportError: + # attempt import from current directory + sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + try: + from basex import BaseXSerdesSink + finally: + del sys.path[0] + + +class TB: + def __init__(self, dut, gbx_cfg=None): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + if gbx_cfg: + self.clk_period = 8 + else: + self.clk_period = 8 + + cocotb.start_soon(Clock(dut.clk, self.clk_period, units="ns").start()) + + self.source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis_tx), dut.clk, dut.rst) + self.sink = BaseXSerdesSink( + data=dut.encoded_tx_data, + data_k=dut.encoded_tx_data_k, + data_valid=dut.encoded_tx_data_valid, + gbx_req_sync=dut.tx_gbx_req_sync, + gbx_req_stall=dut.tx_gbx_req_stall, + gbx_sync=dut.tx_gbx_sync, + clock=dut.clk, + dec_8b10b=False, + gbx_cfg=gbx_cfg + ) + + 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_tx_max_pkt_len.setimmediatevalue(0) + dut.cfg_tx_ifg.setimmediatevalue(0) + dut.cfg_tx_enable.setimmediatevalue(0) + + self.stats = {} + self.stats["stat_tx_byte"] = 0 + self.stats["stat_tx_pkt_len"] = 0 + self.stats["stat_tx_pkt_ucast"] = 0 + self.stats["stat_tx_pkt_mcast"] = 0 + self.stats["stat_tx_pkt_bcast"] = 0 + self.stats["stat_tx_pkt_vlan"] = 0 + self.stats["stat_tx_pkt_good"] = 0 + self.stats["stat_tx_pkt_bad"] = 0 + self.stats["stat_tx_err_oversize"] = 0 + self.stats["stat_tx_err_user"] = 0 + self.stats["stat_tx_err_underflow"] = 0 + + cocotb.start_soon(self._run_stats_counters()) + + 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) + + self.stats_reset() + + def stats_reset(self): + for stat in self.stats: + self.stats[stat] = 0 + + async def _run_stats_counters(self): + while True: + await RisingEdge(self.dut.clk) + for stat in self.stats: + self.stats[stat] += int(getattr(self.dut, stat).value) + + +async def run_test(dut, gbx_cfg=None, payload_lengths=None, payload_data=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.dut.cfg_tx_max_pkt_len.value = 9218-1 + tb.dut.cfg_tx_ifg.value = ifg + + await tb.reset() + + for k in range(100): + await RisingEdge(dut.clk) + + tb.dut.cfg_tx_enable.value = 1 + + test_frames = [payload_data(x) for x in payload_lengths()] + + total_bytes = 0 + total_pkts = 0 + + for test_data in test_frames: + await tb.source.send(AxiStreamFrame(test_data, tid=0, tuser=0)) + total_bytes += max(len(test_data), 60)+4 + total_pkts += 1 + + 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.ljust(60, b'\x00') + assert rx_frame.check_fcs() + assert rx_frame.error is None + if gbx_cfg is None: + assert abs(rx_frame_sfd_ns - ptp_ts_ns - tb.clk_period*1) < 0.01 + + assert tb.sink.empty() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_tx_byte"] == total_bytes + assert tb.stats["stat_tx_pkt_len"] == total_bytes + assert tb.stats["stat_tx_pkt_ucast"] == total_pkts + assert tb.stats["stat_tx_pkt_mcast"] == 0 + assert tb.stats["stat_tx_pkt_bcast"] == 0 + assert tb.stats["stat_tx_pkt_vlan"] == 0 + assert tb.stats["stat_tx_pkt_good"] == total_pkts + assert tb.stats["stat_tx_pkt_bad"] == 0 + assert tb.stats["stat_tx_err_oversize"] == 0 + assert tb.stats["stat_tx_err_user"] == 0 + assert tb.stats["stat_tx_err_underflow"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +async def run_test_underrun(dut, gbx_cfg=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.dut.cfg_tx_max_pkt_len.value = 9218-1 + tb.dut.cfg_tx_ifg.value = ifg + + await tb.reset() + + for k in range(100): + await RisingEdge(dut.clk) + + tb.dut.cfg_tx_enable.value = 1 + + 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(120): + 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.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() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_tx_byte"] > 64*2 + 8 + assert tb.stats["stat_tx_pkt_len"] > 64*2 + 8 + assert tb.stats["stat_tx_pkt_ucast"] == 3 + assert tb.stats["stat_tx_pkt_mcast"] == 0 + assert tb.stats["stat_tx_pkt_bcast"] == 0 + assert tb.stats["stat_tx_pkt_vlan"] == 0 + assert tb.stats["stat_tx_pkt_good"] == 2 + assert tb.stats["stat_tx_pkt_bad"] == 1 + assert tb.stats["stat_tx_err_oversize"] == 0 + assert tb.stats["stat_tx_err_user"] == 0 + assert tb.stats["stat_tx_err_underflow"] == 1 + + for k in range(10): + await RisingEdge(dut.clk) + + +async def run_test_error(dut, gbx_cfg=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.dut.cfg_tx_max_pkt_len.value = 9218-1 + tb.dut.cfg_tx_ifg.value = ifg + + await tb.reset() + + for k in range(100): + await RisingEdge(dut.clk) + + tb.dut.cfg_tx_enable.value = 1 + + 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.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() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_tx_byte"] > 64*2 + 32 + assert tb.stats["stat_tx_pkt_len"] > 64*2 + 32 + assert tb.stats["stat_tx_pkt_ucast"] == 3 + assert tb.stats["stat_tx_pkt_mcast"] == 0 + assert tb.stats["stat_tx_pkt_bcast"] == 0 + assert tb.stats["stat_tx_pkt_vlan"] == 0 + assert tb.stats["stat_tx_pkt_good"] == 2 + assert tb.stats["stat_tx_pkt_bad"] == 1 + assert tb.stats["stat_tx_err_oversize"] == 0 + assert tb.stats["stat_tx_err_user"] == 1 + assert tb.stats["stat_tx_err_underflow"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +async def run_test_oversize(dut, gbx_cfg=None, ifg=12): + + tb = TB(dut, gbx_cfg) + + tb.dut.cfg_tx_max_pkt_len.value = 1518-1 + tb.dut.cfg_tx_ifg.value = ifg + + await tb.reset() + + for k in range(100): + await RisingEdge(dut.clk) + + tb.dut.cfg_tx_enable.value = 1 + + for max_len in range(128-4-8, 128-4+9): + + tb.stats_reset() + + total_bytes = 0 + total_pkts = 0 + good_bytes = 0 + oversz_pkts = 0 + oversz_bytes_in = 0 + oversz_bytes_out = 0 + + for test_pkt_len in range(max_len-8, max_len+9): + + tb.log.info("max len %d (without FCS), test len %d (without FCS)", max_len, test_pkt_len) + + tb.dut.cfg_tx_max_pkt_len.value = max_len+4-1 + + test_data_1 = bytes(x for x in range(60)) + test_data_2 = bytes(x for x in range(test_pkt_len)) + + for k in range(3): + if k == 1: + test_data = test_data_2 + else: + test_data = test_data_1 + test_frame = AxiStreamFrame(test_data) + await tb.source.send(test_frame) + total_bytes += max(len(test_data), 60)+4 + total_pkts += 1 + if len(test_data) > max_len: + oversz_pkts += 1 + oversz_bytes_in += len(test_data)+4 + oversz_bytes_out += max_len + else: + good_bytes += len(test_data)+4 + + for k in range(3): + rx_frame = await tb.sink.recv() + + if k == 1: + if test_pkt_len > max_len: + assert rx_frame.data[-1] == 0xFE + assert rx_frame.error[-1] == 1 + else: + assert rx_frame.get_payload() == test_data_2 + assert rx_frame.check_fcs() + assert rx_frame.error is None + else: + assert rx_frame.get_payload() == test_data_1 + assert rx_frame.check_fcs() + assert rx_frame.error is None + + assert tb.sink.empty() + + for stat, val in tb.stats.items(): + tb.log.info("%s: %d", stat, val) + + assert tb.stats["stat_tx_byte"] >= good_bytes+oversz_bytes_out-8*oversz_pkts + assert tb.stats["stat_tx_byte"] <= good_bytes+oversz_bytes_in + assert tb.stats["stat_tx_pkt_len"] >= good_bytes+oversz_bytes_out-8*oversz_pkts + assert tb.stats["stat_tx_pkt_len"] <= good_bytes+oversz_bytes_in + assert tb.stats["stat_tx_pkt_ucast"] == total_pkts + assert tb.stats["stat_tx_pkt_mcast"] == 0 + assert tb.stats["stat_tx_pkt_bcast"] == 0 + assert tb.stats["stat_tx_pkt_vlan"] == 0 + assert tb.stats["stat_tx_pkt_good"] == total_pkts - oversz_pkts + assert tb.stats["stat_tx_pkt_bad"] == oversz_pkts + assert tb.stats["stat_tx_err_oversize"] == oversz_pkts + assert tb.stats["stat_tx_err_user"] == 0 + assert tb.stats["stat_tx_err_underflow"] == 0 + + for k in range(10): + await RisingEdge(dut.clk) + + +def size_list(): + return list(range(16, 128)) + [512, 1514, 9214] + [60]*10 + [i for i in range(64, 73) for k in range(8)] + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +def cycle_en(): + return itertools.cycle([0, 0, 0, 1]) + + +if getattr(cocotb, 'top', None) is not None: + + gbx_cfgs = [None] + + if cocotb.top.GBX_IF_EN.value: + gbx_cfgs.append((5, [4])) + + 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("gbx_cfg", gbx_cfgs) + factory.generate_tests() + + for test in [ + run_test_underrun, + run_test_error, + run_test_oversize + ]: + + factory = TestFactory(test) + factory.add_option("ifg", [12]) + factory.add_option("gbx_cfg", gbx_cfgs) + 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')) +lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib')) +taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src')) + + +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]) +@pytest.mark.parametrize("gbx_en", [1, 0]) +def test_taxi_axis_basex_tx_8(request, gbx_en, dic_en): + dut = "taxi_axis_basex_tx_8" + 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, f"{dut}.sv"), + os.path.join(taxi_src_dir, "lfsr", "rtl", "taxi_lfsr.sv"), + os.path.join(taxi_src_dir, "axis", "rtl", "taxi_axis_if.sv"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['DATA_W'] = 8 + parameters['CTRL_W'] = parameters['DATA_W'] // 8 + parameters['PADDING_EN'] = 1 + parameters['MIN_FRAME_LEN'] = 64 + parameters['GBX_IF_EN'] = gbx_en + parameters['GBX_CNT'] = 1 + parameters['DIC_EN'] = dic_en + parameters['PTP_TS_EN'] = 1 + parameters['PTP_TS_FMT_TOD'] = 1 + parameters['PTP_TS_W'] = 96 if parameters['PTP_TS_FMT_TOD'] else 64 + 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/src/eth/tb/taxi_axis_basex_tx_8/test_taxi_axis_basex_tx_8.sv b/src/eth/tb/taxi_axis_basex_tx_8/test_taxi_axis_basex_tx_8.sv new file mode 100644 index 0000000..9e2c8d3 --- /dev/null +++ b/src/eth/tb/taxi_axis_basex_tx_8/test_taxi_axis_basex_tx_8.sv @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream 10000BASE-X frame transmitter testbench + */ +module test_taxi_axis_basex_tx_8 # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 8, + parameter CTRL_W = DATA_W / 8, + parameter logic PADDING_EN = 1'b1, + parameter MIN_FRAME_LEN = 64, + parameter logic GBX_IF_EN = 1'b0, + parameter GBX_CNT = 1, + parameter logic DIC_EN = 1'b1, + parameter logic PTP_TS_EN = 1'b0, + parameter logic PTP_TS_FMT_TOD = 1'b1, + parameter PTP_TS_W = PTP_TS_FMT_TOD ? 96 : 64, + parameter TX_TAG_W = 8, + 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_EN(1), .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] encoded_tx_data; +logic [CTRL_W-1:0] encoded_tx_data_k; +logic [CTRL_W-1:0] encoded_tx_data_dm; +logic [CTRL_W-1:0] encoded_tx_data_dv; +logic encoded_tx_data_valid; +logic [GBX_CNT-1:0] tx_gbx_req_sync; +logic tx_gbx_req_stall; +logic [GBX_CNT-1:0] tx_gbx_sync; + +logic [PTP_TS_W-1:0] ptp_ts; + +logic [15:0] cfg_tx_max_pkt_len; +logic [7:0] cfg_tx_ifg; +logic cfg_tx_enable; + +logic tx_start_packet; +logic stat_tx_byte; +logic [15:0] stat_tx_pkt_len; +logic stat_tx_pkt_ucast; +logic stat_tx_pkt_mcast; +logic stat_tx_pkt_bcast; +logic stat_tx_pkt_vlan; +logic stat_tx_pkt_good; +logic stat_tx_pkt_bad; +logic stat_tx_err_oversize; +logic stat_tx_err_user; +logic stat_tx_err_underflow; + +taxi_axis_basex_tx_8 #( + .DATA_W(DATA_W), + .CTRL_W(CTRL_W), + .PADDING_EN(PADDING_EN), + .MIN_FRAME_LEN(MIN_FRAME_LEN), + .GBX_IF_EN(GBX_IF_EN), + .GBX_CNT(GBX_CNT), + // .DIC_EN(DIC_EN), + .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), + + /* + * 10000BASE-X encoded interface + */ + .encoded_tx_data(encoded_tx_data), + .encoded_tx_data_k(encoded_tx_data_k), + .encoded_tx_data_dm(encoded_tx_data_dm), + .encoded_tx_data_dv(encoded_tx_data_dv), + .encoded_tx_data_valid(encoded_tx_data_valid), + .tx_gbx_req_sync(tx_gbx_req_sync), + .tx_gbx_req_stall(tx_gbx_req_stall), + .tx_gbx_sync(tx_gbx_sync), + + /* + * PTP + */ + .ptp_ts(ptp_ts), + + /* + * Configuration + */ + .cfg_tx_max_pkt_len(cfg_tx_max_pkt_len), + .cfg_tx_ifg(cfg_tx_ifg), + .cfg_tx_enable(cfg_tx_enable), + + /* + * Status + */ + .tx_start_packet(tx_start_packet), + .stat_tx_byte(stat_tx_byte), + .stat_tx_pkt_len(stat_tx_pkt_len), + .stat_tx_pkt_ucast(stat_tx_pkt_ucast), + .stat_tx_pkt_mcast(stat_tx_pkt_mcast), + .stat_tx_pkt_bcast(stat_tx_pkt_bcast), + .stat_tx_pkt_vlan(stat_tx_pkt_vlan), + .stat_tx_pkt_good(stat_tx_pkt_good), + .stat_tx_pkt_bad(stat_tx_pkt_bad), + .stat_tx_err_oversize(stat_tx_err_oversize), + .stat_tx_err_user(stat_tx_err_user), + .stat_tx_err_underflow(stat_tx_err_underflow) +); + +endmodule + +`resetall