//
// 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<major>, 8'd<minor>}
//   - 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 (domain: eth_rx.clk)
  input  logic [15:0] device_id,
  // Device addresses (domain: eth_rx.clk)
  input  logic [47:0] my_mac,
  input  logic [31:0] my_ip,
  input  logic [15:0] my_udp_chdr_port,

  // Ethernet (domain: eth_rx.clk)
  AxiStreamIf.slave  eth_rx, // tUser={*not used*}
  AxiStreamIf.master eth_tx, // tUser={1'b0,trailing bytes}

  // CHDR (domain: eth_rx.clk)
  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
