Files
b210-k7-fpga/lib/rfnoc/xport_sv/chdr_xport_adapter.sv
T
Wade Fife ab09eb9b09 fpga: lib: Add clock domain comments to interfaces
Original-commit: c64258e09b221d0bfeb55e01085a20e37c5b62ba
2021-06-03 11:26:54 -05:00

544 lines
21 KiB
Systemverilog

//
// 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