// // Copyright 2020 Ettus Research, A National Instruments Brand // // SPDX-License-Identifier: LGPL-3.0-or-later // // Module: chdr_xport_adapter (Ethernet IPV4) // // Description: An Xport transport adapter module that does the following: // - Exposes a configuration port for mgmt packets to configure the node. // (chdr_mgmt_pkt_handler) // - Implements a return-address map for packets with metadata other than // the CHDR. Additional metadata can be passed as a tuser to this module // which will store it in a map indexed by the SrcEPID in a management // packet. For all returning packets, the metadata will be looked up in // the map and attached as the outgoing tuser. (kv_map) // - Implements a loopback path for node-info discovery (axi_switch/axi_mux) // - Strip UDP headers and extract mac/ip/udp src addresses // - Add UDP header for outgoing ethernet traffic // // Parameters: // - PROTOVER: RFNoC protocol version {8'd, 8'd} // - TBL_SIZE: Log2 of the depth of the routing table // - NODE_SUBTYPE: The node subtype to return for a node-info discovery // - NODE_INST: The node type to return for a node-info discovery // - ALLOW_DISC: Controls if the external transport network should be // discoverable by management packets from RFNoC side. // // Signals: // - device_id : The ID of the device that has instantiated this module // - my_* : MAC address, IP address, and UDP port that responds/accepts CHDR traffic // - eth_rx : The input CHDR stream from the transport // - eth_tx : The output CHDR stream to transport // - v2e : The input CHDR stream from the rfnoc infrastructure // - e2v : The output CHDR stream to the rfnoc infrastructure // `include "../xport/rfnoc_xport_types.vh" module chdr_xport_adapter #( int PREAMBLE_BYTES = 6, int MAX_PACKET_BYTES = 2**16, logic [15:0] PROTOVER = {8'd1, 8'd0}, int TBL_SIZE = 6, logic [7:0] NODE_SUBTYPE = NODE_SUBTYPE_XPORT_IPV4_CHDR64, int NODE_INST = 0, bit ALLOW_DISC = 1 )( // Device info input logic [15:0] device_id, // Device addresses input logic [47:0] my_mac, input logic [31:0] my_ip, input logic [15:0] my_udp_chdr_port, // Ethernet AxiStreamIf.slave eth_rx, // tUser={*not used*} AxiStreamIf.master eth_tx, // tUser={1'b0,trailing bytes} // CHDR AxiStreamIf.slave v2e, // tUser={*not used*} AxiStreamIf.master e2v // tUser={*not used*} ); //used to store {udp, ipv4, mac} localparam USER_META_W = 96; localparam ENET_USER_W = $clog2(eth_rx.DATA_WIDTH/8)+1; // --------------------------------------------------- // RFNoC Includes // --------------------------------------------------- `include "../core/rfnoc_chdr_utils.vh" `include "../core/rfnoc_chdr_internal_utils.vh" `include "eth_constants.vh" `include "../../axi4s_sv/axi4s.vh" // tUser={None} AxiStreamPacketIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.TKEEP(0),.TUSER(0), .MAX_PACKET_BYTES(MAX_PACKET_BYTES)) ru1(eth_rx.clk,eth_rx.rst);// Packet handler input // tUser={None} AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.TKEEP(0),.TUSER(0), .MAX_PACKET_BYTES(MAX_PACKET_BYTES)) ru2(eth_rx.clk,eth_rx.rst);// Packet handler input // tUser={udp_src_port,ipv4_src_addr,eth_src_addr} AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.USER_WIDTH(USER_META_W),.TKEEP(0)) ru3(eth_rx.clk,eth_rx.rst);// Packet handler input AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.USER_WIDTH(USER_META_W),.TKEEP(0)) ru4(eth_rx.clk,eth_rx.rst);// Packet handler input AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.USER_WIDTH(USER_META_W),.TKEEP(0)) ph(eth_rx.clk,eth_rx.rst);// Packet handler input // tUser={udp_src_port,ipv4_src_addr,eth_src_addr} AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.USER_WIDTH(USER_META_W),.TKEEP(0)) e2d(eth_rx.clk,eth_rx.rst);// Eth => Demux logic [1:0] e2d_tid; // tUser={udp_src_port,ipv4_src_addr,eth_src_addr} AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.USER_WIDTH(USER_META_W),.TKEEP(0)) e2e(eth_rx.clk,eth_rx.rst);// Eth => Eth (loopback) // tUser={udp_dst_port, ipv4_dst_addr, eth_dst_addr} AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.USER_WIDTH(USER_META_W),.TKEEP(0)) m2e(eth_rx.clk,eth_rx.rst);// Mux => Eth logic m2e_tdest; // 1: Return to src, 0: CHDR input // --------------------------------------------------- // Strip UDP and grab {udp_src_port_old, ipv4_src_addr_old, eth_src_addr_old} // --------------------------------------------------- always_comb begin : assign_ru1 `AXI4S_ASSIGN(ru1,eth_rx) end // Cached fields logic [47:0] eth_src_addr_new, eth_src_addr_old; logic [31:0] ipv4_src_addr_new, ipv4_src_addr_old; logic [15:0] udp_src_port_new, udp_src_port_old; // save the fields always_ff @(posedge eth_rx.clk) begin : field_ff if (eth_rx.rst) begin eth_src_addr_old <= '0; ipv4_src_addr_old <= '0; udp_src_port_old <= '0; end else begin eth_src_addr_old <= eth_src_addr_new; ipv4_src_addr_old <= ipv4_src_addr_new; udp_src_port_old <= udp_src_port_new; end end // get the fields - don't use assign. assign will not activate with changes to eth_rx. always_comb begin : get_fields eth_src_addr_new = ru1.get_packet_field48(eth_src_addr_old,SRC_MAC_BYTE,.NETWORK_ORDER(1)); ipv4_src_addr_new = ru1.get_packet_field32(ipv4_src_addr_old,SRC_IP_BYTE,.NETWORK_ORDER(1)); udp_src_port_new = ru1.get_packet_field16(udp_src_port_old,SRC_PORT_BYTE,.NETWORK_ORDER(1)); end // Strip the udp header axi4s_remove_bytes #(.REM_START(0),.REM_END(UDP_END) ) strip_udp ( .i(ru1), .o(ru2) ); // start driving the port information always_comb begin : assign_ru3 `AXI4S_ASSIGN(ru3,ru2) ru3.tuser = {udp_src_port_old, ipv4_src_addr_old, eth_src_addr_old}; end chdr_trim_payload #( .CHDR_W(eth_rx.DATA_WIDTH), .USER_W(USER_META_W) ) chdr_trim_i ( .clk(eth_rx.clk), .rst(eth_rx.rst), .s_axis_tdata(ru3.tdata), .s_axis_tuser(ru3.tuser), .s_axis_tlast(ru3.tlast), .s_axis_tvalid(ru3.tvalid), .s_axis_tready(ru3.tready), .m_axis_tdata(ru4.tdata), .m_axis_tuser(ru4.tuser), .m_axis_tlast(ru4.tlast), .m_axis_tvalid(ru4.tvalid), .m_axis_tready(ru4.tready) ); // Pay close attention to when ph.tuser swtiches versus when it is needed! always_comb begin : assign_ph `AXI4S_ASSIGN(ph,ru4) end // --------------------------------------------------- // Transport => DEMUX // --------------------------------------------------- logic op_stb; logic [15:0] op_src_epid; logic [USER_META_W-1:0] op_data; logic lookup_stb, lookup_done_stb, lookup_result_match; logic [15:0] lookup_epid; logic [USER_META_W-1:0] lookup_result_value; logic [47:0] node_info; always_comb node_info = chdr_mgmt_build_node_info( { 10'h0, NODE_SUBTYPE}, NODE_INST, NODE_TYPE_TRANSPORT, device_id); chdr_mgmt_pkt_handler #( .PROTOVER(PROTOVER), .CHDR_W(eth_rx.DATA_WIDTH), .USER_W(USER_META_W), .MGMT_ONLY(0) ) mgmt_ep_i ( .clk(eth_rx.clk), .rst(eth_rx.rst), .node_info(node_info), //ph in .s_axis_chdr_tdata(ph.tdata), .s_axis_chdr_tlast(ph.tlast), .s_axis_chdr_tvalid(ph.tvalid), .s_axis_chdr_tready(ph.tready), .s_axis_chdr_tuser(ph.tuser), //e2d out .m_axis_chdr_tdata(e2d.tdata), .m_axis_chdr_tlast(e2d.tlast), .m_axis_chdr_tdest(/* unused */), .m_axis_chdr_tid(e2d_tid), .m_axis_chdr_tvalid(e2d.tvalid), .m_axis_chdr_tready(e2d.tready), //unused ctrlport .ctrlport_req_wr (/* unused */), .ctrlport_req_rd (/* unused */), .ctrlport_req_addr (/* unused */), .ctrlport_req_data (/* unused */), .ctrlport_resp_ack (1'b0 /* unused */), .ctrlport_resp_data (32'b0 /* unused */), // kv_map lookups .op_stb(op_stb), .op_dst_epid(/* unused */), .op_src_epid(op_src_epid), .op_data(op_data) ); // Key/Value map. // Stores the destination address information for UDP // -- storage is controlled from the chdr_managment_node // -- lookup is done on each packet passing out kv_map #( .KEY_WIDTH(16), .VAL_WIDTH(USER_META_W), .SIZE(TBL_SIZE) ) kv_map_i ( .clk(eth_rx.clk), .reset(eth_rx.rst), .insert_stb(op_stb), .insert_key(op_src_epid), .insert_val(op_data), .insert_busy(/* Time between op_stb > Insertion time */), .find_key_stb(lookup_stb), .find_key(lookup_epid), .find_res_stb(lookup_done_stb), .find_res_match(lookup_result_match), .find_res_val(lookup_result_value), .count(/* unused */) ); logic ph_hdr = 1'b1; always_ff @(posedge eth_rx.clk) begin if (eth_rx.rst) ph_hdr <= 1'b1; else if (ph.tvalid && ph.tready) ph_hdr <= ph.tlast; end // chdr_mgmt_pkt_handler does not buffer packets and has at least one cycle of delay. // The tuser caching logic could be more robust. always_ff @(posedge eth_rx.clk) begin if (ph.tvalid && ph.tready && ph_hdr) e2d.tuser <= ph.tuser; end // --------------------------------------------------- // Optional management filter // --------------------------------------------------- // tUser={*not used*} AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.TUSER(0),.TKEEP(0)) f2m(eth_rx.clk,eth_rx.rst); if (ALLOW_DISC) begin : gen_no_mgmt_filter // Allow all packets to pass through always_comb begin f2m.tdata = v2e.tdata; f2m.tlast = v2e.tlast; f2m.tvalid = v2e.tvalid; v2e.tready = f2m.tready; end end else begin : gen_mgmt_filter // Disallow forwarding of management discovery packets from RFNoC to the // transport interface for transports that don't support them. //vhook_nowarn unused_* logic [eth_rx.DATA_WIDTH-1:0] unused_tdata; logic unused_tlast, unused_tvalid; logic [eth_rx.DATA_WIDTH-1:0] s_header; logic dispose_pkt; // We identify discovery packets by the fact that they are management // packets and that they use the null EPID as the destination. always_comb dispose_pkt = (chdr_get_pkt_type(s_header[63:0]) == CHDR_PKT_TYPE_MGMT) && (chdr_get_dst_epid(s_header[63:0]) == NULL_EPID); axi_demux #( .WIDTH (eth_rx.DATA_WIDTH), .SIZE (2), .PRE_FIFO_SIZE (0), .POST_FIFO_SIZE (1) ) axi_demux_mgmt_filter_i ( .clk (eth_rx.clk), .reset (eth_rx.rst), .clear (1'b0), .header (s_header), .dest (dispose_pkt), .i_tdata (v2e.tdata), .i_tlast (v2e.tlast), .i_tvalid (v2e.tvalid), .i_tready (v2e.tready), .o_tdata ({unused_tdata, f2m.tdata}), .o_tlast ({unused_tlast, f2m.tlast}), .o_tvalid ({unused_tvalid, f2m.tvalid}), .o_tready ({1'b1, f2m.tready}) ); end // --------------------------------------------------- // MUX and DEMUX for return path // --------------------------------------------------- logic [USER_META_W-1:0] unused_tuser; axis_switch #( .DATA_W(eth_rx.DATA_WIDTH+USER_META_W), .DEST_W(1), .IN_PORTS(1), .OUT_PORTS(2), .PIPELINE(0) ) rtn_demux_i ( .clk(eth_rx.clk), .reset(eth_rx.rst), .s_axis_tdata({e2d.tuser, e2d.tdata}), .s_axis_alloc(1'b0), .s_axis_tdest(e2d_tid == CHDR_MGMT_RETURN_TO_SRC ? 2'b01 : 2'b00), .s_axis_tlast(e2d.tlast), .s_axis_tvalid(e2d.tvalid), .s_axis_tready(e2d.tready), .m_axis_tdata({e2e.tuser, e2e.tdata, unused_tuser, e2v.tdata}), .m_axis_tdest(/* unused */), .m_axis_tlast({e2e.tlast, e2v.tlast}), .m_axis_tvalid({e2e.tvalid, e2v.tvalid}), .m_axis_tready({e2e.tready, e2v.tready}) ); axi_mux #( .WIDTH(eth_rx.DATA_WIDTH+USER_META_W+1), .SIZE(2), .PRE_FIFO_SIZE(0), .POST_FIFO_SIZE(0) ) rtn_mux_i ( .clk(eth_rx.clk), .reset(eth_rx.rst), .clear(1'b0), .i_tdata({1'b1, e2e.tuser, e2e.tdata, 1'b0, {USER_META_W{1'b0}}, f2m.tdata}), .i_tlast({e2e.tlast, f2m.tlast}), .i_tvalid({e2e.tvalid, f2m.tvalid}), .i_tready({e2e.tready, f2m.tready}), .o_tdata({m2e_tdest, m2e.tuser, m2e.tdata}), .o_tlast(m2e.tlast), .o_tvalid(m2e.tvalid), .o_tready(m2e.tready) ); // --------------------------------------------------- // MUX => Transport // --------------------------------------------------- // In this section we must determine what value to put in tuser. If tdest is // 1 then tuser is passed through unchanged. If tdest is 0 then the tuser // value is looked up in the KV map using the EPID in the packet header. // // To do this we split the data (tdata, tlast) and the routing information // (tdest, tuser, and the EPID) into two FIFOs. This allows us to perform a // routing lookup and decide what to do while we continue to buffer data. // // With small packets, multiple routing lookups might be enqueued in the // lookup_fifo, but we can only do one lookup at a time. Output logic // controls release of packets from the data FIFO to ensure we only output // one packet per lookup after the lookup is complete. AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.TUSER(0),.TKEEP(0)) data_fifo_o(eth_rx.clk,eth_rx.rst); AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.TUSER(0),.TKEEP(0)) data_fifo_i(eth_rx.clk,eth_rx.rst); AxiStreamIf #(.DATA_WIDTH(1+USER_META_W+16),.TUSER(0),.TKEEP(0)) lookup_fifo_o(eth_rx.clk,eth_rx.rst); AxiStreamIf #(.DATA_WIDTH(1+USER_META_W+16),.TUSER(0),.TKEEP(0)) lookup_fifo_i(eth_rx.clk,eth_rx.rst); logic lookup_fifo_tdest; logic [USER_META_W-1:0] lookup_fifo_tuser; logic [ 15:0] lookup_fifo_tepid; logic non_lookup_done_stb; logic data_fifo_o_hdr = 1'b1; logic pass_packet; logic [USER_META_W-1:0] result_tuser; logic result_tuser_valid; logic [USER_META_W-1:0] reg_o_tuser; // Track when the next m2e word contains is the start of a new packet logic m2e_hdr = 1'b1; always_ff @(posedge eth_rx.clk) begin : m2e_hdr_ff if (eth_rx.rst) m2e_hdr <= 1'b1; else if (m2e.tvalid && m2e.tready) m2e_hdr <= m2e.tlast; end // We can only accept data from the mux when when both the data_fifo and // lookup_fifo are ready. always_comb data_fifo_i.tdata = m2e.tdata; always_comb data_fifo_i.tlast = m2e.tlast; always_comb data_fifo_i.tvalid = m2e.tvalid && m2e.tready; always_comb m2e.tready = data_fifo_i.tready && lookup_fifo_i.tready; // The data_fifo only takes the packet data (tdata, tlast). We use an // axi_fifo_short module for the data_fifo because it can tolerate tvalid // going low before a transfer completes. axi_fifo_short #( .WIDTH (1+eth_rx.DATA_WIDTH) ) data_fifo ( .clk (eth_rx.clk), .reset (eth_rx.rst), .clear (1'b0), .i_tdata ({data_fifo_i.tlast, data_fifo_i.tdata}), .i_tvalid (data_fifo_i.tvalid), .i_tready (data_fifo_i.tready), .o_tdata ({data_fifo_o.tlast, data_fifo_o.tdata}), .o_tvalid (data_fifo_o.tvalid), .o_tready (data_fifo_o.tready), .space (), .occupied () ); // The lookup FIFO only takes the header routing info (tdest, tuser, epid). // We use axi_fifo_short since it can tolerate tvalid going low before a // transfer completes. always_comb lookup_fifo_i.tdata = {m2e_tdest, m2e.tuser, chdr_get_dst_epid(m2e.tdata[63:0])}; always_comb {lookup_fifo_tdest, lookup_fifo_tuser, lookup_fifo_tepid} = lookup_fifo_o.tdata; always_comb lookup_fifo_i.tvalid = m2e.tvalid && m2e.tready && m2e_hdr; axi_fifo_short #( .WIDTH (1+USER_META_W+16) ) lookup_fifo ( .clk (eth_rx.clk), .reset (eth_rx.rst), .clear (1'b0), .i_tdata (lookup_fifo_i.tdata), .i_tvalid (lookup_fifo_i.tvalid), .i_tready (lookup_fifo_i.tready), .o_tdata (lookup_fifo_o.tdata), .o_tvalid (lookup_fifo_o.tvalid), .o_tready (lookup_fifo_o.tready), .space (), .occupied () ); // Keep track of when we are busy doing a lookup in the KV map. logic lookup_busy = 1'b0; always_ff @(posedge eth_rx.clk) begin : lookup_busy_ff if (eth_rx.rst) lookup_busy <= 1'b0; else begin if (lookup_stb) lookup_busy <= 1'b1; else if (lookup_done_stb) lookup_busy <= 1'b0; end end // Determine if we can use the output of the lookup_fifo to do a KV map // lookup. We only perform a KV map lookup if tdest is 0 and we can only do // so if the KV map is free and the holding register for the tuser value is // available. always_comb lookup_epid = lookup_fifo_tepid; always_comb lookup_stb = lookup_fifo_o.tvalid && !lookup_busy && !lookup_fifo_tdest && !result_tuser_valid; // Determine if we can use the output of the lookup FIFO directly (no lookup // is needed). We can only use it if we're not already doing a KV lookup and // if the holding register for the tuser value is available. always_comb non_lookup_done_stb = lookup_fifo_o.tvalid && !lookup_busy && lookup_fifo_tdest && !result_tuser_valid; // Pop the routing info off of the lookup_fifo if we've started its lookup always_comb lookup_fifo_o.tready = lookup_stb || non_lookup_done_stb; // Track when the next data_fifo_o word is the start of a new packet always_ff @(posedge eth_rx.clk) begin : data_fifo_o_hdr_ff if (eth_rx.rst) data_fifo_o_hdr <= 1'b1; else if (data_fifo_o.tvalid && data_fifo_o.tready && pass_packet) data_fifo_o_hdr <= data_fifo_o.tlast; end // Store the lookup result in a holding register. This can come from the KV // map or the incoming tuser. always_ff @(posedge eth_rx.clk) begin : result_tuser_ff if (eth_rx.rst) begin result_tuser <= {USER_META_W{1'bX}}; // Don't care result_tuser_valid <= 1'b0; end else begin // The tuser holding register becomes available as soon as we start // transmitting the corresponding packet. if (data_fifo_o.tvalid && data_fifo_o.tready && data_fifo_o_hdr && pass_packet) begin result_tuser_valid <= 1'b0; end // Load the result of the lookup if (lookup_done_stb) begin result_tuser <= lookup_result_match ? lookup_result_value : {USER_META_W{1'b0}}; result_tuser_valid <= 1'b1; end else if (non_lookup_done_stb) begin result_tuser <= lookup_fifo_tuser; result_tuser_valid <= 1'b1; end end end // Control when the packet from the data_fifo can be passed through. Put the // tuser value into a register for the duration of the packet. always_ff @(posedge eth_rx.clk) begin : pass_packet_ff if (eth_rx.rst) begin pass_packet <= 1'b0; reg_o_tuser <= {USER_META_W{1'bX}}; // Don't care end else begin // We're done passing through a packet when tlast goes out if (data_fifo_o.tvalid && data_fifo_o.tready && data_fifo_o.tlast && pass_packet) begin pass_packet <= 1'b0; end // We can pass the next packet through when we're at the start of a // packet and we have the tuser value waiting in the holding register. if (data_fifo_o_hdr && result_tuser_valid && !pass_packet) begin reg_o_tuser <= result_tuser; pass_packet <= 1'b1; end end end // Device addresses logic [15:0] au_udp_dst; logic [31:0] au_ip_dst; logic [47:0] au_mac_dst; AxiStreamIf #(.DATA_WIDTH(eth_rx.DATA_WIDTH),.TKEEP(0),.TUSER(0)) au(eth_rx.clk,eth_rx.rst);// Add UDP input always_comb begin {au_udp_dst,au_ip_dst,au_mac_dst} = reg_o_tuser; au.tdata = data_fifo_o.tdata; au.tlast = data_fifo_o.tlast; au.tvalid = data_fifo_o.tvalid & pass_packet; data_fifo_o.tready = au.tready & pass_packet; end // Clock Crossing to the ethernet clock domain logic [47:0] e_my_mac; logic [31:0] e_my_ip; logic [15:0] e_my_udp_chdr_port; // crossing clock boundaries. // my_mac, my_ip, my_udp_chdr_port must be written // prior to traffic, or an inconsistent version will // exist for a clock period or 2. This would be better // done with a full handshake. synchronizer #(.WIDTH(96),.STAGES(1)) e_info_sync (.clk(eth_rx.clk),.rst(eth_rx.rst), .in({my_mac,my_ip,my_udp_chdr_port}), .out({e_my_mac,e_my_ip,e_my_udp_chdr_port})); // add the UDP header back on before sending to EthTx eth_ipv4_add_udp #( .PREAMBLE_BYTES(PREAMBLE_BYTES), .MAX_PACKET_BYTES(MAX_PACKET_BYTES) ) add_udp_i ( .i(au), .o(eth_tx), .mac_src(e_my_mac), .ip_src(e_my_ip), .udp_src(e_my_udp_chdr_port), .mac_dst(au_mac_dst), .ip_dst(au_ip_dst), .udp_dst(au_udp_dst) ); endmodule : chdr_xport_adapter