diff --git a/rtl/mac_ctrl_rx.v b/rtl/mac_ctrl_rx.v new file mode 100644 index 000000000..d0de0dab4 --- /dev/null +++ b/rtl/mac_ctrl_rx.v @@ -0,0 +1,448 @@ +/* + +Copyright (c) 2023 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * MAC control receive + */ +module mac_ctrl_rx # +( + parameter DATA_WIDTH = 8, + parameter KEEP_ENABLE = DATA_WIDTH>8, + parameter KEEP_WIDTH = DATA_WIDTH/8, + parameter ID_ENABLE = 0, + parameter ID_WIDTH = 8, + parameter DEST_ENABLE = 0, + parameter DEST_WIDTH = 8, + parameter USER_ENABLE = 1, + parameter USER_WIDTH = 1, + parameter USE_READY = 0, + parameter MCF_PARAMS_SIZE = 18 +) +( + input wire clk, + input wire rst, + + /* + * AXI stream input + */ + input wire [DATA_WIDTH-1:0] s_axis_tdata, + input wire [KEEP_WIDTH-1:0] s_axis_tkeep, + input wire s_axis_tvalid, + output wire s_axis_tready, + input wire s_axis_tlast, + input wire [ID_WIDTH-1:0] s_axis_tid, + input wire [DEST_WIDTH-1:0] s_axis_tdest, + input wire [USER_WIDTH-1:0] s_axis_tuser, + + /* + * AXI stream output + */ + output wire [DATA_WIDTH-1:0] m_axis_tdata, + output wire [KEEP_WIDTH-1:0] m_axis_tkeep, + output wire m_axis_tvalid, + input wire m_axis_tready, + output wire m_axis_tlast, + output wire [ID_WIDTH-1:0] m_axis_tid, + output wire [DEST_WIDTH-1:0] m_axis_tdest, + output wire [USER_WIDTH-1:0] m_axis_tuser, + + /* + * MAC control frame interface + */ + output wire mcf_valid, + output wire [47:0] mcf_eth_dst, + output wire [47:0] mcf_eth_src, + output wire [15:0] mcf_eth_type, + output wire [15:0] mcf_opcode, + output wire [MCF_PARAMS_SIZE*8-1:0] mcf_params, + output wire [ID_WIDTH-1:0] mcf_id, + output wire [DEST_WIDTH-1:0] mcf_dest, + output wire [USER_WIDTH-1:0] mcf_user, + + /* + * Configuration + */ + input wire [47:0] cfg_mcf_rx_eth_dst_mcast, + input wire cfg_mcf_rx_check_eth_dst_mcast, + input wire [47:0] cfg_mcf_rx_eth_dst_ucast, + input wire cfg_mcf_rx_check_eth_dst_ucast, + input wire [47:0] cfg_mcf_rx_eth_src, + input wire cfg_mcf_rx_check_eth_src, + input wire [15:0] cfg_mcf_rx_eth_type, + input wire [15:0] cfg_mcf_rx_opcode_lfc, + input wire cfg_mcf_rx_check_opcode_lfc, + input wire [15:0] cfg_mcf_rx_opcode_pfc, + input wire cfg_mcf_rx_check_opcode_pfc, + input wire cfg_mcf_rx_forward, + input wire cfg_mcf_rx_enable, + + /* + * Status + */ + output wire stat_rx_mcf +); + +parameter BYTE_LANES = KEEP_ENABLE ? KEEP_WIDTH : 1; + +parameter HDR_SIZE = 60; + +parameter CYCLE_COUNT = (HDR_SIZE+BYTE_LANES-1)/BYTE_LANES; + +parameter PTR_WIDTH = $clog2(CYCLE_COUNT); + +parameter OFFSET = HDR_SIZE % BYTE_LANES; + +// check configuration +initial begin + if (BYTE_LANES * 8 != DATA_WIDTH) begin + $error("Error: AXI stream interface requires byte (8-bit) granularity (instance %m)"); + $finish; + end + + if (MCF_PARAMS_SIZE > 44) begin + $error("Error: Maximum MCF_PARAMS_SIZE is 44 bytes (instance %m)"); + $finish; + end +end + +/* + +MAC control frame + + Field Length + Destination MAC address 6 octets [01:80:C2:00:00:01] + Source MAC address 6 octets + Ethertype 2 octets [0x8808] + Opcode 2 octets + Parameters 0-44 octets + +This module manages the reception of MAC control frames. Incoming frames are +checked based on the ethertype and (optionally) MAC addresses. Matching control +frames are marked by setting tuser[0] on the data output and forwarded through +a separate interface for processing. + +*/ + +reg read_mcf_reg = 1'b1, read_mcf_next; +reg mcf_frame_reg = 1'b0, mcf_frame_next; +reg [PTR_WIDTH-1:0] ptr_reg = 0, ptr_next; + +reg s_axis_tready_reg = 1'b0, s_axis_tready_next; + +// internal datapath +reg [DATA_WIDTH-1:0] m_axis_tdata_int; +reg [KEEP_WIDTH-1:0] m_axis_tkeep_int; +reg m_axis_tvalid_int; +reg m_axis_tready_int_reg = 1'b0; +reg m_axis_tlast_int; +reg [ID_WIDTH-1:0] m_axis_tid_int; +reg [DEST_WIDTH-1:0] m_axis_tdest_int; +reg [USER_WIDTH-1:0] m_axis_tuser_int; +wire m_axis_tready_int_early; + +reg mcf_valid_reg = 0, mcf_valid_next; +reg [47:0] mcf_eth_dst_reg = 0, mcf_eth_dst_next; +reg [47:0] mcf_eth_src_reg = 0, mcf_eth_src_next; +reg [15:0] mcf_eth_type_reg = 0, mcf_eth_type_next; +reg [15:0] mcf_opcode_reg = 0, mcf_opcode_next; +reg [MCF_PARAMS_SIZE*8-1:0] mcf_params_reg = 0, mcf_params_next; +reg [ID_WIDTH-1:0] mcf_id_reg = 0, mcf_id_next; +reg [DEST_WIDTH-1:0] mcf_dest_reg = 0, mcf_dest_next; +reg [USER_WIDTH-1:0] mcf_user_reg = 0, mcf_user_next; + +reg stat_rx_mcf_reg = 1'b0, stat_rx_mcf_next; + +assign s_axis_tready = s_axis_tready_reg; + +assign mcf_valid = mcf_valid_reg; +assign mcf_eth_dst = mcf_eth_dst_reg; +assign mcf_eth_src = mcf_eth_src_reg; +assign mcf_eth_type = mcf_eth_type_reg; +assign mcf_opcode = mcf_opcode_reg; +assign mcf_params = mcf_params_reg; +assign mcf_id = mcf_id_reg; +assign mcf_dest = mcf_dest_reg; +assign mcf_user = mcf_user_reg; + +assign stat_rx_mcf = stat_rx_mcf_reg; + +wire mcf_eth_dst_mcast_match = mcf_eth_dst_next == cfg_mcf_rx_eth_dst_mcast; +wire mcf_eth_dst_ucast_match = mcf_eth_dst_next == cfg_mcf_rx_eth_dst_ucast; +wire mcf_eth_src_match = mcf_eth_src_next == cfg_mcf_rx_eth_src; +wire mcf_eth_type_match = mcf_eth_type_next == cfg_mcf_rx_eth_type; +wire mcf_opcode_lfc_match = mcf_opcode_next == cfg_mcf_rx_opcode_lfc; +wire mcf_opcode_pfc_match = mcf_opcode_next == cfg_mcf_rx_opcode_pfc; + +wire mcf_eth_dst_match = ((mcf_eth_dst_mcast_match && cfg_mcf_rx_check_eth_dst_mcast) || + (mcf_eth_dst_ucast_match && cfg_mcf_rx_check_eth_dst_ucast) || + (!cfg_mcf_rx_check_eth_dst_mcast && !cfg_mcf_rx_check_eth_dst_ucast)); + +wire mcf_opcode_match = ((mcf_opcode_lfc_match && cfg_mcf_rx_check_opcode_lfc) || + (mcf_opcode_pfc_match && cfg_mcf_rx_check_opcode_pfc) || + (!cfg_mcf_rx_check_opcode_lfc && !cfg_mcf_rx_check_opcode_pfc)); + +wire mcf_match = (mcf_eth_dst_match && + (mcf_eth_src_match || !cfg_mcf_rx_check_eth_src) && + mcf_eth_type_match && mcf_opcode_match); + +integer k; + +always @* begin + read_mcf_next = read_mcf_reg; + mcf_frame_next = mcf_frame_reg; + ptr_next = ptr_reg; + + // pass through data + m_axis_tdata_int = s_axis_tdata; + m_axis_tkeep_int = s_axis_tkeep; + m_axis_tvalid_int = s_axis_tvalid; + m_axis_tlast_int = s_axis_tlast; + m_axis_tid_int = s_axis_tid; + m_axis_tdest_int = s_axis_tdest; + m_axis_tuser_int = s_axis_tuser; + + s_axis_tready_next = m_axis_tready_int_early || !USE_READY; + + mcf_valid_next = 1'b0; + mcf_eth_dst_next = mcf_eth_dst_reg; + mcf_eth_src_next = mcf_eth_src_reg; + mcf_eth_type_next = mcf_eth_type_reg; + mcf_opcode_next = mcf_opcode_reg; + mcf_params_next = mcf_params_reg; + mcf_id_next = mcf_id_reg; + mcf_dest_next = mcf_dest_reg; + mcf_user_next = mcf_user_reg; + + stat_rx_mcf_next = 1'b0; + + if ((s_axis_tready || !USE_READY) && s_axis_tvalid) begin + if (read_mcf_reg) begin + ptr_next = ptr_reg + 1; + + mcf_id_next = s_axis_tid; + mcf_dest_next = s_axis_tdest; + mcf_user_next = s_axis_tuser; + + `define _HEADER_FIELD_(offset, field) \ + if (ptr_reg == offset/BYTE_LANES) begin \ + field = s_axis_tdata[(offset%BYTE_LANES)*8 +: 8]; \ + end + + `_HEADER_FIELD_(0, mcf_eth_dst_next[5*8 +: 8]) + `_HEADER_FIELD_(1, mcf_eth_dst_next[4*8 +: 8]) + `_HEADER_FIELD_(2, mcf_eth_dst_next[3*8 +: 8]) + `_HEADER_FIELD_(3, mcf_eth_dst_next[2*8 +: 8]) + `_HEADER_FIELD_(4, mcf_eth_dst_next[1*8 +: 8]) + `_HEADER_FIELD_(5, mcf_eth_dst_next[0*8 +: 8]) + `_HEADER_FIELD_(6, mcf_eth_src_next[5*8 +: 8]) + `_HEADER_FIELD_(7, mcf_eth_src_next[4*8 +: 8]) + `_HEADER_FIELD_(8, mcf_eth_src_next[3*8 +: 8]) + `_HEADER_FIELD_(9, mcf_eth_src_next[2*8 +: 8]) + `_HEADER_FIELD_(10, mcf_eth_src_next[1*8 +: 8]) + `_HEADER_FIELD_(11, mcf_eth_src_next[0*8 +: 8]) + `_HEADER_FIELD_(12, mcf_eth_type_next[1*8 +: 8]) + `_HEADER_FIELD_(13, mcf_eth_type_next[0*8 +: 8]) + `_HEADER_FIELD_(14, mcf_opcode_next[1*8 +: 8]) + `_HEADER_FIELD_(15, mcf_opcode_next[0*8 +: 8]) + + if (ptr_reg == 0/BYTE_LANES) begin + // ensure params field gets cleared + mcf_params_next = 0; + end + + for (k = 0; k < MCF_PARAMS_SIZE; k = k + 1) begin + if (ptr_reg == (16+k)/BYTE_LANES) begin + mcf_params_next[k*8 +: 8] = s_axis_tdata[((16+k)%BYTE_LANES)*8 +: 8]; + end + end + + if (ptr_reg == 15/BYTE_LANES && (!KEEP_ENABLE || s_axis_tkeep[13%BYTE_LANES])) begin + // record match at end of opcode field + mcf_frame_next = mcf_match && cfg_mcf_rx_enable; + end + + if (ptr_reg == (HDR_SIZE-1)/BYTE_LANES) begin + read_mcf_next = 1'b0; + end + + `undef _HEADER_FIELD_ + end + + if (s_axis_tlast) begin + if (s_axis_tuser[0]) begin + // frame marked invalid + end else if (mcf_frame_next) begin + if (!cfg_mcf_rx_forward) begin + // mark frame invalid + m_axis_tuser_int[0] = 1'b1; + end + // transfer out MAC control frame + mcf_valid_next = 1'b1; + stat_rx_mcf_next = 1'b1; + end + + read_mcf_next = 1'b1; + mcf_frame_next = 1'b0; + ptr_next = 0; + end + end +end + +always @(posedge clk) begin + read_mcf_reg <= read_mcf_next; + mcf_frame_reg <= mcf_frame_next; + ptr_reg <= ptr_next; + + s_axis_tready_reg <= s_axis_tready_next; + + mcf_valid_reg <= mcf_valid_next; + mcf_eth_dst_reg <= mcf_eth_dst_next; + mcf_eth_src_reg <= mcf_eth_src_next; + mcf_eth_type_reg <= mcf_eth_type_next; + mcf_opcode_reg <= mcf_opcode_next; + mcf_params_reg <= mcf_params_next; + mcf_id_reg <= mcf_id_next; + mcf_dest_reg <= mcf_dest_next; + mcf_user_reg <= mcf_user_next; + + stat_rx_mcf_reg <= stat_rx_mcf_next; + + if (rst) begin + read_mcf_reg <= 1'b1; + mcf_frame_reg <= 1'b0; + ptr_reg <= 0; + s_axis_tready_reg <= 1'b0; + mcf_valid_reg <= 1'b0; + stat_rx_mcf_reg <= 1'b0; + end +end + +// output datapath logic +reg [DATA_WIDTH-1:0] m_axis_tdata_reg = {DATA_WIDTH{1'b0}}; +reg [KEEP_WIDTH-1:0] m_axis_tkeep_reg = {KEEP_WIDTH{1'b0}}; +reg m_axis_tvalid_reg = 1'b0, m_axis_tvalid_next; +reg m_axis_tlast_reg = 1'b0; +reg [ID_WIDTH-1:0] m_axis_tid_reg = {ID_WIDTH{1'b0}}; +reg [DEST_WIDTH-1:0] m_axis_tdest_reg = {DEST_WIDTH{1'b0}}; +reg [USER_WIDTH-1:0] m_axis_tuser_reg = {USER_WIDTH{1'b0}}; + +reg [DATA_WIDTH-1:0] temp_m_axis_tdata_reg = {DATA_WIDTH{1'b0}}; +reg [KEEP_WIDTH-1:0] temp_m_axis_tkeep_reg = {KEEP_WIDTH{1'b0}}; +reg temp_m_axis_tvalid_reg = 1'b0, temp_m_axis_tvalid_next; +reg temp_m_axis_tlast_reg = 1'b0; +reg [ID_WIDTH-1:0] temp_m_axis_tid_reg = {ID_WIDTH{1'b0}}; +reg [DEST_WIDTH-1:0] temp_m_axis_tdest_reg = {DEST_WIDTH{1'b0}}; +reg [USER_WIDTH-1:0] temp_m_axis_tuser_reg = {USER_WIDTH{1'b0}}; + +// datapath control +reg store_axis_int_to_output; +reg store_axis_int_to_temp; +reg store_axis_temp_to_output; + +assign m_axis_tdata = m_axis_tdata_reg; +assign m_axis_tkeep = KEEP_ENABLE ? m_axis_tkeep_reg : {KEEP_WIDTH{1'b1}}; +assign m_axis_tvalid = m_axis_tvalid_reg; +assign m_axis_tlast = m_axis_tlast_reg; +assign m_axis_tid = ID_ENABLE ? m_axis_tid_reg : {ID_WIDTH{1'b0}}; +assign m_axis_tdest = DEST_ENABLE ? m_axis_tdest_reg : {DEST_WIDTH{1'b0}}; +assign m_axis_tuser = USER_ENABLE ? m_axis_tuser_reg : {USER_WIDTH{1'b0}}; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +assign m_axis_tready_int_early = m_axis_tready || !USE_READY || (!temp_m_axis_tvalid_reg && (!m_axis_tvalid_reg || !m_axis_tvalid_int)); + +always @* begin + // transfer sink ready state to source + m_axis_tvalid_next = m_axis_tvalid_reg; + temp_m_axis_tvalid_next = temp_m_axis_tvalid_reg; + + store_axis_int_to_output = 1'b0; + store_axis_int_to_temp = 1'b0; + store_axis_temp_to_output = 1'b0; + + if (m_axis_tready_int_reg) begin + // input is ready + if (m_axis_tready || !USE_READY || !m_axis_tvalid_reg) begin + // output is ready or currently not valid, transfer data to output + m_axis_tvalid_next = m_axis_tvalid_int; + store_axis_int_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_m_axis_tvalid_next = m_axis_tvalid_int; + store_axis_int_to_temp = 1'b1; + end + end else if (m_axis_tready || !USE_READY) begin + // input is not ready, but output is ready + m_axis_tvalid_next = temp_m_axis_tvalid_reg; + temp_m_axis_tvalid_next = 1'b0; + store_axis_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + m_axis_tvalid_reg <= m_axis_tvalid_next; + m_axis_tready_int_reg <= m_axis_tready_int_early; + temp_m_axis_tvalid_reg <= temp_m_axis_tvalid_next; + + // datapath + if (store_axis_int_to_output) begin + m_axis_tdata_reg <= m_axis_tdata_int; + m_axis_tkeep_reg <= m_axis_tkeep_int; + m_axis_tlast_reg <= m_axis_tlast_int; + m_axis_tid_reg <= m_axis_tid_int; + m_axis_tdest_reg <= m_axis_tdest_int; + m_axis_tuser_reg <= m_axis_tuser_int; + end else if (store_axis_temp_to_output) begin + m_axis_tdata_reg <= temp_m_axis_tdata_reg; + m_axis_tkeep_reg <= temp_m_axis_tkeep_reg; + m_axis_tlast_reg <= temp_m_axis_tlast_reg; + m_axis_tid_reg <= temp_m_axis_tid_reg; + m_axis_tdest_reg <= temp_m_axis_tdest_reg; + m_axis_tuser_reg <= temp_m_axis_tuser_reg; + end + + if (store_axis_int_to_temp) begin + temp_m_axis_tdata_reg <= m_axis_tdata_int; + temp_m_axis_tkeep_reg <= m_axis_tkeep_int; + temp_m_axis_tlast_reg <= m_axis_tlast_int; + temp_m_axis_tid_reg <= m_axis_tid_int; + temp_m_axis_tdest_reg <= m_axis_tdest_int; + temp_m_axis_tuser_reg <= m_axis_tuser_int; + end + + if (rst) begin + m_axis_tvalid_reg <= 1'b0; + m_axis_tready_int_reg <= 1'b0; + temp_m_axis_tvalid_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/rtl/mac_ctrl_tx.v b/rtl/mac_ctrl_tx.v new file mode 100644 index 000000000..c06d50aad --- /dev/null +++ b/rtl/mac_ctrl_tx.v @@ -0,0 +1,421 @@ +/* + +Copyright (c) 2023 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * MAC control transmit + */ +module mac_ctrl_tx # +( + parameter DATA_WIDTH = 8, + parameter KEEP_ENABLE = DATA_WIDTH>8, + parameter KEEP_WIDTH = DATA_WIDTH/8, + parameter ID_ENABLE = 0, + parameter ID_WIDTH = 8, + parameter DEST_ENABLE = 0, + parameter DEST_WIDTH = 8, + parameter USER_ENABLE = 1, + parameter USER_WIDTH = 1, + parameter MCF_PARAMS_SIZE = 18 +) +( + input wire clk, + input wire rst, + + /* + * AXI stream input + */ + input wire [DATA_WIDTH-1:0] s_axis_tdata, + input wire [KEEP_WIDTH-1:0] s_axis_tkeep, + input wire s_axis_tvalid, + output wire s_axis_tready, + input wire s_axis_tlast, + input wire [ID_WIDTH-1:0] s_axis_tid, + input wire [DEST_WIDTH-1:0] s_axis_tdest, + input wire [USER_WIDTH-1:0] s_axis_tuser, + + /* + * AXI stream output + */ + output wire [DATA_WIDTH-1:0] m_axis_tdata, + output wire [KEEP_WIDTH-1:0] m_axis_tkeep, + output wire m_axis_tvalid, + input wire m_axis_tready, + output wire m_axis_tlast, + output wire [ID_WIDTH-1:0] m_axis_tid, + output wire [DEST_WIDTH-1:0] m_axis_tdest, + output wire [USER_WIDTH-1:0] m_axis_tuser, + + /* + * MAC control frame interface + */ + input wire mcf_valid, + output wire mcf_ready, + input wire [47:0] mcf_eth_dst, + input wire [47:0] mcf_eth_src, + input wire [15:0] mcf_eth_type, + input wire [15:0] mcf_opcode, + input wire [MCF_PARAMS_SIZE*8-1:0] mcf_params, + input wire [ID_WIDTH-1:0] mcf_id, + input wire [DEST_WIDTH-1:0] mcf_dest, + input wire [USER_WIDTH-1:0] mcf_user, + + /* + * Pause interface + */ + input wire tx_pause_req, + output wire tx_pause_ack, + + /* + * Status + */ + output wire stat_tx_mcf +); + +parameter BYTE_LANES = KEEP_ENABLE ? KEEP_WIDTH : 1; + +parameter HDR_SIZE = 60; + +parameter CYCLE_COUNT = (HDR_SIZE+BYTE_LANES-1)/BYTE_LANES; + +parameter PTR_WIDTH = $clog2(CYCLE_COUNT); + +parameter OFFSET = HDR_SIZE % BYTE_LANES; + +// check configuration +initial begin + if (BYTE_LANES * 8 != DATA_WIDTH) begin + $error("Error: AXI stream interface requires byte (8-bit) granularity (instance %m)"); + $finish; + end + + if (MCF_PARAMS_SIZE > 44) begin + $error("Error: Maximum MCF_PARAMS_SIZE is 44 bytes (instance %m)"); + $finish; + end +end + +/* + +MAC control frame + + Field Length + Destination MAC address 6 octets [01:80:C2:00:00:01] + Source MAC address 6 octets + Ethertype 2 octets [0x8808] + Opcode 2 octets + Parameters 0-44 octets + +This module manages the transmission of MAC control frames. Control frames +are accepted in parallel, serialized, and merged at a higher priority with +data traffic. + +*/ + +reg send_data_reg = 1'b0, send_data_next; +reg send_mcf_reg = 1'b0, send_mcf_next; +reg [PTR_WIDTH-1:0] ptr_reg = 0, ptr_next; + +reg s_axis_tready_reg = 1'b0, s_axis_tready_next; +reg mcf_ready_reg = 1'b0, mcf_ready_next; +reg tx_pause_ack_reg = 1'b0, tx_pause_ack_next; +reg stat_tx_mcf_reg = 1'b0, stat_tx_mcf_next; + +// internal datapath +reg [DATA_WIDTH-1:0] m_axis_tdata_int; +reg [KEEP_WIDTH-1:0] m_axis_tkeep_int; +reg m_axis_tvalid_int; +reg m_axis_tready_int_reg = 1'b0; +reg m_axis_tlast_int; +reg [ID_WIDTH-1:0] m_axis_tid_int; +reg [DEST_WIDTH-1:0] m_axis_tdest_int; +reg [USER_WIDTH-1:0] m_axis_tuser_int; +wire m_axis_tready_int_early; + +assign s_axis_tready = s_axis_tready_reg; +assign mcf_ready = mcf_ready_reg; +assign tx_pause_ack = tx_pause_ack_reg; +assign stat_tx_mcf = stat_tx_mcf_reg; + +integer k; + +always @* begin + send_data_next = send_data_reg; + send_mcf_next = send_mcf_reg; + ptr_next = ptr_reg; + + s_axis_tready_next = 1'b0; + mcf_ready_next = 1'b0; + tx_pause_ack_next = tx_pause_ack_reg; + stat_tx_mcf_next = 1'b0; + + m_axis_tdata_int = 0; + m_axis_tkeep_int = 0; + m_axis_tvalid_int = 1'b0; + m_axis_tlast_int = 1'b0; + m_axis_tid_int = 0; + m_axis_tdest_int = 0; + m_axis_tuser_int = 0; + + if (!send_data_reg && !send_mcf_reg) begin + m_axis_tdata_int = s_axis_tdata; + m_axis_tkeep_int = s_axis_tkeep; + m_axis_tvalid_int = 1'b0; + m_axis_tlast_int = s_axis_tlast; + m_axis_tid_int = s_axis_tid; + m_axis_tdest_int = s_axis_tdest; + m_axis_tuser_int = s_axis_tuser; + s_axis_tready_next = m_axis_tready_int_early && !tx_pause_req; + tx_pause_ack_next = tx_pause_req; + if (s_axis_tvalid && s_axis_tready) begin + s_axis_tready_next = m_axis_tready_int_early; + tx_pause_ack_next = 1'b0; + m_axis_tvalid_int = 1'b1; + if (s_axis_tlast) begin + s_axis_tready_next = m_axis_tready_int_early && !mcf_valid && !mcf_ready; + send_data_next = 1'b0; + end else begin + send_data_next = 1'b1; + end + end else if (mcf_valid) begin + s_axis_tready_next = 1'b0; + ptr_next = 0; + send_mcf_next = 1'b1; + mcf_ready_next = (CYCLE_COUNT == 1) && m_axis_tready_int_early; + end + end + + if (send_data_reg) begin + m_axis_tdata_int = s_axis_tdata; + m_axis_tkeep_int = s_axis_tkeep; + m_axis_tvalid_int = 1'b0; + m_axis_tlast_int = s_axis_tlast; + m_axis_tid_int = s_axis_tid; + m_axis_tdest_int = s_axis_tdest; + m_axis_tuser_int = s_axis_tuser; + s_axis_tready_next = m_axis_tready_int_early; + if (s_axis_tvalid && s_axis_tready) begin + m_axis_tvalid_int = 1'b1; + if (s_axis_tlast) begin + s_axis_tready_next = m_axis_tready_int_early && !tx_pause_req; + send_data_next = 1'b0; + if (mcf_valid) begin + s_axis_tready_next = 1'b0; + ptr_next = 0; + send_mcf_next = 1'b1; + mcf_ready_next = (CYCLE_COUNT == 1) && m_axis_tready_int_early; + end + end else begin + send_data_next = 1'b1; + end + end + end + + if (send_mcf_reg) begin + mcf_ready_next = (CYCLE_COUNT == 1 || ptr_reg == CYCLE_COUNT-1) && m_axis_tready_int_early; + if (m_axis_tready_int_reg) begin + ptr_next = ptr_reg + 1; + + m_axis_tvalid_int = 1'b1; + m_axis_tid_int = mcf_id; + m_axis_tdest_int = mcf_dest; + m_axis_tuser_int = mcf_user; + + `define _HEADER_FIELD_(offset, field) \ + if (ptr_reg == offset/BYTE_LANES) begin \ + m_axis_tdata_int[(offset%BYTE_LANES)*8 +: 8] = field; \ + m_axis_tkeep_int[offset%BYTE_LANES] = 1'b1; \ + end + + `_HEADER_FIELD_(0, mcf_eth_dst[5*8 +: 8]) + `_HEADER_FIELD_(1, mcf_eth_dst[4*8 +: 8]) + `_HEADER_FIELD_(2, mcf_eth_dst[3*8 +: 8]) + `_HEADER_FIELD_(3, mcf_eth_dst[2*8 +: 8]) + `_HEADER_FIELD_(4, mcf_eth_dst[1*8 +: 8]) + `_HEADER_FIELD_(5, mcf_eth_dst[0*8 +: 8]) + `_HEADER_FIELD_(6, mcf_eth_src[5*8 +: 8]) + `_HEADER_FIELD_(7, mcf_eth_src[4*8 +: 8]) + `_HEADER_FIELD_(8, mcf_eth_src[3*8 +: 8]) + `_HEADER_FIELD_(9, mcf_eth_src[2*8 +: 8]) + `_HEADER_FIELD_(10, mcf_eth_src[1*8 +: 8]) + `_HEADER_FIELD_(11, mcf_eth_src[0*8 +: 8]) + `_HEADER_FIELD_(12, mcf_eth_type[1*8 +: 8]) + `_HEADER_FIELD_(13, mcf_eth_type[0*8 +: 8]) + `_HEADER_FIELD_(14, mcf_opcode[1*8 +: 8]) + `_HEADER_FIELD_(15, mcf_opcode[0*8 +: 8]) + + for (k = 0; k < HDR_SIZE-16; k = k + 1) begin + if (ptr_reg == (16+k)/BYTE_LANES) begin + if (k < MCF_PARAMS_SIZE) begin + m_axis_tdata_int[((16+k)%BYTE_LANES)*8 +: 8] = mcf_params[k*8 +: 8]; + end else begin + m_axis_tdata_int[((16+k)%BYTE_LANES)*8 +: 8] = 0; + end + m_axis_tkeep_int[(16+k)%BYTE_LANES] = 1'b1; + end + end + + if (ptr_reg == (HDR_SIZE-1)/BYTE_LANES) begin + s_axis_tready_next = m_axis_tready_int_early && !tx_pause_req; + mcf_ready_next = 1'b0; + m_axis_tlast_int = 1'b1; + send_mcf_next = 1'b0; + stat_tx_mcf_next = 1'b1; + end else begin + mcf_ready_next = (ptr_next == CYCLE_COUNT-1) && m_axis_tready_int_early; + end + + `undef _HEADER_FIELD_ + end + end +end + +always @(posedge clk) begin + send_data_reg <= send_data_next; + send_mcf_reg <= send_mcf_next; + ptr_reg <= ptr_next; + + s_axis_tready_reg <= s_axis_tready_next; + mcf_ready_reg <= mcf_ready_next; + tx_pause_ack_reg <= tx_pause_ack_next; + stat_tx_mcf_reg <= stat_tx_mcf_next; + + if (rst) begin + send_data_reg <= 1'b0; + send_mcf_reg <= 1'b0; + ptr_reg <= 0; + s_axis_tready_reg <= 1'b0; + mcf_ready_reg <= 1'b0; + tx_pause_ack_reg <= 1'b0; + stat_tx_mcf_reg <= 1'b0; + end +end + +// output datapath logic +reg [DATA_WIDTH-1:0] m_axis_tdata_reg = {DATA_WIDTH{1'b0}}; +reg [KEEP_WIDTH-1:0] m_axis_tkeep_reg = {KEEP_WIDTH{1'b0}}; +reg m_axis_tvalid_reg = 1'b0, m_axis_tvalid_next; +reg m_axis_tlast_reg = 1'b0; +reg [ID_WIDTH-1:0] m_axis_tid_reg = {ID_WIDTH{1'b0}}; +reg [DEST_WIDTH-1:0] m_axis_tdest_reg = {DEST_WIDTH{1'b0}}; +reg [USER_WIDTH-1:0] m_axis_tuser_reg = {USER_WIDTH{1'b0}}; + +reg [DATA_WIDTH-1:0] temp_m_axis_tdata_reg = {DATA_WIDTH{1'b0}}; +reg [KEEP_WIDTH-1:0] temp_m_axis_tkeep_reg = {KEEP_WIDTH{1'b0}}; +reg temp_m_axis_tvalid_reg = 1'b0, temp_m_axis_tvalid_next; +reg temp_m_axis_tlast_reg = 1'b0; +reg [ID_WIDTH-1:0] temp_m_axis_tid_reg = {ID_WIDTH{1'b0}}; +reg [DEST_WIDTH-1:0] temp_m_axis_tdest_reg = {DEST_WIDTH{1'b0}}; +reg [USER_WIDTH-1:0] temp_m_axis_tuser_reg = {USER_WIDTH{1'b0}}; + +// datapath control +reg store_axis_int_to_output; +reg store_axis_int_to_temp; +reg store_axis_temp_to_output; + +assign m_axis_tdata = m_axis_tdata_reg; +assign m_axis_tkeep = KEEP_ENABLE ? m_axis_tkeep_reg : {KEEP_WIDTH{1'b1}}; +assign m_axis_tvalid = m_axis_tvalid_reg; +assign m_axis_tlast = m_axis_tlast_reg; +assign m_axis_tid = ID_ENABLE ? m_axis_tid_reg : {ID_WIDTH{1'b0}}; +assign m_axis_tdest = DEST_ENABLE ? m_axis_tdest_reg : {DEST_WIDTH{1'b0}}; +assign m_axis_tuser = USER_ENABLE ? m_axis_tuser_reg : {USER_WIDTH{1'b0}}; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +assign m_axis_tready_int_early = m_axis_tready || (!temp_m_axis_tvalid_reg && (!m_axis_tvalid_reg || !m_axis_tvalid_int)); + +always @* begin + // transfer sink ready state to source + m_axis_tvalid_next = m_axis_tvalid_reg; + temp_m_axis_tvalid_next = temp_m_axis_tvalid_reg; + + store_axis_int_to_output = 1'b0; + store_axis_int_to_temp = 1'b0; + store_axis_temp_to_output = 1'b0; + + if (m_axis_tready_int_reg) begin + // input is ready + if (m_axis_tready || !m_axis_tvalid_reg) begin + // output is ready or currently not valid, transfer data to output + m_axis_tvalid_next = m_axis_tvalid_int; + store_axis_int_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_m_axis_tvalid_next = m_axis_tvalid_int; + store_axis_int_to_temp = 1'b1; + end + end else if (m_axis_tready) begin + // input is not ready, but output is ready + m_axis_tvalid_next = temp_m_axis_tvalid_reg; + temp_m_axis_tvalid_next = 1'b0; + store_axis_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + m_axis_tvalid_reg <= m_axis_tvalid_next; + m_axis_tready_int_reg <= m_axis_tready_int_early; + temp_m_axis_tvalid_reg <= temp_m_axis_tvalid_next; + + // datapath + if (store_axis_int_to_output) begin + m_axis_tdata_reg <= m_axis_tdata_int; + m_axis_tkeep_reg <= m_axis_tkeep_int; + m_axis_tlast_reg <= m_axis_tlast_int; + m_axis_tid_reg <= m_axis_tid_int; + m_axis_tdest_reg <= m_axis_tdest_int; + m_axis_tuser_reg <= m_axis_tuser_int; + end else if (store_axis_temp_to_output) begin + m_axis_tdata_reg <= temp_m_axis_tdata_reg; + m_axis_tkeep_reg <= temp_m_axis_tkeep_reg; + m_axis_tlast_reg <= temp_m_axis_tlast_reg; + m_axis_tid_reg <= temp_m_axis_tid_reg; + m_axis_tdest_reg <= temp_m_axis_tdest_reg; + m_axis_tuser_reg <= temp_m_axis_tuser_reg; + end + + if (store_axis_int_to_temp) begin + temp_m_axis_tdata_reg <= m_axis_tdata_int; + temp_m_axis_tkeep_reg <= m_axis_tkeep_int; + temp_m_axis_tlast_reg <= m_axis_tlast_int; + temp_m_axis_tid_reg <= m_axis_tid_int; + temp_m_axis_tdest_reg <= m_axis_tdest_int; + temp_m_axis_tuser_reg <= m_axis_tuser_int; + end + + if (rst) begin + m_axis_tvalid_reg <= 1'b0; + m_axis_tready_int_reg <= 1'b0; + temp_m_axis_tvalid_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/tb/mac_ctrl_rx/Makefile b/tb/mac_ctrl_rx/Makefile new file mode 100644 index 000000000..da1157add --- /dev/null +++ b/tb/mac_ctrl_rx/Makefile @@ -0,0 +1,78 @@ +# Copyright (c) 2023 Alex Forencich +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +TOPLEVEL_LANG = verilog + +SIM ?= icarus +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = mac_ctrl_rx +TOPLEVEL = $(DUT) +MODULE = test_$(DUT) +VERILOG_SOURCES += ../../rtl/$(DUT).v + +# module parameters +export PARAM_DATA_WIDTH := 8 +export PARAM_KEEP_ENABLE := $(shell expr $(PARAM_DATA_WIDTH) \> 8 ) +export PARAM_KEEP_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 ) +export PARAM_ID_ENABLE := 1 +export PARAM_ID_WIDTH := 8 +export PARAM_DEST_ENABLE := 1 +export PARAM_DEST_WIDTH := 8 +export PARAM_USER_ENABLE := 1 +export PARAM_USER_WIDTH := 1 +export PARAM_USE_READY := 1 +export PARAM_MCF_PARAMS_SIZE := 18 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + VERILOG_SOURCES += iverilog_dump.v + COMPILE_ARGS += -s iverilog_dump + endif +else ifeq ($(SIM), verilator) + COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim + +iverilog_dump.v: + echo 'module iverilog_dump();' > $@ + echo 'initial begin' >> $@ + echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@ + echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@ + echo 'end' >> $@ + echo 'endmodule' >> $@ + +clean:: + @rm -rf iverilog_dump.v + @rm -rf dump.fst $(TOPLEVEL).fst diff --git a/tb/mac_ctrl_rx/test_mac_ctrl_rx.py b/tb/mac_ctrl_rx/test_mac_ctrl_rx.py new file mode 100644 index 000000000..241c897b5 --- /dev/null +++ b/tb/mac_ctrl_rx/test_mac_ctrl_rx.py @@ -0,0 +1,594 @@ +#!/usr/bin/env python +""" + +Copyright (c) 2023 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import itertools +import logging +import os +import random + +from scapy.layers.l2 import Ether + +import pytest +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamFrame +from cocotbext.axi.stream import define_stream + + +McfBus, McfTransaction, McfSource, McfSink, McfMonitor = define_stream("Mcf", + signals=["valid", "eth_dst", "eth_src", "eth_type", "opcode", "params"], + optional_signals=["ready", "id", "dest", "user"] +) + + +class TB: + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.clk, 8, units="ns").start()) + + self.source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "s_axis"), dut.clk, dut.rst) + self.sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "m_axis"), dut.clk, dut.rst) + self.mcf_sink = McfSink(McfBus.from_prefix(dut, "mcf"), dut.clk, dut.rst) + + dut.cfg_mcf_rx_eth_dst_mcast.setimmediatevalue(0) + dut.cfg_mcf_rx_check_eth_dst_mcast.setimmediatevalue(0) + dut.cfg_mcf_rx_eth_dst_ucast.setimmediatevalue(0) + dut.cfg_mcf_rx_check_eth_dst_ucast.setimmediatevalue(0) + dut.cfg_mcf_rx_eth_src.setimmediatevalue(0) + dut.cfg_mcf_rx_check_eth_src.setimmediatevalue(0) + dut.cfg_mcf_rx_eth_type.setimmediatevalue(0) + dut.cfg_mcf_rx_opcode_lfc.setimmediatevalue(0) + dut.cfg_mcf_rx_check_opcode_lfc.setimmediatevalue(0) + dut.cfg_mcf_rx_opcode_pfc.setimmediatevalue(0) + dut.cfg_mcf_rx_check_opcode_pfc.setimmediatevalue(0) + + dut.cfg_mcf_rx_forward.setimmediatevalue(0) + dut.cfg_mcf_rx_enable.setimmediatevalue(0) + + def set_idle_generator(self, generator=None): + if generator: + self.source.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + self.sink.set_pause_generator(generator()) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + async def send(self, pkt): + await self.source.send(bytes(pkt)) + + async def recv(self): + rx_frame = await self.sink.recv() + + assert not rx_frame.tuser + + return Ether(bytes(rx_frame)) + + async def recv_mcf(self): + rx_frame = await self.mcf_sink.recv() + + data = bytearray() + data.extend(rx_frame.eth_dst.integer.to_bytes(6, 'big')) + data.extend(rx_frame.eth_src.integer.to_bytes(6, 'big')) + data.extend(rx_frame.eth_type.integer.to_bytes(2, 'big')) + data.extend(rx_frame.opcode.integer.to_bytes(2, 'big')) + data.extend(rx_frame.params.integer.to_bytes(44, 'little')) + + return Ether(data) + + +async def run_test_data(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_frames = [] + + for test_data in [payload_data(x) for x in payload_lengths()]: + test_frame = AxiStreamFrame(test_data) + test_frame.tid = cur_id + test_frame.tdest = cur_id | (0 << src_shift) + + test_frames.append(test_frame) + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for test_frame in test_frames: + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_mcf(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.cfg_mcf_rx_eth_dst_mcast.value = 0x0180C2000001 + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + dut.cfg_mcf_rx_eth_dst_ucast.value = 0xDAD1D2D3D4D5 + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + dut.cfg_mcf_rx_eth_src.value = 0x5A5152535455 + dut.cfg_mcf_rx_check_eth_src.value = 0 + dut.cfg_mcf_rx_eth_type.value = 0x8808 + dut.cfg_mcf_rx_opcode_lfc.value = 0x0001 + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_opcode_pfc.value = 0x0101 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + dut.cfg_mcf_rx_forward.value = 0 + dut.cfg_mcf_rx_enable.value = 1 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_pkts = [] + + for payload in [payload_data(x) for x in payload_lengths()]: + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + + test_frame = AxiStreamFrame(bytes(test_pkt)) + test_frame.tid = cur_id + test_frame.tdest = cur_id | (1 << src_shift) + + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for cur_id, test_pkt in test_pkts: + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == bytes(test_pkt) + assert rx_frame.tid == cur_id + assert rx_frame.tdest == cur_id | (1 << src_shift) + assert rx_frame.tuser + + rx_pkt = await tb.recv_mcf() + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + # check prefix as padding may be different + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_tuser_assert(dut): + + tb = TB(dut) + + byte_lanes = tb.source.byte_lanes + + await tb.reset() + + dut.cfg_mcf_rx_eth_dst_mcast.value = 0x0180C2000001 + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + dut.cfg_mcf_rx_eth_dst_ucast.value = 0xDAD1D2D3D4D5 + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + dut.cfg_mcf_rx_eth_src.value = 0x5A5152535455 + dut.cfg_mcf_rx_check_eth_src.value = 0 + dut.cfg_mcf_rx_eth_type.value = 0x8808 + dut.cfg_mcf_rx_opcode_lfc.value = 0x0001 + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_opcode_pfc.value = 0x0101 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + dut.cfg_mcf_rx_forward.value = 0 + dut.cfg_mcf_rx_enable.value = 1 + + # data + payload = bytearray(itertools.islice(itertools.cycle(range(256)), byte_lanes*16)) + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8000) + test_pkt = eth / payload + test_frame = AxiStreamFrame(bytes(test_pkt), tuser=1) + await tb.source.send(test_frame) + + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tuser + + # MAC control + payload = bytearray(itertools.islice(itertools.cycle(range(256)), 18)) + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + test_frame = AxiStreamFrame(bytes(test_pkt), tuser=1) + await tb.source.send(test_frame) + + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tuser + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_mcf_filter(dut): + + tb = TB(dut) + + await tb.reset() + + dut.cfg_mcf_rx_eth_dst_mcast.value = 0x0180C2000001 + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + dut.cfg_mcf_rx_eth_dst_ucast.value = 0xDAD1D2D3D4D5 + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + dut.cfg_mcf_rx_eth_src.value = 0x5A5152535455 + dut.cfg_mcf_rx_check_eth_src.value = 0 + dut.cfg_mcf_rx_eth_type.value = 0x8808 + dut.cfg_mcf_rx_opcode_lfc.value = 0x0001 + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_opcode_pfc.value = 0x0101 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + dut.cfg_mcf_rx_forward.value = 0 + dut.cfg_mcf_rx_enable.value = 1 + + async def check(tb, pkt, should_match): + await tb.source.send(bytes(pkt)) + + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == bytes(pkt) + + if should_match: + assert rx_frame.tuser + + rx_pkt = await tb.recv_mcf() + + assert bytes(rx_pkt).find(bytes(pkt)) == 0 + else: + assert not rx_frame.tuser + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + payload = bytearray(itertools.islice(itertools.cycle(range(256)), 18)) + + # Multicast destination address + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 1 + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + + # Unicast destination address + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 1 + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + + # Source address + dut.cfg_mcf_rx_check_eth_src.value = 1 + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:AA:AA:AA', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + dut.cfg_mcf_rx_check_eth_src.value = 0 + + # Ethertype + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8880) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + # Opcode + dut.cfg_mcf_rx_check_opcode_lfc.value = 1 + dut.cfg_mcf_rx_check_opcode_pfc.value = 1 + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x01' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x01\x01' + payload) + await check(tb, test_pkt, True) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (b'\x00\x00' + payload) + await check(tb, test_pkt, False) + + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = tb.source.byte_lanes + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.cfg_mcf_rx_eth_dst_mcast.value = 0x0180C2000001 + dut.cfg_mcf_rx_check_eth_dst_mcast.value = 0 + dut.cfg_mcf_rx_eth_dst_ucast.value = 0xDAD1D2D3D4D5 + dut.cfg_mcf_rx_check_eth_dst_ucast.value = 0 + dut.cfg_mcf_rx_eth_src.value = 0x5A5152535455 + dut.cfg_mcf_rx_check_eth_src.value = 0 + dut.cfg_mcf_rx_eth_type.value = 0x8808 + dut.cfg_mcf_rx_opcode_lfc.value = 0x0001 + dut.cfg_mcf_rx_check_opcode_lfc.value = 0 + dut.cfg_mcf_rx_opcode_pfc.value = 0x0101 + dut.cfg_mcf_rx_check_opcode_pfc.value = 0 + + dut.cfg_mcf_rx_forward.value = 0 + dut.cfg_mcf_rx_enable.value = 1 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_pkts = [] + + for k in range(256): + if random.randrange(8) != 0: + length = random.randint(1, byte_lanes*16) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8000) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + dest = cur_id | (0 << src_shift) + else: + length = random.randint(1, 18) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + dest = cur_id | (1 << src_shift) + + test_frame = AxiStreamFrame(bytes(test_pkt)) + test_frame.tid = cur_id + test_frame.tdest = dest + + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for cur_id, test_pkt in test_pkts: + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == bytes(test_pkt) + assert rx_frame.tid == cur_id + assert (rx_frame.tdest & count_mask) == cur_id + + if rx_frame.tdest >> src_shift: + assert rx_frame.tuser + + rx_pkt = await tb.recv_mcf() + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + # check prefix as padding may be different + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + else: + assert not rx_frame.tuser + + for k in range(1000): + await RisingEdge(dut.clk) + + assert tb.sink.empty() + assert tb.mcf_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +def size_list(): + return list(range(1, 128)) + [512, 1514, 9214] + [60]*10 + + +def mcf_size_list(): + return list(range(1, 19)) + + +def incrementing_payload(length): + return bytes(itertools.islice(itertools.cycle(range(256)), length)) + + +if cocotb.SIM_NAME: + + factory = TestFactory(run_test_data) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_test_mcf) + factory.add_option("payload_lengths", [mcf_size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_test_tuser_assert) + factory.generate_tests() + + factory = TestFactory(run_test_mcf_filter) + factory.generate_tests() + + factory = TestFactory(run_stress_test) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.abspath(os.path.dirname(__file__)) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) +lib_dir = os.path.abspath(os.path.join(rtl_dir, '..', 'lib')) +axis_rtl_dir = os.path.abspath(os.path.join(lib_dir, 'axis', 'rtl')) + + +@pytest.mark.parametrize("data_width", [8, 16, 32, 64, 128, 256, 512]) +def test_mac_ctrl_rx(request, data_width): + dut = "mac_ctrl_rx" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(rtl_dir, f"{dut}.v"), + ] + + parameters = {} + + parameters['DATA_WIDTH'] = data_width + parameters['KEEP_ENABLE'] = int(parameters['DATA_WIDTH'] > 8) + parameters['KEEP_WIDTH'] = parameters['DATA_WIDTH'] // 8 + parameters['ID_ENABLE'] = 1 + parameters['ID_WIDTH'] = 8 + parameters['DEST_ENABLE'] = 1 + parameters['DEST_WIDTH'] = 8 + parameters['USER_WIDTH'] = 1 + parameters['USE_READY'] = 1 + parameters['MCF_PARAMS_SIZE'] = 18 + + extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()} + + sim_build = os.path.join(tests_dir, "sim_build", + request.node.name.replace('[', '-').replace(']', '')) + + cocotb_test.simulator.run( + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + ) diff --git a/tb/mac_ctrl_tx/Makefile b/tb/mac_ctrl_tx/Makefile new file mode 100644 index 000000000..c97e953aa --- /dev/null +++ b/tb/mac_ctrl_tx/Makefile @@ -0,0 +1,77 @@ +# Copyright (c) 2023 Alex Forencich +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +TOPLEVEL_LANG = verilog + +SIM ?= icarus +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = mac_ctrl_tx +TOPLEVEL = $(DUT) +MODULE = test_$(DUT) +VERILOG_SOURCES += ../../rtl/$(DUT).v + +# module parameters +export PARAM_DATA_WIDTH := 8 +export PARAM_KEEP_ENABLE := $(shell expr $(PARAM_DATA_WIDTH) \> 8 ) +export PARAM_KEEP_WIDTH := $(shell expr $(PARAM_DATA_WIDTH) / 8 ) +export PARAM_ID_ENABLE := 1 +export PARAM_ID_WIDTH := 8 +export PARAM_DEST_ENABLE := 1 +export PARAM_DEST_WIDTH := 8 +export PARAM_USER_ENABLE := 1 +export PARAM_USER_WIDTH := 1 +export PARAM_MCF_PARAMS_SIZE := 18 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + VERILOG_SOURCES += iverilog_dump.v + COMPILE_ARGS += -s iverilog_dump + endif +else ifeq ($(SIM), verilator) + COMPILE_ARGS += -Wno-SELRANGE -Wno-WIDTH + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim + +iverilog_dump.v: + echo 'module iverilog_dump();' > $@ + echo 'initial begin' >> $@ + echo ' $$dumpfile("$(TOPLEVEL).fst");' >> $@ + echo ' $$dumpvars(0, $(TOPLEVEL));' >> $@ + echo 'end' >> $@ + echo 'endmodule' >> $@ + +clean:: + @rm -rf iverilog_dump.v + @rm -rf dump.fst $(TOPLEVEL).fst diff --git a/tb/mac_ctrl_tx/test_mac_ctrl_tx.py b/tb/mac_ctrl_tx/test_mac_ctrl_tx.py new file mode 100644 index 000000000..243fc242f --- /dev/null +++ b/tb/mac_ctrl_tx/test_mac_ctrl_tx.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python +""" + +Copyright (c) 2023 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import itertools +import logging +import os +import random + +from scapy.layers.l2 import Ether +from scapy.utils import mac2str + +import pytest +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Event +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink, AxiStreamFrame +from cocotbext.axi.stream import define_stream + + +McfBus, McfTransaction, McfSource, McfSink, McfMonitor = define_stream("Mcf", + signals=["valid", "eth_dst", "eth_src", "eth_type", "opcode", "params"], + optional_signals=["ready", "id", "dest", "user"] +) + + +class TB: + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.clk, 8, units="ns").start()) + + self.source = AxiStreamSource(AxiStreamBus.from_prefix(dut, "s_axis"), dut.clk, dut.rst) + self.sink = AxiStreamSink(AxiStreamBus.from_prefix(dut, "m_axis"), dut.clk, dut.rst) + self.mcf_source = McfSource(McfBus.from_prefix(dut, "mcf"), dut.clk, dut.rst) + + dut.tx_pause_req.setimmediatevalue(0) + + def set_idle_generator(self, generator=None): + if generator: + self.source.set_pause_generator(generator()) + self.mcf_source.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + self.sink.set_pause_generator(generator()) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + async def send(self, pkt): + await self.source.send(bytes(pkt)) + + async def send_mcf(self, pkt): + mcf = McfTransaction() + mcf.eth_dst = int.from_bytes(mac2str(pkt[Ether].dst), 'big') + mcf.eth_src = int.from_bytes(mac2str(pkt[Ether].src), 'big') + mcf.eth_type = pkt[Ether].type + mcf.opcode = int.from_bytes(bytes(pkt[Ether].payload)[0:2], 'big') + mcf.params = int.from_bytes(bytes(pkt[Ether].payload)[2:], 'little') + + await self.mcf_source.send(mcf) + + async def recv(self): + rx_frame = await self.sink.recv() + + assert not rx_frame.tuser + + return Ether(bytes(rx_frame)) + + +async def run_test_data(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.tx_pause_req.value = 0 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_frames = [] + + for test_data in [payload_data(x) for x in payload_lengths()]: + test_frame = AxiStreamFrame(test_data) + test_frame.tid = cur_id | (0 << src_shift) + test_frame.tdest = cur_id + + test_frames.append(test_frame) + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for test_frame in test_frames: + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tid == test_frame.tid + assert rx_frame.tdest == test_frame.tdest + assert not rx_frame.tuser + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_mcf(dut, payload_lengths=None, payload_data=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + await tb.reset() + + dut.tx_pause_req.value = 0 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_pkts = [] + + opcode = 1 + + for payload in [payload_data(x) for x in payload_lengths()]: + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (opcode.to_bytes(2, 'big') + payload) + test_pkts.append(test_pkt.copy()) + + await tb.send_mcf(test_pkt) + + opcode += 1 + + for test_pkt in test_pkts: + rx_pkt = await tb.recv() + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + # check prefix as frame gets zero-padded + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_tuser_assert(dut): + + tb = TB(dut) + + await tb.reset() + + dut.tx_pause_req.value = 0 + + test_data = bytearray(itertools.islice(itertools.cycle(range(256)), 32)) + test_frame = AxiStreamFrame(test_data, tuser=1) + await tb.source.send(test_frame) + + rx_frame = await tb.sink.recv() + + assert rx_frame.tdata == test_frame.tdata + assert rx_frame.tuser + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_arb_test(dut): + + tb = TB(dut) + + byte_lanes = tb.source.byte_lanes + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.tx_pause_req.value = 0 + + test_pkts = [] + test_frames = [] + + for k in range(4): + length = byte_lanes*16 + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8000) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + + test_frame = AxiStreamFrame(bytes(test_pkt), tx_complete=Event()) + test_frame.tid = cur_id | (0 << src_shift) + test_frame.tdest = cur_id + test_frames.append(test_frame) + + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + length = random.randint(1, 18) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts.append((cur_id, test_pkt.copy())) + + # start transmit in the middle of frame 2 + await test_frames[1].tx_complete.wait() + for j in range(8): + await RisingEdge(dut.clk) + await tb.send_mcf(test_pkt) + await FallingEdge(dut.mcf_valid) + + cur_id = (cur_id + 1) % max_count + + for k in [0, 1, 2, 4, 3]: + rx_frame = await tb.sink.recv() + + rx_pkt = Ether(bytes(rx_frame)) + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + cur_id, test_pkt = test_pkts[k] + + if rx_pkt.type == 0x8808: + # check prefix as frame gets zero-padded + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + assert rx_frame.tid == 0 + assert rx_frame.tdest == 0 + else: + assert bytes(rx_pkt) == bytes(test_pkt) + assert rx_frame.tid == cur_id | (0 << src_shift) + assert rx_frame.tdest == cur_id + + assert not rx_frame.tuser + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_stress_test(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = tb.source.byte_lanes + id_width = len(tb.source.bus.tid) + id_count = 2**id_width + id_mask = id_count-1 + + src_width = 1 + src_mask = 2**src_width-1 if src_width else 0 + src_shift = id_width-src_width + max_count = 2**src_shift + count_mask = max_count-1 + + cur_id = 1 + + await tb.reset() + + dut.tx_pause_req.value = 0 + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + test_pkts = [list() for x in range(2)] + + for k in range(256): + length = random.randint(1, byte_lanes*16) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='DA:D1:D2:D3:D4:D5', type=0x8000) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts[0].append((cur_id, test_pkt.copy())) + + test_frame = AxiStreamFrame(bytes(test_pkt)) + test_frame.tid = cur_id | (0 << src_shift) + test_frame.tdest = cur_id + + await tb.source.send(test_frame) + + cur_id = (cur_id + 1) % max_count + + for k in range(16): + length = random.randint(1, 18) + payload = bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + eth = Ether(src='5A:51:52:53:54:55', dst='01:80:C2:00:00:01', type=0x8808) + test_pkt = eth / (cur_id.to_bytes(2, 'big') + payload) + test_pkts[1].append((cur_id, test_pkt.copy())) + + for c in range(random.randint(8, 64)): + await RisingEdge(dut.clk) + await tb.send_mcf(test_pkt) + await FallingEdge(dut.mcf_valid) + + cur_id = (cur_id + 1) % max_count + + while any(test_pkts): + rx_frame = await tb.sink.recv() + + rx_pkt = Ether(bytes(rx_frame)) + + tb.log.info("RX packet: %s", repr(rx_pkt)) + + test_pkt = None + + if rx_pkt.type == 0x8808: + cur_id, test_pkt = test_pkts[1].pop(0) + # check prefix as frame gets zero-padded + assert bytes(rx_pkt).find(bytes(test_pkt)) == 0 + assert rx_frame.tid == 0 + assert rx_frame.tdest == 0 + else: + cur_id, test_pkt = test_pkts[0].pop(0) + assert bytes(rx_pkt) == bytes(test_pkt) + assert rx_frame.tid == cur_id | (0 << src_shift) + assert rx_frame.tdest == cur_id + + assert not rx_frame.tuser + + assert tb.sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +def size_list(): + return list(range(1, 128)) + [512, 1514, 9214] + [60]*10 + + +def mcf_size_list(): + return list(range(1, 19)) + + +def incrementing_payload(length): + return bytes(itertools.islice(itertools.cycle(range(256)), length)) + + +if cocotb.SIM_NAME: + + factory = TestFactory(run_test_data) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_test_mcf) + factory.add_option("payload_lengths", [mcf_size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + factory = TestFactory(run_test_tuser_assert) + factory.generate_tests() + + factory = TestFactory(run_arb_test) + factory.generate_tests() + + factory = TestFactory(run_stress_test) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.abspath(os.path.dirname(__file__)) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) +lib_dir = os.path.abspath(os.path.join(rtl_dir, '..', 'lib')) +axis_rtl_dir = os.path.abspath(os.path.join(lib_dir, 'axis', 'rtl')) + + +@pytest.mark.parametrize("data_width", [8, 16, 32, 64, 128, 256, 512]) +def test_mac_ctrl_tx(request, data_width): + dut = "mac_ctrl_tx" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + + verilog_sources = [ + os.path.join(rtl_dir, f"{dut}.v"), + ] + + parameters = {} + + parameters['DATA_WIDTH'] = data_width + parameters['KEEP_ENABLE'] = int(parameters['DATA_WIDTH'] > 8) + parameters['KEEP_WIDTH'] = parameters['DATA_WIDTH'] // 8 + parameters['ID_ENABLE'] = 1 + parameters['ID_WIDTH'] = 8 + parameters['DEST_ENABLE'] = 1 + parameters['DEST_WIDTH'] = 8 + parameters['USER_WIDTH'] = 1 + parameters['MCF_PARAMS_SIZE'] = 18 + + extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()} + + sim_build = os.path.join(tests_dir, "sim_build", + request.node.name.replace('[', '-').replace(']', '')) + + cocotb_test.simulator.run( + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + )