Add files via upload
diff --git a/verilog/rtl/arbiter.v b/verilog/rtl/arbiter.v
new file mode 100644
index 0000000..ba98afb
--- /dev/null
+++ b/verilog/rtl/arbiter.v
@@ -0,0 +1,156 @@
+/*
+
+Copyright (c) 2014-2021 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
+
+`timescale 1ns / 1ps
+
+/*
+ * Arbiter module
+ */
+module arbiter #
+(
+    parameter PORTS = 4,
+    // select round robin arbitration
+    parameter ARB_TYPE_ROUND_ROBIN = 0,
+    // blocking arbiter enable
+    parameter ARB_BLOCK = 0,
+    // block on acknowledge assert when nonzero, request deassert when 0
+    parameter ARB_BLOCK_ACK = 1,
+    // LSB priority selection
+    parameter ARB_LSB_HIGH_PRIORITY = 0
+)
+(
+    input  wire                     clk,
+    input  wire                     rst,
+
+    input  wire [PORTS-1:0]         request,
+    input  wire [PORTS-1:0]         acknowledge,
+
+    output wire [PORTS-1:0]         grant,
+    output wire                     grant_valid,
+    output wire [$clog2(PORTS)-1:0] grant_encoded
+);
+
+reg [PORTS-1:0] grant_reg, grant_next;
+reg grant_valid_reg, grant_valid_next;
+reg [$clog2(PORTS)-1:0] grant_encoded_reg, grant_encoded_next;
+
+assign grant_valid = grant_valid_reg;
+assign grant = grant_reg;
+assign grant_encoded = grant_encoded_reg;
+
+wire request_valid;
+wire [$clog2(PORTS)-1:0] request_index;
+wire [PORTS-1:0] request_mask;
+
+priority_encoder #(
+    .WIDTH(PORTS),
+    .LSB_HIGH_PRIORITY(ARB_LSB_HIGH_PRIORITY)
+)
+priority_encoder_inst (
+    .input_unencoded(request),
+    .output_valid(request_valid),
+    .output_encoded(request_index),
+    .output_unencoded(request_mask)
+);
+
+reg [PORTS-1:0] mask_reg, mask_next;
+
+wire masked_request_valid;
+wire [$clog2(PORTS)-1:0] masked_request_index;
+wire [PORTS-1:0] masked_request_mask;
+
+priority_encoder #(
+    .WIDTH(PORTS),
+    .LSB_HIGH_PRIORITY(ARB_LSB_HIGH_PRIORITY)
+)
+priority_encoder_masked (
+    .input_unencoded(request & mask_reg),
+    .output_valid(masked_request_valid),
+    .output_encoded(masked_request_index),
+    .output_unencoded(masked_request_mask)
+);
+
+always @* begin
+    grant_next = 0;
+    grant_valid_next = 0;
+    grant_encoded_next = 0;
+    mask_next = mask_reg;
+
+    if (ARB_BLOCK && !ARB_BLOCK_ACK && grant_reg & request) begin
+        // granted request still asserted; hold it
+        grant_valid_next = grant_valid_reg;
+        grant_next = grant_reg;
+        grant_encoded_next = grant_encoded_reg;
+    end else if (ARB_BLOCK && ARB_BLOCK_ACK && grant_valid && !(grant_reg & acknowledge)) begin
+        // granted request not yet acknowledged; hold it
+        grant_valid_next = grant_valid_reg;
+        grant_next = grant_reg;
+        grant_encoded_next = grant_encoded_reg;
+    end else if (request_valid) begin
+        if (ARB_TYPE_ROUND_ROBIN) begin
+            if (masked_request_valid) begin
+                grant_valid_next = 1;
+                grant_next = masked_request_mask;
+                grant_encoded_next = masked_request_index;
+                if (ARB_LSB_HIGH_PRIORITY) begin
+                    mask_next = {PORTS{1'b1}} << (masked_request_index + 1);
+                end else begin
+                    mask_next = {PORTS{1'b1}} >> (PORTS - masked_request_index);
+                end
+            end else begin
+                grant_valid_next = 1;
+                grant_next = request_mask;
+                grant_encoded_next = request_index;
+                if (ARB_LSB_HIGH_PRIORITY) begin
+                    mask_next = {PORTS{1'b1}} << (request_index + 1);
+                end else begin
+                    mask_next = {PORTS{1'b1}} >> (PORTS - request_index);
+                end
+            end
+        end else begin
+            grant_valid_next = 1;
+            grant_next = request_mask;
+            grant_encoded_next = request_index;
+        end
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        grant_reg <= 0;
+        grant_valid_reg <= 0;
+        grant_encoded_reg <= 0;
+        mask_reg <= 0;
+    end else begin
+        grant_reg <= grant_next;
+        grant_valid_reg <= grant_valid_next;
+        grant_encoded_reg <= grant_encoded_next;
+        mask_reg <= mask_next;
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/arp.v b/verilog/rtl/arp.v
new file mode 100644
index 0000000..2aa1d06
--- /dev/null
+++ b/verilog/rtl/arp.v
@@ -0,0 +1,450 @@
+/*
+
+Copyright (c) 2014-2020 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
+
+`timescale 1ns / 1ps
+
+/*
+ * ARP block for IPv4, ethernet frame interface
+ */
+module arp #
+(
+    // Width of AXI stream interfaces in bits
+    parameter DATA_WIDTH = 8,
+    // Propagate tkeep signal
+    // If disabled, tkeep assumed to be 1'b1
+    parameter KEEP_ENABLE = (DATA_WIDTH>8),
+    // tkeep signal width (words per cycle)
+    parameter KEEP_WIDTH = (DATA_WIDTH/8),
+    // Log2 of ARP cache size
+    parameter CACHE_ADDR_WIDTH = 9,
+    // ARP request retry count
+    parameter REQUEST_RETRY_COUNT = 4,
+    // ARP request retry interval (in cycles)
+    parameter REQUEST_RETRY_INTERVAL = 125000000*2,
+    // ARP request timeout (in cycles)
+    parameter REQUEST_TIMEOUT = 125000000*30
+)
+(
+    input  wire                   clk,
+    input  wire                   rst,
+
+    /*
+     * Ethernet frame input
+     */
+    input  wire                   s_eth_hdr_valid,
+    output wire                   s_eth_hdr_ready,
+    input  wire [47:0]            s_eth_dest_mac,
+    input  wire [47:0]            s_eth_src_mac,
+    input  wire [15:0]            s_eth_type,
+    input  wire [DATA_WIDTH-1:0]  s_eth_payload_axis_tdata,
+    input  wire [KEEP_WIDTH-1:0]  s_eth_payload_axis_tkeep,
+    input  wire                   s_eth_payload_axis_tvalid,
+    output wire                   s_eth_payload_axis_tready,
+    input  wire                   s_eth_payload_axis_tlast,
+    input  wire                   s_eth_payload_axis_tuser,
+
+    /*
+     * Ethernet frame output
+     */
+    output wire                   m_eth_hdr_valid,
+    input  wire                   m_eth_hdr_ready,
+    output wire [47:0]            m_eth_dest_mac,
+    output wire [47:0]            m_eth_src_mac,
+    output wire [15:0]            m_eth_type,
+    output wire [DATA_WIDTH-1:0]  m_eth_payload_axis_tdata,
+    output wire [KEEP_WIDTH-1:0]  m_eth_payload_axis_tkeep,
+    output wire                   m_eth_payload_axis_tvalid,
+    input  wire                   m_eth_payload_axis_tready,
+    output wire                   m_eth_payload_axis_tlast,
+    output wire                   m_eth_payload_axis_tuser,
+
+    /*
+     * ARP requests
+     */
+    input  wire                   arp_request_valid,
+    output wire                   arp_request_ready,
+    input  wire [31:0]            arp_request_ip,
+    output wire                   arp_response_valid,
+    input  wire                   arp_response_ready,
+    output wire                   arp_response_error,
+    output wire [47:0]            arp_response_mac,
+
+    /*
+     * Configuration
+     */
+    input  wire [47:0]            local_mac,
+    input  wire [31:0]            local_ip,
+    input  wire [31:0]            gateway_ip,
+    input  wire [31:0]            subnet_mask,
+    input  wire                   clear_cache
+);
+
+localparam [15:0]
+    ARP_OPER_ARP_REQUEST = 16'h0001,
+    ARP_OPER_ARP_REPLY = 16'h0002,
+    ARP_OPER_INARP_REQUEST = 16'h0008,
+    ARP_OPER_INARP_REPLY = 16'h0009;
+
+wire incoming_frame_valid;
+reg incoming_frame_ready;
+wire [47:0] incoming_eth_dest_mac;
+wire [47:0] incoming_eth_src_mac;
+wire [15:0] incoming_eth_type;
+wire [15:0] incoming_arp_htype;
+wire [15:0] incoming_arp_ptype;
+wire [7:0]  incoming_arp_hlen;
+wire [7:0]  incoming_arp_plen;
+wire [15:0] incoming_arp_oper;
+wire [47:0] incoming_arp_sha;
+wire [31:0] incoming_arp_spa;
+wire [47:0] incoming_arp_tha;
+wire [31:0] incoming_arp_tpa;
+
+/*
+ * ARP frame processing
+ */
+arp_eth_rx #(
+    .DATA_WIDTH(DATA_WIDTH),
+    .KEEP_ENABLE(KEEP_ENABLE),
+    .KEEP_WIDTH(KEEP_WIDTH)
+)
+arp_eth_rx_inst (
+    .clk(clk),
+    .rst(rst),
+    // Ethernet frame input
+    .s_eth_hdr_valid(s_eth_hdr_valid),
+    .s_eth_hdr_ready(s_eth_hdr_ready),
+    .s_eth_dest_mac(s_eth_dest_mac),
+    .s_eth_src_mac(s_eth_src_mac),
+    .s_eth_type(s_eth_type),
+    .s_eth_payload_axis_tdata(s_eth_payload_axis_tdata),
+    .s_eth_payload_axis_tkeep(s_eth_payload_axis_tkeep),
+    .s_eth_payload_axis_tvalid(s_eth_payload_axis_tvalid),
+    .s_eth_payload_axis_tready(s_eth_payload_axis_tready),
+    .s_eth_payload_axis_tlast(s_eth_payload_axis_tlast),
+    .s_eth_payload_axis_tuser(s_eth_payload_axis_tuser),
+    // ARP frame output
+    .m_frame_valid(incoming_frame_valid),
+    .m_frame_ready(incoming_frame_ready),
+    .m_eth_dest_mac(incoming_eth_dest_mac),
+    .m_eth_src_mac(incoming_eth_src_mac),
+    .m_eth_type(incoming_eth_type),
+    .m_arp_htype(incoming_arp_htype),
+    .m_arp_ptype(incoming_arp_ptype),
+    .m_arp_hlen(incoming_arp_hlen),
+    .m_arp_plen(incoming_arp_plen),
+    .m_arp_oper(incoming_arp_oper),
+    .m_arp_sha(incoming_arp_sha),
+    .m_arp_spa(incoming_arp_spa),
+    .m_arp_tha(incoming_arp_tha),
+    .m_arp_tpa(incoming_arp_tpa),
+    // Status signals
+    .busy(),
+    .error_header_early_termination(),
+    .error_invalid_header()
+);
+
+reg outgoing_frame_valid_reg, outgoing_frame_valid_next;
+wire outgoing_frame_ready;
+reg [47:0] outgoing_eth_dest_mac_reg, outgoing_eth_dest_mac_next;
+reg [15:0] outgoing_arp_oper_reg, outgoing_arp_oper_next;
+reg [47:0] outgoing_arp_tha_reg, outgoing_arp_tha_next;
+reg [31:0] outgoing_arp_tpa_reg, outgoing_arp_tpa_next;
+
+arp_eth_tx #(
+    .DATA_WIDTH(DATA_WIDTH),
+    .KEEP_ENABLE(KEEP_ENABLE),
+    .KEEP_WIDTH(KEEP_WIDTH)
+)
+arp_eth_tx_inst (
+    .clk(clk),
+    .rst(rst),
+    // ARP frame input
+    .s_frame_valid(outgoing_frame_valid_reg),
+    .s_frame_ready(outgoing_frame_ready),
+    .s_eth_dest_mac(outgoing_eth_dest_mac_reg),
+    .s_eth_src_mac(local_mac),
+    .s_eth_type(16'h0806),
+    .s_arp_htype(16'h0001),
+    .s_arp_ptype(16'h0800),
+    .s_arp_oper(outgoing_arp_oper_reg),
+    .s_arp_sha(local_mac),
+    .s_arp_spa(local_ip),
+    .s_arp_tha(outgoing_arp_tha_reg),
+    .s_arp_tpa(outgoing_arp_tpa_reg),
+    // Ethernet frame output
+    .m_eth_hdr_valid(m_eth_hdr_valid),
+    .m_eth_hdr_ready(m_eth_hdr_ready),
+    .m_eth_dest_mac(m_eth_dest_mac),
+    .m_eth_src_mac(m_eth_src_mac),
+    .m_eth_type(m_eth_type),
+    .m_eth_payload_axis_tdata(m_eth_payload_axis_tdata),
+    .m_eth_payload_axis_tkeep(m_eth_payload_axis_tkeep),
+    .m_eth_payload_axis_tvalid(m_eth_payload_axis_tvalid),
+    .m_eth_payload_axis_tready(m_eth_payload_axis_tready),
+    .m_eth_payload_axis_tlast(m_eth_payload_axis_tlast),
+    .m_eth_payload_axis_tuser(m_eth_payload_axis_tuser),
+    // Status signals
+    .busy()
+);
+
+reg cache_query_request_valid_reg, cache_query_request_valid_next;
+reg [31:0] cache_query_request_ip_reg, cache_query_request_ip_next;
+wire cache_query_response_valid;
+wire cache_query_response_error;
+wire [47:0] cache_query_response_mac;
+
+reg cache_write_request_valid_reg, cache_write_request_valid_next;
+reg [31:0] cache_write_request_ip_reg, cache_write_request_ip_next;
+reg [47:0] cache_write_request_mac_reg, cache_write_request_mac_next;
+
+/*
+ * ARP cache
+ */
+arp_cache #(
+    .CACHE_ADDR_WIDTH(CACHE_ADDR_WIDTH)
+)
+arp_cache_inst (
+    .clk(clk),
+    .rst(rst),
+    // Query cache
+    .query_request_valid(cache_query_request_valid_reg),
+    .query_request_ready(),
+    .query_request_ip(cache_query_request_ip_reg),
+    .query_response_valid(cache_query_response_valid),
+    .query_response_ready(1'b1),
+    .query_response_error(cache_query_response_error),
+    .query_response_mac(cache_query_response_mac),
+    // Write cache
+    .write_request_valid(cache_write_request_valid_reg),
+    .write_request_ready(),
+    .write_request_ip(cache_write_request_ip_reg),
+    .write_request_mac(cache_write_request_mac_reg),
+    // Configuration
+    .clear_cache(clear_cache)
+);
+
+reg arp_request_operation_reg, arp_request_operation_next;
+
+reg arp_request_ready_reg, arp_request_ready_next;
+reg [31:0] arp_request_ip_reg, arp_request_ip_next;
+
+reg arp_response_valid_reg, arp_response_valid_next;
+reg arp_response_error_reg, arp_response_error_next;
+reg [47:0] arp_response_mac_reg, arp_response_mac_next;
+
+reg [5:0] arp_request_retry_cnt_reg, arp_request_retry_cnt_next;
+reg [35:0] arp_request_timer_reg, arp_request_timer_next;
+
+assign arp_request_ready = arp_request_ready_reg;
+
+assign arp_response_valid = arp_response_valid_reg;
+assign arp_response_error = arp_response_error_reg;
+assign arp_response_mac = arp_response_mac_reg;
+
+always @* begin
+    incoming_frame_ready = 1'b0;
+
+    outgoing_frame_valid_next = outgoing_frame_valid_reg && !outgoing_frame_ready;
+    outgoing_eth_dest_mac_next = outgoing_eth_dest_mac_reg;
+    outgoing_arp_oper_next = outgoing_arp_oper_reg;
+    outgoing_arp_tha_next = outgoing_arp_tha_reg;
+    outgoing_arp_tpa_next = outgoing_arp_tpa_reg;
+
+    cache_query_request_valid_next = 1'b0;
+    cache_query_request_ip_next = cache_query_request_ip_reg;
+
+    cache_write_request_valid_next = 1'b0;
+    cache_write_request_mac_next = cache_write_request_mac_reg;
+    cache_write_request_ip_next = cache_write_request_ip_reg;
+
+    arp_request_ready_next = 1'b0;
+    arp_request_ip_next = arp_request_ip_reg;
+    arp_request_operation_next = arp_request_operation_reg;
+    arp_request_retry_cnt_next = arp_request_retry_cnt_reg;
+    arp_request_timer_next = arp_request_timer_reg;
+    arp_response_valid_next = arp_response_valid_reg && !arp_response_ready;
+    arp_response_error_next = 1'b0;
+    arp_response_mac_next = 48'd0;
+
+    // manage incoming frames
+    incoming_frame_ready = outgoing_frame_ready;
+    if (incoming_frame_valid && incoming_frame_ready) begin
+        if (incoming_eth_type == 16'h0806 && incoming_arp_htype == 16'h0001 && incoming_arp_ptype == 16'h0800) begin
+            // store sender addresses in cache
+            cache_write_request_valid_next = 1'b1;
+            cache_write_request_ip_next = incoming_arp_spa;
+            cache_write_request_mac_next = incoming_arp_sha;
+            if (incoming_arp_oper == ARP_OPER_ARP_REQUEST) begin
+                // ARP request
+                if (incoming_arp_tpa == local_ip) begin
+                    // send reply frame to valid incoming request
+                    outgoing_frame_valid_next = 1'b1;
+                    outgoing_eth_dest_mac_next = incoming_eth_src_mac;
+                    outgoing_arp_oper_next = ARP_OPER_ARP_REPLY;
+                    outgoing_arp_tha_next = incoming_arp_sha;
+                    outgoing_arp_tpa_next = incoming_arp_spa;
+                end
+            end else if (incoming_arp_oper == ARP_OPER_INARP_REQUEST) begin
+                // INARP request
+                if (incoming_arp_tha == local_mac) begin
+                    // send reply frame to valid incoming request
+                    outgoing_frame_valid_next = 1'b1;
+                    outgoing_eth_dest_mac_next = incoming_eth_src_mac;
+                    outgoing_arp_oper_next = ARP_OPER_INARP_REPLY;
+                    outgoing_arp_tha_next = incoming_arp_sha;
+                    outgoing_arp_tpa_next = incoming_arp_spa;
+                end 
+            end
+        end
+    end
+
+    // manage ARP lookup requests
+    if (arp_request_operation_reg) begin
+        arp_request_ready_next = 1'b0;
+        cache_query_request_valid_next = 1'b1;
+        arp_request_timer_next = arp_request_timer_reg - 1;
+        // if we got a response, it will go in the cache, so when the query succeds, we're done
+        if (cache_query_response_valid && !cache_query_response_error) begin
+            arp_request_operation_next = 1'b0;
+            cache_query_request_valid_next = 1'b0;
+            arp_response_valid_next = 1'b1;
+            arp_response_error_next = 1'b0;
+            arp_response_mac_next = cache_query_response_mac;
+        end
+        // timer timeout
+        if (arp_request_timer_reg == 0) begin
+            if (arp_request_retry_cnt_reg > 0) begin
+                // have more retries
+                // send ARP request frame
+                outgoing_frame_valid_next = 1'b1;
+                outgoing_eth_dest_mac_next = 48'hffffffffffff;
+                outgoing_arp_oper_next = ARP_OPER_ARP_REQUEST;
+                outgoing_arp_tha_next = 48'h000000000000;
+                outgoing_arp_tpa_next = arp_request_ip_reg;
+                arp_request_retry_cnt_next = arp_request_retry_cnt_reg - 1;
+                if (arp_request_retry_cnt_reg > 1) begin
+                    arp_request_timer_next = REQUEST_RETRY_INTERVAL;
+                end else begin
+                    arp_request_timer_next = REQUEST_TIMEOUT;
+                end
+            end else begin
+                // out of retries
+                arp_request_operation_next = 1'b0;
+                arp_response_valid_next = 1'b1;
+                arp_response_error_next = 1'b1;
+                cache_query_request_valid_next = 1'b0;
+            end
+        end
+    end else begin
+        arp_request_ready_next = !arp_response_valid_next;
+        if (cache_query_request_valid_reg) begin
+            cache_query_request_valid_next = 1'b1;
+            if (cache_query_response_valid) begin
+                if (cache_query_response_error) begin
+                    arp_request_operation_next = 1'b1;
+                    // send ARP request frame
+                    outgoing_frame_valid_next = 1'b1;
+                    outgoing_eth_dest_mac_next = 48'hffffffffffff;
+                    outgoing_arp_oper_next = ARP_OPER_ARP_REQUEST;
+                    outgoing_arp_tha_next = 48'h000000000000;
+                    outgoing_arp_tpa_next = arp_request_ip_reg;
+                    arp_request_retry_cnt_next = REQUEST_RETRY_COUNT-1;
+                    arp_request_timer_next = REQUEST_RETRY_INTERVAL;
+                end else begin
+                    cache_query_request_valid_next = 1'b0;
+                    arp_response_valid_next = 1'b1;
+                    arp_response_error_next = 1'b0;
+                    arp_response_mac_next = cache_query_response_mac;
+                end
+            end
+        end else if (arp_request_valid && arp_request_ready) begin
+            if (~(arp_request_ip | subnet_mask) == 0) begin
+                // broadcast address
+                // (all bits in request IP set where subnet mask is clear)
+                arp_response_valid_next = 1'b1;
+                arp_response_error_next = 1'b0;
+                arp_response_mac_next = 48'hffffffffffff;
+            end else if (((arp_request_ip ^ gateway_ip) & subnet_mask) == 0) begin
+                // within subnet, look up IP directly
+                // (no bits differ between request IP and gateway IP where subnet mask is set)
+                cache_query_request_valid_next = 1'b1;
+                cache_query_request_ip_next = arp_request_ip;
+                arp_request_ip_next = arp_request_ip;
+            end else begin
+                // outside of subnet, so look up gateway address
+                cache_query_request_valid_next = 1'b1;
+                cache_query_request_ip_next = gateway_ip;
+                arp_request_ip_next = gateway_ip;
+            end
+        end
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        outgoing_frame_valid_reg <= 1'b0;
+        cache_query_request_valid_reg <= 1'b0;
+        cache_query_request_ip_reg <= 32'd0;
+        cache_write_request_valid_reg <= 1'b0;
+        cache_write_request_ip_reg <= 32'd0;
+        cache_write_request_mac_reg <= 48'd0;
+        outgoing_eth_dest_mac_reg <= 48'd0;
+        outgoing_arp_oper_reg <= 16'd0;
+        outgoing_arp_tha_reg <= 48'd0;
+        outgoing_arp_tpa_reg <= 32'd0;
+        arp_request_operation_reg <= 1'b0;
+        arp_request_ready_reg <= 1'b0;
+        arp_request_ip_reg <= 32'd0;
+        arp_response_valid_reg <= 1'b0;
+        arp_response_error_reg <= 1'b0;
+        arp_response_mac_reg <= 48'd0;
+        arp_request_retry_cnt_reg <= 6'd0;
+        arp_request_timer_reg <= 36'd0;
+    end else begin
+        outgoing_frame_valid_reg <= outgoing_frame_valid_next;
+        cache_query_request_valid_reg <= cache_query_request_valid_next;
+        cache_write_request_valid_reg <= cache_write_request_valid_next;
+        arp_request_ready_reg <= arp_request_ready_next;
+        arp_request_operation_reg <= arp_request_operation_next;
+        arp_request_retry_cnt_reg <= arp_request_retry_cnt_next;
+        arp_request_timer_reg <= arp_request_timer_next;
+        arp_response_valid_reg <= arp_response_valid_next;
+        cache_query_request_ip_reg <= cache_query_request_ip_next;
+        outgoing_eth_dest_mac_reg <= outgoing_eth_dest_mac_next;
+        outgoing_arp_oper_reg <= outgoing_arp_oper_next;
+        outgoing_arp_tha_reg <= outgoing_arp_tha_next;
+        outgoing_arp_tpa_reg <= outgoing_arp_tpa_next;
+        cache_write_request_mac_reg <= cache_write_request_mac_next;
+        cache_write_request_ip_reg <= cache_write_request_ip_next;
+        arp_request_ip_reg <= arp_request_ip_next;
+        arp_response_error_reg <= arp_response_error_next;
+        arp_response_mac_reg <= arp_response_mac_next;
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/arp_cache.v b/verilog/rtl/arp_cache.v
new file mode 100644
index 0000000..b9192ff
--- /dev/null
+++ b/verilog/rtl/arp_cache.v
@@ -0,0 +1,250 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * ARP cache
+ */
+module arp_cache #(
+    parameter CACHE_ADDR_WIDTH = 9
+)
+(
+    input  wire        clk,
+    input  wire        rst,
+
+    /*
+     * Cache query
+     */
+    input  wire        query_request_valid,
+    output wire        query_request_ready,
+    input  wire [31:0] query_request_ip,
+
+    output wire        query_response_valid,
+    input  wire        query_response_ready,
+    output wire        query_response_error,
+    output wire [47:0] query_response_mac,
+
+    /*
+     * Cache write
+     */
+    input  wire        write_request_valid,
+    output wire        write_request_ready,
+    input  wire [31:0] write_request_ip,
+    input  wire [47:0] write_request_mac,
+
+    /*
+     * Configuration
+     */
+    input  wire        clear_cache
+);
+
+reg mem_write;
+reg store_query;
+reg store_write;
+
+reg query_ip_valid_reg, query_ip_valid_next;
+reg [31:0] query_ip_reg;
+reg write_ip_valid_reg, write_ip_valid_next;
+reg [31:0] write_ip_reg;
+reg [47:0] write_mac_reg;
+reg clear_cache_reg, clear_cache_next;
+
+reg [CACHE_ADDR_WIDTH-1:0] wr_ptr_reg, wr_ptr_next;
+reg [CACHE_ADDR_WIDTH-1:0] rd_ptr_reg, rd_ptr_next;
+
+reg valid_mem[(2**CACHE_ADDR_WIDTH)-1:0];
+reg [31:0] ip_addr_mem[(2**CACHE_ADDR_WIDTH)-1:0];
+reg [47:0] mac_addr_mem[(2**CACHE_ADDR_WIDTH)-1:0];
+
+reg query_request_ready_reg, query_request_ready_next;
+
+reg query_response_valid_reg, query_response_valid_next;
+reg query_response_error_reg, query_response_error_next;
+reg [47:0] query_response_mac_reg;
+
+reg write_request_ready_reg, write_request_ready_next;
+
+wire [31:0] query_request_hash;
+wire [31:0] write_request_hash;
+
+assign query_request_ready = query_request_ready_reg;
+
+assign query_response_valid = query_response_valid_reg;
+assign query_response_error = query_response_error_reg;
+assign query_response_mac = query_response_mac_reg;
+
+assign write_request_ready = write_request_ready_reg;
+
+lfsr #(
+    .LFSR_WIDTH(32),
+    .LFSR_POLY(32'h4c11db7),
+    .LFSR_CONFIG("GALOIS"),
+    .LFSR_FEED_FORWARD(0),
+    .REVERSE(1),
+    .DATA_WIDTH(32),
+    .STYLE("AUTO")
+)
+rd_hash (
+    .data_in(query_request_ip),
+    .state_in(32'hffffffff),
+    .data_out(),
+    .state_out(query_request_hash)
+);
+
+lfsr #(
+    .LFSR_WIDTH(32),
+    .LFSR_POLY(32'h4c11db7),
+    .LFSR_CONFIG("GALOIS"),
+    .LFSR_FEED_FORWARD(0),
+    .REVERSE(1),
+    .DATA_WIDTH(32),
+    .STYLE("AUTO")
+)
+wr_hash (
+    .data_in(write_request_ip),
+    .state_in(32'hffffffff),
+    .data_out(),
+    .state_out(write_request_hash)
+);
+
+integer i;
+
+initial begin
+    for (i = 0; i < 2**CACHE_ADDR_WIDTH; i = i + 1) begin
+        valid_mem[i] = 1'b0;
+        ip_addr_mem[i] = 32'd0;
+        mac_addr_mem[i] = 48'd0;
+    end
+end
+
+always @* begin
+    mem_write = 1'b0;
+    store_query = 1'b0;
+    store_write = 1'b0;
+
+    wr_ptr_next = wr_ptr_reg;
+    rd_ptr_next = rd_ptr_reg;
+
+    clear_cache_next = clear_cache_reg | clear_cache;
+
+    query_ip_valid_next = query_ip_valid_reg;
+
+    query_request_ready_next = (~query_ip_valid_reg || ~query_request_valid || query_response_ready) && !clear_cache_next;
+
+    query_response_valid_next = query_response_valid_reg & ~query_response_ready;
+    query_response_error_next = query_response_error_reg;
+
+    if (query_ip_valid_reg && (~query_request_valid || query_response_ready)) begin
+        query_response_valid_next = 1;
+        query_ip_valid_next = 0;
+        if (valid_mem[rd_ptr_reg] && ip_addr_mem[rd_ptr_reg] == query_ip_reg) begin
+            query_response_error_next = 0;
+        end else begin
+            query_response_error_next = 1;
+        end
+    end
+
+    if (query_request_valid && query_request_ready && (~query_ip_valid_reg || ~query_request_valid || query_response_ready)) begin
+        store_query = 1;
+        query_ip_valid_next = 1;
+        rd_ptr_next = query_request_hash[CACHE_ADDR_WIDTH-1:0];
+    end
+
+    write_ip_valid_next = write_ip_valid_reg;
+
+    write_request_ready_next = !clear_cache_next;
+
+    if (write_ip_valid_reg) begin
+        write_ip_valid_next = 0;
+        mem_write = 1;
+    end
+
+    if (write_request_valid && write_request_ready) begin
+        store_write = 1;
+        write_ip_valid_next = 1;
+        wr_ptr_next = write_request_hash[CACHE_ADDR_WIDTH-1:0];
+    end
+
+    if (clear_cache) begin
+        clear_cache_next = 1'b1;
+        wr_ptr_next = 0;
+    end else if (clear_cache_reg) begin
+        wr_ptr_next = wr_ptr_reg + 1;
+        clear_cache_next = wr_ptr_next != 0;
+        mem_write = 1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        query_ip_valid_reg <= 1'b0;
+        query_request_ready_reg <= 1'b0;
+        query_response_valid_reg <= 1'b0;
+        write_ip_valid_reg <= 1'b0;
+        write_request_ready_reg <= 1'b0;
+        clear_cache_reg <= 1'b1;
+        wr_ptr_reg <= 0;
+        query_ip_reg <= 0;
+        write_ip_reg <= 0;
+        write_mac_reg <= 0;
+        rd_ptr_reg <= {CACHE_ADDR_WIDTH{1'b0}};
+        query_response_error_reg <= 0;
+        query_response_mac_reg <= 0;
+    end else begin
+        query_ip_valid_reg <= query_ip_valid_next;
+        query_request_ready_reg <= query_request_ready_next;
+        query_response_valid_reg <= query_response_valid_next;
+        write_ip_valid_reg <= write_ip_valid_next;
+        write_request_ready_reg <= write_request_ready_next;
+        clear_cache_reg <= clear_cache_next;
+        wr_ptr_reg <= wr_ptr_next;
+
+        query_response_error_reg <= query_response_error_next;
+    
+        if (store_query) begin
+            query_ip_reg <= query_request_ip;
+        end
+    
+        if (store_write) begin
+            write_ip_reg <= write_request_ip;
+            write_mac_reg <= write_request_mac;
+        end
+    
+        rd_ptr_reg <= rd_ptr_next;
+    
+        query_response_mac_reg <= mac_addr_mem[rd_ptr_reg];
+    
+        if (mem_write) begin
+            valid_mem[wr_ptr_reg] <= !clear_cache_reg;
+            ip_addr_mem[wr_ptr_reg] <= write_ip_reg;
+            mac_addr_mem[wr_ptr_reg] <= write_mac_reg;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/arp_eth_rx.v b/verilog/rtl/arp_eth_rx.v
new file mode 100644
index 0000000..901f4cc
--- /dev/null
+++ b/verilog/rtl/arp_eth_rx.v
@@ -0,0 +1,337 @@
+/*
+
+Copyright (c) 2014-2020 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
+
+`timescale 1ns / 1ps
+
+/*
+ * ARP ethernet frame receiver (Ethernet frame in, ARP frame out)
+ */
+module arp_eth_rx #
+(
+    // Width of AXI stream interfaces in bits
+    parameter DATA_WIDTH = 8,
+    // Propagate tkeep signal
+    // If disabled, tkeep assumed to be 1'b1
+    parameter KEEP_ENABLE = (DATA_WIDTH>8),
+    // tkeep signal width (words per cycle)
+    parameter KEEP_WIDTH = (DATA_WIDTH/8)
+)
+(
+    input  wire                   clk,
+    input  wire                   rst,
+
+    /*
+     * Ethernet frame input
+     */
+    input  wire                   s_eth_hdr_valid,
+    output wire                   s_eth_hdr_ready,
+    input  wire [47:0]            s_eth_dest_mac,
+    input  wire [47:0]            s_eth_src_mac,
+    input  wire [15:0]            s_eth_type,
+    input  wire [DATA_WIDTH-1:0]  s_eth_payload_axis_tdata,
+    input  wire [KEEP_WIDTH-1:0]  s_eth_payload_axis_tkeep,
+    input  wire                   s_eth_payload_axis_tvalid,
+    output wire                   s_eth_payload_axis_tready,
+    input  wire                   s_eth_payload_axis_tlast,
+    input  wire                   s_eth_payload_axis_tuser,
+
+    /*
+     * ARP frame output
+     */
+    output wire                   m_frame_valid,
+    input  wire                   m_frame_ready,
+    output wire [47:0]            m_eth_dest_mac,
+    output wire [47:0]            m_eth_src_mac,
+    output wire [15:0]            m_eth_type,
+    output wire [15:0]            m_arp_htype,
+    output wire [15:0]            m_arp_ptype,
+    output wire [7:0]             m_arp_hlen,
+    output wire [7:0]             m_arp_plen,
+    output wire [15:0]            m_arp_oper,
+    output wire [47:0]            m_arp_sha,
+    output wire [31:0]            m_arp_spa,
+    output wire [47:0]            m_arp_tha,
+    output wire [31:0]            m_arp_tpa,
+
+    /*
+     * Status signals
+     */
+    output wire                   busy,
+    output wire                   error_header_early_termination,
+    output wire                   error_invalid_header
+);
+
+parameter CYCLE_COUNT = (28+KEEP_WIDTH-1)/KEEP_WIDTH;
+
+parameter PTR_WIDTH = $clog2(CYCLE_COUNT);
+
+parameter OFFSET = 28 % KEEP_WIDTH;
+
+// bus width assertions
+initial begin
+    if (KEEP_WIDTH * 8 != DATA_WIDTH) begin
+        $error("Error: AXI stream interface requires byte (8-bit) granularity (instance %m)");
+        $finish;
+    end
+end
+
+/*
+
+ARP Frame
+
+ Field                       Length
+ Destination MAC address     6 octets
+ Source MAC address          6 octets
+ Ethertype (0x0806)          2 octets
+ HTYPE (1)                   2 octets
+ PTYPE (0x0800)              2 octets
+ HLEN (6)                    1 octets
+ PLEN (4)                    1 octets
+ OPER                        2 octets
+ SHA Sender MAC              6 octets
+ SPA Sender IP               4 octets
+ THA Target MAC              6 octets
+ TPA Target IP               4 octets
+
+This module receives an Ethernet frame with header fields in parallel and
+payload on an AXI stream interface, decodes the ARP packet fields, and
+produces the frame fields in parallel.  
+
+*/
+
+// datapath control signals
+reg store_eth_hdr;
+
+reg read_eth_header_reg, read_eth_header_next;
+reg read_arp_header_reg, read_arp_header_next;
+reg [PTR_WIDTH-1:0] ptr_reg, ptr_next;
+
+reg s_eth_hdr_ready_reg, s_eth_hdr_ready_next;
+reg s_eth_payload_axis_tready_reg, s_eth_payload_axis_tready_next;
+
+reg m_frame_valid_reg, m_frame_valid_next;
+reg [47:0] m_eth_dest_mac_reg;
+reg [47:0] m_eth_src_mac_reg;
+reg [15:0] m_eth_type_reg;
+reg [15:0] m_arp_htype_reg, m_arp_htype_next;
+reg [15:0] m_arp_ptype_reg, m_arp_ptype_next;
+reg [7:0]  m_arp_hlen_reg, m_arp_hlen_next;
+reg [7:0]  m_arp_plen_reg, m_arp_plen_next;
+reg [15:0] m_arp_oper_reg, m_arp_oper_next;
+reg [47:0] m_arp_sha_reg, m_arp_sha_next;
+reg [31:0] m_arp_spa_reg, m_arp_spa_next;
+reg [47:0] m_arp_tha_reg, m_arp_tha_next;
+reg [31:0] m_arp_tpa_reg, m_arp_tpa_next;
+
+reg busy_reg;
+reg error_header_early_termination_reg, error_header_early_termination_next;
+reg error_invalid_header_reg, error_invalid_header_next;
+
+assign s_eth_hdr_ready = s_eth_hdr_ready_reg;
+assign s_eth_payload_axis_tready = s_eth_payload_axis_tready_reg;
+
+assign m_frame_valid = m_frame_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+assign m_arp_htype = m_arp_htype_reg;
+assign m_arp_ptype = m_arp_ptype_reg;
+assign m_arp_hlen = m_arp_hlen_reg;
+assign m_arp_plen = m_arp_plen_reg;
+assign m_arp_oper = m_arp_oper_reg;
+assign m_arp_sha = m_arp_sha_reg;
+assign m_arp_spa = m_arp_spa_reg;
+assign m_arp_tha = m_arp_tha_reg;
+assign m_arp_tpa = m_arp_tpa_reg;
+
+assign busy = busy_reg;
+assign error_header_early_termination = error_header_early_termination_reg;
+assign error_invalid_header = error_invalid_header_reg;
+
+always @* begin
+    read_eth_header_next = read_eth_header_reg;
+    read_arp_header_next = read_arp_header_reg;
+    ptr_next = ptr_reg;
+
+    s_eth_hdr_ready_next = 1'b0;
+    s_eth_payload_axis_tready_next = 1'b0;
+
+    store_eth_hdr = 1'b0;
+
+    m_frame_valid_next = m_frame_valid_reg && !m_frame_ready;
+
+    m_arp_htype_next = m_arp_htype_reg;
+    m_arp_ptype_next = m_arp_ptype_reg;
+    m_arp_hlen_next = m_arp_hlen_reg;
+    m_arp_plen_next = m_arp_plen_reg;
+    m_arp_oper_next = m_arp_oper_reg;
+    m_arp_sha_next = m_arp_sha_reg;
+    m_arp_spa_next = m_arp_spa_reg;
+    m_arp_tha_next = m_arp_tha_reg;
+    m_arp_tpa_next = m_arp_tpa_reg;
+
+    error_header_early_termination_next = 1'b0;
+    error_invalid_header_next = 1'b0;
+
+    if (s_eth_hdr_ready && s_eth_hdr_valid) begin
+        if (read_eth_header_reg) begin
+            store_eth_hdr = 1'b1;
+            ptr_next = 0;
+            read_eth_header_next = 1'b0;
+            read_arp_header_next = 1'b1;
+        end
+    end
+
+    if (s_eth_payload_axis_tready && s_eth_payload_axis_tvalid) begin
+        if (read_arp_header_reg) begin
+            // word transfer in - store it
+            ptr_next = ptr_reg + 1;
+
+            `define _HEADER_FIELD_(offset, field) \
+                if (ptr_reg == offset/KEEP_WIDTH && (!KEEP_ENABLE || s_eth_payload_axis_tkeep[offset%KEEP_WIDTH])) begin \
+                    field = s_eth_payload_axis_tdata[(offset%KEEP_WIDTH)*8 +: 8]; \
+                end
+
+            `_HEADER_FIELD_(0,  m_arp_htype_next[1*8 +: 8])
+            `_HEADER_FIELD_(1,  m_arp_htype_next[0*8 +: 8])
+            `_HEADER_FIELD_(2,  m_arp_ptype_next[1*8 +: 8])
+            `_HEADER_FIELD_(3,  m_arp_ptype_next[0*8 +: 8])
+            `_HEADER_FIELD_(4,  m_arp_hlen_next[0*8 +: 8])
+            `_HEADER_FIELD_(5,  m_arp_plen_next[0*8 +: 8])
+            `_HEADER_FIELD_(6,  m_arp_oper_next[1*8 +: 8])
+            `_HEADER_FIELD_(7,  m_arp_oper_next[0*8 +: 8])
+            `_HEADER_FIELD_(8,  m_arp_sha_next[5*8 +: 8])
+            `_HEADER_FIELD_(9,  m_arp_sha_next[4*8 +: 8])
+            `_HEADER_FIELD_(10, m_arp_sha_next[3*8 +: 8])
+            `_HEADER_FIELD_(11, m_arp_sha_next[2*8 +: 8])
+            `_HEADER_FIELD_(12, m_arp_sha_next[1*8 +: 8])
+            `_HEADER_FIELD_(13, m_arp_sha_next[0*8 +: 8])
+            `_HEADER_FIELD_(14, m_arp_spa_next[3*8 +: 8])
+            `_HEADER_FIELD_(15, m_arp_spa_next[2*8 +: 8])
+            `_HEADER_FIELD_(16, m_arp_spa_next[1*8 +: 8])
+            `_HEADER_FIELD_(17, m_arp_spa_next[0*8 +: 8])
+            `_HEADER_FIELD_(18, m_arp_tha_next[5*8 +: 8])
+            `_HEADER_FIELD_(19, m_arp_tha_next[4*8 +: 8])
+            `_HEADER_FIELD_(20, m_arp_tha_next[3*8 +: 8])
+            `_HEADER_FIELD_(21, m_arp_tha_next[2*8 +: 8])
+            `_HEADER_FIELD_(22, m_arp_tha_next[1*8 +: 8])
+            `_HEADER_FIELD_(23, m_arp_tha_next[0*8 +: 8])
+            `_HEADER_FIELD_(24, m_arp_tpa_next[3*8 +: 8])
+            `_HEADER_FIELD_(25, m_arp_tpa_next[2*8 +: 8])
+            `_HEADER_FIELD_(26, m_arp_tpa_next[1*8 +: 8])
+            `_HEADER_FIELD_(27, m_arp_tpa_next[0*8 +: 8])
+
+            if (ptr_reg == 27/KEEP_WIDTH && (!KEEP_ENABLE || s_eth_payload_axis_tkeep[27%KEEP_WIDTH])) begin
+                read_arp_header_next = 1'b0;
+            end
+
+            `undef _HEADER_FIELD_
+        end
+
+        if (s_eth_payload_axis_tlast) begin
+            if (read_arp_header_next) begin
+                // don't have the whole header
+                error_header_early_termination_next = 1'b1;
+            end else if (m_arp_hlen_next != 4'd6 || m_arp_plen_next != 4'd4) begin
+                // lengths not valid
+                error_invalid_header_next = 1'b1;
+            end else begin
+                // otherwise, transfer tuser
+                m_frame_valid_next = !s_eth_payload_axis_tuser;
+            end
+
+            ptr_next = 1'b0;
+            read_eth_header_next = 1'b1;
+            read_arp_header_next = 1'b0;
+        end
+    end
+
+    if (read_eth_header_next) begin
+        s_eth_hdr_ready_next = !m_frame_valid_next;
+    end else begin
+        s_eth_payload_axis_tready_next = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    read_eth_header_reg <= read_eth_header_next;
+    read_arp_header_reg <= read_arp_header_next;
+    ptr_reg <= ptr_next;
+
+    s_eth_hdr_ready_reg <= s_eth_hdr_ready_next;
+    s_eth_payload_axis_tready_reg <= s_eth_payload_axis_tready_next;
+
+    m_frame_valid_reg <= m_frame_valid_next;
+
+    m_arp_htype_reg <= m_arp_htype_next;
+    m_arp_ptype_reg <= m_arp_ptype_next;
+    m_arp_hlen_reg <= m_arp_hlen_next;
+    m_arp_plen_reg <= m_arp_plen_next;
+    m_arp_oper_reg <= m_arp_oper_next;
+    m_arp_sha_reg <= m_arp_sha_next;
+    m_arp_spa_reg <= m_arp_spa_next;
+    m_arp_tha_reg <= m_arp_tha_next;
+    m_arp_tpa_reg <= m_arp_tpa_next;
+
+    error_header_early_termination_reg <= error_header_early_termination_next;
+    error_invalid_header_reg <= error_invalid_header_next;
+
+    busy_reg <= read_arp_header_next;
+
+    // datapath
+    if (store_eth_hdr) begin
+        m_eth_dest_mac_reg <= s_eth_dest_mac;
+        m_eth_src_mac_reg <= s_eth_src_mac;
+        m_eth_type_reg <= s_eth_type;
+    end
+
+    if (rst) begin
+        read_eth_header_reg <= 1'b1;
+        read_arp_header_reg <= 1'b0;
+        ptr_reg <= 0;
+        s_eth_hdr_ready_reg <= 1'b0;
+        s_eth_payload_axis_tready_reg <= 1'b0;
+        m_frame_valid_reg <= 1'b0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+        m_arp_htype_reg <= 16'd0;
+        m_arp_ptype_reg <= 16'd0;
+        m_arp_hlen_reg <= 8'd0;
+        m_arp_plen_reg <= 8'd0;
+        m_arp_oper_reg <= 16'd0;
+        m_arp_sha_reg <= 48'd0;
+        m_arp_spa_reg <= 32'd0;
+        m_arp_tha_reg <= 48'd0;
+        m_arp_tpa_reg <= 32'd0;
+        busy_reg <= 1'b0;
+        error_header_early_termination_reg <= 1'b0;
+        error_invalid_header_reg <= 1'b0;
+    end 
+end
+
+endmodule
+
diff --git a/verilog/rtl/arp_eth_tx.v b/verilog/rtl/arp_eth_tx.v
new file mode 100644
index 0000000..9f593f0
--- /dev/null
+++ b/verilog/rtl/arp_eth_tx.v
@@ -0,0 +1,379 @@
+/*
+
+Copyright (c) 2014-2020 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
+
+`timescale 1ns / 1ps
+
+/*
+ * ARP ethernet frame transmitter (ARP frame in, Ethernet frame out)
+ */
+module arp_eth_tx #
+(
+    // Width of AXI stream interfaces in bits
+    parameter DATA_WIDTH = 8,
+    // Propagate tkeep signal
+    // If disabled, tkeep assumed to be 1'b1
+    parameter KEEP_ENABLE = (DATA_WIDTH>8),
+    // tkeep signal width (words per cycle)
+    parameter KEEP_WIDTH = (DATA_WIDTH/8)
+)
+(
+    input  wire                   clk,
+    input  wire                   rst,
+
+    /*
+     * ARP frame input
+     */
+    input  wire                   s_frame_valid,
+    output wire                   s_frame_ready,
+    input  wire [47:0]            s_eth_dest_mac,
+    input  wire [47:0]            s_eth_src_mac,
+    input  wire [15:0]            s_eth_type,
+    input  wire [15:0]            s_arp_htype,
+    input  wire [15:0]            s_arp_ptype,
+    input  wire [15:0]            s_arp_oper,
+    input  wire [47:0]            s_arp_sha,
+    input  wire [31:0]            s_arp_spa,
+    input  wire [47:0]            s_arp_tha,
+    input  wire [31:0]            s_arp_tpa,
+
+    /*
+     * Ethernet frame output
+     */
+    output wire                   m_eth_hdr_valid,
+    input  wire                   m_eth_hdr_ready,
+    output wire [47:0]            m_eth_dest_mac,
+    output wire [47:0]            m_eth_src_mac,
+    output wire [15:0]            m_eth_type,
+    output wire [DATA_WIDTH-1:0]  m_eth_payload_axis_tdata,
+    output wire [KEEP_WIDTH-1:0]  m_eth_payload_axis_tkeep,
+    output wire                   m_eth_payload_axis_tvalid,
+    input  wire                   m_eth_payload_axis_tready,
+    output wire                   m_eth_payload_axis_tlast,
+    output wire                   m_eth_payload_axis_tuser,
+
+    /*
+     * Status signals
+     */
+    output wire                   busy
+);
+
+parameter CYCLE_COUNT = (28+KEEP_WIDTH-1)/KEEP_WIDTH;
+
+parameter PTR_WIDTH = $clog2(CYCLE_COUNT);
+
+parameter OFFSET = 28 % KEEP_WIDTH;
+
+// bus width assertions
+initial begin
+    if (KEEP_WIDTH * 8 != DATA_WIDTH) begin
+        $error("Error: AXI stream interface requires byte (8-bit) granularity (instance %m)");
+        $finish;
+    end
+end
+
+/*
+
+ARP Frame
+
+ Field                       Length
+ Destination MAC address     6 octets
+ Source MAC address          6 octets
+ Ethertype (0x0806)          2 octets
+ HTYPE (1)                   2 octets
+ PTYPE (0x0800)              2 octets
+ HLEN (6)                    1 octets
+ PLEN (4)                    1 octets
+ OPER                        2 octets
+ SHA Sender MAC              6 octets
+ SPA Sender IP               4 octets
+ THA Target MAC              6 octets
+ TPA Target IP               4 octets
+
+This module receives an ARP frame with header fields in parallel  and
+transmits the complete Ethernet payload on an AXI interface.
+
+*/
+
+// datapath control signals
+reg store_frame;
+
+reg send_arp_header_reg, send_arp_header_next;
+reg [PTR_WIDTH-1:0] ptr_reg, ptr_next;
+
+reg [15:0] arp_htype_reg;
+reg [15:0] arp_ptype_reg;
+reg [15:0] arp_oper_reg;
+reg [47:0] arp_sha_reg;
+reg [31:0] arp_spa_reg;
+reg [47:0] arp_tha_reg;
+reg [31:0] arp_tpa_reg;
+
+reg s_frame_ready_reg, s_frame_ready_next;
+
+reg m_eth_hdr_valid_reg, m_eth_hdr_valid_next;
+reg [47:0] m_eth_dest_mac_reg;
+reg [47:0] m_eth_src_mac_reg;
+reg [15:0] m_eth_type_reg;
+
+reg busy_reg;
+
+// internal datapath
+reg [DATA_WIDTH-1:0] m_eth_payload_axis_tdata_int;
+reg [KEEP_WIDTH-1:0] m_eth_payload_axis_tkeep_int;
+reg                  m_eth_payload_axis_tvalid_int;
+reg                  m_eth_payload_axis_tready_int_reg;
+reg                  m_eth_payload_axis_tlast_int;
+reg                  m_eth_payload_axis_tuser_int;
+wire                 m_eth_payload_axis_tready_int_early;
+
+assign s_frame_ready = s_frame_ready_reg;
+
+assign m_eth_hdr_valid = m_eth_hdr_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+
+assign busy = busy_reg;
+
+always @* begin
+    send_arp_header_next = send_arp_header_reg;
+    ptr_next = ptr_reg;
+
+    s_frame_ready_next = 1'b0;
+
+    store_frame = 1'b0;
+
+    m_eth_hdr_valid_next = m_eth_hdr_valid_reg && !m_eth_hdr_ready;
+
+    m_eth_payload_axis_tdata_int = {DATA_WIDTH{1'b0}};
+    m_eth_payload_axis_tkeep_int = {KEEP_WIDTH{1'b0}};
+    m_eth_payload_axis_tvalid_int = 1'b0;
+    m_eth_payload_axis_tlast_int = 1'b0;
+    m_eth_payload_axis_tuser_int = 1'b0;
+
+    if (s_frame_ready && s_frame_valid) begin
+        store_frame = 1'b1;
+        m_eth_hdr_valid_next = 1'b1;
+        ptr_next = 0;
+        send_arp_header_next = 1'b1;
+    end
+
+    if (m_eth_payload_axis_tready_int_reg) begin
+        if (send_arp_header_reg) begin
+            ptr_next = ptr_reg + 1;
+
+            m_eth_payload_axis_tdata_int = {DATA_WIDTH{1'b0}};
+            m_eth_payload_axis_tkeep_int = {KEEP_WIDTH{1'b0}};
+            m_eth_payload_axis_tvalid_int = 1'b1;
+            m_eth_payload_axis_tlast_int = 1'b0;
+            m_eth_payload_axis_tuser_int = 1'b0;
+
+            `define _HEADER_FIELD_(offset, field) \
+                if (ptr_reg == offset/KEEP_WIDTH) begin \
+                    m_eth_payload_axis_tdata_int[(offset%KEEP_WIDTH)*8 +: 8] = field; \
+                    m_eth_payload_axis_tkeep_int[offset%KEEP_WIDTH] = 1'b1; \
+                end
+
+            `_HEADER_FIELD_(0,  arp_htype_reg[1*8 +: 8])
+            `_HEADER_FIELD_(1,  arp_htype_reg[0*8 +: 8])
+            `_HEADER_FIELD_(2,  arp_ptype_reg[1*8 +: 8])
+            `_HEADER_FIELD_(3,  arp_ptype_reg[0*8 +: 8])
+            `_HEADER_FIELD_(4,  8'd6)
+            `_HEADER_FIELD_(5,  8'd4)
+            `_HEADER_FIELD_(6,  arp_oper_reg[1*8 +: 8])
+            `_HEADER_FIELD_(7,  arp_oper_reg[0*8 +: 8])
+            `_HEADER_FIELD_(8,  arp_sha_reg[5*8 +: 8])
+            `_HEADER_FIELD_(9,  arp_sha_reg[4*8 +: 8])
+            `_HEADER_FIELD_(10, arp_sha_reg[3*8 +: 8])
+            `_HEADER_FIELD_(11, arp_sha_reg[2*8 +: 8])
+            `_HEADER_FIELD_(12, arp_sha_reg[1*8 +: 8])
+            `_HEADER_FIELD_(13, arp_sha_reg[0*8 +: 8])
+            `_HEADER_FIELD_(14, arp_spa_reg[3*8 +: 8])
+            `_HEADER_FIELD_(15, arp_spa_reg[2*8 +: 8])
+            `_HEADER_FIELD_(16, arp_spa_reg[1*8 +: 8])
+            `_HEADER_FIELD_(17, arp_spa_reg[0*8 +: 8])
+            `_HEADER_FIELD_(18, arp_tha_reg[5*8 +: 8])
+            `_HEADER_FIELD_(19, arp_tha_reg[4*8 +: 8])
+            `_HEADER_FIELD_(20, arp_tha_reg[3*8 +: 8])
+            `_HEADER_FIELD_(21, arp_tha_reg[2*8 +: 8])
+            `_HEADER_FIELD_(22, arp_tha_reg[1*8 +: 8])
+            `_HEADER_FIELD_(23, arp_tha_reg[0*8 +: 8])
+            `_HEADER_FIELD_(24, arp_tpa_reg[3*8 +: 8])
+            `_HEADER_FIELD_(25, arp_tpa_reg[2*8 +: 8])
+            `_HEADER_FIELD_(26, arp_tpa_reg[1*8 +: 8])
+            `_HEADER_FIELD_(27, arp_tpa_reg[0*8 +: 8])
+
+            if (ptr_reg == 27/KEEP_WIDTH) begin
+                m_eth_payload_axis_tlast_int = 1'b1;
+                send_arp_header_next = 1'b0;
+            end
+
+            `undef _HEADER_FIELD_
+        end
+    end
+
+    s_frame_ready_next = !m_eth_hdr_valid_next && !send_arp_header_next;
+end
+
+always @(posedge clk) begin
+    send_arp_header_reg <= send_arp_header_next;
+    ptr_reg <= ptr_next;
+
+    s_frame_ready_reg <= s_frame_ready_next;
+
+    m_eth_hdr_valid_reg <= m_eth_hdr_valid_next;
+
+    busy_reg <= send_arp_header_next;
+
+    if (store_frame) begin
+        m_eth_dest_mac_reg <= s_eth_dest_mac;
+        m_eth_src_mac_reg <= s_eth_src_mac;
+        m_eth_type_reg <= s_eth_type;
+        arp_htype_reg <= s_arp_htype;
+        arp_ptype_reg <= s_arp_ptype;
+        arp_oper_reg <= s_arp_oper;
+        arp_sha_reg <= s_arp_sha;
+        arp_spa_reg <= s_arp_spa;
+        arp_tha_reg <= s_arp_tha;
+        arp_tpa_reg <= s_arp_tpa;
+    end
+
+    if (rst) begin
+        send_arp_header_reg <= 1'b0;
+        ptr_reg <= 0;
+        arp_htype_reg <= 16'd0;
+        arp_ptype_reg <= 16'd0;
+        arp_oper_reg <= 16'd0;
+        arp_sha_reg <= 48'd0;
+        arp_spa_reg <= 32'd0;
+        arp_tha_reg <= 48'd0;
+        arp_tpa_reg <= 32'd0;
+        s_frame_ready_reg <= 1'b0;
+        m_eth_hdr_valid_reg <= 1'b0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+        busy_reg <= 1'b0;
+    end
+end
+
+// output datapath logic
+reg [DATA_WIDTH-1:0] m_eth_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] m_eth_payload_axis_tkeep_reg;
+reg                  m_eth_payload_axis_tvalid_reg, m_eth_payload_axis_tvalid_next;
+reg                  m_eth_payload_axis_tlast_reg;
+reg                  m_eth_payload_axis_tuser_reg;
+
+reg [DATA_WIDTH-1:0] temp_m_eth_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] temp_m_eth_payload_axis_tkeep_reg;
+reg                  temp_m_eth_payload_axis_tvalid_reg, temp_m_eth_payload_axis_tvalid_next;
+reg                  temp_m_eth_payload_axis_tlast_reg;
+reg                  temp_m_eth_payload_axis_tuser_reg;
+
+// datapath control
+reg store_eth_payload_int_to_output;
+reg store_eth_payload_int_to_temp;
+reg store_eth_payload_axis_temp_to_output;
+
+assign m_eth_payload_axis_tdata = m_eth_payload_axis_tdata_reg;
+assign m_eth_payload_axis_tkeep = KEEP_ENABLE ? m_eth_payload_axis_tkeep_reg : {KEEP_WIDTH{1'b1}};
+assign m_eth_payload_axis_tvalid = m_eth_payload_axis_tvalid_reg;
+assign m_eth_payload_axis_tlast = m_eth_payload_axis_tlast_reg;
+assign m_eth_payload_axis_tuser = m_eth_payload_axis_tuser_reg;
+
+// 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_eth_payload_axis_tready_int_early = m_eth_payload_axis_tready || (!temp_m_eth_payload_axis_tvalid_reg && (!m_eth_payload_axis_tvalid_reg || !m_eth_payload_axis_tvalid_int));
+
+always @* begin
+    // transfer sink ready state to source
+    m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_reg;
+    temp_m_eth_payload_axis_tvalid_next = temp_m_eth_payload_axis_tvalid_reg;
+
+    store_eth_payload_int_to_output = 1'b0;
+    store_eth_payload_int_to_temp = 1'b0;
+    store_eth_payload_axis_temp_to_output = 1'b0;
+
+    if (m_eth_payload_axis_tready_int_reg) begin
+        // input is ready
+        if (m_eth_payload_axis_tready || !m_eth_payload_axis_tvalid_reg) begin
+            // output is ready or currently not valid, transfer data to output
+            m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_int;
+            store_eth_payload_int_to_output = 1'b1;
+        end else begin
+            // output is not ready, store input in temp
+            temp_m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_int;
+            store_eth_payload_int_to_temp = 1'b1;
+        end
+    end else if (m_eth_payload_axis_tready) begin
+        // input is not ready, but output is ready
+        m_eth_payload_axis_tvalid_next = temp_m_eth_payload_axis_tvalid_reg;
+        temp_m_eth_payload_axis_tvalid_next = 1'b0;
+        store_eth_payload_axis_temp_to_output = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        m_eth_payload_axis_tready_int_reg <= 1'b0;
+        m_eth_payload_axis_tdata_reg <= {DATA_WIDTH{1'b0}};
+        m_eth_payload_axis_tkeep_reg <= {KEEP_WIDTH{1'b0}};
+        m_eth_payload_axis_tvalid_reg <= 1'b0;
+        m_eth_payload_axis_tlast_reg <= 1'b0;
+        m_eth_payload_axis_tuser_reg <= 1'b0;
+        temp_m_eth_payload_axis_tdata_reg <= {DATA_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tkeep_reg <= {KEEP_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tvalid_reg <= 1'b0;
+        temp_m_eth_payload_axis_tlast_reg <= 1'b0;
+        temp_m_eth_payload_axis_tuser_reg <= 1'b0;
+    end else begin
+        m_eth_payload_axis_tvalid_reg <= m_eth_payload_axis_tvalid_next;
+        m_eth_payload_axis_tready_int_reg <= m_eth_payload_axis_tready_int_early;
+        temp_m_eth_payload_axis_tvalid_reg <= temp_m_eth_payload_axis_tvalid_next;
+
+        // datapath
+        if (store_eth_payload_int_to_output) begin
+            m_eth_payload_axis_tdata_reg <= m_eth_payload_axis_tdata_int;
+            m_eth_payload_axis_tkeep_reg <= m_eth_payload_axis_tkeep_int;
+            m_eth_payload_axis_tlast_reg <= m_eth_payload_axis_tlast_int;
+            m_eth_payload_axis_tuser_reg <= m_eth_payload_axis_tuser_int;
+        end else if (store_eth_payload_axis_temp_to_output) begin
+            m_eth_payload_axis_tdata_reg <= temp_m_eth_payload_axis_tdata_reg;
+            m_eth_payload_axis_tkeep_reg <= temp_m_eth_payload_axis_tkeep_reg;
+            m_eth_payload_axis_tlast_reg <= temp_m_eth_payload_axis_tlast_reg;
+            m_eth_payload_axis_tuser_reg <= temp_m_eth_payload_axis_tuser_reg;
+        end
+    
+        if (store_eth_payload_int_to_temp) begin
+            temp_m_eth_payload_axis_tdata_reg <= m_eth_payload_axis_tdata_int;
+            temp_m_eth_payload_axis_tkeep_reg <= m_eth_payload_axis_tkeep_int;
+            temp_m_eth_payload_axis_tlast_reg <= m_eth_payload_axis_tlast_int;
+            temp_m_eth_payload_axis_tuser_reg <= m_eth_payload_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/axis_async_fifo.v b/verilog/rtl/axis_async_fifo.v
new file mode 100644
index 0000000..d64231c
--- /dev/null
+++ b/verilog/rtl/axis_async_fifo.v
@@ -0,0 +1,469 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * AXI4-Stream asynchronous FIFO
+ */
+module axis_async_fifo #
+(
+    // FIFO depth in words
+    // KEEP_WIDTH words per cycle if KEEP_ENABLE set
+    // Rounded up to nearest power of 2 cycles
+    parameter DEPTH = 4096,
+    // Width of AXI stream interfaces in bits
+    parameter DATA_WIDTH = 8,
+    // Propagate tkeep signal
+    // If disabled, tkeep assumed to be 1'b1
+    parameter KEEP_ENABLE = (DATA_WIDTH>8),
+    // tkeep signal width (words per cycle)
+    parameter KEEP_WIDTH = (DATA_WIDTH/8),
+    // Propagate tlast signal
+    parameter LAST_ENABLE = 1,
+    // Propagate tid signal
+    parameter ID_ENABLE = 0,
+    // tid signal width
+    parameter ID_WIDTH = 8,
+    // Propagate tdest signal
+    parameter DEST_ENABLE = 0,
+    // tdest signal width
+    parameter DEST_WIDTH = 8,
+    // Propagate tuser signal
+    parameter USER_ENABLE = 1,
+    // tuser signal width
+    parameter USER_WIDTH = 1,
+    // number of output pipeline registers
+    parameter PIPELINE_OUTPUT = 2,
+    // Frame FIFO mode - operate on frames instead of cycles
+    // When set, m_axis_tvalid will not be deasserted within a frame
+    // Requires LAST_ENABLE set
+    parameter FRAME_FIFO = 0,
+    // tuser value for bad frame marker
+    parameter USER_BAD_FRAME_VALUE = 1'b1,
+    // tuser mask for bad frame marker
+    parameter USER_BAD_FRAME_MASK = 1'b1,
+    // Drop frames marked bad
+    // Requires FRAME_FIFO set
+    parameter DROP_BAD_FRAME = 0,
+    // Drop incoming frames when full
+    // When set, s_axis_tready is always asserted
+    // Requires FRAME_FIFO set
+    parameter DROP_WHEN_FULL = 0
+)
+(
+    /*
+     * Common asynchronous reset
+     */
+    input  wire                   async_rst,
+
+    /*
+     * AXI input
+     */
+    input  wire                   s_clk,
+    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 output
+     */
+    input  wire                   m_clk,
+    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,
+
+    /*
+     * Status
+     */
+    output wire                   s_status_overflow,
+    output wire                   s_status_bad_frame,
+    output wire                   s_status_good_frame,
+    output wire                   m_status_overflow,
+    output wire                   m_status_bad_frame,
+    output wire                   m_status_good_frame
+);
+
+parameter ADDR_WIDTH = (KEEP_ENABLE && KEEP_WIDTH > 1) ? $clog2(DEPTH/KEEP_WIDTH) : $clog2(DEPTH);
+
+localparam KEEP_OFFSET = DATA_WIDTH;
+localparam LAST_OFFSET = KEEP_OFFSET + (KEEP_ENABLE ? KEEP_WIDTH : 0);
+localparam ID_OFFSET   = LAST_OFFSET + (LAST_ENABLE ? 1          : 0);
+localparam DEST_OFFSET = ID_OFFSET   + (ID_ENABLE   ? ID_WIDTH   : 0);
+localparam USER_OFFSET = DEST_OFFSET + (DEST_ENABLE ? DEST_WIDTH : 0);
+localparam WIDTH       = USER_OFFSET + (USER_ENABLE ? USER_WIDTH : 0);
+
+reg [ADDR_WIDTH:0] wr_ptr_reg;
+reg [ADDR_WIDTH:0] wr_ptr_cur_reg;
+reg [ADDR_WIDTH:0] wr_ptr_gray_reg;
+reg [ADDR_WIDTH:0] wr_ptr_sync_gray_reg;
+reg [ADDR_WIDTH:0] wr_ptr_cur_gray_reg;
+reg [ADDR_WIDTH:0] rd_ptr_reg;
+reg [ADDR_WIDTH:0] rd_ptr_gray_reg;
+
+reg [ADDR_WIDTH:0] wr_ptr_temp;
+reg [ADDR_WIDTH:0] rd_ptr_temp;
+
+(* SHREG_EXTRACT = "NO" *)
+reg [ADDR_WIDTH:0] wr_ptr_gray_sync1_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg [ADDR_WIDTH:0] wr_ptr_gray_sync2_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg [ADDR_WIDTH:0] rd_ptr_gray_sync1_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg [ADDR_WIDTH:0] rd_ptr_gray_sync2_reg;
+
+reg wr_ptr_update_valid_reg;
+reg wr_ptr_update_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg wr_ptr_update_sync1_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg wr_ptr_update_sync2_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg wr_ptr_update_sync3_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg wr_ptr_update_ack_sync1_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg wr_ptr_update_ack_sync2_reg;
+
+(* SHREG_EXTRACT = "NO" *)
+reg s_rst_sync1_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg s_rst_sync2_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg s_rst_sync3_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg m_rst_sync1_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg m_rst_sync2_reg;
+(* SHREG_EXTRACT = "NO" *)
+reg m_rst_sync3_reg;
+
+reg [WIDTH-1:0] mem[(2**ADDR_WIDTH)-1:0];
+reg [WIDTH-1:0] mem_read_data_reg;
+
+wire [WIDTH-1:0] s_axis;
+
+(* SHREG_EXTRACT = "NO" *)
+reg [WIDTH-1:0] m_axis_pipe_reg[PIPELINE_OUTPUT-1:0];
+(* SHREG_EXTRACT = "NO" *)
+reg [PIPELINE_OUTPUT-1:0] m_axis_tvalid_pipe_reg;
+
+// full when first TWO MSBs do NOT match, but rest matches
+// (gray code equivalent of first MSB different but rest same)
+wire full = wr_ptr_gray_reg == (rd_ptr_gray_sync2_reg ^ {2'b11, {ADDR_WIDTH-1{1'b0}}});
+wire full_cur = wr_ptr_cur_gray_reg == (rd_ptr_gray_sync2_reg ^ {2'b11, {ADDR_WIDTH-1{1'b0}}});
+// empty when pointers match exactly
+wire empty = rd_ptr_gray_reg == (FRAME_FIFO ? wr_ptr_gray_sync1_reg : wr_ptr_gray_sync2_reg);
+// overflow within packet
+wire full_wr = wr_ptr_reg == (wr_ptr_cur_reg ^ {1'b1, {ADDR_WIDTH{1'b0}}});
+
+// control signals
+reg write;
+reg read;
+reg store_output;
+
+reg drop_frame_reg;
+reg overflow_reg;
+reg bad_frame_reg;
+reg good_frame_reg;
+
+reg overflow_sync1_reg;
+reg overflow_sync2_reg;
+reg overflow_sync3_reg;
+reg overflow_sync4_reg;
+reg bad_frame_sync1_reg;
+reg bad_frame_sync2_reg;
+reg bad_frame_sync3_reg;
+reg bad_frame_sync4_reg;
+reg good_frame_sync1_reg;
+reg good_frame_sync2_reg;
+reg good_frame_sync3_reg;
+reg good_frame_sync4_reg;
+
+assign s_axis_tready = (FRAME_FIFO ? (!full_cur || full_wr || DROP_WHEN_FULL) : !full) && !s_rst_sync3_reg;
+
+generate
+    assign s_axis[DATA_WIDTH-1:0] = s_axis_tdata;
+    if (KEEP_ENABLE) assign s_axis[KEEP_OFFSET +: KEEP_WIDTH] = s_axis_tkeep;
+    if (LAST_ENABLE) assign s_axis[LAST_OFFSET]               = s_axis_tlast;
+    if (ID_ENABLE)   assign s_axis[ID_OFFSET   +: ID_WIDTH]   = s_axis_tid;
+    if (DEST_ENABLE) assign s_axis[DEST_OFFSET +: DEST_WIDTH] = s_axis_tdest;
+    if (USER_ENABLE) assign s_axis[USER_OFFSET +: USER_WIDTH] = s_axis_tuser;
+endgenerate
+
+assign m_axis_tvalid = m_axis_tvalid_pipe_reg[PIPELINE_OUTPUT-1];
+
+assign m_axis_tdata = m_axis_pipe_reg[PIPELINE_OUTPUT-1][DATA_WIDTH-1:0];
+assign m_axis_tkeep = KEEP_ENABLE ? m_axis_pipe_reg[PIPELINE_OUTPUT-1][KEEP_OFFSET +: KEEP_WIDTH] : {KEEP_WIDTH{1'b1}};
+assign m_axis_tlast = LAST_ENABLE ? m_axis_pipe_reg[PIPELINE_OUTPUT-1][LAST_OFFSET]               : 1'b1;
+assign m_axis_tid   = ID_ENABLE   ? m_axis_pipe_reg[PIPELINE_OUTPUT-1][ID_OFFSET   +: ID_WIDTH]   : {ID_WIDTH{1'b0}};
+assign m_axis_tdest = DEST_ENABLE ? m_axis_pipe_reg[PIPELINE_OUTPUT-1][DEST_OFFSET +: DEST_WIDTH] : {DEST_WIDTH{1'b0}};
+assign m_axis_tuser = USER_ENABLE ? m_axis_pipe_reg[PIPELINE_OUTPUT-1][USER_OFFSET +: USER_WIDTH] : {USER_WIDTH{1'b0}};
+
+assign s_status_overflow = overflow_reg;
+assign s_status_bad_frame = bad_frame_reg;
+assign s_status_good_frame = good_frame_reg;
+
+assign m_status_overflow = overflow_sync3_reg ^ overflow_sync4_reg;
+assign m_status_bad_frame = bad_frame_sync3_reg ^ bad_frame_sync4_reg;
+assign m_status_good_frame = good_frame_sync3_reg ^ good_frame_sync4_reg;
+
+// reset synchronization
+always @(posedge s_clk or posedge async_rst) begin
+    if (async_rst) begin
+        s_rst_sync1_reg <= 1'b1;
+        s_rst_sync2_reg <= 1'b1;
+        s_rst_sync3_reg <= 1'b1;
+    end else begin
+        s_rst_sync1_reg <= 1'b0;
+        s_rst_sync2_reg <= s_rst_sync1_reg || m_rst_sync1_reg;
+        s_rst_sync3_reg <= s_rst_sync2_reg;
+    end
+end
+
+always @(posedge m_clk or posedge async_rst) begin
+    if (async_rst) begin
+        m_rst_sync1_reg <= 1'b1;
+        m_rst_sync2_reg <= 1'b1;
+        m_rst_sync3_reg <= 1'b1;
+    end else begin
+        m_rst_sync1_reg <= 1'b0;
+        m_rst_sync2_reg <= s_rst_sync1_reg || m_rst_sync1_reg;
+        m_rst_sync3_reg <= m_rst_sync2_reg;
+    end
+end
+
+// Write logic
+always @(posedge s_clk) begin
+    overflow_reg <= 1'b0;
+    bad_frame_reg <= 1'b0;
+    good_frame_reg <= 1'b0;
+
+    if (FRAME_FIFO && wr_ptr_update_valid_reg) begin
+        // have updated pointer to sync
+        if (wr_ptr_update_reg == wr_ptr_update_ack_sync2_reg) begin
+            // no sync in progress; sync update
+            wr_ptr_update_valid_reg <= 1'b0;
+            wr_ptr_sync_gray_reg <= wr_ptr_gray_reg;
+            wr_ptr_update_reg <= !wr_ptr_update_ack_sync2_reg;
+        end
+    end
+
+    if (s_axis_tready && s_axis_tvalid) begin
+        // transfer in
+        if (!FRAME_FIFO) begin
+            // normal FIFO mode
+            mem[wr_ptr_reg[ADDR_WIDTH-1:0]] <= s_axis;
+            wr_ptr_temp = wr_ptr_reg + 1;
+            wr_ptr_reg <= wr_ptr_temp;
+            wr_ptr_gray_reg <= wr_ptr_temp ^ (wr_ptr_temp >> 1);
+        end else if (full_cur || full_wr || drop_frame_reg) begin
+            // full, packet overflow, or currently dropping frame
+            // drop frame
+            drop_frame_reg <= 1'b1;
+            if (s_axis_tlast) begin
+                // end of frame, reset write pointer
+                wr_ptr_temp = wr_ptr_reg;
+                wr_ptr_cur_reg <= wr_ptr_temp;
+                wr_ptr_cur_gray_reg <= wr_ptr_temp ^ (wr_ptr_temp >> 1);
+                drop_frame_reg <= 1'b0;
+                overflow_reg <= 1'b1;
+            end
+        end else begin
+            mem[wr_ptr_cur_reg[ADDR_WIDTH-1:0]] <= s_axis;
+            wr_ptr_temp = wr_ptr_cur_reg + 1;
+            wr_ptr_cur_reg <= wr_ptr_temp;
+            wr_ptr_cur_gray_reg <= wr_ptr_temp ^ (wr_ptr_temp >> 1);
+            if (s_axis_tlast) begin
+                // end of frame
+                if (DROP_BAD_FRAME && USER_BAD_FRAME_MASK & ~(s_axis_tuser ^ USER_BAD_FRAME_VALUE)) begin
+                    // bad packet, reset write pointer
+                    wr_ptr_temp = wr_ptr_reg;
+                    wr_ptr_cur_reg <= wr_ptr_temp;
+                    wr_ptr_cur_gray_reg <= wr_ptr_temp ^ (wr_ptr_temp >> 1);
+                    bad_frame_reg <= 1'b1;
+                end else begin
+                    // good packet, update write pointer
+                    wr_ptr_temp = wr_ptr_cur_reg + 1;
+                    wr_ptr_reg <= wr_ptr_temp;
+                    wr_ptr_gray_reg <= wr_ptr_temp ^ (wr_ptr_temp >> 1);
+
+                    if (wr_ptr_update_reg == wr_ptr_update_ack_sync2_reg) begin
+                        // no sync in progress; sync update
+                        wr_ptr_update_valid_reg <= 1'b0;
+                        wr_ptr_sync_gray_reg <= wr_ptr_temp ^ (wr_ptr_temp >> 1);
+                        wr_ptr_update_reg <= !wr_ptr_update_ack_sync2_reg;
+                    end else begin
+                        // sync in progress; flag it for later
+                        wr_ptr_update_valid_reg <= 1'b1;
+                    end
+
+                    good_frame_reg <= 1'b1;
+                end
+            end
+        end
+    end
+
+    if (s_rst_sync3_reg) begin
+        wr_ptr_reg <= {ADDR_WIDTH+1{1'b0}};
+        wr_ptr_cur_reg <= {ADDR_WIDTH+1{1'b0}};
+        wr_ptr_gray_reg <= {ADDR_WIDTH+1{1'b0}};
+        wr_ptr_sync_gray_reg <= {ADDR_WIDTH+1{1'b0}};
+        wr_ptr_cur_gray_reg <= {ADDR_WIDTH+1{1'b0}};
+
+        wr_ptr_update_valid_reg <= 1'b0;
+        wr_ptr_update_reg <= 1'b0;
+
+        drop_frame_reg <= 1'b0;
+        overflow_reg <= 1'b0;
+        bad_frame_reg <= 1'b0;
+        good_frame_reg <= 1'b0;
+    end
+end
+
+// pointer synchronization
+always @(posedge s_clk) begin
+    rd_ptr_gray_sync1_reg <= rd_ptr_gray_reg;
+    rd_ptr_gray_sync2_reg <= rd_ptr_gray_sync1_reg;
+    wr_ptr_update_ack_sync1_reg <= wr_ptr_update_sync3_reg;
+    wr_ptr_update_ack_sync2_reg <= wr_ptr_update_ack_sync1_reg;
+
+    if (s_rst_sync3_reg) begin
+        rd_ptr_gray_sync1_reg <= {ADDR_WIDTH+1{1'b0}};
+        rd_ptr_gray_sync2_reg <= {ADDR_WIDTH+1{1'b0}};
+        wr_ptr_update_ack_sync1_reg <= 1'b0;
+        wr_ptr_update_ack_sync2_reg <= 1'b0;
+    end
+end
+
+always @(posedge m_clk) begin
+    if (!FRAME_FIFO) begin
+        wr_ptr_gray_sync1_reg <= wr_ptr_gray_reg;
+    end else if (wr_ptr_update_sync2_reg ^ wr_ptr_update_sync3_reg) begin
+        wr_ptr_gray_sync1_reg <= wr_ptr_sync_gray_reg;
+    end
+    wr_ptr_gray_sync2_reg <= wr_ptr_gray_sync1_reg;
+    wr_ptr_update_sync1_reg <= wr_ptr_update_reg;
+    wr_ptr_update_sync2_reg <= wr_ptr_update_sync1_reg;
+    wr_ptr_update_sync3_reg <= wr_ptr_update_sync2_reg;
+
+    if (m_rst_sync3_reg) begin
+        wr_ptr_gray_sync1_reg <= {ADDR_WIDTH+1{1'b0}};
+        wr_ptr_gray_sync2_reg <= {ADDR_WIDTH+1{1'b0}};
+        wr_ptr_update_sync1_reg <= 1'b0;
+        wr_ptr_update_sync2_reg <= 1'b0;
+        wr_ptr_update_sync3_reg <= 1'b0;
+    end
+end
+
+// status synchronization
+always @(posedge s_clk) begin
+    overflow_sync1_reg <= overflow_sync1_reg ^ overflow_reg;
+    bad_frame_sync1_reg <= bad_frame_sync1_reg ^ bad_frame_reg;
+    good_frame_sync1_reg <= good_frame_sync1_reg ^ good_frame_reg;
+
+    if (s_rst_sync3_reg) begin
+        overflow_sync1_reg <= 1'b0;
+        bad_frame_sync1_reg <= 1'b0;
+        good_frame_sync1_reg <= 1'b0;
+    end
+end
+
+always @(posedge m_clk) begin
+    overflow_sync2_reg <= overflow_sync1_reg;
+    overflow_sync3_reg <= overflow_sync2_reg;
+    overflow_sync4_reg <= overflow_sync3_reg;
+    bad_frame_sync2_reg <= bad_frame_sync1_reg;
+    bad_frame_sync3_reg <= bad_frame_sync2_reg;
+    bad_frame_sync4_reg <= bad_frame_sync3_reg;
+    good_frame_sync2_reg <= good_frame_sync1_reg;
+    good_frame_sync3_reg <= good_frame_sync2_reg;
+    good_frame_sync4_reg <= good_frame_sync3_reg;
+
+    if (m_rst_sync3_reg) begin
+        overflow_sync2_reg <= 1'b0;
+        overflow_sync3_reg <= 1'b0;
+        overflow_sync4_reg <= 1'b0;
+        bad_frame_sync2_reg <= 1'b0;
+        bad_frame_sync3_reg <= 1'b0;
+        bad_frame_sync4_reg <= 1'b0;
+        good_frame_sync2_reg <= 1'b0;
+        good_frame_sync3_reg <= 1'b0;
+        good_frame_sync4_reg <= 1'b0;
+    end
+end
+
+// Read logic
+integer j;
+
+always @(posedge m_clk) begin
+    if (m_axis_tready) begin
+        // output ready; invalidate stage
+        m_axis_tvalid_pipe_reg[PIPELINE_OUTPUT-1] <= 1'b0;
+    end
+
+    for (j = PIPELINE_OUTPUT-1; j > 0; j = j - 1) begin
+        if (m_axis_tready || ((~m_axis_tvalid_pipe_reg) >> j)) begin
+            // output ready or bubble in pipeline; transfer down pipeline
+            m_axis_tvalid_pipe_reg[j] <= m_axis_tvalid_pipe_reg[j-1];
+            m_axis_pipe_reg[j] <= m_axis_pipe_reg[j-1];
+            m_axis_tvalid_pipe_reg[j-1] <= 1'b0;
+        end
+    end
+
+    if (m_axis_tready || ~m_axis_tvalid_pipe_reg) begin
+        // output ready or bubble in pipeline; read new data from FIFO
+        m_axis_tvalid_pipe_reg[0] <= 1'b0;
+        m_axis_pipe_reg[0] <= mem[rd_ptr_reg[ADDR_WIDTH-1:0]];
+        if (!empty) begin
+            // not empty, increment pointer
+            m_axis_tvalid_pipe_reg[0] <= 1'b1;
+            rd_ptr_temp = rd_ptr_reg + 1;
+            rd_ptr_reg <= rd_ptr_temp;
+            rd_ptr_gray_reg <= rd_ptr_temp ^ (rd_ptr_temp >> 1);
+        end
+    end
+
+    if (m_rst_sync3_reg) begin
+        rd_ptr_reg <= {ADDR_WIDTH+1{1'b0}};
+        rd_ptr_gray_reg <= {ADDR_WIDTH+1{1'b0}};
+        m_axis_tvalid_pipe_reg <= {PIPELINE_OUTPUT{1'b0}};
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/axis_async_fifo_adapter.v b/verilog/rtl/axis_async_fifo_adapter.v
new file mode 100644
index 0000000..c04e527
--- /dev/null
+++ b/verilog/rtl/axis_async_fifo_adapter.v
@@ -0,0 +1,351 @@
+/*
+
+Copyright (c) 2019 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
+
+`timescale 1ns / 1ps
+
+/*
+ * AXI4-Stream asynchronous FIFO with width converter
+ */
+module axis_async_fifo_adapter #
+(
+    // FIFO depth in words
+    // KEEP_WIDTH words per cycle if KEEP_ENABLE set
+    // Rounded up to nearest power of 2 cycles
+    parameter DEPTH = 4096,
+    // Width of input AXI stream interface in bits
+    parameter S_DATA_WIDTH = 8,
+    // Propagate tkeep signal on input interface
+    // If disabled, tkeep assumed to be 1'b1
+    parameter S_KEEP_ENABLE = (S_DATA_WIDTH>8),
+    // tkeep signal width (words per cycle) on input interface
+    parameter S_KEEP_WIDTH = (S_DATA_WIDTH/8),
+    // Width of output AXI stream interface in bits
+    parameter M_DATA_WIDTH = 8,
+    // Propagate tkeep signal on output interface
+    // If disabled, tkeep assumed to be 1'b1
+    parameter M_KEEP_ENABLE = (M_DATA_WIDTH>8),
+    // tkeep signal width (words per cycle) on output interface
+    parameter M_KEEP_WIDTH = (M_DATA_WIDTH/8),
+    // Propagate tid signal
+    parameter ID_ENABLE = 0,
+    // tid signal width
+    parameter ID_WIDTH = 8,
+    // Propagate tdest signal
+    parameter DEST_ENABLE = 0,
+    // tdest signal width
+    parameter DEST_WIDTH = 8,
+    // Propagate tuser signal
+    parameter USER_ENABLE = 1,
+    // tuser signal width
+    parameter USER_WIDTH = 1,
+    // number of output pipeline registers
+    parameter PIPELINE_OUTPUT = 2,
+    // Frame FIFO mode - operate on frames instead of cycles
+    // When set, m_axis_tvalid will not be deasserted within a frame
+    // Requires LAST_ENABLE set
+    parameter FRAME_FIFO = 0,
+    // tuser value for bad frame marker
+    parameter USER_BAD_FRAME_VALUE = 1'b1,
+    // tuser mask for bad frame marker
+    parameter USER_BAD_FRAME_MASK = 1'b1,
+    // Drop frames marked bad
+    // Requires FRAME_FIFO set
+    parameter DROP_BAD_FRAME = 0,
+    // Drop incoming frames when full
+    // When set, s_axis_tready is always asserted
+    // Requires FRAME_FIFO set
+    parameter DROP_WHEN_FULL = 0
+)
+(
+    /*
+     * AXI input
+     */
+    input  wire                     s_clk,
+    input  wire                     s_rst,
+    input  wire [S_DATA_WIDTH-1:0]  s_axis_tdata,
+    input  wire [S_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 output
+     */
+    input  wire                     m_clk,
+    input  wire                     m_rst,
+    output wire [M_DATA_WIDTH-1:0]  m_axis_tdata,
+    output wire [M_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,
+
+    /*
+     * Status
+     */
+    output wire                     s_status_overflow,
+    output wire                     s_status_bad_frame,
+    output wire                     s_status_good_frame,
+    output wire                     m_status_overflow,
+    output wire                     m_status_bad_frame,
+    output wire                     m_status_good_frame
+);
+
+// force keep width to 1 when disabled
+parameter S_KEEP_WIDTH_INT = S_KEEP_ENABLE ? S_KEEP_WIDTH : 1;
+parameter M_KEEP_WIDTH_INT = M_KEEP_ENABLE ? M_KEEP_WIDTH : 1;
+
+// bus word sizes (must be identical)
+parameter S_DATA_WORD_SIZE = S_DATA_WIDTH / S_KEEP_WIDTH_INT;
+parameter M_DATA_WORD_SIZE = M_DATA_WIDTH / M_KEEP_WIDTH_INT;
+// output bus is wider
+parameter EXPAND_BUS = M_KEEP_WIDTH_INT > S_KEEP_WIDTH_INT;
+// total data and keep widths
+parameter DATA_WIDTH = EXPAND_BUS ? M_DATA_WIDTH : S_DATA_WIDTH;
+parameter KEEP_WIDTH = EXPAND_BUS ? M_KEEP_WIDTH_INT : S_KEEP_WIDTH_INT;
+
+// bus width assertions
+initial begin
+    if (S_DATA_WORD_SIZE * S_KEEP_WIDTH_INT != S_DATA_WIDTH) begin
+        $error("Error: input data width not evenly divisble (instance %m)");
+        $finish;
+    end
+
+    if (M_DATA_WORD_SIZE * M_KEEP_WIDTH_INT != M_DATA_WIDTH) begin
+        $error("Error: output data width not evenly divisble (instance %m)");
+        $finish;
+    end
+
+    if (S_DATA_WORD_SIZE != M_DATA_WORD_SIZE) begin
+        $error("Error: word size mismatch (instance %m)");
+        $finish;
+    end
+end
+
+wire [DATA_WIDTH-1:0]  pre_fifo_axis_tdata;
+wire [KEEP_WIDTH-1:0]  pre_fifo_axis_tkeep;
+wire                   pre_fifo_axis_tvalid;
+wire                   pre_fifo_axis_tready;
+wire                   pre_fifo_axis_tlast;
+wire [ID_WIDTH-1:0]    pre_fifo_axis_tid;
+wire [DEST_WIDTH-1:0]  pre_fifo_axis_tdest;
+wire [USER_WIDTH-1:0]  pre_fifo_axis_tuser;
+
+wire [DATA_WIDTH-1:0]  post_fifo_axis_tdata;
+wire [KEEP_WIDTH-1:0]  post_fifo_axis_tkeep;
+wire                   post_fifo_axis_tvalid;
+wire                   post_fifo_axis_tready;
+wire                   post_fifo_axis_tlast;
+wire [ID_WIDTH-1:0]    post_fifo_axis_tid;
+wire [DEST_WIDTH-1:0]  post_fifo_axis_tdest;
+wire [USER_WIDTH-1:0]  post_fifo_axis_tuser;
+
+generate
+
+if (M_KEEP_WIDTH == S_KEEP_WIDTH) begin
+
+    // same width, no adapter needed
+
+    assign pre_fifo_axis_tdata = s_axis_tdata;
+    assign pre_fifo_axis_tkeep = s_axis_tkeep;
+    assign pre_fifo_axis_tvalid = s_axis_tvalid;
+    assign s_axis_tready = pre_fifo_axis_tready;
+    assign pre_fifo_axis_tlast = s_axis_tlast;
+    assign pre_fifo_axis_tid = s_axis_tid;
+    assign pre_fifo_axis_tdest = s_axis_tdest;
+    assign pre_fifo_axis_tuser = s_axis_tuser;
+
+    assign m_axis_tdata = post_fifo_axis_tdata;
+    assign m_axis_tkeep = post_fifo_axis_tkeep;
+    assign m_axis_tvalid = post_fifo_axis_tvalid;
+    assign post_fifo_axis_tready = m_axis_tready;
+    assign m_axis_tlast = post_fifo_axis_tlast;
+    assign m_axis_tid = post_fifo_axis_tid;
+    assign m_axis_tdest = post_fifo_axis_tdest;
+    assign m_axis_tuser = post_fifo_axis_tuser;
+
+
+end else if (EXPAND_BUS) begin
+
+    // output wider, adapt width before FIFO
+
+    axis_adapter #(
+        .S_DATA_WIDTH(S_DATA_WIDTH),
+        .S_KEEP_ENABLE(S_KEEP_ENABLE),
+        .S_KEEP_WIDTH(S_KEEP_WIDTH),
+        .M_DATA_WIDTH(M_DATA_WIDTH),
+        .M_KEEP_ENABLE(M_KEEP_ENABLE),
+        .M_KEEP_WIDTH(M_KEEP_WIDTH),
+        .ID_ENABLE(ID_ENABLE),
+        .ID_WIDTH(ID_WIDTH),
+        .DEST_ENABLE(DEST_ENABLE),
+        .DEST_WIDTH(DEST_WIDTH),
+        .USER_ENABLE(USER_ENABLE),
+        .USER_WIDTH(USER_WIDTH)
+    )
+    adapter_inst (
+        .clk(s_clk),
+        .rst(s_rst),
+        // AXI input
+        .s_axis_tdata(s_axis_tdata),
+        .s_axis_tkeep(s_axis_tkeep),
+        .s_axis_tvalid(s_axis_tvalid),
+        .s_axis_tready(s_axis_tready),
+        .s_axis_tlast(s_axis_tlast),
+        .s_axis_tid(s_axis_tid),
+        .s_axis_tdest(s_axis_tdest),
+        .s_axis_tuser(s_axis_tuser),
+        // AXI output
+        .m_axis_tdata(pre_fifo_axis_tdata),
+        .m_axis_tkeep(pre_fifo_axis_tkeep),
+        .m_axis_tvalid(pre_fifo_axis_tvalid),
+        .m_axis_tready(pre_fifo_axis_tready),
+        .m_axis_tlast(pre_fifo_axis_tlast),
+        .m_axis_tid(pre_fifo_axis_tid),
+        .m_axis_tdest(pre_fifo_axis_tdest),
+        .m_axis_tuser(pre_fifo_axis_tuser)
+    );
+
+    assign m_axis_tdata = post_fifo_axis_tdata;
+    assign m_axis_tkeep = post_fifo_axis_tkeep;
+    assign m_axis_tvalid = post_fifo_axis_tvalid;
+    assign post_fifo_axis_tready = m_axis_tready;
+    assign m_axis_tlast = post_fifo_axis_tlast;
+    assign m_axis_tid = post_fifo_axis_tid;
+    assign m_axis_tdest = post_fifo_axis_tdest;
+    assign m_axis_tuser = post_fifo_axis_tuser;
+    
+end else begin
+
+    // input wider, adapt width after FIFO
+    
+    assign pre_fifo_axis_tdata = s_axis_tdata;
+    assign pre_fifo_axis_tkeep = s_axis_tkeep;
+    assign pre_fifo_axis_tvalid = s_axis_tvalid;
+    assign s_axis_tready = pre_fifo_axis_tready;
+    assign pre_fifo_axis_tlast = s_axis_tlast;
+    assign pre_fifo_axis_tid = s_axis_tid;
+    assign pre_fifo_axis_tdest = s_axis_tdest;
+    assign pre_fifo_axis_tuser = s_axis_tuser;
+
+    axis_adapter #(
+        .S_DATA_WIDTH(S_DATA_WIDTH),
+        .S_KEEP_ENABLE(S_KEEP_ENABLE),
+        .S_KEEP_WIDTH(S_KEEP_WIDTH),
+        .M_DATA_WIDTH(M_DATA_WIDTH),
+        .M_KEEP_ENABLE(M_KEEP_ENABLE),
+        .M_KEEP_WIDTH(M_KEEP_WIDTH),
+        .ID_ENABLE(ID_ENABLE),
+        .ID_WIDTH(ID_WIDTH),
+        .DEST_ENABLE(DEST_ENABLE),
+        .DEST_WIDTH(DEST_WIDTH),
+        .USER_ENABLE(USER_ENABLE),
+        .USER_WIDTH(USER_WIDTH)
+    )
+    adapter_inst (
+        .clk(m_clk),
+        .rst(m_rst),
+        // AXI input
+        .s_axis_tdata(post_fifo_axis_tdata),
+        .s_axis_tkeep(post_fifo_axis_tkeep),
+        .s_axis_tvalid(post_fifo_axis_tvalid),
+        .s_axis_tready(post_fifo_axis_tready),
+        .s_axis_tlast(post_fifo_axis_tlast),
+        .s_axis_tid(post_fifo_axis_tid),
+        .s_axis_tdest(post_fifo_axis_tdest),
+        .s_axis_tuser(post_fifo_axis_tuser),
+        // AXI output
+        .m_axis_tdata(m_axis_tdata),
+        .m_axis_tkeep(m_axis_tkeep),
+        .m_axis_tvalid(m_axis_tvalid),
+        .m_axis_tready(m_axis_tready),
+        .m_axis_tlast(m_axis_tlast),
+        .m_axis_tid(m_axis_tid),
+        .m_axis_tdest(m_axis_tdest),
+        .m_axis_tuser(m_axis_tuser)
+    );
+
+end
+
+endgenerate
+
+axis_async_fifo #(
+    .DEPTH(DEPTH),
+    .DATA_WIDTH(DATA_WIDTH),
+    .KEEP_ENABLE(EXPAND_BUS ? M_KEEP_ENABLE : S_KEEP_ENABLE),
+    .KEEP_WIDTH(KEEP_WIDTH),
+    .LAST_ENABLE(1),
+    .ID_ENABLE(ID_ENABLE),
+    .ID_WIDTH(ID_WIDTH),
+    .DEST_ENABLE(DEST_ENABLE),
+    .DEST_WIDTH(DEST_WIDTH),
+    .USER_ENABLE(USER_ENABLE),
+    .USER_WIDTH(USER_WIDTH),
+    .PIPELINE_OUTPUT(PIPELINE_OUTPUT),
+    .FRAME_FIFO(FRAME_FIFO),
+    .USER_BAD_FRAME_VALUE(USER_BAD_FRAME_VALUE),
+    .USER_BAD_FRAME_MASK(USER_BAD_FRAME_MASK),
+    .DROP_BAD_FRAME(DROP_BAD_FRAME),
+    .DROP_WHEN_FULL(DROP_WHEN_FULL)
+)
+fifo_inst (
+    // Common reset
+    .async_rst(s_rst | m_rst),
+    // AXI input
+    .s_clk(s_clk),
+    .s_axis_tdata(pre_fifo_axis_tdata),
+    .s_axis_tkeep(pre_fifo_axis_tkeep),
+    .s_axis_tvalid(pre_fifo_axis_tvalid),
+    .s_axis_tready(pre_fifo_axis_tready),
+    .s_axis_tlast(pre_fifo_axis_tlast),
+    .s_axis_tid(pre_fifo_axis_tid),
+    .s_axis_tdest(pre_fifo_axis_tdest),
+    .s_axis_tuser(pre_fifo_axis_tuser),
+    // AXI output
+    .m_clk(m_clk),
+    .m_axis_tdata(post_fifo_axis_tdata),
+    .m_axis_tkeep(post_fifo_axis_tkeep),
+    .m_axis_tvalid(post_fifo_axis_tvalid),
+    .m_axis_tready(post_fifo_axis_tready),
+    .m_axis_tlast(post_fifo_axis_tlast),
+    .m_axis_tid(post_fifo_axis_tid),
+    .m_axis_tdest(post_fifo_axis_tdest),
+    .m_axis_tuser(post_fifo_axis_tuser),
+    // Status
+    .s_status_overflow(s_status_overflow),
+    .s_status_bad_frame(s_status_bad_frame),
+    .s_status_good_frame(s_status_good_frame),
+    .m_status_overflow(m_status_overflow),
+    .m_status_bad_frame(m_status_bad_frame),
+    .m_status_good_frame(m_status_good_frame)
+);
+
+endmodule
diff --git a/verilog/rtl/axis_gmii_rx.v b/verilog/rtl/axis_gmii_rx.v
new file mode 100644
index 0000000..7d75b96
--- /dev/null
+++ b/verilog/rtl/axis_gmii_rx.v
@@ -0,0 +1,370 @@
+/*
+
+Copyright (c) 2015-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * AXI4-Stream GMII frame receiver (GMII in, AXI out)
+ */
+module axis_gmii_rx #
+(
+    parameter DATA_WIDTH = 8,
+    parameter PTP_TS_ENABLE = 0,
+    parameter PTP_TS_WIDTH = 96,
+    parameter USER_WIDTH = (PTP_TS_ENABLE ? PTP_TS_WIDTH : 0) + 1
+)
+(
+    input  wire                     clk,
+    input  wire                     rst,
+
+    /*
+     * GMII input
+     */
+    input  wire [DATA_WIDTH-1:0]    gmii_rxd,
+    input  wire                     gmii_rx_dv,
+    input  wire                     gmii_rx_er,
+
+    /*
+     * AXI output
+     */
+    output wire [DATA_WIDTH-1:0]    m_axis_tdata,
+    output wire                     m_axis_tvalid,
+    output wire                     m_axis_tlast,
+    output wire [USER_WIDTH-1:0]    m_axis_tuser,
+
+    /*
+     * PTP
+     */
+    input  wire [PTP_TS_WIDTH-1:0]  ptp_ts,
+
+    /*
+     * Control
+     */
+    input  wire                     clk_enable,
+    input  wire                     mii_select,
+
+    /*
+     * Status
+     */
+    output wire                     start_packet,
+    output wire                     error_bad_frame,
+    output wire                     error_bad_fcs
+);
+
+// bus width assertions
+initial begin
+    if (DATA_WIDTH != 8) begin
+        $error("Error: Interface width must be 8");
+        $finish;
+    end
+end
+
+localparam [7:0]
+    ETH_PRE = 8'h55,
+    ETH_SFD = 8'hD5;
+
+localparam [2:0]
+    STATE_IDLE = 3'd0,
+    STATE_PAYLOAD = 3'd1,
+    STATE_WAIT_LAST = 3'd2;
+
+reg [2:0] state_reg, state_next;
+
+// datapath control signals
+reg reset_crc;
+reg update_crc;
+
+reg mii_odd;
+reg mii_locked;
+
+reg [DATA_WIDTH-1:0] gmii_rxd_d0;
+reg [DATA_WIDTH-1:0] gmii_rxd_d1;
+reg [DATA_WIDTH-1:0] gmii_rxd_d2;
+reg [DATA_WIDTH-1:0] gmii_rxd_d3;
+reg [DATA_WIDTH-1:0] gmii_rxd_d4;
+
+reg gmii_rx_dv_d0;
+reg gmii_rx_dv_d1;
+reg gmii_rx_dv_d2;
+reg gmii_rx_dv_d3;
+reg gmii_rx_dv_d4;
+
+reg gmii_rx_er_d0;
+reg gmii_rx_er_d1;
+reg gmii_rx_er_d2;
+reg gmii_rx_er_d3;
+reg gmii_rx_er_d4;
+
+reg [DATA_WIDTH-1:0] m_axis_tdata_reg, m_axis_tdata_next;
+reg m_axis_tvalid_reg, m_axis_tvalid_next;
+reg m_axis_tlast_reg, m_axis_tlast_next;
+reg m_axis_tuser_reg, m_axis_tuser_next;
+
+reg start_packet_reg, start_packet_next;
+reg error_bad_frame_reg, error_bad_frame_next;
+reg error_bad_fcs_reg, error_bad_fcs_next;
+
+reg [PTP_TS_WIDTH-1:0] ptp_ts_reg, ptp_ts_next;
+
+reg [31:0] crc_state;
+wire [31:0] crc_next;
+
+assign m_axis_tdata = m_axis_tdata_reg;
+assign m_axis_tvalid = m_axis_tvalid_reg;
+assign m_axis_tlast = m_axis_tlast_reg;
+assign m_axis_tuser = PTP_TS_ENABLE ? {ptp_ts_reg, m_axis_tuser_reg} : m_axis_tuser_reg;
+
+assign start_packet = start_packet_reg;
+assign error_bad_frame = error_bad_frame_reg;
+assign error_bad_fcs = error_bad_fcs_reg;
+
+lfsr #(
+    .LFSR_WIDTH(32),
+    .LFSR_POLY(32'h4c11db7),
+    .LFSR_CONFIG("GALOIS"),
+    .LFSR_FEED_FORWARD(0),
+    .REVERSE(1),
+    .DATA_WIDTH(8),
+    .STYLE("AUTO")
+)
+eth_crc_8 (
+    .data_in(gmii_rxd_d4),
+    .state_in(crc_state),
+    .data_out(),
+    .state_out(crc_next)
+);
+
+always @* begin
+    state_next = STATE_IDLE;
+
+    reset_crc = 1'b0;
+    update_crc = 1'b0;
+
+    m_axis_tdata_next = {DATA_WIDTH{1'b0}};
+    m_axis_tvalid_next = 1'b0;
+    m_axis_tlast_next = 1'b0;
+    m_axis_tuser_next = 1'b0;
+
+    start_packet_next = 1'b0;
+    error_bad_frame_next = 1'b0;
+    error_bad_fcs_next = 1'b0;
+
+    ptp_ts_next = ptp_ts_reg;
+
+    if (!clk_enable) begin
+        // clock disabled - hold state
+        state_next = state_reg;
+    end else if (mii_select && !mii_odd) begin
+        // MII even cycle - hold state
+        state_next = state_reg;
+    end else begin
+        case (state_reg)
+            STATE_IDLE: begin
+                // idle state - wait for packet
+                reset_crc = 1'b1;
+
+                if (gmii_rx_dv_d4 && !gmii_rx_er_d4 && gmii_rxd_d4 == ETH_SFD) begin
+                    ptp_ts_next = ptp_ts;
+                    start_packet_next = 1'b1;
+                    state_next = STATE_PAYLOAD;
+                end else begin
+                    state_next = STATE_IDLE;
+                end
+            end
+            STATE_PAYLOAD: begin
+                // read payload
+                update_crc = 1'b1;
+
+                m_axis_tdata_next = gmii_rxd_d4;
+                m_axis_tvalid_next = 1'b1;
+
+                if (gmii_rx_dv_d4 && gmii_rx_er_d4) begin
+                    // error
+                    m_axis_tlast_next = 1'b1;
+                    m_axis_tuser_next = 1'b1;
+                    error_bad_frame_next = 1'b1;
+                    state_next = STATE_WAIT_LAST;
+                end else if (!gmii_rx_dv) begin
+                    // end of packet
+                    m_axis_tlast_next = 1'b1;
+                    if (gmii_rx_er_d0 || gmii_rx_er_d1 || gmii_rx_er_d2 || gmii_rx_er_d3) begin
+                        // error received in FCS bytes
+                        m_axis_tuser_next = 1'b1;
+                        error_bad_frame_next = 1'b1;
+                    end else if ({gmii_rxd_d0, gmii_rxd_d1, gmii_rxd_d2, gmii_rxd_d3} == ~crc_next) begin
+                        // FCS good
+                        m_axis_tuser_next = 1'b0;
+                    end else begin
+                        // FCS bad
+                        m_axis_tuser_next = 1'b1;
+                        error_bad_frame_next = 1'b1;
+                        error_bad_fcs_next = 1'b1;
+                    end
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_PAYLOAD;
+                end
+            end
+            STATE_WAIT_LAST: begin
+                // wait for end of packet
+
+                if (~gmii_rx_dv) begin
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_WAIT_LAST;
+                end
+            end
+        endcase
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        state_reg <= STATE_IDLE;
+
+        m_axis_tdata_reg <= {DATA_WIDTH{1'b0}};
+        m_axis_tvalid_reg <= 1'b0;
+        m_axis_tlast_reg <= 1'b0;
+        m_axis_tuser_reg <= 1'b0;
+
+        start_packet_reg <= 1'b0;
+        error_bad_frame_reg <= 1'b0;
+        error_bad_fcs_reg <= 1'b0;
+
+        crc_state <= 32'hFFFFFFFF;
+
+        mii_locked <= 1'b0;
+        mii_odd <= 1'b0;
+
+        gmii_rx_dv_d0 <= 1'b0;
+        gmii_rx_dv_d1 <= 1'b0;
+        gmii_rx_dv_d2 <= 1'b0;
+        gmii_rx_dv_d3 <= 1'b0;
+        gmii_rx_dv_d4 <= 1'b0;
+        
+        ptp_ts_reg <= 0;
+    end else begin
+        state_reg <= state_next;
+
+        m_axis_tvalid_reg <= m_axis_tvalid_next;
+
+        start_packet_reg <= start_packet_next;
+        error_bad_frame_reg <= error_bad_frame_next;
+        error_bad_fcs_reg <= error_bad_fcs_next;
+
+        // datapath
+        if (reset_crc) begin
+            crc_state <= 32'hFFFFFFFF;
+        end else if (update_crc) begin
+            crc_state <= crc_next;
+        end
+
+        if (clk_enable) begin
+            if (mii_select) begin
+                mii_odd <= !mii_odd;
+
+                if (mii_locked) begin
+                    mii_locked <= gmii_rx_dv;
+                end else if (gmii_rx_dv && {gmii_rxd[3:0], gmii_rxd_d0[7:4]} == ETH_SFD) begin
+                    mii_locked <= 1'b1;
+                    mii_odd <= 1'b1;
+                end
+
+                if (mii_odd) begin
+                    gmii_rx_dv_d0 <= gmii_rx_dv & gmii_rx_dv_d0;
+                    gmii_rx_dv_d1 <= gmii_rx_dv_d0 & gmii_rx_dv;
+                    gmii_rx_dv_d2 <= gmii_rx_dv_d1 & gmii_rx_dv;
+                    gmii_rx_dv_d3 <= gmii_rx_dv_d2 & gmii_rx_dv;
+                    gmii_rx_dv_d4 <= gmii_rx_dv_d3 & gmii_rx_dv;
+                end else begin
+                    gmii_rx_dv_d0 <= gmii_rx_dv;
+                end
+            end else begin
+                gmii_rx_dv_d0 <= gmii_rx_dv;
+                gmii_rx_dv_d1 <= gmii_rx_dv_d0 & gmii_rx_dv;
+                gmii_rx_dv_d2 <= gmii_rx_dv_d1 & gmii_rx_dv;
+                gmii_rx_dv_d3 <= gmii_rx_dv_d2 & gmii_rx_dv;
+                gmii_rx_dv_d4 <= gmii_rx_dv_d3 & gmii_rx_dv;
+            end
+        end
+        
+        ptp_ts_reg <= ptp_ts_next;
+    
+        m_axis_tdata_reg <= m_axis_tdata_next;
+        m_axis_tlast_reg <= m_axis_tlast_next;
+        m_axis_tuser_reg <= m_axis_tuser_next;
+    end
+
+
+    // delay input
+    if (rst) begin
+        gmii_rxd_d0 <= {DATA_WIDTH{1'b0}};
+        gmii_rxd_d1 <= {DATA_WIDTH{1'b0}};
+        gmii_rxd_d2 <= {DATA_WIDTH{1'b0}};
+        gmii_rxd_d3 <= {DATA_WIDTH{1'b0}};
+        gmii_rxd_d4 <= {DATA_WIDTH{1'b0}};
+                
+        gmii_rx_er_d0 <= 1'b0;
+        gmii_rx_er_d1 <= 1'b0;
+        gmii_rx_er_d2 <= 1'b0;
+        gmii_rx_er_d3 <= 1'b0;
+        gmii_rx_er_d4 <= 1'b0;        
+    end else if (clk_enable) begin
+        if (mii_select) begin
+            gmii_rxd_d0 <= {gmii_rxd[3:0], gmii_rxd_d0[7:4]};
+
+            if (mii_odd) begin
+                gmii_rxd_d1 <= gmii_rxd_d0;
+                gmii_rxd_d2 <= gmii_rxd_d1;
+                gmii_rxd_d3 <= gmii_rxd_d2;
+                gmii_rxd_d4 <= gmii_rxd_d3;
+
+                gmii_rx_er_d0 <= gmii_rx_er | gmii_rx_er_d0;
+                gmii_rx_er_d1 <= gmii_rx_er_d0;
+                gmii_rx_er_d2 <= gmii_rx_er_d1;
+                gmii_rx_er_d3 <= gmii_rx_er_d2;
+                gmii_rx_er_d4 <= gmii_rx_er_d3;
+            end else begin
+                gmii_rx_er_d0 <= gmii_rx_er;
+            end
+        end else begin
+            gmii_rxd_d0 <= gmii_rxd;
+            gmii_rxd_d1 <= gmii_rxd_d0;
+            gmii_rxd_d2 <= gmii_rxd_d1;
+            gmii_rxd_d3 <= gmii_rxd_d2;
+            gmii_rxd_d4 <= gmii_rxd_d3;
+
+            gmii_rx_er_d0 <= gmii_rx_er;
+            gmii_rx_er_d1 <= gmii_rx_er_d0;
+            gmii_rx_er_d2 <= gmii_rx_er_d1;
+            gmii_rx_er_d3 <= gmii_rx_er_d2;
+            gmii_rx_er_d4 <= gmii_rx_er_d3;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/axis_gmii_tx.v b/verilog/rtl/axis_gmii_tx.v
new file mode 100644
index 0000000..18e11e0
--- /dev/null
+++ b/verilog/rtl/axis_gmii_tx.v
@@ -0,0 +1,459 @@
+/*
+
+Copyright (c) 2015-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * AXI4-Stream GMII frame transmitter (AXI in, GMII out)
+ */
+module axis_gmii_tx #
+(
+    parameter DATA_WIDTH = 8,
+    parameter ENABLE_PADDING = 1,
+    parameter MIN_FRAME_LENGTH = 64,
+    parameter PTP_TS_ENABLE = 0,
+    parameter PTP_TS_WIDTH = 96,
+    parameter PTP_TAG_ENABLE = PTP_TS_ENABLE,
+    parameter PTP_TAG_WIDTH = 16,
+    parameter USER_WIDTH = (PTP_TAG_ENABLE ? PTP_TAG_WIDTH : 0) + 1
+)
+(
+    input  wire                      clk,
+    input  wire                      rst,
+
+    /*
+     * AXI input
+     */
+    input  wire [DATA_WIDTH-1:0]     s_axis_tdata,
+    input  wire                      s_axis_tvalid,
+    output wire                      s_axis_tready,
+    input  wire                      s_axis_tlast,
+    input  wire [USER_WIDTH-1:0]     s_axis_tuser,
+
+    /*
+     * GMII output
+     */
+    output wire [DATA_WIDTH-1:0]     gmii_txd,
+    output wire                      gmii_tx_en,
+    output wire                      gmii_tx_er,
+
+    /*
+     * PTP
+     */
+    input  wire [PTP_TS_WIDTH-1:0]   ptp_ts,
+    output wire [PTP_TS_WIDTH-1:0]   m_axis_ptp_ts,
+    output wire [PTP_TAG_WIDTH-1:0]  m_axis_ptp_ts_tag,
+    output wire                      m_axis_ptp_ts_valid,
+
+    /*
+     * Control
+     */
+    input  wire                      clk_enable,
+    input  wire                      mii_select,
+
+    /*
+     * Configuration
+     */
+    input  wire [7:0]                ifg_delay,
+
+    /*
+     * Status
+     */
+    output wire                      start_packet,
+    output wire                      error_underflow
+);
+
+// bus width assertions
+initial begin
+    if (DATA_WIDTH != 8) begin
+        $error("Error: Interface width must be 8");
+        $finish;
+    end
+end
+
+localparam [7:0]
+    ETH_PRE = 8'h55,
+    ETH_SFD = 8'hD5;
+
+localparam [2:0]
+    STATE_IDLE = 3'd0,
+    STATE_PREAMBLE = 3'd1,
+    STATE_PAYLOAD = 3'd2,
+    STATE_LAST = 3'd3,
+    STATE_PAD = 3'd4,
+    STATE_FCS = 3'd5,
+    STATE_WAIT_END = 3'd6,
+    STATE_IFG = 3'd7;
+
+reg [2:0] state_reg, state_next;
+
+// datapath control signals
+reg reset_crc;
+reg update_crc;
+
+reg [7:0] s_tdata_reg, s_tdata_next;
+
+reg mii_odd_reg, mii_odd_next;
+reg [3:0] mii_msn_reg, mii_msn_next;
+
+reg [15:0] frame_ptr_reg, frame_ptr_next;
+
+reg [7:0] gmii_txd_reg, gmii_txd_next;
+reg gmii_tx_en_reg, gmii_tx_en_next;
+reg gmii_tx_er_reg, gmii_tx_er_next;
+
+reg s_axis_tready_reg, s_axis_tready_next;
+
+reg [PTP_TS_WIDTH-1:0] m_axis_ptp_ts_reg, m_axis_ptp_ts_next;
+reg [PTP_TAG_WIDTH-1:0] m_axis_ptp_ts_tag_reg, m_axis_ptp_ts_tag_next;
+reg m_axis_ptp_ts_valid_reg, m_axis_ptp_ts_valid_next;
+
+reg start_packet_reg, start_packet_next;
+reg error_underflow_reg, error_underflow_next;
+
+reg [31:0] crc_state;
+wire [31:0] crc_next;
+
+assign s_axis_tready = s_axis_tready_reg;
+
+assign gmii_txd = gmii_txd_reg;
+assign gmii_tx_en = gmii_tx_en_reg;
+assign gmii_tx_er = gmii_tx_er_reg;
+
+assign m_axis_ptp_ts = PTP_TS_ENABLE ? m_axis_ptp_ts_reg : 0;
+assign m_axis_ptp_ts_tag = PTP_TAG_ENABLE ? m_axis_ptp_ts_tag_reg : 0;
+assign m_axis_ptp_ts_valid = PTP_TS_ENABLE || PTP_TAG_ENABLE ? m_axis_ptp_ts_valid_reg : 1'b0;
+
+assign start_packet = start_packet_reg;
+assign error_underflow = error_underflow_reg;
+
+lfsr #(
+    .LFSR_WIDTH(32),
+    .LFSR_POLY(32'h4c11db7),
+    .LFSR_CONFIG("GALOIS"),
+    .LFSR_FEED_FORWARD(0),
+    .REVERSE(1),
+    .DATA_WIDTH(8),
+    .STYLE("AUTO")
+)
+eth_crc_8 (
+    .data_in(s_tdata_reg),
+    .state_in(crc_state),
+    .data_out(),
+    .state_out(crc_next)
+);
+
+always @* begin
+    state_next = STATE_IDLE;
+
+    reset_crc = 1'b0;
+    update_crc = 1'b0;
+
+    mii_odd_next = mii_odd_reg;
+    mii_msn_next = mii_msn_reg;
+
+    frame_ptr_next = frame_ptr_reg;
+
+    s_axis_tready_next = 1'b0;
+
+    s_tdata_next = s_tdata_reg;
+
+    m_axis_ptp_ts_next = m_axis_ptp_ts_reg;
+    m_axis_ptp_ts_tag_next = m_axis_ptp_ts_tag_reg;
+    m_axis_ptp_ts_valid_next = 1'b0;
+
+    gmii_txd_next = {DATA_WIDTH{1'b0}};
+    gmii_tx_en_next = 1'b0;
+    gmii_tx_er_next = 1'b0;
+
+    start_packet_next = 1'b0;
+    error_underflow_next = 1'b0;
+
+    if (!clk_enable) begin
+        // clock disabled - hold state and outputs
+        gmii_txd_next = gmii_txd_reg;
+        gmii_tx_en_next = gmii_tx_en_reg;
+        gmii_tx_er_next = gmii_tx_er_reg;
+        state_next = state_reg;
+    end else if (mii_select && mii_odd_reg) begin
+        // MII odd cycle - hold state, output MSN
+        mii_odd_next = 1'b0;
+        gmii_txd_next = {4'd0, mii_msn_reg};
+        gmii_tx_en_next = gmii_tx_en_reg;
+        gmii_tx_er_next = gmii_tx_er_reg;
+        state_next = state_reg;
+    end else begin
+        case (state_reg)
+            STATE_IDLE: begin
+                // idle state - wait for packet
+                reset_crc = 1'b1;
+                mii_odd_next = 1'b0;
+
+                if (s_axis_tvalid) begin
+                    mii_odd_next = 1'b1;
+                    frame_ptr_next = 16'd1;
+                    gmii_txd_next = ETH_PRE;
+                    gmii_tx_en_next = 1'b1;
+                    state_next = STATE_PREAMBLE;
+                end else begin
+                    state_next = STATE_IDLE;
+                end
+            end
+            STATE_PREAMBLE: begin
+                // send preamble
+                reset_crc = 1'b1;
+
+                mii_odd_next = 1'b1;
+                frame_ptr_next = frame_ptr_reg + 16'd1;
+
+                gmii_txd_next = ETH_PRE;
+                gmii_tx_en_next = 1'b1;
+
+                if (frame_ptr_reg == 16'd6) begin
+                    s_axis_tready_next = 1'b1;
+                    s_tdata_next = s_axis_tdata;
+                    state_next = STATE_PREAMBLE;
+                end else if (frame_ptr_reg == 16'd7) begin
+                    // end of preamble; start payload
+                    frame_ptr_next = 16'd0;
+                    if (s_axis_tready_reg) begin
+                        s_axis_tready_next = 1'b1;
+                        s_tdata_next = s_axis_tdata;
+                    end
+                    gmii_txd_next = ETH_SFD;
+                    m_axis_ptp_ts_next = ptp_ts;
+                    m_axis_ptp_ts_tag_next = s_axis_tuser >> 1;
+                    m_axis_ptp_ts_valid_next = 1'b1;
+                    start_packet_next = 1'b1;
+                    state_next = STATE_PAYLOAD;
+                end else begin
+                    state_next = STATE_PREAMBLE;
+                end
+            end
+            STATE_PAYLOAD: begin
+                // send payload
+
+                update_crc = 1'b1;
+                s_axis_tready_next = 1'b1;
+
+                mii_odd_next = 1'b1;
+                frame_ptr_next = frame_ptr_reg + 16'd1;
+
+                gmii_txd_next = s_tdata_reg;
+                gmii_tx_en_next = 1'b1;
+
+                s_tdata_next = s_axis_tdata;
+
+                if (s_axis_tvalid) begin
+                    if (s_axis_tlast) begin
+                        s_axis_tready_next = !s_axis_tready_reg;
+                        if (s_axis_tuser[0]) begin
+                            gmii_tx_er_next = 1'b1;
+                            frame_ptr_next = 1'b0;
+                            state_next = STATE_IFG;
+                        end else begin
+                            state_next = STATE_LAST;
+                        end
+                    end else begin
+                        state_next = STATE_PAYLOAD;
+                    end
+                end else begin
+                    // tvalid deassert, fail frame
+                    gmii_tx_er_next = 1'b1;
+                    frame_ptr_next = 16'd0;
+                    error_underflow_next = 1'b1;
+                    state_next = STATE_WAIT_END;
+                end
+            end
+            STATE_LAST: begin
+                // last payload word
+
+                update_crc = 1'b1;
+
+                mii_odd_next = 1'b1;
+                frame_ptr_next = frame_ptr_reg + 16'd1;
+
+                gmii_txd_next = s_tdata_reg;
+                gmii_tx_en_next = 1'b1;
+
+                if (ENABLE_PADDING && frame_ptr_reg < MIN_FRAME_LENGTH-5) begin
+                    s_tdata_next = 8'd0;
+                    state_next = STATE_PAD;
+                end else begin
+                    frame_ptr_next = 16'd0;
+                    state_next = STATE_FCS;
+                end
+            end
+            STATE_PAD: begin
+                // send padding
+
+                update_crc = 1'b1;
+                mii_odd_next = 1'b1;
+                frame_ptr_next = frame_ptr_reg + 16'd1;
+
+                gmii_txd_next = 8'd0;
+                gmii_tx_en_next = 1'b1;
+
+                s_tdata_next = 8'd0;
+
+                if (frame_ptr_reg < MIN_FRAME_LENGTH-5) begin
+                    state_next = STATE_PAD;
+                end else begin
+                    frame_ptr_next = 16'd0;
+                    state_next = STATE_FCS;
+                end
+            end
+            STATE_FCS: begin
+                // send FCS
+
+                mii_odd_next = 1'b1;
+                frame_ptr_next = frame_ptr_reg + 16'd1;
+
+                case (frame_ptr_reg)
+                    2'd0: gmii_txd_next = ~crc_state[7:0];
+                    2'd1: gmii_txd_next = ~crc_state[15:8];
+                    2'd2: gmii_txd_next = ~crc_state[23:16];
+                    2'd3: gmii_txd_next = ~crc_state[31:24];
+                endcase
+                gmii_tx_en_next = 1'b1;
+
+                if (frame_ptr_reg < 3) begin
+                    state_next = STATE_FCS;
+                end else begin
+                    frame_ptr_next = 16'd0;
+                    state_next = STATE_IFG;
+                end
+            end
+            STATE_WAIT_END: begin
+                // wait for end of frame
+
+                reset_crc = 1'b1;
+
+                mii_odd_next = 1'b1;
+                frame_ptr_next = frame_ptr_reg + 16'd1;
+                s_axis_tready_next = 1'b1;
+
+                if (s_axis_tvalid) begin
+                    if (s_axis_tlast) begin
+                        s_axis_tready_next = 1'b0;
+                        if (frame_ptr_reg < ifg_delay-1) begin
+                            state_next = STATE_IFG;
+                        end else begin
+                            state_next = STATE_IDLE;
+                        end
+                    end else begin
+                        state_next = STATE_WAIT_END;
+                    end
+                end else begin
+                    state_next = STATE_WAIT_END;
+                end
+            end
+            STATE_IFG: begin
+                // send IFG
+
+                reset_crc = 1'b1;
+
+                mii_odd_next = 1'b1;
+                frame_ptr_next = frame_ptr_reg + 16'd1;
+
+                if (frame_ptr_reg < ifg_delay-1) begin
+                    state_next = STATE_IFG;
+                end else begin
+                    state_next = STATE_IDLE;
+                end
+            end
+        endcase
+
+        if (mii_select) begin
+            mii_msn_next = gmii_txd_next[7:4];
+            gmii_txd_next[7:4] = 4'd0;
+        end
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        state_reg <= STATE_IDLE;
+
+        frame_ptr_reg <= 16'd0;
+
+        s_axis_tready_reg <= 1'b0;
+
+
+        m_axis_ptp_ts_valid_reg <= 1'b0;
+
+        gmii_tx_en_reg <= 1'b0;
+        gmii_tx_er_reg <= 1'b0;
+
+        start_packet_reg <= 1'b0;
+        error_underflow_reg <= 1'b0;
+        
+        m_axis_ptp_ts_reg <= 0;
+        m_axis_ptp_ts_tag_reg <= 0;
+    
+        mii_odd_reg <= 1'b0;
+        mii_msn_reg <= 4'b0;
+    
+        s_tdata_reg <= 8'd0;
+    
+        gmii_txd_reg <= 8'd0;
+
+        crc_state <= 32'hFFFFFFFF;
+    end else begin
+        state_reg <= state_next;
+
+        frame_ptr_reg <= frame_ptr_next;
+
+        s_axis_tready_reg <= s_axis_tready_next;
+    
+        m_axis_ptp_ts_valid_reg <= m_axis_ptp_ts_valid_next;
+
+        gmii_tx_en_reg <= gmii_tx_en_next;
+        gmii_tx_er_reg <= gmii_tx_er_next;
+
+        start_packet_reg <= start_packet_next;
+        error_underflow_reg <= error_underflow_next;
+
+        m_axis_ptp_ts_reg <= m_axis_ptp_ts_next;
+        m_axis_ptp_ts_tag_reg <= m_axis_ptp_ts_tag_next;
+    
+        mii_odd_reg <= mii_odd_next;
+        mii_msn_reg <= mii_msn_next;
+    
+        s_tdata_reg <= s_tdata_next;
+    
+        gmii_txd_reg <= gmii_txd_next;
+        // datapath
+        if (reset_crc) begin
+            crc_state <= 32'hFFFFFFFF;
+        end else if (update_crc) begin
+            crc_state <= crc_next;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/eth_arb_mux.v b/verilog/rtl/eth_arb_mux.v
new file mode 100644
index 0000000..a418c76
--- /dev/null
+++ b/verilog/rtl/eth_arb_mux.v
@@ -0,0 +1,327 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * Ethernet arbitrated multiplexer
+ */
+module eth_arb_mux #
+(
+    parameter S_COUNT = 4,
+    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,
+    // select round robin arbitration
+    parameter ARB_TYPE_ROUND_ROBIN = 0,
+    // LSB priority selection
+    parameter ARB_LSB_HIGH_PRIORITY = 1
+)
+(
+    input  wire                          clk,
+    input  wire                          rst,
+
+    /*
+     * Ethernet frame inputs
+     */
+    input  wire [S_COUNT-1:0]            s_eth_hdr_valid,
+    output wire [S_COUNT-1:0]            s_eth_hdr_ready,
+    input  wire [S_COUNT*48-1:0]         s_eth_dest_mac,
+    input  wire [S_COUNT*48-1:0]         s_eth_src_mac,
+    input  wire [S_COUNT*16-1:0]         s_eth_type,
+    input  wire [S_COUNT*DATA_WIDTH-1:0] s_eth_payload_axis_tdata,
+    input  wire [S_COUNT*KEEP_WIDTH-1:0] s_eth_payload_axis_tkeep,
+    input  wire [S_COUNT-1:0]            s_eth_payload_axis_tvalid,
+    output wire [S_COUNT-1:0]            s_eth_payload_axis_tready,
+    input  wire [S_COUNT-1:0]            s_eth_payload_axis_tlast,
+    input  wire [S_COUNT*ID_WIDTH-1:0]   s_eth_payload_axis_tid,
+    input  wire [S_COUNT*DEST_WIDTH-1:0] s_eth_payload_axis_tdest,
+    input  wire [S_COUNT*USER_WIDTH-1:0] s_eth_payload_axis_tuser,
+
+    /*
+     * Ethernet frame output
+     */
+    output wire                          m_eth_hdr_valid,
+    input  wire                          m_eth_hdr_ready,
+    output wire [47:0]                   m_eth_dest_mac,
+    output wire [47:0]                   m_eth_src_mac,
+    output wire [15:0]                   m_eth_type,
+    output wire [DATA_WIDTH-1:0]         m_eth_payload_axis_tdata,
+    output wire [KEEP_WIDTH-1:0]         m_eth_payload_axis_tkeep,
+    output wire                          m_eth_payload_axis_tvalid,
+    input  wire                          m_eth_payload_axis_tready,
+    output wire                          m_eth_payload_axis_tlast,
+    output wire [ID_WIDTH-1:0]           m_eth_payload_axis_tid,
+    output wire [DEST_WIDTH-1:0]         m_eth_payload_axis_tdest,
+    output wire [USER_WIDTH-1:0]         m_eth_payload_axis_tuser
+);
+
+parameter CL_S_COUNT = $clog2(S_COUNT);
+
+reg frame_reg, frame_next;
+
+reg [S_COUNT-1:0] s_eth_hdr_ready_reg, s_eth_hdr_ready_next;
+
+reg m_eth_hdr_valid_reg, m_eth_hdr_valid_next;
+reg [47:0] m_eth_dest_mac_reg, m_eth_dest_mac_next;
+reg [47:0] m_eth_src_mac_reg, m_eth_src_mac_next;
+reg [15:0] m_eth_type_reg, m_eth_type_next;
+
+wire [S_COUNT-1:0] request;
+wire [S_COUNT-1:0] acknowledge;
+wire [S_COUNT-1:0] grant;
+wire grant_valid;
+wire [CL_S_COUNT-1:0] grant_encoded;
+
+// internal datapath
+reg  [DATA_WIDTH-1:0] m_eth_payload_axis_tdata_int;
+reg  [KEEP_WIDTH-1:0] m_eth_payload_axis_tkeep_int;
+reg                   m_eth_payload_axis_tvalid_int;
+reg                   m_eth_payload_axis_tready_int_reg;
+reg                   m_eth_payload_axis_tlast_int;
+reg  [ID_WIDTH-1:0]   m_eth_payload_axis_tid_int;
+reg  [DEST_WIDTH-1:0] m_eth_payload_axis_tdest_int;
+reg  [USER_WIDTH-1:0] m_eth_payload_axis_tuser_int;
+wire                  m_eth_payload_axis_tready_int_early;
+
+assign s_eth_hdr_ready = s_eth_hdr_ready_reg;
+
+assign s_eth_payload_axis_tready = (m_eth_payload_axis_tready_int_reg && grant_valid) << grant_encoded;
+
+assign m_eth_hdr_valid = m_eth_hdr_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+
+// mux for incoming packet
+wire [DATA_WIDTH-1:0] current_s_tdata  = s_eth_payload_axis_tdata[grant_encoded*DATA_WIDTH +: DATA_WIDTH];
+wire [KEEP_WIDTH-1:0] current_s_tkeep  = s_eth_payload_axis_tkeep[grant_encoded*KEEP_WIDTH +: KEEP_WIDTH];
+wire                  current_s_tvalid = s_eth_payload_axis_tvalid[grant_encoded];
+wire                  current_s_tready = s_eth_payload_axis_tready[grant_encoded];
+wire                  current_s_tlast  = s_eth_payload_axis_tlast[grant_encoded];
+wire [ID_WIDTH-1:0]   current_s_tid    = s_eth_payload_axis_tid[grant_encoded*ID_WIDTH +: ID_WIDTH];
+wire [DEST_WIDTH-1:0] current_s_tdest  = s_eth_payload_axis_tdest[grant_encoded*DEST_WIDTH +: DEST_WIDTH];
+wire [USER_WIDTH-1:0] current_s_tuser  = s_eth_payload_axis_tuser[grant_encoded*USER_WIDTH +: USER_WIDTH];
+
+// arbiter instance
+arbiter #(
+    .PORTS(S_COUNT),
+    .ARB_TYPE_ROUND_ROBIN(ARB_TYPE_ROUND_ROBIN),
+    .ARB_BLOCK(1),
+    .ARB_BLOCK_ACK(1),
+    .ARB_LSB_HIGH_PRIORITY(ARB_LSB_HIGH_PRIORITY)
+)
+arb_inst (
+    .clk(clk),
+    .rst(rst),
+    .request(request),
+    .acknowledge(acknowledge),
+    .grant(grant),
+    .grant_valid(grant_valid),
+    .grant_encoded(grant_encoded)
+);
+
+assign request = s_eth_hdr_valid & ~grant;
+assign acknowledge = grant & s_eth_payload_axis_tvalid & s_eth_payload_axis_tready & s_eth_payload_axis_tlast;
+
+always @* begin
+    frame_next = frame_reg;
+
+    s_eth_hdr_ready_next = {S_COUNT{1'b0}};
+
+    m_eth_hdr_valid_next = m_eth_hdr_valid_reg && !m_eth_hdr_ready;
+    m_eth_dest_mac_next = m_eth_dest_mac_reg;
+    m_eth_src_mac_next = m_eth_src_mac_reg;
+    m_eth_type_next = m_eth_type_reg;
+
+    if (s_eth_payload_axis_tvalid[grant_encoded] && s_eth_payload_axis_tready[grant_encoded]) begin
+        // end of frame detection
+        if (s_eth_payload_axis_tlast[grant_encoded]) begin
+            frame_next = 1'b0;
+        end
+    end
+
+    if (!frame_reg && grant_valid && (m_eth_hdr_ready || !m_eth_hdr_valid)) begin
+        // start of frame
+        frame_next = 1'b1;
+
+        s_eth_hdr_ready_next = grant;
+
+        m_eth_hdr_valid_next = 1'b1;
+        m_eth_dest_mac_next = s_eth_dest_mac[grant_encoded*48 +: 48];
+        m_eth_src_mac_next = s_eth_src_mac[grant_encoded*48 +: 48];
+        m_eth_type_next = s_eth_type[grant_encoded*16 +: 16];
+    end
+
+    // pass through selected packet data
+    m_eth_payload_axis_tdata_int  = current_s_tdata;
+    m_eth_payload_axis_tkeep_int  = current_s_tkeep;
+    m_eth_payload_axis_tvalid_int = current_s_tvalid && m_eth_payload_axis_tready_int_reg && grant_valid;
+    m_eth_payload_axis_tlast_int  = current_s_tlast;
+    m_eth_payload_axis_tid_int    = current_s_tid;
+    m_eth_payload_axis_tdest_int  = current_s_tdest;
+    m_eth_payload_axis_tuser_int  = current_s_tuser;
+end
+
+always @(posedge clk) begin
+    frame_reg <= frame_next;
+
+    s_eth_hdr_ready_reg <= s_eth_hdr_ready_next;
+
+    m_eth_hdr_valid_reg <= m_eth_hdr_valid_next;
+    m_eth_dest_mac_reg <= m_eth_dest_mac_next;
+    m_eth_src_mac_reg <= m_eth_src_mac_next;
+    m_eth_type_reg <= m_eth_type_next;
+
+    if (rst) begin
+        frame_reg <= 1'b0;
+        s_eth_hdr_ready_reg <= {S_COUNT{1'b0}};
+        m_eth_hdr_valid_reg <= 1'b0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+    end
+end
+
+// output datapath logic
+reg [DATA_WIDTH-1:0] m_eth_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] m_eth_payload_axis_tkeep_reg;
+reg                  m_eth_payload_axis_tvalid_reg, m_eth_payload_axis_tvalid_next;
+reg                  m_eth_payload_axis_tlast_reg;
+reg [ID_WIDTH-1:0]   m_eth_payload_axis_tid_reg;
+reg [DEST_WIDTH-1:0] m_eth_payload_axis_tdest_reg;
+reg [USER_WIDTH-1:0] m_eth_payload_axis_tuser_reg;
+
+reg [DATA_WIDTH-1:0] temp_m_eth_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] temp_m_eth_payload_axis_tkeep_reg;
+reg                  temp_m_eth_payload_axis_tvalid_reg, temp_m_eth_payload_axis_tvalid_next;
+reg                  temp_m_eth_payload_axis_tlast_reg;
+reg [ID_WIDTH-1:0]   temp_m_eth_payload_axis_tid_reg;
+reg [DEST_WIDTH-1:0] temp_m_eth_payload_axis_tdest_reg;
+reg [USER_WIDTH-1:0] temp_m_eth_payload_axis_tuser_reg;
+
+// datapath control
+reg store_axis_int_to_output;
+reg store_axis_int_to_temp;
+reg store_eth_payload_axis_temp_to_output;
+
+assign m_eth_payload_axis_tdata  = m_eth_payload_axis_tdata_reg;
+assign m_eth_payload_axis_tkeep  = KEEP_ENABLE ? m_eth_payload_axis_tkeep_reg : {KEEP_WIDTH{1'b1}};
+assign m_eth_payload_axis_tvalid = m_eth_payload_axis_tvalid_reg;
+assign m_eth_payload_axis_tlast  = m_eth_payload_axis_tlast_reg;
+assign m_eth_payload_axis_tid    = ID_ENABLE   ? m_eth_payload_axis_tid_reg   : {ID_WIDTH{1'b0}};
+assign m_eth_payload_axis_tdest  = DEST_ENABLE ? m_eth_payload_axis_tdest_reg : {DEST_WIDTH{1'b0}};
+assign m_eth_payload_axis_tuser  = USER_ENABLE ? m_eth_payload_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_eth_payload_axis_tready_int_early = m_eth_payload_axis_tready || (!temp_m_eth_payload_axis_tvalid_reg && (!m_eth_payload_axis_tvalid_reg || !m_eth_payload_axis_tvalid_int));
+
+always @* begin
+    // transfer sink ready state to source
+    m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_reg;
+    temp_m_eth_payload_axis_tvalid_next = temp_m_eth_payload_axis_tvalid_reg;
+
+    store_axis_int_to_output = 1'b0;
+    store_axis_int_to_temp = 1'b0;
+    store_eth_payload_axis_temp_to_output = 1'b0;
+
+    if (m_eth_payload_axis_tready_int_reg) begin
+        // input is ready
+        if (m_eth_payload_axis_tready || !m_eth_payload_axis_tvalid_reg) begin
+            // output is ready or currently not valid, transfer data to output
+            m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_int;
+            store_axis_int_to_output = 1'b1;
+        end else begin
+            // output is not ready, store input in temp
+            temp_m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_int;
+            store_axis_int_to_temp = 1'b1;
+        end
+    end else if (m_eth_payload_axis_tready) begin
+        // input is not ready, but output is ready
+        m_eth_payload_axis_tvalid_next = temp_m_eth_payload_axis_tvalid_reg;
+        temp_m_eth_payload_axis_tvalid_next = 1'b0;
+        store_eth_payload_axis_temp_to_output = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        m_eth_payload_axis_tready_int_reg <= 1'b0;
+        m_eth_payload_axis_tdata_reg  <= {DATA_WIDTH{1'b0}};
+        m_eth_payload_axis_tkeep_reg  <= {KEEP_WIDTH{1'b0}};
+        m_eth_payload_axis_tvalid_reg <= 1'b0;
+        m_eth_payload_axis_tlast_reg  <= 1'b0;
+        m_eth_payload_axis_tid_reg    <= {ID_WIDTH{1'b0}};
+        m_eth_payload_axis_tdest_reg  <= {DEST_WIDTH{1'b0}};
+        m_eth_payload_axis_tuser_reg  <= {USER_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tdata_reg  <= {DATA_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tkeep_reg  <= {KEEP_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tvalid_reg <= 1'b0;
+        temp_m_eth_payload_axis_tlast_reg  <= 1'b0;
+        temp_m_eth_payload_axis_tid_reg    <= {ID_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tdest_reg  <= {DEST_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tuser_reg  <= {USER_WIDTH{1'b0}};
+    end else begin
+        m_eth_payload_axis_tvalid_reg <= m_eth_payload_axis_tvalid_next;
+        m_eth_payload_axis_tready_int_reg <= m_eth_payload_axis_tready_int_early;
+        temp_m_eth_payload_axis_tvalid_reg <= temp_m_eth_payload_axis_tvalid_next;
+
+        // datapath
+        if (store_axis_int_to_output) begin
+            m_eth_payload_axis_tdata_reg <= m_eth_payload_axis_tdata_int;
+            m_eth_payload_axis_tkeep_reg <= m_eth_payload_axis_tkeep_int;
+            m_eth_payload_axis_tlast_reg <= m_eth_payload_axis_tlast_int;
+            m_eth_payload_axis_tid_reg   <= m_eth_payload_axis_tid_int;
+            m_eth_payload_axis_tdest_reg <= m_eth_payload_axis_tdest_int;
+            m_eth_payload_axis_tuser_reg <= m_eth_payload_axis_tuser_int;
+        end else if (store_eth_payload_axis_temp_to_output) begin
+            m_eth_payload_axis_tdata_reg <= temp_m_eth_payload_axis_tdata_reg;
+            m_eth_payload_axis_tkeep_reg <= temp_m_eth_payload_axis_tkeep_reg;
+            m_eth_payload_axis_tlast_reg <= temp_m_eth_payload_axis_tlast_reg;
+            m_eth_payload_axis_tid_reg   <= temp_m_eth_payload_axis_tid_reg;
+            m_eth_payload_axis_tdest_reg <= temp_m_eth_payload_axis_tdest_reg;
+            m_eth_payload_axis_tuser_reg <= temp_m_eth_payload_axis_tuser_reg;
+        end
+    
+        if (store_axis_int_to_temp) begin
+            temp_m_eth_payload_axis_tdata_reg <= m_eth_payload_axis_tdata_int;
+            temp_m_eth_payload_axis_tkeep_reg <= m_eth_payload_axis_tkeep_int;
+            temp_m_eth_payload_axis_tlast_reg <= m_eth_payload_axis_tlast_int;
+            temp_m_eth_payload_axis_tid_reg   <= m_eth_payload_axis_tid_int;
+            temp_m_eth_payload_axis_tdest_reg <= m_eth_payload_axis_tdest_int;
+            temp_m_eth_payload_axis_tuser_reg <= m_eth_payload_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/eth_axis_rx.v b/verilog/rtl/eth_axis_rx.v
new file mode 100644
index 0000000..aa9150e
--- /dev/null
+++ b/verilog/rtl/eth_axis_rx.v
@@ -0,0 +1,416 @@
+/*
+
+Copyright (c) 2014-2020 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
+
+`timescale 1ns / 1ps
+
+/*
+ * AXI4-Stream ethernet frame receiver (AXI in, Ethernet frame out)
+ */
+module eth_axis_rx #
+(
+    // Width of AXI stream interfaces in bits
+    parameter DATA_WIDTH = 8,
+    // Propagate tkeep signal
+    // If disabled, tkeep assumed to be 1'b1
+    parameter KEEP_ENABLE = (DATA_WIDTH>8),
+    // tkeep signal width (words per cycle)
+    parameter KEEP_WIDTH = (DATA_WIDTH/8)
+)
+(
+    input  wire                  clk,
+    input  wire                  rst,
+
+    /*
+     * AXI 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                  s_axis_tuser,
+
+    /*
+     * Ethernet frame output
+     */
+    output wire                  m_eth_hdr_valid,
+    input  wire                  m_eth_hdr_ready,
+    output wire [47:0]           m_eth_dest_mac,
+    output wire [47:0]           m_eth_src_mac,
+    output wire [15:0]           m_eth_type,
+    output wire [DATA_WIDTH-1:0] m_eth_payload_axis_tdata,
+    output wire [KEEP_WIDTH-1:0] m_eth_payload_axis_tkeep,
+    output wire                  m_eth_payload_axis_tvalid,
+    input  wire                  m_eth_payload_axis_tready,
+    output wire                  m_eth_payload_axis_tlast,
+    output wire                  m_eth_payload_axis_tuser,
+
+    /*
+     * Status signals
+     */
+    output wire                  busy,
+    output wire                  error_header_early_termination
+);
+
+parameter CYCLE_COUNT = (14+KEEP_WIDTH-1)/KEEP_WIDTH;
+
+parameter PTR_WIDTH = $clog2(CYCLE_COUNT);
+
+parameter OFFSET = 14 % KEEP_WIDTH;
+
+// bus width assertions
+initial begin
+    if (KEEP_WIDTH * 8 != DATA_WIDTH) begin
+        $error("Error: AXI stream interface requires byte (8-bit) granularity (instance %m)");
+        $finish;
+    end
+end
+
+/*
+
+Ethernet frame
+
+ Field                       Length
+ Destination MAC address     6 octets
+ Source MAC address          6 octets
+ Ethertype                   2 octets
+
+This module receives an Ethernet frame on an AXI stream interface, decodes
+and strips the headers, then produces the header fields in parallel along
+with the payload in a separate AXI stream.
+
+*/
+
+reg read_eth_header_reg, read_eth_header_next;
+reg read_eth_payload_reg, read_eth_payload_next;
+reg [PTR_WIDTH-1:0] ptr_reg, ptr_next;
+
+reg flush_save;
+reg transfer_in_save;
+
+reg s_axis_tready_reg, s_axis_tready_next;
+
+reg m_eth_hdr_valid_reg, m_eth_hdr_valid_next;
+reg [47:0] m_eth_dest_mac_reg, m_eth_dest_mac_next;
+reg [47:0] m_eth_src_mac_reg, m_eth_src_mac_next;
+reg [15:0] m_eth_type_reg, m_eth_type_next;
+
+reg busy_reg;
+reg error_header_early_termination_reg, error_header_early_termination_next;
+
+reg [DATA_WIDTH-1:0] save_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] save_axis_tkeep_reg;
+reg save_axis_tlast_reg;
+reg save_axis_tuser_reg;
+
+reg [DATA_WIDTH-1:0] shift_axis_tdata;
+reg [KEEP_WIDTH-1:0] shift_axis_tkeep;
+reg shift_axis_tvalid;
+reg shift_axis_tlast;
+reg shift_axis_tuser;
+reg shift_axis_input_tready;
+reg shift_axis_extra_cycle_reg;
+
+// internal datapath
+reg [DATA_WIDTH-1:0] m_eth_payload_axis_tdata_int;
+reg [KEEP_WIDTH-1:0] m_eth_payload_axis_tkeep_int;
+reg                  m_eth_payload_axis_tvalid_int;
+reg                  m_eth_payload_axis_tready_int_reg;
+reg                  m_eth_payload_axis_tlast_int;
+reg                  m_eth_payload_axis_tuser_int;
+wire                 m_eth_payload_axis_tready_int_early;
+
+assign s_axis_tready = s_axis_tready_reg;
+
+assign m_eth_hdr_valid = m_eth_hdr_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+
+assign busy = busy_reg;
+assign error_header_early_termination = error_header_early_termination_reg;
+
+always @* begin
+    if (OFFSET == 0) begin
+        // passthrough if no overlap
+        shift_axis_tdata = s_axis_tdata;
+        shift_axis_tkeep = s_axis_tkeep;
+        shift_axis_tvalid = s_axis_tvalid;
+        shift_axis_tlast = s_axis_tlast;
+        shift_axis_tuser = s_axis_tuser;
+        shift_axis_input_tready = 1'b1;
+    end else if (shift_axis_extra_cycle_reg) begin
+        shift_axis_tdata = {s_axis_tdata, save_axis_tdata_reg} >> (OFFSET*8);
+        shift_axis_tkeep = {{KEEP_WIDTH{1'b0}}, save_axis_tkeep_reg} >> OFFSET;
+        shift_axis_tvalid = 1'b1;
+        shift_axis_tlast = save_axis_tlast_reg;
+        shift_axis_tuser = save_axis_tuser_reg;
+        shift_axis_input_tready = flush_save;
+    end else begin
+        shift_axis_tdata = {s_axis_tdata, save_axis_tdata_reg} >> (OFFSET*8);
+        shift_axis_tkeep = {s_axis_tkeep, save_axis_tkeep_reg} >> OFFSET;
+        shift_axis_tvalid = s_axis_tvalid;
+        shift_axis_tlast = (s_axis_tlast && ((s_axis_tkeep & ({KEEP_WIDTH{1'b1}} << OFFSET)) == 0));
+        shift_axis_tuser = (s_axis_tuser && ((s_axis_tkeep & ({KEEP_WIDTH{1'b1}} << OFFSET)) == 0));
+        shift_axis_input_tready = !(s_axis_tlast && s_axis_tready && s_axis_tvalid);
+    end
+end
+
+always @* begin
+    read_eth_header_next = read_eth_header_reg;
+    read_eth_payload_next = read_eth_payload_reg;
+    ptr_next = ptr_reg;
+
+    s_axis_tready_next = m_eth_payload_axis_tready_int_early && shift_axis_input_tready && (!m_eth_hdr_valid || m_eth_hdr_ready);
+
+    flush_save = 1'b0;
+    transfer_in_save = 1'b0;
+
+    m_eth_hdr_valid_next = m_eth_hdr_valid_reg && !m_eth_hdr_ready;
+
+    m_eth_dest_mac_next = m_eth_dest_mac_reg;
+    m_eth_src_mac_next = m_eth_src_mac_reg;
+    m_eth_type_next = m_eth_type_reg;
+
+    error_header_early_termination_next = 1'b0;
+
+    m_eth_payload_axis_tdata_int = shift_axis_tdata;
+    m_eth_payload_axis_tkeep_int = shift_axis_tkeep;
+    m_eth_payload_axis_tvalid_int = 1'b0;
+    m_eth_payload_axis_tlast_int = shift_axis_tlast;
+    m_eth_payload_axis_tuser_int = shift_axis_tuser;
+
+    if ((s_axis_tready && s_axis_tvalid) || (m_eth_payload_axis_tready_int_reg && shift_axis_extra_cycle_reg)) begin
+        transfer_in_save = 1'b1;
+
+        if (read_eth_header_reg) begin
+            // word transfer in - store it
+            ptr_next = ptr_reg + 1;
+
+            `define _HEADER_FIELD_(offset, field) \
+                if (ptr_reg == offset/KEEP_WIDTH && (!KEEP_ENABLE || s_axis_tkeep[offset%KEEP_WIDTH])) begin \
+                    field = s_axis_tdata[(offset%KEEP_WIDTH)*8 +: 8]; \
+                end
+
+            `_HEADER_FIELD_(0,  m_eth_dest_mac_next[5*8 +: 8])
+            `_HEADER_FIELD_(1,  m_eth_dest_mac_next[4*8 +: 8])
+            `_HEADER_FIELD_(2,  m_eth_dest_mac_next[3*8 +: 8])
+            `_HEADER_FIELD_(3,  m_eth_dest_mac_next[2*8 +: 8])
+            `_HEADER_FIELD_(4,  m_eth_dest_mac_next[1*8 +: 8])
+            `_HEADER_FIELD_(5,  m_eth_dest_mac_next[0*8 +: 8])
+            `_HEADER_FIELD_(6,  m_eth_src_mac_next[5*8 +: 8])
+            `_HEADER_FIELD_(7,  m_eth_src_mac_next[4*8 +: 8])
+            `_HEADER_FIELD_(8,  m_eth_src_mac_next[3*8 +: 8])
+            `_HEADER_FIELD_(9,  m_eth_src_mac_next[2*8 +: 8])
+            `_HEADER_FIELD_(10, m_eth_src_mac_next[1*8 +: 8])
+            `_HEADER_FIELD_(11, m_eth_src_mac_next[0*8 +: 8])
+            `_HEADER_FIELD_(12, m_eth_type_next[1*8 +: 8])
+            `_HEADER_FIELD_(13, m_eth_type_next[0*8 +: 8])
+
+            if (ptr_reg == 13/KEEP_WIDTH && (!KEEP_ENABLE || s_axis_tkeep[13%KEEP_WIDTH])) begin
+                if (!shift_axis_tlast) begin
+                    m_eth_hdr_valid_next = 1'b1;
+                    read_eth_header_next = 1'b0;
+                    read_eth_payload_next = 1'b1;
+                end
+            end
+
+            `undef _HEADER_FIELD_
+        end
+
+        if (read_eth_payload_reg) begin
+            // transfer payload
+            m_eth_payload_axis_tdata_int = shift_axis_tdata;
+            m_eth_payload_axis_tkeep_int = shift_axis_tkeep;
+            m_eth_payload_axis_tvalid_int = 1'b1;
+            m_eth_payload_axis_tlast_int = shift_axis_tlast;
+            m_eth_payload_axis_tuser_int = shift_axis_tuser;
+        end
+
+        if (shift_axis_tlast) begin
+            if (read_eth_header_next) begin
+                // don't have the whole header
+                error_header_early_termination_next = 1'b1;
+            end
+
+            flush_save = 1'b1;
+            ptr_next = 1'b0;
+            read_eth_header_next = 1'b1;
+            read_eth_payload_next = 1'b0;
+        end
+    end
+end
+
+always @(posedge clk) begin
+    read_eth_header_reg <= read_eth_header_next;
+    read_eth_payload_reg <= read_eth_payload_next;
+    ptr_reg <= ptr_next;
+
+    s_axis_tready_reg <= s_axis_tready_next;
+
+    m_eth_hdr_valid_reg <= m_eth_hdr_valid_next;
+    m_eth_dest_mac_reg <= m_eth_dest_mac_next;
+    m_eth_src_mac_reg <= m_eth_src_mac_next;
+    m_eth_type_reg <= m_eth_type_next;
+
+    error_header_early_termination_reg <= error_header_early_termination_next;
+
+    busy_reg <= (read_eth_payload_next || ptr_next != 0);
+
+    if (transfer_in_save) begin
+        save_axis_tdata_reg <= s_axis_tdata;
+        save_axis_tkeep_reg <= s_axis_tkeep;
+        save_axis_tuser_reg <= s_axis_tuser;
+    end
+
+    if (flush_save) begin
+        save_axis_tlast_reg <= 1'b0;
+        shift_axis_extra_cycle_reg <= 1'b0;
+    end else if (transfer_in_save) begin
+        save_axis_tlast_reg <= s_axis_tlast;
+        shift_axis_extra_cycle_reg <= OFFSET ? s_axis_tlast && ((s_axis_tkeep & ({KEEP_WIDTH{1'b1}} << OFFSET)) != 0) : 1'b0;
+    end
+
+    if (rst) begin
+        read_eth_header_reg <= 1'b1;
+        read_eth_payload_reg <= 1'b0;
+        ptr_reg <= 0;
+        s_axis_tready_reg <= 1'b0;
+        m_eth_hdr_valid_reg <= 1'b0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+        save_axis_tlast_reg <= 1'b0;
+        shift_axis_extra_cycle_reg <= 1'b0;
+        busy_reg <= 1'b0;
+        error_header_early_termination_reg <= 1'b0;
+        save_axis_tdata_reg <= 64'd0;
+        save_axis_tkeep_reg <= 8'd0;
+        save_axis_tlast_reg <= 1'b0;
+        save_axis_tuser_reg <= 1'b0;
+    end
+end
+
+// output datapath logic
+reg [DATA_WIDTH-1:0] m_eth_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] m_eth_payload_axis_tkeep_reg;
+reg                  m_eth_payload_axis_tvalid_reg, m_eth_payload_axis_tvalid_next;
+reg                  m_eth_payload_axis_tlast_reg;
+reg                  m_eth_payload_axis_tuser_reg;
+
+reg [DATA_WIDTH-1:0] temp_m_eth_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] temp_m_eth_payload_axis_tkeep_reg;
+reg                  temp_m_eth_payload_axis_tvalid_reg, temp_m_eth_payload_axis_tvalid_next;
+reg                  temp_m_eth_payload_axis_tlast_reg;
+reg                  temp_m_eth_payload_axis_tuser_reg;
+
+// datapath control
+reg store_eth_payload_int_to_output;
+reg store_eth_payload_int_to_temp;
+reg store_eth_payload_axis_temp_to_output;
+
+assign m_eth_payload_axis_tdata = m_eth_payload_axis_tdata_reg;
+assign m_eth_payload_axis_tkeep = KEEP_ENABLE ? m_eth_payload_axis_tkeep_reg : {KEEP_WIDTH{1'b1}};
+assign m_eth_payload_axis_tvalid = m_eth_payload_axis_tvalid_reg;
+assign m_eth_payload_axis_tlast = m_eth_payload_axis_tlast_reg;
+assign m_eth_payload_axis_tuser = m_eth_payload_axis_tuser_reg;
+
+// 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_eth_payload_axis_tready_int_early = m_eth_payload_axis_tready || (!temp_m_eth_payload_axis_tvalid_reg && (!m_eth_payload_axis_tvalid_reg || !m_eth_payload_axis_tvalid_int));
+
+always @* begin
+    // transfer sink ready state to source
+    m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_reg;
+    temp_m_eth_payload_axis_tvalid_next = temp_m_eth_payload_axis_tvalid_reg;
+
+    store_eth_payload_int_to_output = 1'b0;
+    store_eth_payload_int_to_temp = 1'b0;
+    store_eth_payload_axis_temp_to_output = 1'b0;
+    
+    if (m_eth_payload_axis_tready_int_reg) begin
+        // input is ready
+        if (m_eth_payload_axis_tready || !m_eth_payload_axis_tvalid_reg) begin
+            // output is ready or currently not valid, transfer data to output
+            m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_int;
+            store_eth_payload_int_to_output = 1'b1;
+        end else begin
+            // output is not ready, store input in temp
+            temp_m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_int;
+            store_eth_payload_int_to_temp = 1'b1;
+        end
+    end else if (m_eth_payload_axis_tready) begin
+        // input is not ready, but output is ready
+        m_eth_payload_axis_tvalid_next = temp_m_eth_payload_axis_tvalid_reg;
+        temp_m_eth_payload_axis_tvalid_next = 1'b0;
+        store_eth_payload_axis_temp_to_output = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        m_eth_payload_axis_tdata_reg <= {DATA_WIDTH{1'b0}};
+        m_eth_payload_axis_tkeep_reg <= {KEEP_WIDTH{1'b0}};
+        m_eth_payload_axis_tvalid_reg <= 1'b0;
+        m_eth_payload_axis_tlast_reg <= 1'b0;
+        m_eth_payload_axis_tuser_reg <= 1'b0;
+        
+        m_eth_payload_axis_tready_int_reg <= 1'b0;
+        
+        temp_m_eth_payload_axis_tdata_reg <= {DATA_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tkeep_reg <= {KEEP_WIDTH{1'b0}};
+        temp_m_eth_payload_axis_tvalid_reg <= 1'b0;
+        temp_m_eth_payload_axis_tlast_reg <= 1'b0;
+        temp_m_eth_payload_axis_tuser_reg <= 1'b0;
+        
+    end else begin
+        m_eth_payload_axis_tvalid_reg <= m_eth_payload_axis_tvalid_next;
+        m_eth_payload_axis_tready_int_reg <= m_eth_payload_axis_tready_int_early;
+        temp_m_eth_payload_axis_tvalid_reg <= temp_m_eth_payload_axis_tvalid_next;
+
+        // datapath
+        if (store_eth_payload_int_to_output) begin
+            m_eth_payload_axis_tdata_reg <= m_eth_payload_axis_tdata_int;
+            m_eth_payload_axis_tkeep_reg <= m_eth_payload_axis_tkeep_int;
+            m_eth_payload_axis_tlast_reg <= m_eth_payload_axis_tlast_int;
+            m_eth_payload_axis_tuser_reg <= m_eth_payload_axis_tuser_int;
+        end else if (store_eth_payload_axis_temp_to_output) begin
+            m_eth_payload_axis_tdata_reg <= temp_m_eth_payload_axis_tdata_reg;
+            m_eth_payload_axis_tkeep_reg <= temp_m_eth_payload_axis_tkeep_reg;
+            m_eth_payload_axis_tlast_reg <= temp_m_eth_payload_axis_tlast_reg;
+            m_eth_payload_axis_tuser_reg <= temp_m_eth_payload_axis_tuser_reg;
+        end
+    
+        if (store_eth_payload_int_to_temp) begin
+            temp_m_eth_payload_axis_tdata_reg <= m_eth_payload_axis_tdata_int;
+            temp_m_eth_payload_axis_tkeep_reg <= m_eth_payload_axis_tkeep_int;
+            temp_m_eth_payload_axis_tlast_reg <= m_eth_payload_axis_tlast_int;
+            temp_m_eth_payload_axis_tuser_reg <= m_eth_payload_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/eth_axis_tx.v b/verilog/rtl/eth_axis_tx.v
new file mode 100644
index 0000000..8aaa588
--- /dev/null
+++ b/verilog/rtl/eth_axis_tx.v
@@ -0,0 +1,419 @@
+/*
+
+Copyright (c) 2014-2020 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
+
+`timescale 1ns / 1ps
+
+/*
+ * AXI4-Stream ethernet frame transmitter (Ethernet frame in, AXI out)
+ */
+module eth_axis_tx #
+(
+    // Width of AXI stream interfaces in bits
+    parameter DATA_WIDTH = 8,
+    // Propagate tkeep signal
+    // If disabled, tkeep assumed to be 1'b1
+    parameter KEEP_ENABLE = (DATA_WIDTH>8),
+    // tkeep signal width (words per cycle)
+    parameter KEEP_WIDTH = (DATA_WIDTH/8)
+)
+(
+    input  wire                  clk,
+    input  wire                  rst,
+
+    /*
+     * Ethernet frame input
+     */
+    input  wire                  s_eth_hdr_valid,
+    output wire                  s_eth_hdr_ready,
+    input  wire [47:0]           s_eth_dest_mac,
+    input  wire [47:0]           s_eth_src_mac,
+    input  wire [15:0]           s_eth_type,
+    input  wire [DATA_WIDTH-1:0] s_eth_payload_axis_tdata,
+    input  wire [KEEP_WIDTH-1:0] s_eth_payload_axis_tkeep,
+    input  wire                  s_eth_payload_axis_tvalid,
+    output wire                  s_eth_payload_axis_tready,
+    input  wire                  s_eth_payload_axis_tlast,
+    input  wire                  s_eth_payload_axis_tuser,
+
+    /*
+     * AXI 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                  m_axis_tuser,
+
+    /*
+     * Status signals
+     */
+    output wire                  busy
+);
+
+parameter CYCLE_COUNT = (14+KEEP_WIDTH-1)/KEEP_WIDTH;
+
+parameter PTR_WIDTH = $clog2(CYCLE_COUNT);
+
+parameter OFFSET = 14 % KEEP_WIDTH;
+
+// bus width assertions
+initial begin
+    if (KEEP_WIDTH * 8 != DATA_WIDTH) begin
+        $error("Error: AXI stream interface requires byte (8-bit) granularity (instance %m)");
+        $finish;
+    end
+end
+
+/*
+
+Ethernet frame
+
+ Field                       Length
+ Destination MAC address     6 octets
+ Source MAC address          6 octets
+ Ethertype                   2 octets
+
+This module receives an Ethernet frame with header fields in parallel along
+with the payload in an AXI stream, combines the header with the payload, and
+transmits the complete Ethernet frame on the output AXI stream interface.
+
+*/
+
+// datapath control signals
+reg store_eth_hdr;
+
+reg send_eth_header_reg, send_eth_header_next;
+reg send_eth_payload_reg, send_eth_payload_next;
+reg [PTR_WIDTH-1:0] ptr_reg, ptr_next;
+
+reg flush_save;
+reg transfer_in_save;
+
+reg [47:0] eth_dest_mac_reg;
+reg [47:0] eth_src_mac_reg;
+reg [15:0] eth_type_reg;
+
+reg s_eth_hdr_ready_reg, s_eth_hdr_ready_next;
+reg s_eth_payload_axis_tready_reg, s_eth_payload_axis_tready_next;
+
+reg busy_reg;
+
+reg [DATA_WIDTH-1:0] save_eth_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] save_eth_payload_axis_tkeep_reg;
+reg save_eth_payload_axis_tlast_reg;
+reg save_eth_payload_axis_tuser_reg;
+
+reg [DATA_WIDTH-1:0] shift_eth_payload_axis_tdata;
+reg [KEEP_WIDTH-1:0] shift_eth_payload_axis_tkeep;
+reg shift_eth_payload_axis_tvalid;
+reg shift_eth_payload_axis_tlast;
+reg shift_eth_payload_axis_tuser;
+reg shift_eth_payload_axis_input_tready;
+reg shift_eth_payload_axis_extra_cycle_reg;
+
+// 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;
+reg                  m_axis_tlast_int;
+reg                  m_axis_tuser_int;
+wire                 m_axis_tready_int_early;
+
+assign s_eth_hdr_ready = s_eth_hdr_ready_reg;
+assign s_eth_payload_axis_tready = s_eth_payload_axis_tready_reg;
+
+assign busy = busy_reg;
+
+always @* begin
+    if (OFFSET == 0) begin
+        // passthrough if no overlap
+        shift_eth_payload_axis_tdata = s_eth_payload_axis_tdata;
+        shift_eth_payload_axis_tkeep = s_eth_payload_axis_tkeep;
+        shift_eth_payload_axis_tvalid = s_eth_payload_axis_tvalid;
+        shift_eth_payload_axis_tlast = s_eth_payload_axis_tlast;
+        shift_eth_payload_axis_tuser = s_eth_payload_axis_tuser;
+        shift_eth_payload_axis_input_tready = 1'b1;
+    end else if (shift_eth_payload_axis_extra_cycle_reg) begin
+        shift_eth_payload_axis_tdata = {s_eth_payload_axis_tdata, save_eth_payload_axis_tdata_reg} >> ((KEEP_WIDTH-OFFSET)*8);
+        shift_eth_payload_axis_tkeep = {{KEEP_WIDTH{1'b0}}, save_eth_payload_axis_tkeep_reg} >> (KEEP_WIDTH-OFFSET);
+        shift_eth_payload_axis_tvalid = 1'b1;
+        shift_eth_payload_axis_tlast = save_eth_payload_axis_tlast_reg;
+        shift_eth_payload_axis_tuser = save_eth_payload_axis_tuser_reg;
+        shift_eth_payload_axis_input_tready = flush_save;
+    end else begin
+        shift_eth_payload_axis_tdata = {s_eth_payload_axis_tdata, save_eth_payload_axis_tdata_reg} >> ((KEEP_WIDTH-OFFSET)*8);
+        shift_eth_payload_axis_tkeep = {s_eth_payload_axis_tkeep, save_eth_payload_axis_tkeep_reg} >> (KEEP_WIDTH-OFFSET);
+        shift_eth_payload_axis_tvalid = s_eth_payload_axis_tvalid;
+        shift_eth_payload_axis_tlast = (s_eth_payload_axis_tlast && ((s_eth_payload_axis_tkeep & ({KEEP_WIDTH{1'b1}} << (KEEP_WIDTH-OFFSET))) == 0));
+        shift_eth_payload_axis_tuser = (s_eth_payload_axis_tuser && ((s_eth_payload_axis_tkeep & ({KEEP_WIDTH{1'b1}} << (KEEP_WIDTH-OFFSET))) == 0));
+        shift_eth_payload_axis_input_tready = !(s_eth_payload_axis_tlast && s_eth_payload_axis_tready && s_eth_payload_axis_tvalid);
+    end
+end
+
+always @* begin
+    send_eth_header_next = send_eth_header_reg;
+    send_eth_payload_next = send_eth_payload_reg;
+    ptr_next = ptr_reg;
+
+    s_eth_hdr_ready_next = 1'b0;
+    s_eth_payload_axis_tready_next = 1'b0;
+
+    store_eth_hdr = 1'b0;
+
+    flush_save = 1'b0;
+    transfer_in_save = 1'b0;
+
+    m_axis_tdata_int = {DATA_WIDTH{1'b0}};
+    m_axis_tkeep_int = {KEEP_WIDTH{1'b0}};
+    m_axis_tvalid_int = 1'b0;
+    m_axis_tlast_int = 1'b0;
+    m_axis_tuser_int = 1'b0;
+
+    if (s_eth_hdr_ready && s_eth_hdr_valid) begin
+        store_eth_hdr = 1'b1;
+        ptr_next = 0;
+        send_eth_header_next = 1'b1;
+        send_eth_payload_next = (OFFSET != 0) && (CYCLE_COUNT == 1);
+        s_eth_payload_axis_tready_next = send_eth_payload_next && m_axis_tready_int_early;
+    end
+
+    if (send_eth_payload_reg) begin
+        s_eth_payload_axis_tready_next = m_axis_tready_int_early && shift_eth_payload_axis_input_tready;
+
+        if ((s_eth_payload_axis_tready && s_eth_payload_axis_tvalid) || (m_axis_tready_int_reg && shift_eth_payload_axis_extra_cycle_reg)) begin
+            transfer_in_save = 1'b1;
+
+            m_axis_tdata_int = shift_eth_payload_axis_tdata;
+            m_axis_tkeep_int = shift_eth_payload_axis_tkeep;
+            m_axis_tvalid_int = 1'b1;
+            m_axis_tlast_int = shift_eth_payload_axis_tlast;
+            m_axis_tuser_int = shift_eth_payload_axis_tuser;
+
+            if (shift_eth_payload_axis_tlast) begin
+                flush_save = 1'b1;
+                s_eth_payload_axis_tready_next = 1'b0;
+                ptr_next = 0;
+                send_eth_payload_next = 1'b0;
+            end
+        end
+    end
+
+    if (m_axis_tready_int_reg && (!OFFSET || !send_eth_payload_reg || m_axis_tvalid_int)) begin
+        if (send_eth_header_reg) begin
+            ptr_next = ptr_reg + 1;
+
+            if ((OFFSET != 0) && (CYCLE_COUNT == 1 || ptr_next == CYCLE_COUNT-1) && !send_eth_payload_reg) begin
+                send_eth_payload_next = 1'b1;
+                s_eth_payload_axis_tready_next = m_axis_tready_int_early && shift_eth_payload_axis_input_tready;
+            end
+
+            m_axis_tvalid_int = 1'b1;
+
+            `define _HEADER_FIELD_(offset, field) \
+                if (ptr_reg == offset/KEEP_WIDTH) begin \
+                    m_axis_tdata_int[(offset%KEEP_WIDTH)*8 +: 8] = field; \
+                    m_axis_tkeep_int[offset%KEEP_WIDTH] = 1'b1; \
+                end
+
+            `_HEADER_FIELD_(0,  eth_dest_mac_reg[5*8 +: 8])
+            `_HEADER_FIELD_(1,  eth_dest_mac_reg[4*8 +: 8])
+            `_HEADER_FIELD_(2,  eth_dest_mac_reg[3*8 +: 8])
+            `_HEADER_FIELD_(3,  eth_dest_mac_reg[2*8 +: 8])
+            `_HEADER_FIELD_(4,  eth_dest_mac_reg[1*8 +: 8])
+            `_HEADER_FIELD_(5,  eth_dest_mac_reg[0*8 +: 8])
+            `_HEADER_FIELD_(6,  eth_src_mac_reg[5*8 +: 8])
+            `_HEADER_FIELD_(7,  eth_src_mac_reg[4*8 +: 8])
+            `_HEADER_FIELD_(8,  eth_src_mac_reg[3*8 +: 8])
+            `_HEADER_FIELD_(9,  eth_src_mac_reg[2*8 +: 8])
+            `_HEADER_FIELD_(10, eth_src_mac_reg[1*8 +: 8])
+            `_HEADER_FIELD_(11, eth_src_mac_reg[0*8 +: 8])
+            `_HEADER_FIELD_(12, eth_type_reg[1*8 +: 8])
+            `_HEADER_FIELD_(13, eth_type_reg[0*8 +: 8])
+
+            if (ptr_reg == 13/KEEP_WIDTH) begin
+                if (!send_eth_payload_reg) begin
+                    s_eth_payload_axis_tready_next = m_axis_tready_int_early;
+                    send_eth_payload_next = 1'b1;
+                end
+                send_eth_header_next = 1'b0;
+            end
+
+            `undef _HEADER_FIELD_
+        end
+    end
+
+    s_eth_hdr_ready_next = !(send_eth_header_next || send_eth_payload_next);
+end
+
+always @(posedge clk) begin
+    send_eth_header_reg <= send_eth_header_next;
+    send_eth_payload_reg <= send_eth_payload_next;
+    ptr_reg <= ptr_next;
+
+    s_eth_hdr_ready_reg <= s_eth_hdr_ready_next;
+    s_eth_payload_axis_tready_reg <= s_eth_payload_axis_tready_next;
+
+    busy_reg <= send_eth_header_next || send_eth_payload_next;
+
+    if (store_eth_hdr) begin
+        eth_dest_mac_reg <= s_eth_dest_mac;
+        eth_src_mac_reg <= s_eth_src_mac;
+        eth_type_reg <= s_eth_type;
+    end
+
+    if (transfer_in_save) begin
+        save_eth_payload_axis_tdata_reg <= s_eth_payload_axis_tdata;
+        save_eth_payload_axis_tkeep_reg <= s_eth_payload_axis_tkeep;
+        save_eth_payload_axis_tuser_reg <= s_eth_payload_axis_tuser;
+    end
+
+    if (flush_save) begin
+        save_eth_payload_axis_tlast_reg <= 1'b0;
+        shift_eth_payload_axis_extra_cycle_reg <= 1'b0;
+    end else if (transfer_in_save) begin
+        save_eth_payload_axis_tlast_reg <= s_eth_payload_axis_tlast;
+        shift_eth_payload_axis_extra_cycle_reg <= OFFSET ? s_eth_payload_axis_tlast && ((s_eth_payload_axis_tkeep & ({KEEP_WIDTH{1'b1}} << (KEEP_WIDTH-OFFSET))) != 0) : 1'b0;
+    end
+
+    if (rst) begin
+        send_eth_header_reg <= 1'b0;
+        send_eth_payload_reg <= 1'b0;
+        ptr_reg <= 0;
+        s_eth_hdr_ready_reg <= 1'b0;
+        s_eth_payload_axis_tready_reg <= 1'b0;
+        busy_reg <= 1'b0;
+        eth_dest_mac_reg <= 48'd0;
+        eth_src_mac_reg <= 48'd0;
+        eth_type_reg <= 16'd0;
+        save_eth_payload_axis_tdata_reg <= {DATA_WIDTH{1'b0}};
+        save_eth_payload_axis_tkeep_reg <= {KEEP_WIDTH{1'b0}};
+        save_eth_payload_axis_tlast_reg <= 1'b0;
+        save_eth_payload_axis_tuser_reg <= 1'b0;
+        shift_eth_payload_axis_extra_cycle_reg <= 1'b0;
+    end
+end
+
+// output datapath logic
+reg [DATA_WIDTH-1:0] m_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] m_axis_tkeep_reg;
+reg                  m_axis_tvalid_reg, m_axis_tvalid_next;
+reg                  m_axis_tlast_reg;
+reg                  m_axis_tuser_reg;
+
+reg [DATA_WIDTH-1:0] temp_m_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] temp_m_axis_tkeep_reg;
+reg                  temp_m_axis_tvalid_reg, temp_m_axis_tvalid_next;
+reg                  temp_m_axis_tlast_reg;
+reg                  temp_m_axis_tuser_reg;
+
+// 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_tuser = m_axis_tuser_reg;
+
+// 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
+    if (rst) begin
+        m_axis_tready_int_reg <= 1'b0;
+        
+        m_axis_tdata_reg <= {DATA_WIDTH{1'b0}};
+        m_axis_tkeep_reg <= {KEEP_WIDTH{1'b0}};
+        m_axis_tvalid_reg <= 1'b0;
+        m_axis_tlast_reg <= 1'b0;
+        m_axis_tuser_reg <= 1'b0;
+        
+        temp_m_axis_tdata_reg <= {DATA_WIDTH{1'b0}};
+        temp_m_axis_tkeep_reg <= {KEEP_WIDTH{1'b0}};
+        temp_m_axis_tvalid_reg <= 1'b0;
+        temp_m_axis_tlast_reg <= 1'b0;
+        temp_m_axis_tuser_reg <= 1'b0;
+    end else 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_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_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_tuser_reg <= m_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/eth_mac_1g.v b/verilog/rtl/eth_mac_1g.v
new file mode 100644
index 0000000..4f1075c
--- /dev/null
+++ b/verilog/rtl/eth_mac_1g.v
@@ -0,0 +1,167 @@
+/*
+
+Copyright (c) 2015-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * 1G Ethernet MAC
+ */
+module eth_mac_1g #
+(
+    parameter DATA_WIDTH = 8,
+    parameter ENABLE_PADDING = 1,
+    parameter MIN_FRAME_LENGTH = 64,
+    parameter TX_PTP_TS_ENABLE = 0,
+    parameter TX_PTP_TS_WIDTH = 96,
+    parameter TX_PTP_TAG_ENABLE = TX_PTP_TS_ENABLE,
+    parameter TX_PTP_TAG_WIDTH = 16,
+    parameter RX_PTP_TS_ENABLE = 0,
+    parameter RX_PTP_TS_WIDTH = 96,
+    parameter TX_USER_WIDTH = (TX_PTP_TAG_ENABLE ? TX_PTP_TAG_WIDTH : 0) + 1,
+    parameter RX_USER_WIDTH = (RX_PTP_TS_ENABLE ? RX_PTP_TS_WIDTH : 0) + 1
+)
+(
+    input  wire                         rx_clk,
+    input  wire                         rx_rst,
+    input  wire                         tx_clk,
+    input  wire                         tx_rst,
+
+    /*
+     * AXI input
+     */
+    input  wire [DATA_WIDTH-1:0]        tx_axis_tdata,
+    input  wire                         tx_axis_tvalid,
+    output wire                         tx_axis_tready,
+    input  wire                         tx_axis_tlast,
+    input  wire [TX_USER_WIDTH-1:0]     tx_axis_tuser,
+
+    /*
+     * AXI output
+     */
+    output wire [DATA_WIDTH-1:0]        rx_axis_tdata,
+    output wire                         rx_axis_tvalid,
+    output wire                         rx_axis_tlast,
+    output wire [RX_USER_WIDTH-1:0]     rx_axis_tuser,
+
+    /*
+     * GMII interface
+     */
+    input  wire [DATA_WIDTH-1:0]        gmii_rxd,
+    input  wire                         gmii_rx_dv,
+    input  wire                         gmii_rx_er,
+    output wire [DATA_WIDTH-1:0]        gmii_txd,
+    output wire                         gmii_tx_en,
+    output wire                         gmii_tx_er,
+
+    /*
+     * PTP
+     */
+    input  wire [TX_PTP_TS_WIDTH-1:0]   tx_ptp_ts,
+    input  wire [RX_PTP_TS_WIDTH-1:0]   rx_ptp_ts,
+    output wire [TX_PTP_TS_WIDTH-1:0]   tx_axis_ptp_ts,
+    output wire [TX_PTP_TAG_WIDTH-1:0]  tx_axis_ptp_ts_tag,
+    output wire                         tx_axis_ptp_ts_valid,
+
+    /*
+     * Control
+     */
+    input  wire                         rx_clk_enable,
+    input  wire                         tx_clk_enable,
+    input  wire                         rx_mii_select,
+    input  wire                         tx_mii_select,
+
+    /*
+     * Status
+     */
+    output wire                         tx_start_packet,
+    output wire                         tx_error_underflow,
+    output wire                         rx_start_packet,
+    output wire                         rx_error_bad_frame,
+    output wire                         rx_error_bad_fcs,
+
+    /*
+     * Configuration
+     */
+    input  wire [7:0]                   ifg_delay
+);
+
+axis_gmii_rx #(
+    .DATA_WIDTH(DATA_WIDTH),
+    .PTP_TS_ENABLE(RX_PTP_TS_ENABLE),
+    .PTP_TS_WIDTH(RX_PTP_TS_WIDTH),
+    .USER_WIDTH(RX_USER_WIDTH)
+)
+axis_gmii_rx_inst (
+    .clk(rx_clk),
+    .rst(rx_rst),
+    .gmii_rxd(gmii_rxd),
+    .gmii_rx_dv(gmii_rx_dv),
+    .gmii_rx_er(gmii_rx_er),
+    .m_axis_tdata(rx_axis_tdata),
+    .m_axis_tvalid(rx_axis_tvalid),
+    .m_axis_tlast(rx_axis_tlast),
+    .m_axis_tuser(rx_axis_tuser),
+    .ptp_ts(rx_ptp_ts),
+    .clk_enable(rx_clk_enable),
+    .mii_select(rx_mii_select),
+    .start_packet(rx_start_packet),
+    .error_bad_frame(rx_error_bad_frame),
+    .error_bad_fcs(rx_error_bad_fcs)
+);
+
+axis_gmii_tx #(
+    .DATA_WIDTH(DATA_WIDTH),
+    .ENABLE_PADDING(ENABLE_PADDING),
+    .MIN_FRAME_LENGTH(MIN_FRAME_LENGTH),
+    .PTP_TS_ENABLE(TX_PTP_TS_ENABLE),
+    .PTP_TS_WIDTH(TX_PTP_TS_WIDTH),
+    .PTP_TAG_ENABLE(TX_PTP_TAG_ENABLE),
+    .PTP_TAG_WIDTH(TX_PTP_TAG_WIDTH),
+    .USER_WIDTH(TX_USER_WIDTH)
+)
+axis_gmii_tx_inst (
+    .clk(tx_clk),
+    .rst(tx_rst),
+    .s_axis_tdata(tx_axis_tdata),
+    .s_axis_tvalid(tx_axis_tvalid),
+    .s_axis_tready(tx_axis_tready),
+    .s_axis_tlast(tx_axis_tlast),
+    .s_axis_tuser(tx_axis_tuser),
+    .gmii_txd(gmii_txd),
+    .gmii_tx_en(gmii_tx_en),
+    .gmii_tx_er(gmii_tx_er),
+    .ptp_ts(tx_ptp_ts),
+    .m_axis_ptp_ts(tx_axis_ptp_ts),
+    .m_axis_ptp_ts_tag(tx_axis_ptp_ts_tag),
+    .m_axis_ptp_ts_valid(tx_axis_ptp_ts_valid),
+    .clk_enable(tx_clk_enable),
+    .mii_select(tx_mii_select),
+    .ifg_delay(ifg_delay),
+    .start_packet(tx_start_packet),
+    .error_underflow(tx_error_underflow)
+);
+
+endmodule
diff --git a/verilog/rtl/eth_mac_mii.v b/verilog/rtl/eth_mac_mii.v
new file mode 100644
index 0000000..75c1f86
--- /dev/null
+++ b/verilog/rtl/eth_mac_mii.v
@@ -0,0 +1,166 @@
+/*
+
+Copyright (c) 2019 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
+
+`timescale 1ns / 1ps
+
+/*
+ * 10M/100M Ethernet MAC with MII interface
+ */
+module eth_mac_mii #
+(
+    // target ("SIM", "GENERIC", "XILINX", "ALTERA")
+    parameter TARGET = "GENERIC",
+    // Clock input style ("BUFG", "BUFR", "BUFIO", "BUFIO2")
+    // Use BUFR for Virtex-5, Virtex-6, 7-series
+    // Use BUFG for Ultrascale
+    // Use BUFIO2 for Spartan-6
+    parameter CLOCK_INPUT_STYLE = "BUFIO2",
+    parameter ENABLE_PADDING = 1,
+    parameter MIN_FRAME_LENGTH = 64
+)
+(
+    input  wire        rst,
+    output wire        rx_clk,
+    output wire        rx_rst,
+    output wire        tx_clk,
+    output wire        tx_rst,
+
+    /*
+     * AXI input
+     */
+    input  wire [7:0]  tx_axis_tdata,
+    input  wire        tx_axis_tvalid,
+    output wire        tx_axis_tready,
+    input  wire        tx_axis_tlast,
+    input  wire        tx_axis_tuser,
+
+    /*
+     * AXI output
+     */
+    output wire [7:0]  rx_axis_tdata,
+    output wire        rx_axis_tvalid,
+    output wire        rx_axis_tlast,
+    output wire        rx_axis_tuser,
+
+    /*
+     * MII interface
+     */
+    input  wire        mii_rx_clk,
+    input  wire [3:0]  mii_rxd,
+    input  wire        mii_rx_dv,
+    input  wire        mii_rx_er,
+    input  wire        mii_tx_clk,
+    output wire [3:0]  mii_txd,
+    output wire        mii_tx_en,
+    output wire        mii_tx_er,
+
+    /*
+     * Status
+     */
+    output wire        tx_start_packet,
+    output wire        tx_error_underflow,
+    output wire        rx_start_packet,
+    output wire        rx_error_bad_frame,
+    output wire        rx_error_bad_fcs,
+
+    /*
+     * Configuration
+     */
+    input  wire [7:0]  ifg_delay
+);
+
+wire [3:0]  mac_mii_rxd;
+wire        mac_mii_rx_dv;
+wire        mac_mii_rx_er;
+wire [3:0]  mac_mii_txd;
+wire        mac_mii_tx_en;
+wire        mac_mii_tx_er;
+
+mii_phy_if #(
+    .TARGET(TARGET),
+    .CLOCK_INPUT_STYLE(CLOCK_INPUT_STYLE)
+)
+mii_phy_if_inst (
+    .rst(rst),
+
+    .mac_mii_rx_clk(rx_clk),
+    .mac_mii_rx_rst(rx_rst),
+    .mac_mii_rxd(mac_mii_rxd),
+    .mac_mii_rx_dv(mac_mii_rx_dv),
+    .mac_mii_rx_er(mac_mii_rx_er),
+    .mac_mii_tx_clk(tx_clk),
+    .mac_mii_tx_rst(tx_rst),
+    .mac_mii_txd(mac_mii_txd),
+    .mac_mii_tx_en(mac_mii_tx_en),
+    .mac_mii_tx_er(mac_mii_tx_er),
+
+    .phy_mii_rx_clk(mii_rx_clk),
+    .phy_mii_rxd(mii_rxd),
+    .phy_mii_rx_dv(mii_rx_dv),
+    .phy_mii_rx_er(mii_rx_er),
+    .phy_mii_tx_clk(mii_tx_clk),
+    .phy_mii_txd(mii_txd),
+    .phy_mii_tx_en(mii_tx_en),
+    .phy_mii_tx_er(mii_tx_er)
+);
+
+eth_mac_1g #(
+    .ENABLE_PADDING(ENABLE_PADDING),
+    .MIN_FRAME_LENGTH(MIN_FRAME_LENGTH)
+)
+eth_mac_1g_inst (
+    .tx_clk(tx_clk),
+    .tx_rst(tx_rst),
+    .rx_clk(rx_clk),
+    .rx_rst(rx_rst),
+    .tx_axis_tdata(tx_axis_tdata),
+    .tx_axis_tvalid(tx_axis_tvalid),
+    .tx_axis_tready(tx_axis_tready),
+    .tx_axis_tlast(tx_axis_tlast),
+    .tx_axis_tuser(tx_axis_tuser),
+    .rx_axis_tdata(rx_axis_tdata),
+    .rx_axis_tvalid(rx_axis_tvalid),
+    .rx_axis_tlast(rx_axis_tlast),
+    .rx_axis_tuser(rx_axis_tuser),
+    .gmii_rxd(mac_mii_rxd),
+    .gmii_rx_dv(mac_mii_rx_dv),
+    .gmii_rx_er(mac_mii_rx_er),
+    .gmii_txd(mac_mii_txd),
+    .gmii_tx_en(mac_mii_tx_en),
+    .gmii_tx_er(mac_mii_tx_er),
+    .rx_clk_enable(1'b1),
+    .tx_clk_enable(1'b1),
+    .rx_mii_select(1'b1),
+    .tx_mii_select(1'b1),
+    .tx_start_packet(tx_start_packet),
+    .tx_error_underflow(tx_error_underflow),
+    .rx_start_packet(rx_start_packet),
+    .rx_error_bad_frame(rx_error_bad_frame),
+    .rx_error_bad_fcs(rx_error_bad_fcs),
+    .ifg_delay(ifg_delay)
+);
+
+endmodule
diff --git a/verilog/rtl/eth_mac_mii_fifo.v b/verilog/rtl/eth_mac_mii_fifo.v
new file mode 100644
index 0000000..95ef99a
--- /dev/null
+++ b/verilog/rtl/eth_mac_mii_fifo.v
@@ -0,0 +1,326 @@
+/*
+
+Copyright (c) 2019 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
+
+`timescale 1ns / 1ps
+
+/*
+ * 10M/100M Ethernet MAC with MII interface and TX and RX FIFOs
+ */
+module eth_mac_mii_fifo #
+(
+    // target ("SIM", "GENERIC", "XILINX", "ALTERA")
+    parameter TARGET = "GENERIC",
+    // Clock input style ("BUFG", "BUFR", "BUFIO", "BUFIO2")
+    // Use BUFR for Virtex-5, Virtex-6, 7-series
+    // Use BUFG for Ultrascale
+    // Use BUFIO2 for Spartan-6
+    parameter CLOCK_INPUT_STYLE = "BUFIO2",
+    parameter AXIS_DATA_WIDTH = 8,
+    parameter AXIS_KEEP_ENABLE = (AXIS_DATA_WIDTH>8),
+    parameter AXIS_KEEP_WIDTH = (AXIS_DATA_WIDTH/8),
+    parameter ENABLE_PADDING = 1,
+    parameter MIN_FRAME_LENGTH = 64,
+    parameter TX_FIFO_DEPTH = 4096,
+    parameter TX_FIFO_PIPELINE_OUTPUT = 2,
+    parameter TX_FRAME_FIFO = 1,
+    parameter TX_DROP_BAD_FRAME = TX_FRAME_FIFO,
+    parameter TX_DROP_WHEN_FULL = 0,
+    parameter RX_FIFO_DEPTH = 4096,
+    parameter RX_FIFO_PIPELINE_OUTPUT = 2,
+    parameter RX_FRAME_FIFO = 1,
+    parameter RX_DROP_BAD_FRAME = RX_FRAME_FIFO,
+    parameter RX_DROP_WHEN_FULL = RX_FRAME_FIFO
+)
+(
+    input  wire                       rst,
+    input  wire                       logic_clk,
+    input  wire                       logic_rst,
+
+    /*
+     * AXI input
+     */
+    input  wire [AXIS_DATA_WIDTH-1:0] tx_axis_tdata,
+    input  wire [AXIS_KEEP_WIDTH-1:0] tx_axis_tkeep,
+    input  wire                       tx_axis_tvalid,
+    output wire                       tx_axis_tready,
+    input  wire                       tx_axis_tlast,
+    input  wire                       tx_axis_tuser,
+
+    /*
+     * AXI output
+     */
+    output wire [AXIS_DATA_WIDTH-1:0] rx_axis_tdata,
+    output wire [AXIS_KEEP_WIDTH-1:0] rx_axis_tkeep,
+    output wire                       rx_axis_tvalid,
+    input  wire                       rx_axis_tready,
+    output wire                       rx_axis_tlast,
+    output wire                       rx_axis_tuser,
+
+    /*
+     * MII interface
+     */
+    input  wire                       mii_rx_clk,
+    input  wire [3:0]                 mii_rxd,
+    input  wire                       mii_rx_dv,
+    input  wire                       mii_rx_er,
+    input  wire                       mii_tx_clk,
+    output wire [3:0]                 mii_txd,
+    output wire                       mii_tx_en,
+    output wire                       mii_tx_er,
+
+    /*
+     * Status
+     */
+    output wire                       tx_error_underflow,
+    output wire                       tx_fifo_overflow,
+    output wire                       tx_fifo_bad_frame,
+    output wire                       tx_fifo_good_frame,
+    output wire                       rx_error_bad_frame,
+    output wire                       rx_error_bad_fcs,
+    output wire                       rx_fifo_overflow,
+    output wire                       rx_fifo_bad_frame,
+    output wire                       rx_fifo_good_frame,
+
+    /*
+     * Configuration
+     */
+    input  wire [7:0]                 ifg_delay
+);
+
+wire tx_clk;
+wire rx_clk;
+wire tx_rst;
+wire rx_rst;
+
+wire [7:0]  tx_fifo_axis_tdata;
+wire        tx_fifo_axis_tvalid;
+wire        tx_fifo_axis_tready;
+wire        tx_fifo_axis_tlast;
+wire        tx_fifo_axis_tuser;
+
+wire [7:0]  rx_fifo_axis_tdata;
+wire        rx_fifo_axis_tvalid;
+wire        rx_fifo_axis_tlast;
+wire        rx_fifo_axis_tuser;
+
+// synchronize MAC status signals into logic clock domain
+wire tx_error_underflow_int;
+
+reg [0:0] tx_sync_reg_1;
+reg [0:0] tx_sync_reg_2;
+reg [0:0] tx_sync_reg_3;
+reg [0:0] tx_sync_reg_4;
+
+assign tx_error_underflow = tx_sync_reg_3[0] ^ tx_sync_reg_4[0];
+
+always @(posedge tx_clk or posedge tx_rst) begin
+    if (tx_rst) begin
+        tx_sync_reg_1 <= 1'b0;
+    end else begin
+        tx_sync_reg_1 <= tx_sync_reg_1 ^ {tx_error_underflow_int};
+    end
+end
+
+always @(posedge logic_clk or posedge logic_rst) begin
+    if (logic_rst) begin
+        tx_sync_reg_2 <= 1'b0;
+        tx_sync_reg_3 <= 1'b0;
+        tx_sync_reg_4 <= 1'b0;
+    end else begin
+        tx_sync_reg_2 <= tx_sync_reg_1;
+        tx_sync_reg_3 <= tx_sync_reg_2;
+        tx_sync_reg_4 <= tx_sync_reg_3;
+    end
+end
+
+wire rx_error_bad_frame_int;
+wire rx_error_bad_fcs_int;
+
+reg [1:0] rx_sync_reg_1;
+reg [1:0] rx_sync_reg_2;
+reg [1:0] rx_sync_reg_3;
+reg [1:0] rx_sync_reg_4;
+
+assign rx_error_bad_frame = rx_sync_reg_3[0] ^ rx_sync_reg_4[0];
+assign rx_error_bad_fcs = rx_sync_reg_3[1] ^ rx_sync_reg_4[1];
+
+always @(posedge rx_clk or posedge rx_rst) begin
+    if (rx_rst) begin
+        rx_sync_reg_1 <= 2'd0;
+    end else begin
+        rx_sync_reg_1 <= rx_sync_reg_1 ^ {rx_error_bad_fcs_int, rx_error_bad_frame_int};
+    end
+end
+
+always @(posedge logic_clk or posedge logic_rst) begin
+    if (logic_rst) begin
+        rx_sync_reg_2 <= 2'd0;
+        rx_sync_reg_3 <= 2'd0;
+        rx_sync_reg_4 <= 2'd0;
+    end else begin
+        rx_sync_reg_2 <= rx_sync_reg_1;
+        rx_sync_reg_3 <= rx_sync_reg_2;
+        rx_sync_reg_4 <= rx_sync_reg_3;
+    end
+end
+
+eth_mac_mii #(
+    .TARGET(TARGET),
+    .CLOCK_INPUT_STYLE(CLOCK_INPUT_STYLE),
+    .ENABLE_PADDING(ENABLE_PADDING),
+    .MIN_FRAME_LENGTH(MIN_FRAME_LENGTH)
+)
+eth_mac_1g_mii_inst (
+    .rst(rst),
+    .tx_clk(tx_clk),
+    .tx_rst(tx_rst),
+    .rx_clk(rx_clk),
+    .rx_rst(rx_rst),
+    .tx_axis_tdata(tx_fifo_axis_tdata),
+    .tx_axis_tvalid(tx_fifo_axis_tvalid),
+    .tx_axis_tready(tx_fifo_axis_tready),
+    .tx_axis_tlast(tx_fifo_axis_tlast),
+    .tx_axis_tuser(tx_fifo_axis_tuser),
+    .rx_axis_tdata(rx_fifo_axis_tdata),
+    .rx_axis_tvalid(rx_fifo_axis_tvalid),
+    .rx_axis_tlast(rx_fifo_axis_tlast),
+    .rx_axis_tuser(rx_fifo_axis_tuser),
+    .mii_rx_clk(mii_rx_clk),
+    .mii_rxd(mii_rxd),
+    .mii_rx_dv(mii_rx_dv),
+    .mii_rx_er(mii_rx_er),
+    .mii_tx_clk(mii_tx_clk),
+    .mii_txd(mii_txd),
+    .mii_tx_en(mii_tx_en),
+    .mii_tx_er(mii_tx_er),
+    .tx_error_underflow(tx_error_underflow_int),
+    .rx_error_bad_frame(rx_error_bad_frame_int),
+    .rx_error_bad_fcs(rx_error_bad_fcs_int),
+    .ifg_delay(ifg_delay)
+);
+
+axis_async_fifo_adapter #(
+    .DEPTH(TX_FIFO_DEPTH),
+    .S_DATA_WIDTH(AXIS_DATA_WIDTH),
+    .S_KEEP_ENABLE(AXIS_KEEP_ENABLE),
+    .S_KEEP_WIDTH(AXIS_KEEP_WIDTH),
+    .M_DATA_WIDTH(8),
+    .M_KEEP_ENABLE(0),
+    .ID_ENABLE(0),
+    .DEST_ENABLE(0),
+    .USER_ENABLE(1),
+    .USER_WIDTH(1),
+    .PIPELINE_OUTPUT(TX_FIFO_PIPELINE_OUTPUT),
+    .FRAME_FIFO(TX_FRAME_FIFO),
+    .USER_BAD_FRAME_VALUE(1'b1),
+    .USER_BAD_FRAME_MASK(1'b1),
+    .DROP_BAD_FRAME(TX_DROP_BAD_FRAME),
+    .DROP_WHEN_FULL(TX_DROP_WHEN_FULL)
+)
+tx_fifo (
+    // AXI input
+    .s_clk(logic_clk),
+    .s_rst(logic_rst),
+    .s_axis_tdata(tx_axis_tdata),
+    .s_axis_tkeep(tx_axis_tkeep),
+    .s_axis_tvalid(tx_axis_tvalid),
+    .s_axis_tready(tx_axis_tready),
+    .s_axis_tlast(tx_axis_tlast),
+    .s_axis_tid(0),
+    .s_axis_tdest(0),
+    .s_axis_tuser(tx_axis_tuser),
+    // AXI output
+    .m_clk(tx_clk),
+    .m_rst(tx_rst),
+    .m_axis_tdata(tx_fifo_axis_tdata),
+    .m_axis_tkeep(),
+    .m_axis_tvalid(tx_fifo_axis_tvalid),
+    .m_axis_tready(tx_fifo_axis_tready),
+    .m_axis_tlast(tx_fifo_axis_tlast),
+    .m_axis_tid(),
+    .m_axis_tdest(),
+    .m_axis_tuser(tx_fifo_axis_tuser),
+    // Status
+    .s_status_overflow(tx_fifo_overflow),
+    .s_status_bad_frame(tx_fifo_bad_frame),
+    .s_status_good_frame(tx_fifo_good_frame),
+    .m_status_overflow(),
+    .m_status_bad_frame(),
+    .m_status_good_frame()
+);
+
+axis_async_fifo_adapter #(
+    .DEPTH(RX_FIFO_DEPTH),
+    .S_DATA_WIDTH(8),
+    .S_KEEP_ENABLE(0),
+    .M_DATA_WIDTH(AXIS_DATA_WIDTH),
+    .M_KEEP_ENABLE(AXIS_KEEP_ENABLE),
+    .M_KEEP_WIDTH(AXIS_KEEP_WIDTH),
+    .ID_ENABLE(0),
+    .DEST_ENABLE(0),
+    .USER_ENABLE(1),
+    .USER_WIDTH(1),
+    .PIPELINE_OUTPUT(RX_FIFO_PIPELINE_OUTPUT),
+    .FRAME_FIFO(RX_FRAME_FIFO),
+    .USER_BAD_FRAME_VALUE(1'b1),
+    .USER_BAD_FRAME_MASK(1'b1),
+    .DROP_BAD_FRAME(RX_DROP_BAD_FRAME),
+    .DROP_WHEN_FULL(RX_DROP_WHEN_FULL)
+)
+rx_fifo (
+    // AXI input
+    .s_clk(rx_clk),
+    .s_rst(rx_rst),
+    .s_axis_tdata(rx_fifo_axis_tdata),
+    .s_axis_tkeep(0),
+    .s_axis_tvalid(rx_fifo_axis_tvalid),
+    .s_axis_tready(),
+    .s_axis_tlast(rx_fifo_axis_tlast),
+    .s_axis_tid(0),
+    .s_axis_tdest(0),
+    .s_axis_tuser(rx_fifo_axis_tuser),
+    // AXI output
+    .m_clk(logic_clk),
+    .m_rst(logic_rst),
+    .m_axis_tdata(rx_axis_tdata),
+    .m_axis_tkeep(rx_axis_tkeep),
+    .m_axis_tvalid(rx_axis_tvalid),
+    .m_axis_tready(rx_axis_tready),
+    .m_axis_tlast(rx_axis_tlast),
+    .m_axis_tid(),
+    .m_axis_tdest(),
+    .m_axis_tuser(rx_axis_tuser),
+    // Status
+    .s_status_overflow(),
+    .s_status_bad_frame(),
+    .s_status_good_frame(),
+    .m_status_overflow(rx_fifo_overflow),
+    .m_status_bad_frame(rx_fifo_bad_frame),
+    .m_status_good_frame(rx_fifo_good_frame)
+);
+
+endmodule
+
diff --git a/verilog/rtl/fpga_core.v b/verilog/rtl/fpga_core.v
new file mode 100644
index 0000000..02c1634
--- /dev/null
+++ b/verilog/rtl/fpga_core.v
@@ -0,0 +1,492 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * FPGA core logic
+ */
+module fpga_core #
+(
+    parameter TARGET = "GENERIC",
+    parameter LOCAL_MAC   = 48'h02_00_00_00_00_00,
+    parameter LOCAL_IP    = {8'd192, 8'd168, 8'd1,   8'd128},
+    parameter GATEWAY_IP  = {8'd192, 8'd168, 8'd1,   8'd1},
+    parameter SUBNET_MASK = {8'd255, 8'd255, 8'd255, 8'd0}
+)
+(
+    /*
+     * Clock: 125MHz
+     * Synchronous reset
+     */
+    input  wire       clk,
+    input  wire       rst,
+
+    /*
+     * Ethernet: 100BASE-T MII
+     */
+    input  wire       phy_rx_clk,
+    input  wire [3:0] phy_rxd,
+    input  wire       phy_rx_dv,
+    input  wire       phy_rx_er,
+    input  wire       phy_tx_clk,
+    output wire [3:0] phy_txd,
+    output wire       phy_tx_en,
+    output wire       phy_reset_n,
+    
+    /*
+     * AXIS Interface
+     */
+         
+    output wire [7:0] rx_udp_payload_axis_tdata,
+    output wire rx_udp_payload_axis_tvalid,
+    input  wire rx_udp_payload_axis_tready,
+    output wire rx_udp_payload_axis_tlast,
+    output wire rx_udp_payload_axis_tuser,
+
+    input  wire [7:0] tx_udp_payload_axis_tdata,
+    input  wire tx_udp_payload_axis_tvalid,
+    output wire tx_udp_payload_axis_tready,
+    input  wire tx_udp_payload_axis_tlast,
+    input  wire tx_udp_payload_axis_tuser
+);
+
+// AXI between MAC and Ethernet modules
+wire [7:0] rx_axis_tdata;
+wire rx_axis_tvalid;
+wire rx_axis_tready;
+wire rx_axis_tlast;
+wire rx_axis_tuser;
+
+wire [7:0] tx_axis_tdata;
+wire tx_axis_tvalid;
+wire tx_axis_tready;
+wire tx_axis_tlast;
+wire tx_axis_tuser;
+
+// Ethernet frame between Ethernet modules and UDP stack
+wire rx_eth_hdr_ready;
+wire rx_eth_hdr_valid;
+wire [47:0] rx_eth_dest_mac;
+wire [47:0] rx_eth_src_mac;
+wire [15:0] rx_eth_type;
+wire [7:0] rx_eth_payload_axis_tdata;
+wire rx_eth_payload_axis_tvalid;
+wire rx_eth_payload_axis_tready;
+wire rx_eth_payload_axis_tlast;
+wire rx_eth_payload_axis_tuser;
+
+wire tx_eth_hdr_ready;
+wire tx_eth_hdr_valid;
+wire [47:0] tx_eth_dest_mac;
+wire [47:0] tx_eth_src_mac;
+wire [15:0] tx_eth_type;
+wire [7:0] tx_eth_payload_axis_tdata;
+wire tx_eth_payload_axis_tvalid;
+wire tx_eth_payload_axis_tready;
+wire tx_eth_payload_axis_tlast;
+wire tx_eth_payload_axis_tuser;
+
+// IP frame connections
+wire rx_ip_hdr_valid;
+wire rx_ip_hdr_ready;
+wire [47:0] rx_ip_eth_dest_mac;
+wire [47:0] rx_ip_eth_src_mac;
+wire [15:0] rx_ip_eth_type;
+wire [3:0] rx_ip_version;
+wire [3:0] rx_ip_ihl;
+wire [5:0] rx_ip_dscp;
+wire [1:0] rx_ip_ecn;
+wire [15:0] rx_ip_length;
+wire [15:0] rx_ip_identification;
+wire [2:0] rx_ip_flags;
+wire [12:0] rx_ip_fragment_offset;
+wire [7:0] rx_ip_ttl;
+wire [7:0] rx_ip_protocol;
+wire [15:0] rx_ip_header_checksum;
+wire [31:0] rx_ip_source_ip;
+wire [31:0] rx_ip_dest_ip;
+wire [7:0] rx_ip_payload_axis_tdata;
+wire rx_ip_payload_axis_tvalid;
+wire rx_ip_payload_axis_tready;
+wire rx_ip_payload_axis_tlast;
+wire rx_ip_payload_axis_tuser;
+
+wire tx_ip_hdr_valid;
+wire tx_ip_hdr_ready;
+wire [5:0] tx_ip_dscp;
+wire [1:0] tx_ip_ecn;
+wire [15:0] tx_ip_length;
+wire [7:0] tx_ip_ttl;
+wire [7:0] tx_ip_protocol;
+wire [31:0] tx_ip_source_ip;
+wire [31:0] tx_ip_dest_ip;
+wire [7:0] tx_ip_payload_axis_tdata;
+wire tx_ip_payload_axis_tvalid;
+wire tx_ip_payload_axis_tready;
+wire tx_ip_payload_axis_tlast;
+wire tx_ip_payload_axis_tuser;
+
+// UDP frame connections
+wire rx_udp_hdr_valid;
+wire rx_udp_hdr_ready;
+wire [47:0] rx_udp_eth_dest_mac;
+wire [47:0] rx_udp_eth_src_mac;
+wire [15:0] rx_udp_eth_type;
+wire [3:0] rx_udp_ip_version;
+wire [3:0] rx_udp_ip_ihl;
+wire [5:0] rx_udp_ip_dscp;
+wire [1:0] rx_udp_ip_ecn;
+wire [15:0] rx_udp_ip_length;
+wire [15:0] rx_udp_ip_identification;
+wire [2:0] rx_udp_ip_flags;
+wire [12:0] rx_udp_ip_fragment_offset;
+wire [7:0] rx_udp_ip_ttl;
+wire [7:0] rx_udp_ip_protocol;
+wire [15:0] rx_udp_ip_header_checksum;
+wire [31:0] rx_udp_ip_source_ip;
+wire [31:0] rx_udp_ip_dest_ip;
+wire [15:0] rx_udp_source_port;
+wire [15:0] rx_udp_dest_port;
+wire [15:0] rx_udp_length;
+wire [15:0] rx_udp_checksum;
+
+wire tx_udp_hdr_valid;
+wire tx_udp_hdr_ready;
+wire [5:0] tx_udp_ip_dscp;
+wire [1:0] tx_udp_ip_ecn;
+wire [7:0] tx_udp_ip_ttl;
+wire [31:0] tx_udp_ip_source_ip;
+wire [31:0] tx_udp_ip_dest_ip;
+wire [15:0] tx_udp_source_port;
+wire [15:0] tx_udp_dest_port;
+wire [15:0] tx_udp_length;
+wire [15:0] tx_udp_checksum;
+
+wire [7:0] rx_fifo_udp_payload_axis_tdata;
+wire rx_fifo_udp_payload_axis_tvalid;
+wire rx_fifo_udp_payload_axis_tready;
+wire rx_fifo_udp_payload_axis_tlast;
+wire rx_fifo_udp_payload_axis_tuser;
+
+wire [7:0] tx_fifo_udp_payload_axis_tdata;
+wire tx_fifo_udp_payload_axis_tvalid;
+wire tx_fifo_udp_payload_axis_tready;
+wire tx_fifo_udp_payload_axis_tlast;
+wire tx_fifo_udp_payload_axis_tuser;
+
+// IP ports not used
+assign rx_ip_hdr_ready = 1;
+assign rx_ip_payload_axis_tready = 1;
+
+assign tx_ip_hdr_valid = 0;
+assign tx_ip_dscp = 0;
+assign tx_ip_ecn = 0;
+assign tx_ip_length = 0;
+assign tx_ip_ttl = 0;
+assign tx_ip_protocol = 0;
+assign tx_ip_source_ip = 0;
+assign tx_ip_dest_ip = 0;
+assign tx_ip_payload_axis_tdata = 0;
+assign tx_ip_payload_axis_tvalid = 0;
+assign tx_ip_payload_axis_tlast = 0;
+assign tx_ip_payload_axis_tuser = 0;
+
+// Loop back UDP
+wire match_cond = rx_udp_dest_port == 1234;
+wire no_match = !match_cond;
+
+reg match_cond_reg;
+reg no_match_reg;
+
+always @(posedge clk) begin
+    if (rst) begin
+        match_cond_reg <= 0;
+        no_match_reg <= 0;
+    end else begin
+        if (rx_udp_payload_axis_tvalid) begin
+            if ((!match_cond_reg && !no_match_reg) ||
+                (rx_udp_payload_axis_tvalid && rx_udp_payload_axis_tready && rx_udp_payload_axis_tlast)) begin
+                match_cond_reg <= match_cond;
+                no_match_reg <= no_match;
+            end
+        end else begin
+            match_cond_reg <= 0;
+            no_match_reg <= 0;
+        end
+    end
+end
+
+assign tx_udp_hdr_valid = rx_udp_hdr_valid && match_cond;
+assign rx_udp_hdr_ready = (tx_eth_hdr_ready && match_cond) || no_match;
+assign tx_udp_ip_dscp = 0;
+assign tx_udp_ip_ecn = 0;
+assign tx_udp_ip_ttl = 64;
+assign tx_udp_ip_source_ip = LOCAL_IP;
+assign tx_udp_ip_dest_ip = rx_udp_ip_source_ip;
+assign tx_udp_source_port = rx_udp_dest_port;
+assign tx_udp_dest_port = rx_udp_source_port;
+assign tx_udp_length = rx_udp_length;
+assign tx_udp_checksum = 0;
+
+assign phy_reset_n = !rst;
+
+eth_mac_mii_fifo #(
+    .TARGET(TARGET),
+    .CLOCK_INPUT_STYLE("BUFR"),
+    .ENABLE_PADDING(1),
+    .MIN_FRAME_LENGTH(64),
+    .TX_FIFO_DEPTH(8),
+    .TX_FRAME_FIFO(0),
+    .RX_FIFO_DEPTH(8),
+    .RX_FRAME_FIFO(0)
+)
+eth_mac_inst (
+    .rst(rst),
+    .logic_clk(clk),
+    .logic_rst(rst),
+
+    .tx_axis_tdata(tx_axis_tdata),
+    .tx_axis_tvalid(tx_axis_tvalid),
+    .tx_axis_tready(tx_axis_tready),
+    .tx_axis_tlast(tx_axis_tlast),
+    .tx_axis_tuser(tx_axis_tuser),
+
+    .rx_axis_tdata(rx_axis_tdata),
+    .rx_axis_tvalid(rx_axis_tvalid),
+    .rx_axis_tready(rx_axis_tready),
+    .rx_axis_tlast(rx_axis_tlast),
+    .rx_axis_tuser(rx_axis_tuser),
+
+    .mii_rx_clk(phy_rx_clk),
+    .mii_rxd(phy_rxd),
+    .mii_rx_dv(phy_rx_dv),
+    .mii_rx_er(phy_rx_er),
+    .mii_tx_clk(phy_tx_clk),
+    .mii_txd(phy_txd),
+    .mii_tx_en(phy_tx_en),
+    .mii_tx_er(),
+
+    .tx_fifo_overflow(),
+    .tx_fifo_bad_frame(),
+    .tx_fifo_good_frame(),
+    .rx_error_bad_frame(),
+    .rx_error_bad_fcs(),
+    .rx_fifo_overflow(),
+    .rx_fifo_bad_frame(),
+    .rx_fifo_good_frame(),
+
+    .ifg_delay(12)
+);
+
+eth_axis_rx
+eth_axis_rx_inst (
+    .clk(clk),
+    .rst(rst),
+    // AXI input
+    .s_axis_tdata(rx_axis_tdata),
+    .s_axis_tvalid(rx_axis_tvalid),
+    .s_axis_tready(rx_axis_tready),
+    .s_axis_tlast(rx_axis_tlast),
+    .s_axis_tuser(rx_axis_tuser),
+    // Ethernet frame output
+    .m_eth_hdr_valid(rx_eth_hdr_valid),
+    .m_eth_hdr_ready(rx_eth_hdr_ready),
+    .m_eth_dest_mac(rx_eth_dest_mac),
+    .m_eth_src_mac(rx_eth_src_mac),
+    .m_eth_type(rx_eth_type),
+    .m_eth_payload_axis_tdata(rx_eth_payload_axis_tdata),
+    .m_eth_payload_axis_tvalid(rx_eth_payload_axis_tvalid),
+    .m_eth_payload_axis_tready(rx_eth_payload_axis_tready),
+    .m_eth_payload_axis_tlast(rx_eth_payload_axis_tlast),
+    .m_eth_payload_axis_tuser(rx_eth_payload_axis_tuser),
+    // Status signals
+    .busy(),
+    .error_header_early_termination()
+);
+
+eth_axis_tx
+eth_axis_tx_inst (
+    .clk(clk),
+    .rst(rst),
+    // Ethernet frame input
+    .s_eth_hdr_valid(tx_eth_hdr_valid),
+    .s_eth_hdr_ready(tx_eth_hdr_ready),
+    .s_eth_dest_mac(tx_eth_dest_mac),
+    .s_eth_src_mac(tx_eth_src_mac),
+    .s_eth_type(tx_eth_type),
+    .s_eth_payload_axis_tdata(tx_eth_payload_axis_tdata),
+    .s_eth_payload_axis_tvalid(tx_eth_payload_axis_tvalid),
+    .s_eth_payload_axis_tready(tx_eth_payload_axis_tready),
+    .s_eth_payload_axis_tlast(tx_eth_payload_axis_tlast),
+    .s_eth_payload_axis_tuser(tx_eth_payload_axis_tuser),
+    // AXI output
+    .m_axis_tdata(tx_axis_tdata),
+    .m_axis_tvalid(tx_axis_tvalid),
+    .m_axis_tready(tx_axis_tready),
+    .m_axis_tlast(tx_axis_tlast),
+    .m_axis_tuser(tx_axis_tuser),
+    // Status signals
+    .busy()
+);
+
+udp_complete #(
+    .UDP_CHECKSUM_GEN_ENABLE(0),
+    .ARP_CACHE_ADDR_WIDTH(4)
+)
+udp_complete_inst (
+    .clk(clk),
+    .rst(rst),
+    // Ethernet frame input
+    .s_eth_hdr_valid(rx_eth_hdr_valid),
+    .s_eth_hdr_ready(rx_eth_hdr_ready),
+    .s_eth_dest_mac(rx_eth_dest_mac),
+    .s_eth_src_mac(rx_eth_src_mac),
+    .s_eth_type(rx_eth_type),
+    .s_eth_payload_axis_tdata(rx_eth_payload_axis_tdata),
+    .s_eth_payload_axis_tvalid(rx_eth_payload_axis_tvalid),
+    .s_eth_payload_axis_tready(rx_eth_payload_axis_tready),
+    .s_eth_payload_axis_tlast(rx_eth_payload_axis_tlast),
+    .s_eth_payload_axis_tuser(rx_eth_payload_axis_tuser),
+    // Ethernet frame output
+    .m_eth_hdr_valid(tx_eth_hdr_valid),
+    .m_eth_hdr_ready(tx_eth_hdr_ready),
+    .m_eth_dest_mac(tx_eth_dest_mac),
+    .m_eth_src_mac(tx_eth_src_mac),
+    .m_eth_type(tx_eth_type),
+    .m_eth_payload_axis_tdata(tx_eth_payload_axis_tdata),
+    .m_eth_payload_axis_tvalid(tx_eth_payload_axis_tvalid),
+    .m_eth_payload_axis_tready(tx_eth_payload_axis_tready),
+    .m_eth_payload_axis_tlast(tx_eth_payload_axis_tlast),
+    .m_eth_payload_axis_tuser(tx_eth_payload_axis_tuser),
+    // IP frame input
+    .s_ip_hdr_valid(tx_ip_hdr_valid),
+    .s_ip_hdr_ready(tx_ip_hdr_ready),
+    .s_ip_dscp(tx_ip_dscp),
+    .s_ip_ecn(tx_ip_ecn),
+    .s_ip_length(tx_ip_length),
+    .s_ip_ttl(tx_ip_ttl),
+    .s_ip_protocol(tx_ip_protocol),
+    .s_ip_source_ip(tx_ip_source_ip),
+    .s_ip_dest_ip(tx_ip_dest_ip),
+    .s_ip_payload_axis_tdata(tx_ip_payload_axis_tdata),
+    .s_ip_payload_axis_tvalid(tx_ip_payload_axis_tvalid),
+    .s_ip_payload_axis_tready(tx_ip_payload_axis_tready),
+    .s_ip_payload_axis_tlast(tx_ip_payload_axis_tlast),
+    .s_ip_payload_axis_tuser(tx_ip_payload_axis_tuser),
+    // IP frame output
+    .m_ip_hdr_valid(rx_ip_hdr_valid),
+    .m_ip_hdr_ready(rx_ip_hdr_ready),
+    .m_ip_eth_dest_mac(rx_ip_eth_dest_mac),
+    .m_ip_eth_src_mac(rx_ip_eth_src_mac),
+    .m_ip_eth_type(rx_ip_eth_type),
+    .m_ip_version(rx_ip_version),
+    .m_ip_ihl(rx_ip_ihl),
+    .m_ip_dscp(rx_ip_dscp),
+    .m_ip_ecn(rx_ip_ecn),
+    .m_ip_length(rx_ip_length),
+    .m_ip_identification(rx_ip_identification),
+    .m_ip_flags(rx_ip_flags),
+    .m_ip_fragment_offset(rx_ip_fragment_offset),
+    .m_ip_ttl(rx_ip_ttl),
+    .m_ip_protocol(rx_ip_protocol),
+    .m_ip_header_checksum(rx_ip_header_checksum),
+    .m_ip_source_ip(rx_ip_source_ip),
+    .m_ip_dest_ip(rx_ip_dest_ip),
+    .m_ip_payload_axis_tdata(rx_ip_payload_axis_tdata),
+    .m_ip_payload_axis_tvalid(rx_ip_payload_axis_tvalid),
+    .m_ip_payload_axis_tready(rx_ip_payload_axis_tready),
+    .m_ip_payload_axis_tlast(rx_ip_payload_axis_tlast),
+    .m_ip_payload_axis_tuser(rx_ip_payload_axis_tuser),
+    // UDP frame input
+    .s_udp_hdr_valid(tx_udp_hdr_valid),
+    .s_udp_hdr_ready(tx_udp_hdr_ready),
+    .s_udp_ip_dscp(tx_udp_ip_dscp),
+    .s_udp_ip_ecn(tx_udp_ip_ecn),
+    .s_udp_ip_ttl(tx_udp_ip_ttl),
+    .s_udp_ip_source_ip(tx_udp_ip_source_ip),
+    .s_udp_ip_dest_ip(tx_udp_ip_dest_ip),
+    .s_udp_source_port(tx_udp_source_port),
+    .s_udp_dest_port(tx_udp_dest_port),
+    .s_udp_length(tx_udp_length),
+    .s_udp_checksum(tx_udp_checksum),
+    .s_udp_payload_axis_tdata(tx_udp_payload_axis_tdata),
+    .s_udp_payload_axis_tvalid(tx_udp_payload_axis_tvalid),
+    .s_udp_payload_axis_tready(tx_udp_payload_axis_tready),
+    .s_udp_payload_axis_tlast(tx_udp_payload_axis_tlast),
+    .s_udp_payload_axis_tuser(tx_udp_payload_axis_tuser),
+    // UDP frame output
+    .m_udp_hdr_valid(rx_udp_hdr_valid),
+    .m_udp_hdr_ready(rx_udp_hdr_ready),
+    .m_udp_eth_dest_mac(rx_udp_eth_dest_mac),
+    .m_udp_eth_src_mac(rx_udp_eth_src_mac),
+    .m_udp_eth_type(rx_udp_eth_type),
+    .m_udp_ip_version(rx_udp_ip_version),
+    .m_udp_ip_ihl(rx_udp_ip_ihl),
+    .m_udp_ip_dscp(rx_udp_ip_dscp),
+    .m_udp_ip_ecn(rx_udp_ip_ecn),
+    .m_udp_ip_length(rx_udp_ip_length),
+    .m_udp_ip_identification(rx_udp_ip_identification),
+    .m_udp_ip_flags(rx_udp_ip_flags),
+    .m_udp_ip_fragment_offset(rx_udp_ip_fragment_offset),
+    .m_udp_ip_ttl(rx_udp_ip_ttl),
+    .m_udp_ip_protocol(rx_udp_ip_protocol),
+    .m_udp_ip_header_checksum(rx_udp_ip_header_checksum),
+    .m_udp_ip_source_ip(rx_udp_ip_source_ip),
+    .m_udp_ip_dest_ip(rx_udp_ip_dest_ip),
+    .m_udp_source_port(rx_udp_source_port),
+    .m_udp_dest_port(rx_udp_dest_port),
+    .m_udp_length(rx_udp_length),
+    .m_udp_checksum(rx_udp_checksum),
+    .m_udp_payload_axis_tdata(rx_udp_payload_axis_tdata),
+    .m_udp_payload_axis_tvalid(rx_udp_payload_axis_tvalid),
+    .m_udp_payload_axis_tready(rx_udp_payload_axis_tready),
+    .m_udp_payload_axis_tlast(rx_udp_payload_axis_tlast),
+    .m_udp_payload_axis_tuser(rx_udp_payload_axis_tuser),
+    // Status signals
+    .ip_rx_busy(),
+    .ip_tx_busy(),
+    .udp_rx_busy(),
+    .udp_tx_busy(),
+    .ip_rx_error_header_early_termination(),
+    .ip_rx_error_payload_early_termination(),
+    .ip_rx_error_invalid_header(),
+    .ip_rx_error_invalid_checksum(),
+    .ip_tx_error_payload_early_termination(),
+    .ip_tx_error_arp_failed(),
+    .udp_rx_error_header_early_termination(),
+    .udp_rx_error_payload_early_termination(),
+    .udp_tx_error_payload_early_termination(),
+    // Configuration
+    .local_mac(LOCAL_MAC),
+    .local_ip(LOCAL_IP),
+    .gateway_ip(GATEWAY_IP),
+    .subnet_mask(SUBNET_MASK),
+    .clear_arp_cache(0)
+);
+
+endmodule
+
diff --git a/verilog/rtl/ip.v b/verilog/rtl/ip.v
new file mode 100644
index 0000000..0f5a8b5
--- /dev/null
+++ b/verilog/rtl/ip.v
@@ -0,0 +1,342 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * IPv4 block, ethernet frame interface
+ */
+module ip
+(
+    input  wire        clk,
+    input  wire        rst,
+
+    /*
+     * Ethernet frame input
+     */
+    input  wire        s_eth_hdr_valid,
+    output wire        s_eth_hdr_ready,
+    input  wire [47:0] s_eth_dest_mac,
+    input  wire [47:0] s_eth_src_mac,
+    input  wire [15:0] s_eth_type,
+    input  wire [7:0]  s_eth_payload_axis_tdata,
+    input  wire        s_eth_payload_axis_tvalid,
+    output wire        s_eth_payload_axis_tready,
+    input  wire        s_eth_payload_axis_tlast,
+    input  wire        s_eth_payload_axis_tuser,
+
+    /*
+     * Ethernet frame output
+     */
+    output wire        m_eth_hdr_valid,
+    input  wire        m_eth_hdr_ready,
+    output wire [47:0] m_eth_dest_mac,
+    output wire [47:0] m_eth_src_mac,
+    output wire [15:0] m_eth_type,
+    output wire [7:0]  m_eth_payload_axis_tdata,
+    output wire        m_eth_payload_axis_tvalid,
+    input  wire        m_eth_payload_axis_tready,
+    output wire        m_eth_payload_axis_tlast,
+    output wire        m_eth_payload_axis_tuser,
+
+    /*
+     * ARP requests
+     */
+    output wire        arp_request_valid,
+    input  wire        arp_request_ready,
+    output wire [31:0] arp_request_ip,
+    input  wire        arp_response_valid,
+    output wire        arp_response_ready,
+    input  wire        arp_response_error,
+    input  wire [47:0] arp_response_mac,
+
+    /*
+     * IP input
+     */
+    input  wire        s_ip_hdr_valid,
+    output wire        s_ip_hdr_ready,
+    input  wire [5:0]  s_ip_dscp,
+    input  wire [1:0]  s_ip_ecn,
+    input  wire [15:0] s_ip_length,
+    input  wire [7:0]  s_ip_ttl,
+    input  wire [7:0]  s_ip_protocol,
+    input  wire [31:0] s_ip_source_ip,
+    input  wire [31:0] s_ip_dest_ip,
+    input  wire [7:0]  s_ip_payload_axis_tdata,
+    input  wire        s_ip_payload_axis_tvalid,
+    output wire        s_ip_payload_axis_tready,
+    input  wire        s_ip_payload_axis_tlast,
+    input  wire        s_ip_payload_axis_tuser,
+
+    /*
+     * IP output
+     */
+    output wire        m_ip_hdr_valid,
+    input  wire        m_ip_hdr_ready,
+    output wire [47:0] m_ip_eth_dest_mac,
+    output wire [47:0] m_ip_eth_src_mac,
+    output wire [15:0] m_ip_eth_type,
+    output wire [3:0]  m_ip_version,
+    output wire [3:0]  m_ip_ihl,
+    output wire [5:0]  m_ip_dscp,
+    output wire [1:0]  m_ip_ecn,
+    output wire [15:0] m_ip_length,
+    output wire [15:0] m_ip_identification,
+    output wire [2:0]  m_ip_flags,
+    output wire [12:0] m_ip_fragment_offset,
+    output wire [7:0]  m_ip_ttl,
+    output wire [7:0]  m_ip_protocol,
+    output wire [15:0] m_ip_header_checksum,
+    output wire [31:0] m_ip_source_ip,
+    output wire [31:0] m_ip_dest_ip,
+    output wire [7:0]  m_ip_payload_axis_tdata,
+    output wire        m_ip_payload_axis_tvalid,
+    input  wire        m_ip_payload_axis_tready,
+    output wire        m_ip_payload_axis_tlast,
+    output wire        m_ip_payload_axis_tuser,
+
+    /*
+     * Status
+     */
+    output wire rx_busy,
+    output wire tx_busy,
+    output wire rx_error_header_early_termination,
+    output wire rx_error_payload_early_termination,
+    output wire rx_error_invalid_header,
+    output wire rx_error_invalid_checksum,
+    output wire tx_error_payload_early_termination,
+    output wire tx_error_arp_failed,
+
+    /*
+     * Configuration
+     */
+    input  wire [47:0] local_mac,
+    input  wire [31:0] local_ip
+);
+
+localparam [1:0]
+    STATE_IDLE = 2'd0,
+    STATE_ARP_QUERY = 2'd1,
+    STATE_WAIT_PACKET = 2'd2;
+
+reg [1:0] state_reg, state_next;
+
+reg outgoing_ip_hdr_valid_reg, outgoing_ip_hdr_valid_next;
+wire outgoing_ip_hdr_ready;
+reg [47:0] outgoing_eth_dest_mac_reg, outgoing_eth_dest_mac_next;
+wire outgoing_ip_payload_axis_tready;
+
+/*
+ * IP frame processing
+ */
+ip_eth_rx
+ip_eth_rx_inst (
+    .clk(clk),
+    .rst(rst),
+    // Ethernet frame input
+    .s_eth_hdr_valid(s_eth_hdr_valid),
+    .s_eth_hdr_ready(s_eth_hdr_ready),
+    .s_eth_dest_mac(s_eth_dest_mac),
+    .s_eth_src_mac(s_eth_src_mac),
+    .s_eth_type(s_eth_type),
+    .s_eth_payload_axis_tdata(s_eth_payload_axis_tdata),
+    .s_eth_payload_axis_tvalid(s_eth_payload_axis_tvalid),
+    .s_eth_payload_axis_tready(s_eth_payload_axis_tready),
+    .s_eth_payload_axis_tlast(s_eth_payload_axis_tlast),
+    .s_eth_payload_axis_tuser(s_eth_payload_axis_tuser),
+    // IP frame output
+    .m_ip_hdr_valid(m_ip_hdr_valid),
+    .m_ip_hdr_ready(m_ip_hdr_ready),
+    .m_eth_dest_mac(m_ip_eth_dest_mac),
+    .m_eth_src_mac(m_ip_eth_src_mac),
+    .m_eth_type(m_ip_eth_type),
+    .m_ip_version(m_ip_version),
+    .m_ip_ihl(m_ip_ihl),
+    .m_ip_dscp(m_ip_dscp),
+    .m_ip_ecn(m_ip_ecn),
+    .m_ip_length(m_ip_length),
+    .m_ip_identification(m_ip_identification),
+    .m_ip_flags(m_ip_flags),
+    .m_ip_fragment_offset(m_ip_fragment_offset),
+    .m_ip_ttl(m_ip_ttl),
+    .m_ip_protocol(m_ip_protocol),
+    .m_ip_header_checksum(m_ip_header_checksum),
+    .m_ip_source_ip(m_ip_source_ip),
+    .m_ip_dest_ip(m_ip_dest_ip),
+    .m_ip_payload_axis_tdata(m_ip_payload_axis_tdata),
+    .m_ip_payload_axis_tvalid(m_ip_payload_axis_tvalid),
+    .m_ip_payload_axis_tready(m_ip_payload_axis_tready),
+    .m_ip_payload_axis_tlast(m_ip_payload_axis_tlast),
+    .m_ip_payload_axis_tuser(m_ip_payload_axis_tuser),
+    // Status signals
+    .busy(rx_busy),
+    .error_header_early_termination(rx_error_header_early_termination),
+    .error_payload_early_termination(rx_error_payload_early_termination),
+    .error_invalid_header(rx_error_invalid_header),
+    .error_invalid_checksum(rx_error_invalid_checksum)
+);
+
+ip_eth_tx
+ip_eth_tx_inst (
+    .clk(clk),
+    .rst(rst),
+    // IP frame input
+    .s_ip_hdr_valid(outgoing_ip_hdr_valid_reg),
+    .s_ip_hdr_ready(outgoing_ip_hdr_ready),
+    .s_eth_dest_mac(outgoing_eth_dest_mac_reg),
+    .s_eth_src_mac(local_mac),
+    .s_eth_type(16'h0800),
+    .s_ip_dscp(s_ip_dscp),
+    .s_ip_ecn(s_ip_ecn),
+    .s_ip_length(s_ip_length),
+    .s_ip_identification(16'd0),
+    .s_ip_flags(3'b010),
+    .s_ip_fragment_offset(13'd0),
+    .s_ip_ttl(s_ip_ttl),
+    .s_ip_protocol(s_ip_protocol),
+    .s_ip_source_ip(s_ip_source_ip),
+    .s_ip_dest_ip(s_ip_dest_ip),
+    .s_ip_payload_axis_tdata(s_ip_payload_axis_tdata),
+    .s_ip_payload_axis_tvalid(s_ip_payload_axis_tvalid),
+    .s_ip_payload_axis_tready(outgoing_ip_payload_axis_tready),
+    .s_ip_payload_axis_tlast(s_ip_payload_axis_tlast),
+    .s_ip_payload_axis_tuser(s_ip_payload_axis_tuser),
+    // Ethernet frame output
+    .m_eth_hdr_valid(m_eth_hdr_valid),
+    .m_eth_hdr_ready(m_eth_hdr_ready),
+    .m_eth_dest_mac(m_eth_dest_mac),
+    .m_eth_src_mac(m_eth_src_mac),
+    .m_eth_type(m_eth_type),
+    .m_eth_payload_axis_tdata(m_eth_payload_axis_tdata),
+    .m_eth_payload_axis_tvalid(m_eth_payload_axis_tvalid),
+    .m_eth_payload_axis_tready(m_eth_payload_axis_tready),
+    .m_eth_payload_axis_tlast(m_eth_payload_axis_tlast),
+    .m_eth_payload_axis_tuser(m_eth_payload_axis_tuser),
+    // Status signals
+    .busy(tx_busy),
+    .error_payload_early_termination(tx_error_payload_early_termination)
+);
+
+reg s_ip_hdr_ready_reg, s_ip_hdr_ready_next;
+
+reg arp_request_valid_reg, arp_request_valid_next;
+
+reg arp_response_ready_reg, arp_response_ready_next;
+
+reg drop_packet_reg, drop_packet_next;
+
+assign s_ip_hdr_ready = s_ip_hdr_ready_reg;
+assign s_ip_payload_axis_tready = outgoing_ip_payload_axis_tready || drop_packet_reg;
+
+assign arp_request_valid = arp_request_valid_reg;
+assign arp_request_ip = s_ip_dest_ip;
+assign arp_response_ready = arp_response_ready_reg;
+
+assign tx_error_arp_failed = arp_response_error;
+
+always @* begin
+    state_next = STATE_IDLE;
+
+    arp_request_valid_next = arp_request_valid_reg && !arp_request_ready;
+    arp_response_ready_next = 1'b0;
+    drop_packet_next = 1'b0;
+
+    s_ip_hdr_ready_next = 1'b0;
+
+    outgoing_ip_hdr_valid_next = outgoing_ip_hdr_valid_reg && !outgoing_ip_hdr_ready;
+    outgoing_eth_dest_mac_next = outgoing_eth_dest_mac_reg;
+
+    case (state_reg)
+        STATE_IDLE: begin
+            // wait for outgoing packet
+            if (s_ip_hdr_valid) begin
+                // initiate ARP request
+                arp_request_valid_next = 1'b1;
+                arp_response_ready_next = 1'b1;
+                state_next = STATE_ARP_QUERY;
+            end else begin
+                state_next = STATE_IDLE;
+            end
+        end
+        STATE_ARP_QUERY: begin
+            arp_response_ready_next = 1'b1;
+
+            if (arp_response_valid) begin
+                // wait for ARP reponse
+                if (arp_response_error) begin
+                    // did not get MAC address; drop packet
+                    s_ip_hdr_ready_next = 1'b1;
+                    drop_packet_next = 1'b1;
+                    state_next = STATE_WAIT_PACKET;
+                end else begin
+                    // got MAC address; send packet
+                    s_ip_hdr_ready_next = 1'b1;
+                    outgoing_ip_hdr_valid_next = 1'b1;
+                    outgoing_eth_dest_mac_next = arp_response_mac;
+                    state_next = STATE_WAIT_PACKET;
+                end
+            end else begin
+                state_next = STATE_ARP_QUERY;
+            end
+        end
+        STATE_WAIT_PACKET: begin
+            drop_packet_next = drop_packet_reg;
+
+            // wait for packet transfer to complete
+            if (s_ip_payload_axis_tlast && s_ip_payload_axis_tready && s_ip_payload_axis_tvalid) begin
+                state_next = STATE_IDLE;
+            end else begin
+                state_next = STATE_WAIT_PACKET;
+            end
+        end
+    endcase
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        state_reg <= STATE_IDLE;
+        arp_request_valid_reg <= 1'b0;
+        arp_response_ready_reg <= 1'b0;
+        drop_packet_reg <= 1'b0;
+        s_ip_hdr_ready_reg <= 1'b0;
+        outgoing_eth_dest_mac_reg <= 48'h000000000000;
+        outgoing_ip_hdr_valid_reg <= 1'b0;
+    end else begin
+        state_reg <= state_next;
+
+        arp_request_valid_reg <= arp_request_valid_next;
+        arp_response_ready_reg <= arp_response_ready_next;
+        drop_packet_reg <= drop_packet_next;
+
+        s_ip_hdr_ready_reg <= s_ip_hdr_ready_next;
+
+        outgoing_ip_hdr_valid_reg <= outgoing_ip_hdr_valid_next;
+        outgoing_eth_dest_mac_reg <= outgoing_eth_dest_mac_next;
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/ip_arb_mux.v b/verilog/rtl/ip_arb_mux.v
new file mode 100644
index 0000000..0c705b7
--- /dev/null
+++ b/verilog/rtl/ip_arb_mux.v
@@ -0,0 +1,431 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * IP arbitrated multiplexer
+ */
+module ip_arb_mux #
+(
+    parameter S_COUNT = 4,
+    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,
+    // select round robin arbitration
+    parameter ARB_TYPE_ROUND_ROBIN = 0,
+    // LSB priority selection
+    parameter ARB_LSB_HIGH_PRIORITY = 1
+)
+(
+    input  wire                          clk,
+    input  wire                          rst,
+
+    /*
+     * IP frame inputs
+     */
+    input  wire [S_COUNT-1:0]            s_ip_hdr_valid,
+    output wire [S_COUNT-1:0]            s_ip_hdr_ready,
+    input  wire [S_COUNT*48-1:0]         s_eth_dest_mac,
+    input  wire [S_COUNT*48-1:0]         s_eth_src_mac,
+    input  wire [S_COUNT*16-1:0]         s_eth_type,
+    input  wire [S_COUNT*4-1:0]          s_ip_version,
+    input  wire [S_COUNT*4-1:0]          s_ip_ihl,
+    input  wire [S_COUNT*6-1:0]          s_ip_dscp,
+    input  wire [S_COUNT*2-1:0]          s_ip_ecn,
+    input  wire [S_COUNT*16-1:0]         s_ip_length,
+    input  wire [S_COUNT*16-1:0]         s_ip_identification,
+    input  wire [S_COUNT*3-1:0]          s_ip_flags,
+    input  wire [S_COUNT*13-1:0]         s_ip_fragment_offset,
+    input  wire [S_COUNT*8-1:0]          s_ip_ttl,
+    input  wire [S_COUNT*8-1:0]          s_ip_protocol,
+    input  wire [S_COUNT*16-1:0]         s_ip_header_checksum,
+    input  wire [S_COUNT*32-1:0]         s_ip_source_ip,
+    input  wire [S_COUNT*32-1:0]         s_ip_dest_ip,
+    input  wire [S_COUNT*DATA_WIDTH-1:0] s_ip_payload_axis_tdata,
+    input  wire [S_COUNT*KEEP_WIDTH-1:0] s_ip_payload_axis_tkeep,
+    input  wire [S_COUNT-1:0]            s_ip_payload_axis_tvalid,
+    output wire [S_COUNT-1:0]            s_ip_payload_axis_tready,
+    input  wire [S_COUNT-1:0]            s_ip_payload_axis_tlast,
+    input  wire [S_COUNT*ID_WIDTH-1:0]   s_ip_payload_axis_tid,
+    input  wire [S_COUNT*DEST_WIDTH-1:0] s_ip_payload_axis_tdest,
+    input  wire [S_COUNT*USER_WIDTH-1:0] s_ip_payload_axis_tuser,
+
+    /*
+     * IP frame output
+     */
+    output wire                          m_ip_hdr_valid,
+    input  wire                          m_ip_hdr_ready,
+    output wire [47:0]                   m_eth_dest_mac,
+    output wire [47:0]                   m_eth_src_mac,
+    output wire [15:0]                   m_eth_type,
+    output wire [3:0]                    m_ip_version,
+    output wire [3:0]                    m_ip_ihl,
+    output wire [5:0]                    m_ip_dscp,
+    output wire [1:0]                    m_ip_ecn,
+    output wire [15:0]                   m_ip_length,
+    output wire [15:0]                   m_ip_identification,
+    output wire [2:0]                    m_ip_flags,
+    output wire [12:0]                   m_ip_fragment_offset,
+    output wire [7:0]                    m_ip_ttl,
+    output wire [7:0]                    m_ip_protocol,
+    output wire [15:0]                   m_ip_header_checksum,
+    output wire [31:0]                   m_ip_source_ip,
+    output wire [31:0]                   m_ip_dest_ip,
+    output wire [DATA_WIDTH-1:0]         m_ip_payload_axis_tdata,
+    output wire [KEEP_WIDTH-1:0]         m_ip_payload_axis_tkeep,
+    output wire                          m_ip_payload_axis_tvalid,
+    input  wire                          m_ip_payload_axis_tready,
+    output wire                          m_ip_payload_axis_tlast,
+    output wire [ID_WIDTH-1:0]           m_ip_payload_axis_tid,
+    output wire [DEST_WIDTH-1:0]         m_ip_payload_axis_tdest,
+    output wire [USER_WIDTH-1:0]         m_ip_payload_axis_tuser
+);
+
+parameter CL_S_COUNT = $clog2(S_COUNT);
+
+reg frame_reg, frame_next;
+
+reg [S_COUNT-1:0] s_ip_hdr_ready_reg, s_ip_hdr_ready_next;
+
+reg m_ip_hdr_valid_reg, m_ip_hdr_valid_next;
+reg [47:0] m_eth_dest_mac_reg, m_eth_dest_mac_next;
+reg [47:0] m_eth_src_mac_reg, m_eth_src_mac_next;
+reg [15:0] m_eth_type_reg, m_eth_type_next;
+reg [3:0]  m_ip_version_reg, m_ip_version_next;
+reg [3:0]  m_ip_ihl_reg, m_ip_ihl_next;
+reg [5:0]  m_ip_dscp_reg, m_ip_dscp_next;
+reg [1:0]  m_ip_ecn_reg, m_ip_ecn_next;
+reg [15:0] m_ip_length_reg, m_ip_length_next;
+reg [15:0] m_ip_identification_reg, m_ip_identification_next;
+reg [2:0]  m_ip_flags_reg, m_ip_flags_next;
+reg [12:0] m_ip_fragment_offset_reg, m_ip_fragment_offset_next;
+reg [7:0]  m_ip_ttl_reg, m_ip_ttl_next;
+reg [7:0]  m_ip_protocol_reg, m_ip_protocol_next;
+reg [15:0] m_ip_header_checksum_reg, m_ip_header_checksum_next;
+reg [31:0] m_ip_source_ip_reg, m_ip_source_ip_next;
+reg [31:0] m_ip_dest_ip_reg, m_ip_dest_ip_next;
+
+wire [S_COUNT-1:0] request;
+wire [S_COUNT-1:0] acknowledge;
+wire [S_COUNT-1:0] grant;
+wire grant_valid;
+wire [CL_S_COUNT-1:0] grant_encoded;
+
+// internal datapath
+reg  [DATA_WIDTH-1:0] m_ip_payload_axis_tdata_int;
+reg  [KEEP_WIDTH-1:0] m_ip_payload_axis_tkeep_int;
+reg                   m_ip_payload_axis_tvalid_int;
+reg                   m_ip_payload_axis_tready_int_reg;
+reg                   m_ip_payload_axis_tlast_int;
+reg  [ID_WIDTH-1:0]   m_ip_payload_axis_tid_int;
+reg  [DEST_WIDTH-1:0] m_ip_payload_axis_tdest_int;
+reg  [USER_WIDTH-1:0] m_ip_payload_axis_tuser_int;
+wire                  m_ip_payload_axis_tready_int_early;
+
+assign s_ip_hdr_ready = s_ip_hdr_ready_reg;
+
+assign s_ip_payload_axis_tready = (m_ip_payload_axis_tready_int_reg && grant_valid) << grant_encoded;
+
+assign m_ip_hdr_valid = m_ip_hdr_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+assign m_ip_version = m_ip_version_reg;
+assign m_ip_ihl = m_ip_ihl_reg;
+assign m_ip_dscp = m_ip_dscp_reg;
+assign m_ip_ecn = m_ip_ecn_reg;
+assign m_ip_length = m_ip_length_reg;
+assign m_ip_identification = m_ip_identification_reg;
+assign m_ip_flags = m_ip_flags_reg;
+assign m_ip_fragment_offset = m_ip_fragment_offset_reg;
+assign m_ip_ttl = m_ip_ttl_reg;
+assign m_ip_protocol = m_ip_protocol_reg;
+assign m_ip_header_checksum = m_ip_header_checksum_reg;
+assign m_ip_source_ip = m_ip_source_ip_reg;
+assign m_ip_dest_ip = m_ip_dest_ip_reg;
+
+// mux for incoming packet
+wire [DATA_WIDTH-1:0] current_s_tdata  = s_ip_payload_axis_tdata[grant_encoded*DATA_WIDTH +: DATA_WIDTH];
+wire [KEEP_WIDTH-1:0] current_s_tkeep  = s_ip_payload_axis_tkeep[grant_encoded*KEEP_WIDTH +: KEEP_WIDTH];
+wire                  current_s_tvalid = s_ip_payload_axis_tvalid[grant_encoded];
+wire                  current_s_tready = s_ip_payload_axis_tready[grant_encoded];
+wire                  current_s_tlast  = s_ip_payload_axis_tlast[grant_encoded];
+wire [ID_WIDTH-1:0]   current_s_tid    = s_ip_payload_axis_tid[grant_encoded*ID_WIDTH +: ID_WIDTH];
+wire [DEST_WIDTH-1:0] current_s_tdest  = s_ip_payload_axis_tdest[grant_encoded*DEST_WIDTH +: DEST_WIDTH];
+wire [USER_WIDTH-1:0] current_s_tuser  = s_ip_payload_axis_tuser[grant_encoded*USER_WIDTH +: USER_WIDTH];
+
+// arbiter instance
+arbiter #(
+    .PORTS(S_COUNT),
+    .ARB_TYPE_ROUND_ROBIN(ARB_TYPE_ROUND_ROBIN),
+    .ARB_BLOCK(1),
+    .ARB_BLOCK_ACK(1),
+    .ARB_LSB_HIGH_PRIORITY(ARB_LSB_HIGH_PRIORITY)
+)
+arb_inst (
+    .clk(clk),
+    .rst(rst),
+    .request(request),
+    .acknowledge(acknowledge),
+    .grant(grant),
+    .grant_valid(grant_valid),
+    .grant_encoded(grant_encoded)
+);
+
+assign request = s_ip_hdr_valid & ~grant;
+assign acknowledge = grant & s_ip_payload_axis_tvalid & s_ip_payload_axis_tready & s_ip_payload_axis_tlast;
+
+always @* begin
+    frame_next = frame_reg;
+
+    s_ip_hdr_ready_next = {S_COUNT{1'b0}};
+
+    m_ip_hdr_valid_next = m_ip_hdr_valid_reg && !m_ip_hdr_ready;
+    m_eth_dest_mac_next = m_eth_dest_mac_reg;
+    m_eth_src_mac_next = m_eth_src_mac_reg;
+    m_eth_type_next = m_eth_type_reg;
+    m_ip_version_next = m_ip_version_reg;
+    m_ip_ihl_next = m_ip_ihl_reg;
+    m_ip_dscp_next = m_ip_dscp_reg;
+    m_ip_ecn_next = m_ip_ecn_reg;
+    m_ip_length_next = m_ip_length_reg;
+    m_ip_identification_next = m_ip_identification_reg;
+    m_ip_flags_next = m_ip_flags_reg;
+    m_ip_fragment_offset_next = m_ip_fragment_offset_reg;
+    m_ip_ttl_next = m_ip_ttl_reg;
+    m_ip_protocol_next = m_ip_protocol_reg;
+    m_ip_header_checksum_next = m_ip_header_checksum_reg;
+    m_ip_source_ip_next = m_ip_source_ip_reg;
+    m_ip_dest_ip_next = m_ip_dest_ip_reg;
+
+    if (s_ip_payload_axis_tvalid[grant_encoded] && s_ip_payload_axis_tready[grant_encoded]) begin
+        // end of frame detection
+        if (s_ip_payload_axis_tlast[grant_encoded]) begin
+            frame_next = 1'b0;
+        end
+    end
+
+    if (!frame_reg && grant_valid && (m_ip_hdr_ready || !m_ip_hdr_valid)) begin
+        // start of frame
+        frame_next = 1'b1;
+
+        s_ip_hdr_ready_next = grant;
+
+        m_ip_hdr_valid_next = 1'b1;
+        m_eth_dest_mac_next = s_eth_dest_mac[grant_encoded*48 +: 48];
+        m_eth_src_mac_next = s_eth_src_mac[grant_encoded*48 +: 48];
+        m_eth_type_next = s_eth_type[grant_encoded*16 +: 16];
+        m_ip_version_next = s_ip_version[grant_encoded*4 +: 4];
+        m_ip_ihl_next = s_ip_ihl[grant_encoded*4 +: 4];
+        m_ip_dscp_next = s_ip_dscp[grant_encoded*6 +: 6];
+        m_ip_ecn_next = s_ip_ecn[grant_encoded*2 +: 2];
+        m_ip_length_next = s_ip_length[grant_encoded*16 +: 16];
+        m_ip_identification_next = s_ip_identification[grant_encoded*16 +: 16];
+        m_ip_flags_next = s_ip_flags[grant_encoded*3 +: 3];
+        m_ip_fragment_offset_next = s_ip_fragment_offset[grant_encoded*13 +: 13];
+        m_ip_ttl_next = s_ip_ttl[grant_encoded*8 +: 8];
+        m_ip_protocol_next = s_ip_protocol[grant_encoded*8 +: 8];
+        m_ip_header_checksum_next = s_ip_header_checksum[grant_encoded*16 +: 16];
+        m_ip_source_ip_next = s_ip_source_ip[grant_encoded*32 +: 32];
+        m_ip_dest_ip_next = s_ip_dest_ip[grant_encoded*32 +: 32];
+    end
+
+    // pass through selected packet data
+    m_ip_payload_axis_tdata_int  = current_s_tdata;
+    m_ip_payload_axis_tkeep_int  = current_s_tkeep;
+    m_ip_payload_axis_tvalid_int = current_s_tvalid && m_ip_payload_axis_tready_int_reg && grant_valid;
+    m_ip_payload_axis_tlast_int  = current_s_tlast;
+    m_ip_payload_axis_tid_int    = current_s_tid;
+    m_ip_payload_axis_tdest_int  = current_s_tdest;
+    m_ip_payload_axis_tuser_int  = current_s_tuser;
+end
+
+always @(posedge clk) begin
+    frame_reg <= frame_next;
+
+    s_ip_hdr_ready_reg <= s_ip_hdr_ready_next;
+
+    m_ip_hdr_valid_reg <= m_ip_hdr_valid_next;
+    m_eth_dest_mac_reg <= m_eth_dest_mac_next;
+    m_eth_src_mac_reg <= m_eth_src_mac_next;
+    m_eth_type_reg <= m_eth_type_next;
+    m_ip_version_reg <= m_ip_version_next;
+    m_ip_ihl_reg <= m_ip_ihl_next;
+    m_ip_dscp_reg <= m_ip_dscp_next;
+    m_ip_ecn_reg <= m_ip_ecn_next;
+    m_ip_length_reg <= m_ip_length_next;
+    m_ip_identification_reg <= m_ip_identification_next;
+    m_ip_flags_reg <= m_ip_flags_next;
+    m_ip_fragment_offset_reg <= m_ip_fragment_offset_next;
+    m_ip_ttl_reg <= m_ip_ttl_next;
+    m_ip_protocol_reg <= m_ip_protocol_next;
+    m_ip_header_checksum_reg <= m_ip_header_checksum_next;
+    m_ip_source_ip_reg <= m_ip_source_ip_next;
+    m_ip_dest_ip_reg <= m_ip_dest_ip_next;
+
+    if (rst) begin
+        frame_reg <= 1'b0;
+        s_ip_hdr_ready_reg <= {S_COUNT{1'b0}};
+        m_ip_hdr_valid_reg <= 1'b0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+        m_ip_version_reg <= 4'd0;
+        m_ip_ihl_reg <= 4'd0;
+        m_ip_dscp_reg <= 6'd0;
+        m_ip_ecn_reg <= 2'd0;
+        m_ip_length_reg <= 16'd0;
+        m_ip_identification_reg <= 16'd0;
+        m_ip_flags_reg <= 3'd0;
+        m_ip_fragment_offset_reg <= 13'd0;
+        m_ip_ttl_reg <= 8'd0;
+        m_ip_protocol_reg <= 8'd0;
+        m_ip_header_checksum_reg <= 16'd0;
+        m_ip_source_ip_reg <= 32'd0;
+        m_ip_dest_ip_reg <= 32'd0;
+    end
+end
+
+// output datapath logic
+reg [DATA_WIDTH-1:0] m_ip_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] m_ip_payload_axis_tkeep_reg;
+reg                  m_ip_payload_axis_tvalid_reg, m_ip_payload_axis_tvalid_next;
+reg                  m_ip_payload_axis_tlast_reg;
+reg [ID_WIDTH-1:0]   m_ip_payload_axis_tid_reg;
+reg [DEST_WIDTH-1:0] m_ip_payload_axis_tdest_reg;
+reg [USER_WIDTH-1:0] m_ip_payload_axis_tuser_reg;
+
+reg [DATA_WIDTH-1:0] temp_m_ip_payload_axis_tdata_reg;
+reg [KEEP_WIDTH-1:0] temp_m_ip_payload_axis_tkeep_reg;
+reg                  temp_m_ip_payload_axis_tvalid_reg, temp_m_ip_payload_axis_tvalid_next;
+reg                  temp_m_ip_payload_axis_tlast_reg;
+reg [ID_WIDTH-1:0]   temp_m_ip_payload_axis_tid_reg;
+reg [DEST_WIDTH-1:0] temp_m_ip_payload_axis_tdest_reg;
+reg [USER_WIDTH-1:0] temp_m_ip_payload_axis_tuser_reg;
+
+// datapath control
+reg store_axis_int_to_output;
+reg store_axis_int_to_temp;
+reg store_ip_payload_axis_temp_to_output;
+
+assign m_ip_payload_axis_tdata  = m_ip_payload_axis_tdata_reg;
+assign m_ip_payload_axis_tkeep  = KEEP_ENABLE ? m_ip_payload_axis_tkeep_reg : {KEEP_WIDTH{1'b1}};
+assign m_ip_payload_axis_tvalid = m_ip_payload_axis_tvalid_reg;
+assign m_ip_payload_axis_tlast  = m_ip_payload_axis_tlast_reg;
+assign m_ip_payload_axis_tid    = ID_ENABLE   ? m_ip_payload_axis_tid_reg   : {ID_WIDTH{1'b0}};
+assign m_ip_payload_axis_tdest  = DEST_ENABLE ? m_ip_payload_axis_tdest_reg : {DEST_WIDTH{1'b0}};
+assign m_ip_payload_axis_tuser  = USER_ENABLE ? m_ip_payload_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_ip_payload_axis_tready_int_early = m_ip_payload_axis_tready || (!temp_m_ip_payload_axis_tvalid_reg && (!m_ip_payload_axis_tvalid_reg || !m_ip_payload_axis_tvalid_int));
+
+always @* begin
+    // transfer sink ready state to source
+    m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_reg;
+    temp_m_ip_payload_axis_tvalid_next = temp_m_ip_payload_axis_tvalid_reg;
+
+    store_axis_int_to_output = 1'b0;
+    store_axis_int_to_temp = 1'b0;
+    store_ip_payload_axis_temp_to_output = 1'b0;
+
+    if (m_ip_payload_axis_tready_int_reg) begin
+        // input is ready
+        if (m_ip_payload_axis_tready || !m_ip_payload_axis_tvalid_reg) begin
+            // output is ready or currently not valid, transfer data to output
+            m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_int;
+            store_axis_int_to_output = 1'b1;
+        end else begin
+            // output is not ready, store input in temp
+            temp_m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_int;
+            store_axis_int_to_temp = 1'b1;
+        end
+    end else if (m_ip_payload_axis_tready) begin
+        // input is not ready, but output is ready
+        m_ip_payload_axis_tvalid_next = temp_m_ip_payload_axis_tvalid_reg;
+        temp_m_ip_payload_axis_tvalid_next = 1'b0;
+        store_ip_payload_axis_temp_to_output = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        m_ip_payload_axis_tready_int_reg <= 1'b0;
+        m_ip_payload_axis_tdata_reg  <= {DATA_WIDTH{1'b0}};
+        m_ip_payload_axis_tkeep_reg  <= {KEEP_WIDTH{1'b0}};
+        m_ip_payload_axis_tvalid_reg <= 1'b0;
+        m_ip_payload_axis_tlast_reg  <= 1'b0;
+        m_ip_payload_axis_tid_reg    <= {ID_WIDTH{1'b0}};
+        m_ip_payload_axis_tdest_reg  <= {DEST_WIDTH{1'b0}};
+        m_ip_payload_axis_tuser_reg  <= {USER_WIDTH{1'b0}};
+        temp_m_ip_payload_axis_tdata_reg  <= {DATA_WIDTH{1'b0}};
+        temp_m_ip_payload_axis_tkeep_reg  <= {KEEP_WIDTH{1'b0}};
+        temp_m_ip_payload_axis_tvalid_reg <= 1'b0;
+        temp_m_ip_payload_axis_tlast_reg  <= 1'b0;
+        temp_m_ip_payload_axis_tid_reg    <= {ID_WIDTH{1'b0}};
+        temp_m_ip_payload_axis_tdest_reg  <= {DEST_WIDTH{1'b0}};
+        temp_m_ip_payload_axis_tuser_reg  <= {USER_WIDTH{1'b0}};
+    end else begin
+        m_ip_payload_axis_tvalid_reg <= m_ip_payload_axis_tvalid_next;
+        m_ip_payload_axis_tready_int_reg <= m_ip_payload_axis_tready_int_early;
+        temp_m_ip_payload_axis_tvalid_reg <= temp_m_ip_payload_axis_tvalid_next;
+
+        // datapath
+        if (store_axis_int_to_output) begin
+            m_ip_payload_axis_tdata_reg <= m_ip_payload_axis_tdata_int;
+            m_ip_payload_axis_tkeep_reg <= m_ip_payload_axis_tkeep_int;
+            m_ip_payload_axis_tlast_reg <= m_ip_payload_axis_tlast_int;
+            m_ip_payload_axis_tid_reg   <= m_ip_payload_axis_tid_int;
+            m_ip_payload_axis_tdest_reg <= m_ip_payload_axis_tdest_int;
+            m_ip_payload_axis_tuser_reg <= m_ip_payload_axis_tuser_int;
+        end else if (store_ip_payload_axis_temp_to_output) begin
+            m_ip_payload_axis_tdata_reg <= temp_m_ip_payload_axis_tdata_reg;
+            m_ip_payload_axis_tkeep_reg <= temp_m_ip_payload_axis_tkeep_reg;
+            m_ip_payload_axis_tlast_reg <= temp_m_ip_payload_axis_tlast_reg;
+            m_ip_payload_axis_tid_reg   <= temp_m_ip_payload_axis_tid_reg;
+            m_ip_payload_axis_tdest_reg <= temp_m_ip_payload_axis_tdest_reg;
+            m_ip_payload_axis_tuser_reg <= temp_m_ip_payload_axis_tuser_reg;
+        end
+    
+        if (store_axis_int_to_temp) begin
+            temp_m_ip_payload_axis_tdata_reg <= m_ip_payload_axis_tdata_int;
+            temp_m_ip_payload_axis_tkeep_reg <= m_ip_payload_axis_tkeep_int;
+            temp_m_ip_payload_axis_tlast_reg <= m_ip_payload_axis_tlast_int;
+            temp_m_ip_payload_axis_tid_reg   <= m_ip_payload_axis_tid_int;
+            temp_m_ip_payload_axis_tdest_reg <= m_ip_payload_axis_tdest_int;
+            temp_m_ip_payload_axis_tuser_reg <= m_ip_payload_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/ip_complete.v b/verilog/rtl/ip_complete.v
new file mode 100644
index 0000000..a3c978c
--- /dev/null
+++ b/verilog/rtl/ip_complete.v
@@ -0,0 +1,441 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * IPv4 and ARP block, ethernet frame interface
+ */
+module ip_complete #(
+    parameter ARP_CACHE_ADDR_WIDTH = 9,
+    parameter ARP_REQUEST_RETRY_COUNT = 4,
+    parameter ARP_REQUEST_RETRY_INTERVAL = 125000000*2,
+    parameter ARP_REQUEST_TIMEOUT = 125000000*30
+)
+(
+    input  wire        clk,
+    input  wire        rst,
+
+    /*
+     * Ethernet frame input
+     */
+    input  wire        s_eth_hdr_valid,
+    output wire        s_eth_hdr_ready,
+    input  wire [47:0] s_eth_dest_mac,
+    input  wire [47:0] s_eth_src_mac,
+    input  wire [15:0] s_eth_type,
+    input  wire [7:0]  s_eth_payload_axis_tdata,
+    input  wire        s_eth_payload_axis_tvalid,
+    output wire        s_eth_payload_axis_tready,
+    input  wire        s_eth_payload_axis_tlast,
+    input  wire        s_eth_payload_axis_tuser,
+
+    /*
+     * Ethernet frame output
+     */
+    output wire        m_eth_hdr_valid,
+    input  wire        m_eth_hdr_ready,
+    output wire [47:0] m_eth_dest_mac,
+    output wire [47:0] m_eth_src_mac,
+    output wire [15:0] m_eth_type,
+    output wire [7:0]  m_eth_payload_axis_tdata,
+    output wire        m_eth_payload_axis_tvalid,
+    input  wire        m_eth_payload_axis_tready,
+    output wire        m_eth_payload_axis_tlast,
+    output wire        m_eth_payload_axis_tuser,
+
+    /*
+     * IP input
+     */
+    input  wire        s_ip_hdr_valid,
+    output wire        s_ip_hdr_ready,
+    input  wire [5:0]  s_ip_dscp,
+    input  wire [1:0]  s_ip_ecn,
+    input  wire [15:0] s_ip_length,
+    input  wire [7:0]  s_ip_ttl,
+    input  wire [7:0]  s_ip_protocol,
+    input  wire [31:0] s_ip_source_ip,
+    input  wire [31:0] s_ip_dest_ip,
+    input  wire [7:0]  s_ip_payload_axis_tdata,
+    input  wire        s_ip_payload_axis_tvalid,
+    output wire        s_ip_payload_axis_tready,
+    input  wire        s_ip_payload_axis_tlast,
+    input  wire        s_ip_payload_axis_tuser,
+
+    /*
+     * IP output
+     */
+    output wire        m_ip_hdr_valid,
+    input  wire        m_ip_hdr_ready,
+    output wire [47:0] m_ip_eth_dest_mac,
+    output wire [47:0] m_ip_eth_src_mac,
+    output wire [15:0] m_ip_eth_type,
+    output wire [3:0]  m_ip_version,
+    output wire [3:0]  m_ip_ihl,
+    output wire [5:0]  m_ip_dscp,
+    output wire [1:0]  m_ip_ecn,
+    output wire [15:0] m_ip_length,
+    output wire [15:0] m_ip_identification,
+    output wire [2:0]  m_ip_flags,
+    output wire [12:0] m_ip_fragment_offset,
+    output wire [7:0]  m_ip_ttl,
+    output wire [7:0]  m_ip_protocol,
+    output wire [15:0] m_ip_header_checksum,
+    output wire [31:0] m_ip_source_ip,
+    output wire [31:0] m_ip_dest_ip,
+    output wire [7:0]  m_ip_payload_axis_tdata,
+    output wire        m_ip_payload_axis_tvalid,
+    input  wire        m_ip_payload_axis_tready,
+    output wire        m_ip_payload_axis_tlast,
+    output wire        m_ip_payload_axis_tuser,
+
+    /*
+     * Status
+     */
+    output wire rx_busy,
+    output wire tx_busy,
+    output wire rx_error_header_early_termination,
+    output wire rx_error_payload_early_termination,
+    output wire rx_error_invalid_header,
+    output wire rx_error_invalid_checksum,
+    output wire tx_error_payload_early_termination,
+    output wire tx_error_arp_failed,
+
+    /*
+     * Configuration
+     */
+    input  wire [47:0] local_mac,
+    input  wire [31:0] local_ip,
+    input  wire [31:0] gateway_ip,
+    input  wire [31:0] subnet_mask,
+    input  wire        clear_arp_cache
+);
+
+/*
+
+This module integrates the IP and ARP modules for a complete IP stack
+
+*/
+
+wire arp_request_valid;
+wire arp_request_ready;
+wire [31:0] arp_request_ip;
+wire arp_response_valid;
+wire arp_response_ready;
+wire arp_response_error;
+wire [47:0] arp_response_mac;
+
+wire ip_rx_eth_hdr_valid;
+wire ip_rx_eth_hdr_ready;
+wire [47:0] ip_rx_eth_dest_mac;
+wire [47:0] ip_rx_eth_src_mac;
+wire [15:0] ip_rx_eth_type;
+wire [7:0] ip_rx_eth_payload_axis_tdata;
+wire ip_rx_eth_payload_axis_tvalid;
+wire ip_rx_eth_payload_axis_tready;
+wire ip_rx_eth_payload_axis_tlast;
+wire ip_rx_eth_payload_axis_tuser;
+
+wire ip_tx_eth_hdr_valid;
+wire ip_tx_eth_hdr_ready;
+wire [47:0] ip_tx_eth_dest_mac;
+wire [47:0] ip_tx_eth_src_mac;
+wire [15:0] ip_tx_eth_type;
+wire [7:0] ip_tx_eth_payload_axis_tdata;
+wire ip_tx_eth_payload_axis_tvalid;
+wire ip_tx_eth_payload_axis_tready;
+wire ip_tx_eth_payload_axis_tlast;
+wire ip_tx_eth_payload_axis_tuser;
+
+wire arp_rx_eth_hdr_valid;
+wire arp_rx_eth_hdr_ready;
+wire [47:0] arp_rx_eth_dest_mac;
+wire [47:0] arp_rx_eth_src_mac;
+wire [15:0] arp_rx_eth_type;
+wire [7:0] arp_rx_eth_payload_axis_tdata;
+wire arp_rx_eth_payload_axis_tvalid;
+wire arp_rx_eth_payload_axis_tready;
+wire arp_rx_eth_payload_axis_tlast;
+wire arp_rx_eth_payload_axis_tuser;
+
+wire arp_tx_eth_hdr_valid;
+wire arp_tx_eth_hdr_ready;
+wire [47:0] arp_tx_eth_dest_mac;
+wire [47:0] arp_tx_eth_src_mac;
+wire [15:0] arp_tx_eth_type;
+wire [7:0] arp_tx_eth_payload_axis_tdata;
+wire arp_tx_eth_payload_axis_tvalid;
+wire arp_tx_eth_payload_axis_tready;
+wire arp_tx_eth_payload_axis_tlast;
+wire arp_tx_eth_payload_axis_tuser;
+
+/*
+ * Input classifier (eth_type)
+ */
+wire s_select_ip = (s_eth_type == 16'h0800);
+wire s_select_arp = (s_eth_type == 16'h0806);
+wire s_select_none = !(s_select_ip || s_select_arp);
+
+reg s_select_ip_reg;
+reg s_select_arp_reg;
+reg s_select_none_reg;
+
+always @(posedge clk) begin
+    if (rst) begin
+        s_select_ip_reg <= 1'b0;
+        s_select_arp_reg <= 1'b0;
+        s_select_none_reg <= 1'b0;
+    end else begin
+        if (s_eth_payload_axis_tvalid) begin
+            if ((!s_select_ip_reg && !s_select_arp_reg && !s_select_none_reg) ||
+                (s_eth_payload_axis_tvalid && s_eth_payload_axis_tready && s_eth_payload_axis_tlast)) begin
+                s_select_ip_reg <= s_select_ip;
+                s_select_arp_reg <= s_select_arp;
+                s_select_none_reg <= s_select_none;
+            end
+        end else begin
+            s_select_ip_reg <= 1'b0;
+            s_select_arp_reg <= 1'b0;
+            s_select_none_reg <= 1'b0;
+        end
+    end
+end
+
+assign ip_rx_eth_hdr_valid = s_select_ip && s_eth_hdr_valid;
+assign ip_rx_eth_dest_mac = s_eth_dest_mac;
+assign ip_rx_eth_src_mac = s_eth_src_mac;
+assign ip_rx_eth_type = 16'h0800;
+assign ip_rx_eth_payload_axis_tdata = s_eth_payload_axis_tdata;
+assign ip_rx_eth_payload_axis_tvalid = s_select_ip_reg && s_eth_payload_axis_tvalid;
+assign ip_rx_eth_payload_axis_tlast = s_eth_payload_axis_tlast;
+assign ip_rx_eth_payload_axis_tuser = s_eth_payload_axis_tuser;
+
+assign arp_rx_eth_hdr_valid = s_select_arp && s_eth_hdr_valid;
+assign arp_rx_eth_dest_mac = s_eth_dest_mac;
+assign arp_rx_eth_src_mac = s_eth_src_mac;
+assign arp_rx_eth_type = 16'h0806;
+assign arp_rx_eth_payload_axis_tdata = s_eth_payload_axis_tdata;
+assign arp_rx_eth_payload_axis_tvalid = s_select_arp_reg && s_eth_payload_axis_tvalid;
+assign arp_rx_eth_payload_axis_tlast = s_eth_payload_axis_tlast;
+assign arp_rx_eth_payload_axis_tuser = s_eth_payload_axis_tuser;
+
+assign s_eth_hdr_ready = (s_select_ip && ip_rx_eth_hdr_ready) ||
+                         (s_select_arp && arp_rx_eth_hdr_ready) ||
+                         (s_select_none);
+
+assign s_eth_payload_axis_tready = (s_select_ip_reg && ip_rx_eth_payload_axis_tready) ||
+                                   (s_select_arp_reg && arp_rx_eth_payload_axis_tready) ||
+                                   s_select_none_reg;
+
+/*
+ * Output arbiter
+ */
+eth_arb_mux #(
+    .S_COUNT(2),
+    .DATA_WIDTH(8),
+    .KEEP_ENABLE(0),
+    .ID_ENABLE(0),
+    .DEST_ENABLE(0),
+    .USER_ENABLE(1),
+    .USER_WIDTH(1),
+    .ARB_TYPE_ROUND_ROBIN(0),
+    .ARB_LSB_HIGH_PRIORITY(1)
+)
+eth_arb_mux_inst (
+    .clk(clk),
+    .rst(rst),
+    // Ethernet frame inputs
+    .s_eth_hdr_valid({ip_tx_eth_hdr_valid, arp_tx_eth_hdr_valid}),
+    .s_eth_hdr_ready({ip_tx_eth_hdr_ready, arp_tx_eth_hdr_ready}),
+    .s_eth_dest_mac({ip_tx_eth_dest_mac, arp_tx_eth_dest_mac}),
+    .s_eth_src_mac({ip_tx_eth_src_mac, arp_tx_eth_src_mac}),
+    .s_eth_type({ip_tx_eth_type, arp_tx_eth_type}),
+    .s_eth_payload_axis_tdata({ip_tx_eth_payload_axis_tdata, arp_tx_eth_payload_axis_tdata}),
+    .s_eth_payload_axis_tkeep(0),
+    .s_eth_payload_axis_tvalid({ip_tx_eth_payload_axis_tvalid, arp_tx_eth_payload_axis_tvalid}),
+    .s_eth_payload_axis_tready({ip_tx_eth_payload_axis_tready, arp_tx_eth_payload_axis_tready}),
+    .s_eth_payload_axis_tlast({ip_tx_eth_payload_axis_tlast, arp_tx_eth_payload_axis_tlast}),
+    .s_eth_payload_axis_tid(0),
+    .s_eth_payload_axis_tdest(0),
+    .s_eth_payload_axis_tuser({ip_tx_eth_payload_axis_tuser, arp_tx_eth_payload_axis_tuser}),
+    // Ethernet frame output
+    .m_eth_hdr_valid(m_eth_hdr_valid),
+    .m_eth_hdr_ready(m_eth_hdr_ready),
+    .m_eth_dest_mac(m_eth_dest_mac),
+    .m_eth_src_mac(m_eth_src_mac),
+    .m_eth_type(m_eth_type),
+    .m_eth_payload_axis_tdata(m_eth_payload_axis_tdata),
+    .m_eth_payload_axis_tkeep(),
+    .m_eth_payload_axis_tvalid(m_eth_payload_axis_tvalid),
+    .m_eth_payload_axis_tready(m_eth_payload_axis_tready),
+    .m_eth_payload_axis_tlast(m_eth_payload_axis_tlast),
+    .m_eth_payload_axis_tid(),
+    .m_eth_payload_axis_tdest(),
+    .m_eth_payload_axis_tuser(m_eth_payload_axis_tuser)
+);
+
+/*
+ * IP module
+ */
+ip
+ip_inst (
+    .clk(clk),
+    .rst(rst),
+    // Ethernet frame input
+    .s_eth_hdr_valid(ip_rx_eth_hdr_valid),
+    .s_eth_hdr_ready(ip_rx_eth_hdr_ready),
+    .s_eth_dest_mac(ip_rx_eth_dest_mac),
+    .s_eth_src_mac(ip_rx_eth_src_mac),
+    .s_eth_type(ip_rx_eth_type),
+    .s_eth_payload_axis_tdata(ip_rx_eth_payload_axis_tdata),
+    .s_eth_payload_axis_tvalid(ip_rx_eth_payload_axis_tvalid),
+    .s_eth_payload_axis_tready(ip_rx_eth_payload_axis_tready),
+    .s_eth_payload_axis_tlast(ip_rx_eth_payload_axis_tlast),
+    .s_eth_payload_axis_tuser(ip_rx_eth_payload_axis_tuser),
+    // Ethernet frame output
+    .m_eth_hdr_valid(ip_tx_eth_hdr_valid),
+    .m_eth_hdr_ready(ip_tx_eth_hdr_ready),
+    .m_eth_dest_mac(ip_tx_eth_dest_mac),
+    .m_eth_src_mac(ip_tx_eth_src_mac),
+    .m_eth_type(ip_tx_eth_type),
+    .m_eth_payload_axis_tdata(ip_tx_eth_payload_axis_tdata),
+    .m_eth_payload_axis_tvalid(ip_tx_eth_payload_axis_tvalid),
+    .m_eth_payload_axis_tready(ip_tx_eth_payload_axis_tready),
+    .m_eth_payload_axis_tlast(ip_tx_eth_payload_axis_tlast),
+    .m_eth_payload_axis_tuser(ip_tx_eth_payload_axis_tuser),
+    // IP frame output
+    .m_ip_hdr_valid(m_ip_hdr_valid),
+    .m_ip_hdr_ready(m_ip_hdr_ready),
+    .m_ip_eth_dest_mac(m_ip_eth_dest_mac),
+    .m_ip_eth_src_mac(m_ip_eth_src_mac),
+    .m_ip_eth_type(m_ip_eth_type),
+    .m_ip_version(m_ip_version),
+    .m_ip_ihl(m_ip_ihl),
+    .m_ip_dscp(m_ip_dscp),
+    .m_ip_ecn(m_ip_ecn),
+    .m_ip_length(m_ip_length),
+    .m_ip_identification(m_ip_identification),
+    .m_ip_flags(m_ip_flags),
+    .m_ip_fragment_offset(m_ip_fragment_offset),
+    .m_ip_ttl(m_ip_ttl),
+    .m_ip_protocol(m_ip_protocol),
+    .m_ip_header_checksum(m_ip_header_checksum),
+    .m_ip_source_ip(m_ip_source_ip),
+    .m_ip_dest_ip(m_ip_dest_ip),
+    .m_ip_payload_axis_tdata(m_ip_payload_axis_tdata),
+    .m_ip_payload_axis_tvalid(m_ip_payload_axis_tvalid),
+    .m_ip_payload_axis_tready(m_ip_payload_axis_tready),
+    .m_ip_payload_axis_tlast(m_ip_payload_axis_tlast),
+    .m_ip_payload_axis_tuser(m_ip_payload_axis_tuser),
+    // IP frame input
+    .s_ip_hdr_valid(s_ip_hdr_valid),
+    .s_ip_hdr_ready(s_ip_hdr_ready),
+    .s_ip_dscp(s_ip_dscp),
+    .s_ip_ecn(s_ip_ecn),
+    .s_ip_length(s_ip_length),
+    .s_ip_ttl(s_ip_ttl),
+    .s_ip_protocol(s_ip_protocol),
+    .s_ip_source_ip(s_ip_source_ip),
+    .s_ip_dest_ip(s_ip_dest_ip),
+    .s_ip_payload_axis_tdata(s_ip_payload_axis_tdata),
+    .s_ip_payload_axis_tvalid(s_ip_payload_axis_tvalid),
+    .s_ip_payload_axis_tready(s_ip_payload_axis_tready),
+    .s_ip_payload_axis_tlast(s_ip_payload_axis_tlast),
+    .s_ip_payload_axis_tuser(s_ip_payload_axis_tuser),
+    // ARP requests
+    .arp_request_valid(arp_request_valid),
+    .arp_request_ready(arp_request_ready),
+    .arp_request_ip(arp_request_ip),
+    .arp_response_valid(arp_response_valid),
+    .arp_response_ready(arp_response_ready),
+    .arp_response_error(arp_response_error),
+    .arp_response_mac(arp_response_mac),
+    // Status
+    .rx_busy(rx_busy),
+    .tx_busy(tx_busy),
+    .rx_error_header_early_termination(rx_error_header_early_termination),
+    .rx_error_payload_early_termination(rx_error_payload_early_termination),
+    .rx_error_invalid_header(rx_error_invalid_header),
+    .rx_error_invalid_checksum(rx_error_invalid_checksum),
+    .tx_error_payload_early_termination(tx_error_payload_early_termination),
+    .tx_error_arp_failed(tx_error_arp_failed),
+    // Configuration
+    .local_mac(local_mac),
+    .local_ip(local_ip)
+);
+
+/*
+ * ARP module
+ */
+arp #(
+    .CACHE_ADDR_WIDTH(ARP_CACHE_ADDR_WIDTH),
+    .REQUEST_RETRY_COUNT(ARP_REQUEST_RETRY_COUNT),
+    .REQUEST_RETRY_INTERVAL(ARP_REQUEST_RETRY_INTERVAL),
+    .REQUEST_TIMEOUT(ARP_REQUEST_TIMEOUT)
+)
+arp_inst (
+    .clk(clk),
+    .rst(rst),
+    // Ethernet frame input
+    .s_eth_hdr_valid(arp_rx_eth_hdr_valid),
+    .s_eth_hdr_ready(arp_rx_eth_hdr_ready),
+    .s_eth_dest_mac(arp_rx_eth_dest_mac),
+    .s_eth_src_mac(arp_rx_eth_src_mac),
+    .s_eth_type(arp_rx_eth_type),
+    .s_eth_payload_axis_tdata(arp_rx_eth_payload_axis_tdata),
+    .s_eth_payload_axis_tvalid(arp_rx_eth_payload_axis_tvalid),
+    .s_eth_payload_axis_tready(arp_rx_eth_payload_axis_tready),
+    .s_eth_payload_axis_tlast(arp_rx_eth_payload_axis_tlast),
+    .s_eth_payload_axis_tuser(arp_rx_eth_payload_axis_tuser),
+    // Ethernet frame output
+    .m_eth_hdr_valid(arp_tx_eth_hdr_valid),
+    .m_eth_hdr_ready(arp_tx_eth_hdr_ready),
+    .m_eth_dest_mac(arp_tx_eth_dest_mac),
+    .m_eth_src_mac(arp_tx_eth_src_mac),
+    .m_eth_type(arp_tx_eth_type),
+    .m_eth_payload_axis_tdata(arp_tx_eth_payload_axis_tdata),
+    .m_eth_payload_axis_tvalid(arp_tx_eth_payload_axis_tvalid),
+    .m_eth_payload_axis_tready(arp_tx_eth_payload_axis_tready),
+    .m_eth_payload_axis_tlast(arp_tx_eth_payload_axis_tlast),
+    .m_eth_payload_axis_tuser(arp_tx_eth_payload_axis_tuser),
+    // ARP requests
+    .arp_request_valid(arp_request_valid),
+    .arp_request_ready(arp_request_ready),
+    .arp_request_ip(arp_request_ip),
+    .arp_response_valid(arp_response_valid),
+    .arp_response_ready(arp_response_ready),
+    .arp_response_error(arp_response_error),
+    .arp_response_mac(arp_response_mac),
+    // Configuration
+    .local_mac(local_mac),
+    .local_ip(local_ip),
+    .gateway_ip(gateway_ip),
+    .subnet_mask(subnet_mask),
+    .clear_cache(clear_arp_cache)
+);
+
+endmodule
+
diff --git a/verilog/rtl/ip_eth_rx.v b/verilog/rtl/ip_eth_rx.v
new file mode 100644
index 0000000..82634e2
--- /dev/null
+++ b/verilog/rtl/ip_eth_rx.v
@@ -0,0 +1,604 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * IP ethernet frame receiver (Ethernet frame in, IP frame out)
+ */
+module ip_eth_rx
+(
+    input  wire        clk,
+    input  wire        rst,
+
+    /*
+     * Ethernet frame input
+     */
+    input  wire        s_eth_hdr_valid,
+    output wire        s_eth_hdr_ready,
+    input  wire [47:0] s_eth_dest_mac,
+    input  wire [47:0] s_eth_src_mac,
+    input  wire [15:0] s_eth_type,
+    input  wire [7:0]  s_eth_payload_axis_tdata,
+    input  wire        s_eth_payload_axis_tvalid,
+    output wire        s_eth_payload_axis_tready,
+    input  wire        s_eth_payload_axis_tlast,
+    input  wire        s_eth_payload_axis_tuser,
+
+    /*
+     * IP frame output
+     */
+    output wire        m_ip_hdr_valid,
+    input  wire        m_ip_hdr_ready,
+    output wire [47:0] m_eth_dest_mac,
+    output wire [47:0] m_eth_src_mac,
+    output wire [15:0] m_eth_type,
+    output wire [3:0]  m_ip_version,
+    output wire [3:0]  m_ip_ihl,
+    output wire [5:0]  m_ip_dscp,
+    output wire [1:0]  m_ip_ecn,
+    output wire [15:0] m_ip_length,
+    output wire [15:0] m_ip_identification,
+    output wire [2:0]  m_ip_flags,
+    output wire [12:0] m_ip_fragment_offset,
+    output wire [7:0]  m_ip_ttl,
+    output wire [7:0]  m_ip_protocol,
+    output wire [15:0] m_ip_header_checksum,
+    output wire [31:0] m_ip_source_ip,
+    output wire [31:0] m_ip_dest_ip,
+    output wire [7:0]  m_ip_payload_axis_tdata,
+    output wire        m_ip_payload_axis_tvalid,
+    input  wire        m_ip_payload_axis_tready,
+    output wire        m_ip_payload_axis_tlast,
+    output wire        m_ip_payload_axis_tuser,
+
+    /*
+     * Status signals
+     */
+    output wire        busy,
+    output wire        error_header_early_termination,
+    output wire        error_payload_early_termination,
+    output wire        error_invalid_header,
+    output wire        error_invalid_checksum
+);
+
+/*
+
+IP Frame
+
+ Field                       Length
+ Destination MAC address     6 octets
+ Source MAC address          6 octets
+ Ethertype (0x0800)          2 octets
+ Version (4)                 4 bits
+ IHL (5-15)                  4 bits
+ DSCP (0)                    6 bits
+ ECN (0)                     2 bits
+ length                      2 octets
+ identification (0?)         2 octets
+ flags (010)                 3 bits
+ fragment offset (0)         13 bits
+ time to live (64?)          1 octet
+ protocol                    1 octet
+ header checksum             2 octets
+ source IP                   4 octets
+ destination IP              4 octets
+ options                     (IHL-5)*4 octets
+ payload                     length octets
+
+This module receives an Ethernet frame with header fields in parallel and
+payload on an AXI stream interface, decodes and strips the IP header fields,
+then produces the header fields in parallel along with the IP payload in a
+separate AXI stream.
+
+*/
+
+localparam [2:0]
+    STATE_IDLE = 3'd0,
+    STATE_READ_HEADER = 3'd1,
+    STATE_READ_PAYLOAD = 3'd2,
+    STATE_READ_PAYLOAD_LAST = 3'd3,
+    STATE_WAIT_LAST = 3'd4;
+
+reg [2:0] state_reg, state_next;
+
+// datapath control signals
+reg store_eth_hdr;
+reg store_ip_version_ihl;
+reg store_ip_dscp_ecn;
+reg store_ip_length_0;
+reg store_ip_length_1;
+reg store_ip_identification_0;
+reg store_ip_identification_1;
+reg store_ip_flags_fragment_offset_0;
+reg store_ip_flags_fragment_offset_1;
+reg store_ip_ttl;
+reg store_ip_protocol;
+reg store_ip_header_checksum_0;
+reg store_ip_header_checksum_1;
+reg store_ip_source_ip_0;
+reg store_ip_source_ip_1;
+reg store_ip_source_ip_2;
+reg store_ip_source_ip_3;
+reg store_ip_dest_ip_0;
+reg store_ip_dest_ip_1;
+reg store_ip_dest_ip_2;
+reg store_ip_dest_ip_3;
+reg store_last_word;
+
+reg [5:0] hdr_ptr_reg, hdr_ptr_next;
+reg [15:0] word_count_reg, word_count_next;
+
+reg [15:0] hdr_sum_reg, hdr_sum_next;
+
+reg [7:0] last_word_data_reg;
+
+reg s_eth_hdr_ready_reg, s_eth_hdr_ready_next;
+reg s_eth_payload_axis_tready_reg, s_eth_payload_axis_tready_next;
+
+reg m_ip_hdr_valid_reg, m_ip_hdr_valid_next;
+reg [47:0] m_eth_dest_mac_reg;
+reg [47:0] m_eth_src_mac_reg;
+reg [15:0] m_eth_type_reg;
+reg [3:0] m_ip_version_reg;
+reg [3:0] m_ip_ihl_reg;
+reg [5:0] m_ip_dscp_reg;
+reg [1:0] m_ip_ecn_reg;
+reg [15:0] m_ip_length_reg;
+reg [15:0] m_ip_identification_reg;
+reg [2:0] m_ip_flags_reg;
+reg [12:0] m_ip_fragment_offset_reg;
+reg [7:0] m_ip_ttl_reg;
+reg [7:0] m_ip_protocol_reg;
+reg [15:0] m_ip_header_checksum_reg;
+reg [31:0] m_ip_source_ip_reg;
+reg [31:0] m_ip_dest_ip_reg;
+
+reg busy_reg;
+reg error_header_early_termination_reg, error_header_early_termination_next;
+reg error_payload_early_termination_reg, error_payload_early_termination_next;
+reg error_invalid_header_reg, error_invalid_header_next;
+reg error_invalid_checksum_reg, error_invalid_checksum_next;
+
+// internal datapath
+reg [7:0] m_ip_payload_axis_tdata_int;
+reg       m_ip_payload_axis_tvalid_int;
+reg       m_ip_payload_axis_tready_int_reg;
+reg       m_ip_payload_axis_tlast_int;
+reg       m_ip_payload_axis_tuser_int;
+wire      m_ip_payload_axis_tready_int_early;
+
+assign s_eth_hdr_ready = s_eth_hdr_ready_reg;
+assign s_eth_payload_axis_tready = s_eth_payload_axis_tready_reg;
+
+assign m_ip_hdr_valid = m_ip_hdr_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+assign m_ip_version = m_ip_version_reg;
+assign m_ip_ihl = m_ip_ihl_reg;
+assign m_ip_dscp = m_ip_dscp_reg;
+assign m_ip_ecn = m_ip_ecn_reg;
+assign m_ip_length = m_ip_length_reg;
+assign m_ip_identification = m_ip_identification_reg;
+assign m_ip_flags = m_ip_flags_reg;
+assign m_ip_fragment_offset = m_ip_fragment_offset_reg;
+assign m_ip_ttl = m_ip_ttl_reg;
+assign m_ip_protocol = m_ip_protocol_reg;
+assign m_ip_header_checksum = m_ip_header_checksum_reg;
+assign m_ip_source_ip = m_ip_source_ip_reg;
+assign m_ip_dest_ip = m_ip_dest_ip_reg;
+
+assign busy = busy_reg;
+assign error_header_early_termination = error_header_early_termination_reg;
+assign error_payload_early_termination = error_payload_early_termination_reg;
+assign error_invalid_header = error_invalid_header_reg;
+assign error_invalid_checksum = error_invalid_checksum_reg;
+
+function [15:0] add1c16b;
+    input [15:0] a, b;
+    reg [16:0] t;
+    begin
+        t = a+b;
+        add1c16b = t[15:0] + t[16];
+    end
+endfunction
+
+always @* begin
+    state_next = STATE_IDLE;
+
+    s_eth_hdr_ready_next = 1'b0;
+    s_eth_payload_axis_tready_next = 1'b0;
+
+    store_eth_hdr = 1'b0;
+    store_ip_version_ihl = 1'b0;
+    store_ip_dscp_ecn = 1'b0;
+    store_ip_length_0 = 1'b0;
+    store_ip_length_1 = 1'b0;
+    store_ip_identification_0 = 1'b0;
+    store_ip_identification_1 = 1'b0;
+    store_ip_flags_fragment_offset_0 = 1'b0;
+    store_ip_flags_fragment_offset_1 = 1'b0;
+    store_ip_ttl = 1'b0;
+    store_ip_protocol = 1'b0;
+    store_ip_header_checksum_0 = 1'b0;
+    store_ip_header_checksum_1 = 1'b0;
+    store_ip_source_ip_0 = 1'b0;
+    store_ip_source_ip_1 = 1'b0;
+    store_ip_source_ip_2 = 1'b0;
+    store_ip_source_ip_3 = 1'b0;
+    store_ip_dest_ip_0 = 1'b0;
+    store_ip_dest_ip_1 = 1'b0;
+    store_ip_dest_ip_2 = 1'b0;
+    store_ip_dest_ip_3 = 1'b0;
+
+    store_last_word = 1'b0;
+
+    hdr_ptr_next = hdr_ptr_reg;
+    word_count_next = word_count_reg;
+
+    hdr_sum_next = hdr_sum_reg;
+
+    m_ip_hdr_valid_next = m_ip_hdr_valid_reg && !m_ip_hdr_ready;
+
+    error_header_early_termination_next = 1'b0;
+    error_payload_early_termination_next = 1'b0;
+    error_invalid_header_next = 1'b0;
+    error_invalid_checksum_next = 1'b0;
+
+    m_ip_payload_axis_tdata_int = 8'd0;
+    m_ip_payload_axis_tvalid_int = 1'b0;
+    m_ip_payload_axis_tlast_int = 1'b0;
+    m_ip_payload_axis_tuser_int = 1'b0;
+
+    case (state_reg)
+        STATE_IDLE: begin
+            // idle state - wait for header
+            hdr_ptr_next = 16'd0;
+            hdr_sum_next = 16'd0;
+            s_eth_hdr_ready_next = !m_ip_hdr_valid_next;
+
+            if (s_eth_hdr_ready && s_eth_hdr_valid) begin
+                s_eth_hdr_ready_next = 1'b0;
+                s_eth_payload_axis_tready_next = 1'b1;
+                store_eth_hdr = 1'b1;
+                state_next = STATE_READ_HEADER;
+            end else begin
+                state_next = STATE_IDLE;
+            end
+        end
+        STATE_READ_HEADER: begin
+            // read header
+            s_eth_payload_axis_tready_next = 1'b1;
+            word_count_next = m_ip_length_reg - 5*4;
+
+            if (s_eth_payload_axis_tready && s_eth_payload_axis_tvalid) begin
+                // word transfer in - store it
+                hdr_ptr_next = hdr_ptr_reg + 6'd1;
+                state_next = STATE_READ_HEADER;
+
+                if (hdr_ptr_reg[0]) begin
+                    hdr_sum_next = add1c16b(hdr_sum_reg, {8'd0, s_eth_payload_axis_tdata});
+                end else begin
+                    hdr_sum_next = add1c16b(hdr_sum_reg, {s_eth_payload_axis_tdata, 8'd0});
+                end
+
+                case (hdr_ptr_reg)
+                    6'h00: store_ip_version_ihl = 1'b1;
+                    6'h01: store_ip_dscp_ecn = 1'b1;
+                    6'h02: store_ip_length_1 = 1'b1;
+                    6'h03: store_ip_length_0 = 1'b1;
+                    6'h04: store_ip_identification_1 = 1'b1;
+                    6'h05: store_ip_identification_0 = 1'b1;
+                    6'h06: store_ip_flags_fragment_offset_1 = 1'b1;
+                    6'h07: store_ip_flags_fragment_offset_0 = 1'b1;
+                    6'h08: store_ip_ttl = 1'b1;
+                    6'h09: store_ip_protocol = 1'b1;
+                    6'h0A: store_ip_header_checksum_1 = 1'b1;
+                    6'h0B: store_ip_header_checksum_0 = 1'b1;
+                    6'h0C: store_ip_source_ip_3 = 1'b1;
+                    6'h0D: store_ip_source_ip_2 = 1'b1;
+                    6'h0E: store_ip_source_ip_1 = 1'b1;
+                    6'h0F: store_ip_source_ip_0 = 1'b1;
+                    6'h10: store_ip_dest_ip_3 = 1'b1;
+                    6'h11: store_ip_dest_ip_2 = 1'b1;
+                    6'h12: store_ip_dest_ip_1 = 1'b1;
+                    6'h13: begin
+                        store_ip_dest_ip_0 = 1'b1;
+                        if (m_ip_version_reg != 4'd4 || m_ip_ihl_reg != 4'd5) begin
+                            error_invalid_header_next = 1'b1;
+                            state_next = STATE_WAIT_LAST;
+                        end else if (hdr_sum_next != 16'hffff) begin
+                            error_invalid_checksum_next = 1'b1;
+                            state_next = STATE_WAIT_LAST;
+                        end else begin
+                            m_ip_hdr_valid_next = 1'b1;
+                            s_eth_payload_axis_tready_next = m_ip_payload_axis_tready_int_early;
+                            state_next = STATE_READ_PAYLOAD;
+                        end
+                    end
+                endcase
+
+                if (s_eth_payload_axis_tlast) begin
+                    error_header_early_termination_next = 1'b1;
+                    m_ip_hdr_valid_next = 1'b0;
+                    s_eth_hdr_ready_next = !m_ip_hdr_valid_next;
+                    s_eth_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end
+
+            end else begin
+                state_next = STATE_READ_HEADER;
+            end
+        end
+        STATE_READ_PAYLOAD: begin
+            // read payload
+            s_eth_payload_axis_tready_next = m_ip_payload_axis_tready_int_early;
+
+            m_ip_payload_axis_tdata_int = s_eth_payload_axis_tdata;
+            m_ip_payload_axis_tvalid_int = s_eth_payload_axis_tvalid;
+            m_ip_payload_axis_tlast_int = s_eth_payload_axis_tlast;
+            m_ip_payload_axis_tuser_int = s_eth_payload_axis_tuser;
+
+            if (s_eth_payload_axis_tready && s_eth_payload_axis_tvalid) begin
+                // word transfer through
+                word_count_next = word_count_reg - 16'd1;
+                if (s_eth_payload_axis_tlast) begin
+                    if (word_count_reg > 16'd1) begin
+                        // end of frame, but length does not match
+                        m_ip_payload_axis_tuser_int = 1'b1;
+                        error_payload_early_termination_next = 1'b1;
+                    end
+                    s_eth_hdr_ready_next = !m_ip_hdr_valid_next;
+                    s_eth_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    if (word_count_reg == 16'd1) begin
+                        store_last_word = 1'b1;
+                        m_ip_payload_axis_tvalid_int = 1'b0;
+                        state_next = STATE_READ_PAYLOAD_LAST;
+                    end else begin
+                        state_next = STATE_READ_PAYLOAD;
+                    end
+                end
+            end else begin
+                state_next = STATE_READ_PAYLOAD;
+            end
+        end
+        STATE_READ_PAYLOAD_LAST: begin
+            // read and discard until end of frame
+            s_eth_payload_axis_tready_next = m_ip_payload_axis_tready_int_early;
+
+            m_ip_payload_axis_tdata_int = last_word_data_reg;
+            m_ip_payload_axis_tvalid_int = s_eth_payload_axis_tvalid && s_eth_payload_axis_tlast;
+            m_ip_payload_axis_tlast_int = s_eth_payload_axis_tlast;
+            m_ip_payload_axis_tuser_int = s_eth_payload_axis_tuser;
+
+            if (s_eth_payload_axis_tready && s_eth_payload_axis_tvalid) begin
+                if (s_eth_payload_axis_tlast) begin
+                    s_eth_hdr_ready_next = !m_ip_hdr_valid_next;
+                    s_eth_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_READ_PAYLOAD_LAST;
+                end
+            end else begin
+                state_next = STATE_READ_PAYLOAD_LAST;
+            end
+        end
+        STATE_WAIT_LAST: begin
+            // read and discard until end of frame
+            s_eth_payload_axis_tready_next = 1'b1;
+
+            if (s_eth_payload_axis_tready && s_eth_payload_axis_tvalid) begin
+                if (s_eth_payload_axis_tlast) begin
+                    s_eth_hdr_ready_next = !m_ip_hdr_valid_next;
+                    s_eth_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_WAIT_LAST;
+                end
+            end else begin
+                state_next = STATE_WAIT_LAST;
+            end
+        end
+    endcase
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        state_reg <= STATE_IDLE;
+        s_eth_hdr_ready_reg <= 1'b0;
+        s_eth_payload_axis_tready_reg <= 1'b0;
+        m_ip_hdr_valid_reg <= 1'b0;
+        busy_reg <= 1'b0;
+        error_header_early_termination_reg <= 1'b0;
+        error_payload_early_termination_reg <= 1'b0;
+        error_invalid_header_reg <= 1'b0;
+        error_invalid_checksum_reg <= 1'b0;
+        hdr_ptr_reg <= 6'd0;
+        word_count_reg <= 16'd0;
+        hdr_sum_reg <= 16'd0;
+        last_word_data_reg <= 8'd0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+        m_ip_version_reg <= 4'd0;
+        m_ip_ihl_reg <= 4'd0;
+        m_ip_dscp_reg <= 6'd0;
+        m_ip_ecn_reg <= 2'd0;
+        m_ip_length_reg <= 16'd0;
+        m_ip_identification_reg <= 16'd0;
+        m_ip_flags_reg <= 3'd0;
+        m_ip_fragment_offset_reg <= 13'd0;
+        m_ip_ttl_reg <= 8'd0;
+        m_ip_protocol_reg <= 8'd0;
+        m_ip_header_checksum_reg <= 16'd0;
+        m_ip_source_ip_reg <= 32'd0;
+        m_ip_dest_ip_reg <= 32'd0;
+    end else begin
+        state_reg <= state_next;
+
+        s_eth_hdr_ready_reg <= s_eth_hdr_ready_next;
+        s_eth_payload_axis_tready_reg <= s_eth_payload_axis_tready_next;
+
+        m_ip_hdr_valid_reg <= m_ip_hdr_valid_next;
+
+        error_header_early_termination_reg <= error_header_early_termination_next;
+        error_payload_early_termination_reg <= error_payload_early_termination_next;
+        error_invalid_header_reg <= error_invalid_header_next;
+        error_invalid_checksum_reg <= error_invalid_checksum_next;
+
+        busy_reg <= state_next != STATE_IDLE;
+
+        hdr_ptr_reg <= hdr_ptr_next;
+        word_count_reg <= word_count_next;
+    
+        hdr_sum_reg <= hdr_sum_next;
+    
+        // datapath
+        if (store_eth_hdr) begin
+            m_eth_dest_mac_reg <= s_eth_dest_mac;
+            m_eth_src_mac_reg <= s_eth_src_mac;
+            m_eth_type_reg <= s_eth_type;
+        end
+    
+        if (store_last_word) begin
+            last_word_data_reg <= m_ip_payload_axis_tdata_int;
+        end
+    
+        if (store_ip_version_ihl) {m_ip_version_reg, m_ip_ihl_reg} <= s_eth_payload_axis_tdata;
+        if (store_ip_dscp_ecn) {m_ip_dscp_reg, m_ip_ecn_reg} <= s_eth_payload_axis_tdata;
+        if (store_ip_length_0) m_ip_length_reg[ 7: 0] <= s_eth_payload_axis_tdata;
+        if (store_ip_length_1) m_ip_length_reg[15: 8] <= s_eth_payload_axis_tdata;
+        if (store_ip_identification_0) m_ip_identification_reg[ 7: 0] <= s_eth_payload_axis_tdata;
+        if (store_ip_identification_1) m_ip_identification_reg[15: 8] <= s_eth_payload_axis_tdata;
+        if (store_ip_flags_fragment_offset_0) m_ip_fragment_offset_reg[ 7:0] <= s_eth_payload_axis_tdata;
+        if (store_ip_flags_fragment_offset_1) {m_ip_flags_reg, m_ip_fragment_offset_reg[12:8]} <= s_eth_payload_axis_tdata;
+        if (store_ip_ttl) m_ip_ttl_reg <= s_eth_payload_axis_tdata;
+        if (store_ip_protocol) m_ip_protocol_reg <= s_eth_payload_axis_tdata;
+        if (store_ip_header_checksum_0) m_ip_header_checksum_reg[ 7: 0] <= s_eth_payload_axis_tdata;
+        if (store_ip_header_checksum_1) m_ip_header_checksum_reg[15: 8] <= s_eth_payload_axis_tdata;
+        if (store_ip_source_ip_0) m_ip_source_ip_reg[ 7: 0] <= s_eth_payload_axis_tdata;
+        if (store_ip_source_ip_1) m_ip_source_ip_reg[15: 8] <= s_eth_payload_axis_tdata;
+        if (store_ip_source_ip_2) m_ip_source_ip_reg[23:16] <= s_eth_payload_axis_tdata;
+        if (store_ip_source_ip_3) m_ip_source_ip_reg[31:24] <= s_eth_payload_axis_tdata;
+        if (store_ip_dest_ip_0) m_ip_dest_ip_reg[ 7: 0] <= s_eth_payload_axis_tdata;
+        if (store_ip_dest_ip_1) m_ip_dest_ip_reg[15: 8] <= s_eth_payload_axis_tdata;
+        if (store_ip_dest_ip_2) m_ip_dest_ip_reg[23:16] <= s_eth_payload_axis_tdata;
+        if (store_ip_dest_ip_3) m_ip_dest_ip_reg[31:24] <= s_eth_payload_axis_tdata;
+    end
+end
+
+// output datapath logic
+reg [7:0] m_ip_payload_axis_tdata_reg;
+reg       m_ip_payload_axis_tvalid_reg, m_ip_payload_axis_tvalid_next;
+reg       m_ip_payload_axis_tlast_reg;
+reg       m_ip_payload_axis_tuser_reg;
+
+reg [7:0] temp_m_ip_payload_axis_tdata_reg;
+reg       temp_m_ip_payload_axis_tvalid_reg, temp_m_ip_payload_axis_tvalid_next;
+reg       temp_m_ip_payload_axis_tlast_reg;
+reg       temp_m_ip_payload_axis_tuser_reg;
+
+// datapath control
+reg store_ip_payload_int_to_output;
+reg store_ip_payload_int_to_temp;
+reg store_ip_payload_axis_temp_to_output;
+
+assign m_ip_payload_axis_tdata = m_ip_payload_axis_tdata_reg;
+assign m_ip_payload_axis_tvalid = m_ip_payload_axis_tvalid_reg;
+assign m_ip_payload_axis_tlast = m_ip_payload_axis_tlast_reg;
+assign m_ip_payload_axis_tuser = m_ip_payload_axis_tuser_reg;
+
+// 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_ip_payload_axis_tready_int_early = m_ip_payload_axis_tready || (!temp_m_ip_payload_axis_tvalid_reg && (!m_ip_payload_axis_tvalid_reg || !m_ip_payload_axis_tvalid_int));
+
+always @* begin
+    // transfer sink ready state to source
+    m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_reg;
+    temp_m_ip_payload_axis_tvalid_next = temp_m_ip_payload_axis_tvalid_reg;
+
+    store_ip_payload_int_to_output = 1'b0;
+    store_ip_payload_int_to_temp = 1'b0;
+    store_ip_payload_axis_temp_to_output = 1'b0;
+    
+    if (m_ip_payload_axis_tready_int_reg) begin
+        // input is ready
+        if (m_ip_payload_axis_tready || !m_ip_payload_axis_tvalid_reg) begin
+            // output is ready or currently not valid, transfer data to output
+            m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_int;
+            store_ip_payload_int_to_output = 1'b1;
+        end else begin
+            // output is not ready, store input in temp
+            temp_m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_int;
+            store_ip_payload_int_to_temp = 1'b1;
+        end
+    end else if (m_ip_payload_axis_tready) begin
+        // input is not ready, but output is ready
+        m_ip_payload_axis_tvalid_next = temp_m_ip_payload_axis_tvalid_reg;
+        temp_m_ip_payload_axis_tvalid_next = 1'b0;
+        store_ip_payload_axis_temp_to_output = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        m_ip_payload_axis_tready_int_reg <= 1'b0;
+        m_ip_payload_axis_tdata_reg <= 8'd0;
+        m_ip_payload_axis_tvalid_reg <= 1'b0;
+        m_ip_payload_axis_tlast_reg <= 1'b0;
+        m_ip_payload_axis_tuser_reg <= 1'b0;
+        temp_m_ip_payload_axis_tdata_reg <= 8'd0;
+        temp_m_ip_payload_axis_tvalid_reg <= 1'b0;
+        temp_m_ip_payload_axis_tlast_reg <= 1'b0;
+        temp_m_ip_payload_axis_tuser_reg <= 1'b0;
+    end else begin
+        m_ip_payload_axis_tvalid_reg <= m_ip_payload_axis_tvalid_next;
+        m_ip_payload_axis_tready_int_reg <= m_ip_payload_axis_tready_int_early;
+        temp_m_ip_payload_axis_tvalid_reg <= temp_m_ip_payload_axis_tvalid_next;
+
+        // datapath
+        if (store_ip_payload_int_to_output) begin
+            m_ip_payload_axis_tdata_reg <= m_ip_payload_axis_tdata_int;
+            m_ip_payload_axis_tlast_reg <= m_ip_payload_axis_tlast_int;
+            m_ip_payload_axis_tuser_reg <= m_ip_payload_axis_tuser_int;
+        end else if (store_ip_payload_axis_temp_to_output) begin
+            m_ip_payload_axis_tdata_reg <= temp_m_ip_payload_axis_tdata_reg;
+            m_ip_payload_axis_tlast_reg <= temp_m_ip_payload_axis_tlast_reg;
+            m_ip_payload_axis_tuser_reg <= temp_m_ip_payload_axis_tuser_reg;
+        end
+    
+        if (store_ip_payload_int_to_temp) begin
+            temp_m_ip_payload_axis_tdata_reg <= m_ip_payload_axis_tdata_int;
+            temp_m_ip_payload_axis_tlast_reg <= m_ip_payload_axis_tlast_int;
+            temp_m_ip_payload_axis_tuser_reg <= m_ip_payload_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/ip_eth_tx.v b/verilog/rtl/ip_eth_tx.v
new file mode 100644
index 0000000..ca9ca5c
--- /dev/null
+++ b/verilog/rtl/ip_eth_tx.v
@@ -0,0 +1,521 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * IP ethernet frame transmitter (IP frame in, Ethernet frame out)
+ */
+module ip_eth_tx
+(
+    input  wire        clk,
+    input  wire        rst,
+
+    /*
+     * IP frame input
+     */
+    input  wire        s_ip_hdr_valid,
+    output wire        s_ip_hdr_ready,
+    input  wire [47:0] s_eth_dest_mac,
+    input  wire [47:0] s_eth_src_mac,
+    input  wire [15:0] s_eth_type,
+    input  wire [5:0]  s_ip_dscp,
+    input  wire [1:0]  s_ip_ecn,
+    input  wire [15:0] s_ip_length,
+    input  wire [15:0] s_ip_identification,
+    input  wire [2:0]  s_ip_flags,
+    input  wire [12:0] s_ip_fragment_offset,
+    input  wire [7:0]  s_ip_ttl,
+    input  wire [7:0]  s_ip_protocol,
+    input  wire [31:0] s_ip_source_ip,
+    input  wire [31:0] s_ip_dest_ip,
+    input  wire [7:0]  s_ip_payload_axis_tdata,
+    input  wire        s_ip_payload_axis_tvalid,
+    output wire        s_ip_payload_axis_tready,
+    input  wire        s_ip_payload_axis_tlast,
+    input  wire        s_ip_payload_axis_tuser,
+
+    /*
+     * Ethernet frame output
+     */
+    output wire        m_eth_hdr_valid,
+    input  wire        m_eth_hdr_ready,
+    output wire [47:0] m_eth_dest_mac,
+    output wire [47:0] m_eth_src_mac,
+    output wire [15:0] m_eth_type,
+    output wire [7:0]  m_eth_payload_axis_tdata,
+    output wire        m_eth_payload_axis_tvalid,
+    input  wire        m_eth_payload_axis_tready,
+    output wire        m_eth_payload_axis_tlast,
+    output wire        m_eth_payload_axis_tuser,
+
+    /*
+     * Status signals
+     */
+    output wire        busy,
+    output wire        error_payload_early_termination
+);
+
+/*
+
+IP Frame
+
+ Field                       Length
+ Destination MAC address     6 octets
+ Source MAC address          6 octets
+ Ethertype (0x0800)          2 octets
+ Version (4)                 4 bits
+ IHL (5-15)                  4 bits
+ DSCP (0)                    6 bits
+ ECN (0)                     2 bits
+ length                      2 octets
+ identification (0?)         2 octets
+ flags (010)                 3 bits
+ fragment offset (0)         13 bits
+ time to live (64?)          1 octet
+ protocol                    1 octet
+ header checksum             2 octets
+ source IP                   4 octets
+ destination IP              4 octets
+ options                     (IHL-5)*4 octets
+ payload                     length octets
+
+This module receives an IP frame with header fields in parallel along with the
+payload in an AXI stream, combines the header with the payload, passes through
+the Ethernet headers, and transmits the complete Ethernet payload on an AXI
+interface.
+
+*/
+
+localparam [2:0]
+    STATE_IDLE = 3'd0,
+    STATE_WRITE_HEADER = 3'd1,
+    STATE_WRITE_PAYLOAD = 3'd2,
+    STATE_WRITE_PAYLOAD_LAST = 3'd3,
+    STATE_WAIT_LAST = 3'd4;
+
+reg [2:0] state_reg, state_next;
+
+// datapath control signals
+reg store_ip_hdr;
+reg store_last_word;
+
+reg [5:0] hdr_ptr_reg, hdr_ptr_next;
+reg [15:0] word_count_reg, word_count_next;
+
+reg [15:0] hdr_sum_reg, hdr_sum_next;
+
+reg [7:0] last_word_data_reg;
+
+reg [5:0] ip_dscp_reg;
+reg [1:0] ip_ecn_reg;
+reg [15:0] ip_length_reg;
+reg [15:0] ip_identification_reg;
+reg [2:0] ip_flags_reg;
+reg [12:0] ip_fragment_offset_reg;
+reg [7:0] ip_ttl_reg;
+reg [7:0] ip_protocol_reg;
+reg [31:0] ip_source_ip_reg;
+reg [31:0] ip_dest_ip_reg;
+
+reg s_ip_hdr_ready_reg, s_ip_hdr_ready_next;
+reg s_ip_payload_axis_tready_reg, s_ip_payload_axis_tready_next;
+
+reg m_eth_hdr_valid_reg, m_eth_hdr_valid_next;
+reg [47:0] m_eth_dest_mac_reg;
+reg [47:0] m_eth_src_mac_reg;
+reg [15:0] m_eth_type_reg;
+
+reg busy_reg;
+reg error_payload_early_termination_reg, error_payload_early_termination_next;
+
+// internal datapath
+reg [7:0] m_eth_payload_axis_tdata_int;
+reg       m_eth_payload_axis_tvalid_int;
+reg       m_eth_payload_axis_tready_int_reg;
+reg       m_eth_payload_axis_tlast_int;
+reg       m_eth_payload_axis_tuser_int;
+wire      m_eth_payload_axis_tready_int_early;
+
+assign s_ip_hdr_ready = s_ip_hdr_ready_reg;
+assign s_ip_payload_axis_tready = s_ip_payload_axis_tready_reg;
+
+assign m_eth_hdr_valid = m_eth_hdr_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+
+assign busy = busy_reg;
+assign error_payload_early_termination = error_payload_early_termination_reg;
+
+function [15:0] add1c16b;
+    input [15:0] a, b;
+    reg [16:0] t;
+    begin
+        t = a+b;
+        add1c16b = t[15:0] + t[16];
+    end
+endfunction
+
+always @* begin
+    state_next = STATE_IDLE;
+
+    s_ip_hdr_ready_next = 1'b0;
+    s_ip_payload_axis_tready_next = 1'b0;
+
+    store_ip_hdr = 1'b0;
+
+    store_last_word = 1'b0;
+
+    hdr_ptr_next = hdr_ptr_reg;
+    word_count_next = word_count_reg;
+
+    hdr_sum_next = hdr_sum_reg;
+
+    m_eth_hdr_valid_next = m_eth_hdr_valid_reg && !m_eth_hdr_ready;
+
+    error_payload_early_termination_next = 1'b0;
+
+    m_eth_payload_axis_tdata_int = 8'd0;
+    m_eth_payload_axis_tvalid_int = 1'b0;
+    m_eth_payload_axis_tlast_int = 1'b0;
+    m_eth_payload_axis_tuser_int = 1'b0;
+
+    case (state_reg)
+        STATE_IDLE: begin
+            // idle state - wait for data
+            hdr_ptr_next = 6'd0;
+            s_ip_hdr_ready_next = !m_eth_hdr_valid_next;
+
+            if (s_ip_hdr_ready && s_ip_hdr_valid) begin
+                store_ip_hdr = 1'b1;
+                s_ip_hdr_ready_next = 1'b0;
+                m_eth_hdr_valid_next = 1'b1;
+                if (m_eth_payload_axis_tready_int_reg) begin
+                    m_eth_payload_axis_tvalid_int = 1'b1;
+                    m_eth_payload_axis_tdata_int = {4'd4, 4'd5}; // ip_version, ip_ihl
+                    hdr_ptr_next = 6'd1;
+                end
+                state_next = STATE_WRITE_HEADER;
+            end else begin
+                state_next = STATE_IDLE;
+            end
+        end
+        STATE_WRITE_HEADER: begin
+            // write header
+            word_count_next = ip_length_reg - 5*4;
+
+            if (m_eth_payload_axis_tready_int_reg) begin
+                hdr_ptr_next = hdr_ptr_reg + 6'd1;
+                m_eth_payload_axis_tvalid_int = 1;
+                state_next = STATE_WRITE_HEADER;
+                case (hdr_ptr_reg)
+                    6'h00: begin
+                        m_eth_payload_axis_tdata_int = {4'd4, 4'd5}; // ip_version, ip_ihl
+                    end
+                    6'h01: begin
+                        m_eth_payload_axis_tdata_int = {ip_dscp_reg, ip_ecn_reg};
+                        hdr_sum_next = {4'd4, 4'd5, ip_dscp_reg, ip_ecn_reg};
+                    end
+                    6'h02: begin
+                        m_eth_payload_axis_tdata_int = ip_length_reg[15: 8];
+                        hdr_sum_next = add1c16b(hdr_sum_reg, ip_length_reg);
+                    end
+                    6'h03: begin
+                        m_eth_payload_axis_tdata_int = ip_length_reg[ 7: 0];
+                        hdr_sum_next = add1c16b(hdr_sum_reg, ip_identification_reg);
+                    end
+                    6'h04: begin
+                        m_eth_payload_axis_tdata_int = ip_identification_reg[15: 8];
+                        hdr_sum_next = add1c16b(hdr_sum_reg, {ip_flags_reg, ip_fragment_offset_reg});
+                    end
+                    6'h05: begin
+                        m_eth_payload_axis_tdata_int = ip_identification_reg[ 7: 0];
+                        hdr_sum_next = add1c16b(hdr_sum_reg, {ip_ttl_reg, ip_protocol_reg});
+                    end
+                    6'h06: begin
+                        m_eth_payload_axis_tdata_int = {ip_flags_reg, ip_fragment_offset_reg[12:8]};
+                        hdr_sum_next = add1c16b(hdr_sum_reg, ip_source_ip_reg[31:16]);
+                    end
+                    6'h07: begin
+                        m_eth_payload_axis_tdata_int = ip_fragment_offset_reg[ 7: 0];
+                        hdr_sum_next = add1c16b(hdr_sum_reg, ip_source_ip_reg[15:0]);
+                    end
+                    6'h08: begin
+                        m_eth_payload_axis_tdata_int = ip_ttl_reg;
+                        hdr_sum_next = add1c16b(hdr_sum_reg, ip_dest_ip_reg[31:16]);
+                    end
+                    6'h09: begin
+                        m_eth_payload_axis_tdata_int = ip_protocol_reg;
+                        hdr_sum_next = add1c16b(hdr_sum_reg, ip_dest_ip_reg[15:0]);
+                    end
+                    6'h0A: m_eth_payload_axis_tdata_int = ~hdr_sum_reg[15: 8];
+                    6'h0B: m_eth_payload_axis_tdata_int = ~hdr_sum_reg[ 7: 0];
+                    6'h0C: m_eth_payload_axis_tdata_int = ip_source_ip_reg[31:24];
+                    6'h0D: m_eth_payload_axis_tdata_int = ip_source_ip_reg[23:16];
+                    6'h0E: m_eth_payload_axis_tdata_int = ip_source_ip_reg[15: 8];
+                    6'h0F: m_eth_payload_axis_tdata_int = ip_source_ip_reg[ 7: 0];
+                    6'h10: m_eth_payload_axis_tdata_int = ip_dest_ip_reg[31:24];
+                    6'h11: m_eth_payload_axis_tdata_int = ip_dest_ip_reg[23:16];
+                    6'h12: m_eth_payload_axis_tdata_int = ip_dest_ip_reg[15: 8];
+                    6'h13: begin
+                        m_eth_payload_axis_tdata_int = ip_dest_ip_reg[ 7: 0];
+                        s_ip_payload_axis_tready_next = m_eth_payload_axis_tready_int_early;
+                        state_next = STATE_WRITE_PAYLOAD;
+                    end
+                endcase
+            end else begin
+                state_next = STATE_WRITE_HEADER;
+            end
+        end
+        STATE_WRITE_PAYLOAD: begin
+            // write payload
+            s_ip_payload_axis_tready_next = m_eth_payload_axis_tready_int_early;
+
+            m_eth_payload_axis_tdata_int = s_ip_payload_axis_tdata;
+            m_eth_payload_axis_tvalid_int = s_ip_payload_axis_tvalid;
+            m_eth_payload_axis_tlast_int = s_ip_payload_axis_tlast;
+            m_eth_payload_axis_tuser_int = s_ip_payload_axis_tuser;
+
+            if (s_ip_payload_axis_tready && s_ip_payload_axis_tvalid) begin
+                // word transfer through
+                word_count_next = word_count_reg - 6'd1;
+                if (s_ip_payload_axis_tlast) begin
+                    if (word_count_reg != 16'd1) begin
+                        // end of frame, but length does not match
+                        m_eth_payload_axis_tuser_int = 1'b1;
+                        error_payload_early_termination_next = 1'b1;
+                    end
+                    s_ip_hdr_ready_next = !m_eth_hdr_valid_next;
+                    s_ip_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    if (word_count_reg == 16'd1) begin
+                        store_last_word = 1'b1;
+                        m_eth_payload_axis_tvalid_int = 1'b0;
+                        state_next = STATE_WRITE_PAYLOAD_LAST;
+                    end else begin
+                        state_next = STATE_WRITE_PAYLOAD;
+                    end
+                end
+            end else begin
+                state_next = STATE_WRITE_PAYLOAD;
+            end
+        end
+        STATE_WRITE_PAYLOAD_LAST: begin
+            // read and discard until end of frame
+            s_ip_payload_axis_tready_next = m_eth_payload_axis_tready_int_early;
+
+            m_eth_payload_axis_tdata_int = last_word_data_reg;
+            m_eth_payload_axis_tvalid_int = s_ip_payload_axis_tvalid && s_ip_payload_axis_tlast;
+            m_eth_payload_axis_tlast_int = s_ip_payload_axis_tlast;
+            m_eth_payload_axis_tuser_int = s_ip_payload_axis_tuser;
+
+            if (s_ip_payload_axis_tready && s_ip_payload_axis_tvalid) begin
+                if (s_ip_payload_axis_tlast) begin
+                    s_ip_hdr_ready_next = !m_eth_hdr_valid_next;
+                    s_ip_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_WRITE_PAYLOAD_LAST;
+                end
+            end else begin
+                state_next = STATE_WRITE_PAYLOAD_LAST;
+            end
+        end
+        STATE_WAIT_LAST: begin
+            // read and discard until end of frame
+            s_ip_payload_axis_tready_next = 1'b1;
+
+            if (s_ip_payload_axis_tready && s_ip_payload_axis_tvalid) begin
+                if (s_ip_payload_axis_tlast) begin
+                    s_ip_hdr_ready_next = !m_eth_hdr_valid_next;
+                    s_ip_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_WAIT_LAST;
+                end
+            end else begin
+                state_next = STATE_WAIT_LAST;
+            end
+        end
+    endcase
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        state_reg <= STATE_IDLE;
+        s_ip_hdr_ready_reg <= 1'b0;
+        s_ip_payload_axis_tready_reg <= 1'b0;
+        m_eth_hdr_valid_reg <= 1'b0;
+        busy_reg <= 1'b0;
+        error_payload_early_termination_reg <= 1'b0;
+        hdr_ptr_reg <= 6'd0;
+        word_count_reg <= 16'd0;
+        hdr_sum_reg <= 16'd0;
+        last_word_data_reg <= 8'd0;
+        ip_dscp_reg <= 6'd0;
+        ip_ecn_reg <= 2'd0;
+        ip_length_reg <= 16'd0;
+        ip_identification_reg <= 16'd0;
+        ip_flags_reg <= 3'd0;
+        ip_fragment_offset_reg <= 13'd0;
+        ip_ttl_reg <= 8'd0;
+        ip_protocol_reg <= 8'd0;
+        ip_source_ip_reg <= 32'd0;
+        ip_dest_ip_reg <= 32'd0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+    end else begin
+        state_reg <= state_next;
+
+        s_ip_hdr_ready_reg <= s_ip_hdr_ready_next;
+        s_ip_payload_axis_tready_reg <= s_ip_payload_axis_tready_next;
+
+        m_eth_hdr_valid_reg <= m_eth_hdr_valid_next;
+
+        busy_reg <= state_next != STATE_IDLE;
+
+        error_payload_early_termination_reg <= error_payload_early_termination_next;
+
+        hdr_ptr_reg <= hdr_ptr_next;
+        word_count_reg <= word_count_next;
+    
+        hdr_sum_reg <= hdr_sum_next;
+    
+        // datapath
+        if (store_ip_hdr) begin
+            m_eth_dest_mac_reg <= s_eth_dest_mac;
+            m_eth_src_mac_reg <= s_eth_src_mac;
+            m_eth_type_reg <= s_eth_type;
+            ip_dscp_reg <= s_ip_dscp;
+            ip_ecn_reg <= s_ip_ecn;
+            ip_length_reg <= s_ip_length;
+            ip_identification_reg <= s_ip_identification;
+            ip_flags_reg <= s_ip_flags;
+            ip_fragment_offset_reg <= s_ip_fragment_offset;
+            ip_ttl_reg <= s_ip_ttl;
+            ip_protocol_reg <= s_ip_protocol;
+            ip_source_ip_reg <= s_ip_source_ip;
+            ip_dest_ip_reg <= s_ip_dest_ip;
+        end
+    
+        if (store_last_word) begin
+            last_word_data_reg <= m_eth_payload_axis_tdata_int;
+        end
+    end
+end
+
+// output datapath logic
+reg [7:0] m_eth_payload_axis_tdata_reg;
+reg       m_eth_payload_axis_tvalid_reg, m_eth_payload_axis_tvalid_next;
+reg       m_eth_payload_axis_tlast_reg;
+reg       m_eth_payload_axis_tuser_reg;
+
+reg [7:0] temp_m_eth_payload_axis_tdata_reg;
+reg       temp_m_eth_payload_axis_tvalid_reg, temp_m_eth_payload_axis_tvalid_next;
+reg       temp_m_eth_payload_axis_tlast_reg;
+reg       temp_m_eth_payload_axis_tuser_reg;
+
+// datapath control
+reg store_eth_payload_int_to_output;
+reg store_eth_payload_int_to_temp;
+reg store_eth_payload_axis_temp_to_output;
+
+assign m_eth_payload_axis_tdata = m_eth_payload_axis_tdata_reg;
+assign m_eth_payload_axis_tvalid = m_eth_payload_axis_tvalid_reg;
+assign m_eth_payload_axis_tlast = m_eth_payload_axis_tlast_reg;
+assign m_eth_payload_axis_tuser = m_eth_payload_axis_tuser_reg;
+
+// 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_eth_payload_axis_tready_int_early = m_eth_payload_axis_tready || (!temp_m_eth_payload_axis_tvalid_reg && (!m_eth_payload_axis_tvalid_reg || !m_eth_payload_axis_tvalid_int));
+
+always @* begin
+    // transfer sink ready state to source
+    m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_reg;
+    temp_m_eth_payload_axis_tvalid_next = temp_m_eth_payload_axis_tvalid_reg;
+
+    store_eth_payload_int_to_output = 1'b0;
+    store_eth_payload_int_to_temp = 1'b0;
+    store_eth_payload_axis_temp_to_output = 1'b0;
+    
+    if (m_eth_payload_axis_tready_int_reg) begin
+        // input is ready
+        if (m_eth_payload_axis_tready || !m_eth_payload_axis_tvalid_reg) begin
+            // output is ready or currently not valid, transfer data to output
+            m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_int;
+            store_eth_payload_int_to_output = 1'b1;
+        end else begin
+            // output is not ready, store input in temp
+            temp_m_eth_payload_axis_tvalid_next = m_eth_payload_axis_tvalid_int;
+            store_eth_payload_int_to_temp = 1'b1;
+        end
+    end else if (m_eth_payload_axis_tready) begin
+        // input is not ready, but output is ready
+        m_eth_payload_axis_tvalid_next = temp_m_eth_payload_axis_tvalid_reg;
+        temp_m_eth_payload_axis_tvalid_next = 1'b0;
+        store_eth_payload_axis_temp_to_output = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        m_eth_payload_axis_tready_int_reg <= 1'b0;
+        m_eth_payload_axis_tdata_reg <= 8'd0;
+        m_eth_payload_axis_tvalid_reg <= 1'b0;
+        m_eth_payload_axis_tlast_reg <= 1'b0;
+        m_eth_payload_axis_tuser_reg <= 1'b0;
+        temp_m_eth_payload_axis_tdata_reg <= 8'd0;
+        temp_m_eth_payload_axis_tvalid_reg <= 1'b0;
+        temp_m_eth_payload_axis_tlast_reg <= 1'b0;
+        temp_m_eth_payload_axis_tuser_reg <= 1'b0;
+    end else begin
+        m_eth_payload_axis_tvalid_reg <= m_eth_payload_axis_tvalid_next;
+        m_eth_payload_axis_tready_int_reg <= m_eth_payload_axis_tready_int_early;
+        temp_m_eth_payload_axis_tvalid_reg <= temp_m_eth_payload_axis_tvalid_next;
+
+        // datapath
+        if (store_eth_payload_int_to_output) begin
+            m_eth_payload_axis_tdata_reg <= m_eth_payload_axis_tdata_int;
+            m_eth_payload_axis_tlast_reg <= m_eth_payload_axis_tlast_int;
+            m_eth_payload_axis_tuser_reg <= m_eth_payload_axis_tuser_int;
+        end else if (store_eth_payload_axis_temp_to_output) begin
+            m_eth_payload_axis_tdata_reg <= temp_m_eth_payload_axis_tdata_reg;
+            m_eth_payload_axis_tlast_reg <= temp_m_eth_payload_axis_tlast_reg;
+            m_eth_payload_axis_tuser_reg <= temp_m_eth_payload_axis_tuser_reg;
+        end
+    
+        if (store_eth_payload_int_to_temp) begin
+            temp_m_eth_payload_axis_tdata_reg <= m_eth_payload_axis_tdata_int;
+            temp_m_eth_payload_axis_tlast_reg <= m_eth_payload_axis_tlast_int;
+            temp_m_eth_payload_axis_tuser_reg <= m_eth_payload_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/lfsr.v b/verilog/rtl/lfsr.v
new file mode 100644
index 0000000..f340ab2
--- /dev/null
+++ b/verilog/rtl/lfsr.v
@@ -0,0 +1,383 @@
+/*
+
+Copyright (c) 2016-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * Parametrizable combinatorial parallel LFSR/CRC
+ */
+module lfsr #
+(
+    // width of LFSR
+    parameter LFSR_WIDTH = 32,
+    // LFSR polynomial
+    parameter LFSR_POLY = 31'h10000001,
+    // LFSR configuration: "GALOIS", "FIBONACCI"
+    parameter LFSR_CONFIG = "FIBONACCI",
+    // LFSR feed forward enable
+    parameter LFSR_FEED_FORWARD = 0,
+    // bit-reverse input and output
+    parameter REVERSE = 0,
+    // width of data input
+    parameter DATA_WIDTH = 8,
+    // implementation style: "AUTO", "LOOP", "REDUCTION"
+    parameter STYLE = "AUTO"
+)
+(
+    input  wire [DATA_WIDTH-1:0] data_in,
+    input  wire [LFSR_WIDTH-1:0] state_in,
+    output wire [DATA_WIDTH-1:0] data_out,
+    output wire [LFSR_WIDTH-1:0] state_out
+);
+
+/*
+
+Fully parametrizable combinatorial parallel LFSR/CRC module.  Implements an unrolled LFSR
+next state computation, shifting DATA_WIDTH bits per pass through the module.  Input data
+is XORed with LFSR feedback path, tie data_in to zero if this is not required.
+
+Works in two parts: statically computes a set of bit masks, then uses these bit masks to
+select bits for XORing to compute the next state.  
+
+Ports:
+
+data_in
+
+Data bits to be shifted through the LFSR (DATA_WIDTH bits)
+
+state_in
+
+LFSR/CRC current state input (LFSR_WIDTH bits)
+
+data_out
+
+Data bits shifted out of LFSR (DATA_WIDTH bits)
+
+state_out
+
+LFSR/CRC next state output (LFSR_WIDTH bits)
+
+Parameters:
+
+LFSR_WIDTH
+
+Specify width of LFSR/CRC register
+
+LFSR_POLY
+
+Specify the LFSR/CRC polynomial in hex format.  For example, the polynomial
+
+x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1
+
+would be represented as
+
+32'h04c11db7
+
+Note that the largest term (x^32) is suppressed.  This term is generated automatically based
+on LFSR_WIDTH.
+
+LFSR_CONFIG
+
+Specify the LFSR configuration, either Fibonacci or Galois.  Fibonacci is generally used
+for linear-feedback shift registers (LFSR) for pseudorandom binary sequence (PRBS) generators,
+scramblers, and descrambers, while Galois is generally used for cyclic redundancy check
+generators and checkers.
+
+Fibonacci style (example for 64b66b scrambler, 0x8000000001)
+
+   DIN (LSB first)
+    |
+    V
+   (+)<---------------------------(+)<-----------------------------.
+    |                              ^                               |
+    |  .----.  .----.       .----. |  .----.       .----.  .----.  |
+    +->|  0 |->|  1 |->...->| 38 |-+->| 39 |->...->| 56 |->| 57 |--'
+    |  '----'  '----'       '----'    '----'       '----'  '----'
+    V
+   DOUT
+
+Galois style (example for CRC16, 0x8005)
+
+    ,-------------------+-------------------------+----------(+)<-- DIN (MSB first)
+    |                   |                         |           ^
+    |  .----.  .----.   V   .----.       .----.   V   .----.  |
+    `->|  0 |->|  1 |->(+)->|  2 |->...->| 14 |->(+)->| 15 |--+---> DOUT
+       '----'  '----'       '----'       '----'       '----'
+
+LFSR_FEED_FORWARD
+
+Generate feed forward instead of feed back LFSR.  Enable this for PRBS checking and self-
+synchronous descrambling.
+
+Fibonacci feed-forward style (example for 64b66b descrambler, 0x8000000001)
+
+   DIN (LSB first)
+    |
+    |  .----.  .----.       .----.    .----.       .----.  .----.
+    +->|  0 |->|  1 |->...->| 38 |-+->| 39 |->...->| 56 |->| 57 |--.
+    |  '----'  '----'       '----' |  '----'       '----'  '----'  |
+    |                              V                               |
+   (+)<---------------------------(+)------------------------------'
+    |
+    V
+   DOUT
+
+Galois feed-forward style
+
+    ,-------------------+-------------------------+------------+--- DIN (MSB first)
+    |                   |                         |            |
+    |  .----.  .----.   V   .----.       .----.   V   .----.   V
+    `->|  0 |->|  1 |->(+)->|  2 |->...->| 14 |->(+)->| 15 |->(+)-> DOUT
+       '----'  '----'       '----'       '----'       '----'
+
+REVERSE
+
+Bit-reverse LFSR input and output.  Shifts MSB first by default, set REVERSE for LSB first.
+
+DATA_WIDTH
+
+Specify width of input and output data bus.  The module will perform one shift per input
+data bit, so if the input data bus is not required tie data_in to zero and set DATA_WIDTH
+to the required number of shifts per clock cycle.  
+
+STYLE
+
+Specify implementation style.  Can be "AUTO", "LOOP", or "REDUCTION".  When "AUTO"
+is selected, implemenation will be "LOOP" or "REDUCTION" based on synthesis translate
+directives.  "REDUCTION" and "LOOP" are functionally identical, however they simulate
+and synthesize differently.  "REDUCTION" is implemented with a loop over a Verilog
+reduction operator.  "LOOP" is implemented as a doubly-nested loop with no reduction
+operator.  "REDUCTION" is very fast for simulation in iverilog and synthesizes well in
+Quartus but synthesizes poorly in ISE, likely due to large inferred XOR gates causing
+problems with the optimizer.  "LOOP" synthesizes will in both ISE and Quartus.  "AUTO"
+will default to "REDUCTION" when simulating and "LOOP" for synthesizers that obey
+synthesis translate directives.
+
+Settings for common LFSR/CRC implementations:
+
+Name        Configuration           Length  Polynomial      Initial value   Notes
+CRC16-IBM   Galois, bit-reverse     16      16'h8005        16'hffff
+CRC16-CCITT Galois                  16      16'h1021        16'h1d0f
+CRC32       Galois, bit-reverse     32      32'h04c11db7    32'hffffffff    Ethernet FCS; invert final output
+PRBS6       Fibonacci               6       6'h21           any
+PRBS7       Fibonacci               7       7'h41           any
+PRBS9       Fibonacci               9       9'h021          any             ITU V.52
+PRBS10      Fibonacci               10      10'h081         any             ITU
+PRBS11      Fibonacci               11      11'h201         any             ITU O.152
+PRBS15      Fibonacci, inverted     15      15'h4001        any             ITU O.152
+PRBS17      Fibonacci               17      17'h04001       any
+PRBS20      Fibonacci               20      20'h00009       any             ITU V.57
+PRBS23      Fibonacci, inverted     23      23'h040001      any             ITU O.151
+PRBS29      Fibonacci, inverted     29      29'h08000001    any
+PRBS31      Fibonacci, inverted     31      31'h10000001    any
+64b66b      Fibonacci, bit-reverse  58      58'h8000000001  any             10G Ethernet
+128b130b    Galois, bit-reverse     23      23'h210125      any             PCIe gen 3
+
+*/
+
+wire [LFSR_WIDTH-1:0] lfsr_mask_state[LFSR_WIDTH-1:0];
+wire [DATA_WIDTH-1:0] lfsr_mask_data[LFSR_WIDTH-1:0];
+wire [LFSR_WIDTH-1:0] output_mask_state[DATA_WIDTH-1:0];
+wire [DATA_WIDTH-1:0] output_mask_data[DATA_WIDTH-1:0];
+
+wire [LFSR_WIDTH-1:0] state_val;
+wire [DATA_WIDTH-1:0] data_val;
+
+integer i, j;
+
+assign lfsr_mask_state[31] = 32'h00000082;
+assign lfsr_mask_state[30] = 32'h000000c3;
+assign lfsr_mask_state[29] = 32'h000000e3;
+assign lfsr_mask_state[28] = 32'h00000071;
+assign lfsr_mask_state[27] = 32'h000000ba;
+assign lfsr_mask_state[26] = 32'h000000df;
+assign lfsr_mask_state[25] = 32'h0000006f;
+assign lfsr_mask_state[24] = 32'h000000b5;
+assign lfsr_mask_state[23] = 32'h800000d8;
+assign lfsr_mask_state[22] = 32'h4000006c;
+assign lfsr_mask_state[21] = 32'h200000b4;
+assign lfsr_mask_state[20] = 32'h100000d8;
+assign lfsr_mask_state[19] = 32'h080000ee;
+assign lfsr_mask_state[18] = 32'h04000077;
+assign lfsr_mask_state[17] = 32'h0200003b;
+assign lfsr_mask_state[16] = 32'h0100001d;
+assign lfsr_mask_state[15] = 32'h0080008c;
+assign lfsr_mask_state[14] = 32'h00400046;
+assign lfsr_mask_state[13] = 32'h00200023;
+assign lfsr_mask_state[12] = 32'h00100011;
+assign lfsr_mask_state[11] = 32'h00080008;
+assign lfsr_mask_state[10] = 32'h00040004;
+assign lfsr_mask_state[9] = 32'h00020080;
+assign lfsr_mask_state[8] = 32'h000100c2;
+assign lfsr_mask_state[7] = 32'h00008061;
+assign lfsr_mask_state[6] = 32'h00004030;
+assign lfsr_mask_state[5] = 32'h0000209a;
+assign lfsr_mask_state[4] = 32'h0000104d;
+assign lfsr_mask_state[3] = 32'h00000826;
+assign lfsr_mask_state[2] = 32'h00000413;
+assign lfsr_mask_state[1] = 32'h00000209;
+assign lfsr_mask_state[0] = 32'h00000104;
+
+assign lfsr_mask_data[31] = 8'h82;
+assign lfsr_mask_data[30] = 8'hc3;
+assign lfsr_mask_data[29] = 8'he3;
+assign lfsr_mask_data[28] = 8'h71;
+assign lfsr_mask_data[27] = 8'hba;
+assign lfsr_mask_data[26] = 8'hdf;
+assign lfsr_mask_data[25] = 8'h6f;
+assign lfsr_mask_data[24] = 8'hb5;
+assign lfsr_mask_data[23] = 8'hd8;
+assign lfsr_mask_data[22] = 8'h6c;
+assign lfsr_mask_data[21] = 8'hb4;
+assign lfsr_mask_data[20] = 8'hd8;
+assign lfsr_mask_data[19] = 8'hee;
+assign lfsr_mask_data[18] = 8'h77;
+assign lfsr_mask_data[17] = 8'h3b;
+assign lfsr_mask_data[16] = 8'h1d;
+assign lfsr_mask_data[15] = 8'h8c;
+assign lfsr_mask_data[14] = 8'h46;
+assign lfsr_mask_data[13] = 8'h23;
+assign lfsr_mask_data[12] = 8'h11;
+assign lfsr_mask_data[11] = 8'h08;
+assign lfsr_mask_data[10] = 8'h04;
+assign lfsr_mask_data[9] = 8'h80;
+assign lfsr_mask_data[8] = 8'hc2;
+assign lfsr_mask_data[7] = 8'h61;
+assign lfsr_mask_data[6] = 8'h30;
+assign lfsr_mask_data[5] = 8'h9a;
+assign lfsr_mask_data[4] = 8'h4d;
+assign lfsr_mask_data[3] = 8'h26;
+assign lfsr_mask_data[2] = 8'h13;
+assign lfsr_mask_data[1] = 8'h09;
+assign lfsr_mask_data[0] = 8'h04;
+
+assign output_mask_state[7] = 32'h00000082;
+assign output_mask_state[6] = 32'h00000041;
+assign output_mask_state[5] = 32'h00000020;
+assign output_mask_state[4] = 32'h00000010;
+assign output_mask_state[3] = 32'h00000008;
+assign output_mask_state[2] = 32'h00000004;
+assign output_mask_state[1] = 32'h00000002;
+assign output_mask_state[0] = 32'h00000001;
+
+assign output_mask_data[7] = 8'h82;
+assign output_mask_data[6] = 8'h41;
+assign output_mask_data[5] = 8'h20;
+assign output_mask_data[4] = 8'h10;
+assign output_mask_data[3] = 8'h08;
+assign output_mask_data[2] = 8'h04;
+assign output_mask_data[1] = 8'h02;
+assign output_mask_data[0] = 8'h01;
+
+assign state_val = 32'h00000082;
+assign data_val = 8'h82;
+
+// synthesis translate_off
+`define SIMULATION
+// synthesis translate_on
+
+`ifdef SIMULATION
+// "AUTO" style is "REDUCTION" for faster simulation
+parameter STYLE_INT = (STYLE == "AUTO") ? "REDUCTION" : STYLE;
+`else
+// "AUTO" style is "LOOP" for better synthesis result
+parameter STYLE_INT = (STYLE == "AUTO") ? "LOOP" : STYLE;
+`endif
+
+genvar n;
+
+generate
+
+if (STYLE_INT == "REDUCTION") begin
+
+    // use Verilog reduction operator
+    // fast in iverilog
+    // significantly larger than generated code with ISE (inferred wide XORs may be tripping up optimizer)
+    // slightly smaller than generated code with Quartus
+    // --> better for simulation
+
+    for (n = 0; n < LFSR_WIDTH; n = n + 1) begin : loop1
+        assign state_out[n] = ^{(state_in & lfsr_mask_state[n]), (data_in & lfsr_mask_data[n])};
+    end
+    for (n = 0; n < DATA_WIDTH; n = n + 1) begin : loop2
+        assign data_out[n] = ^{(state_in & output_mask_state[n]), (data_in & output_mask_data[n])};
+    end
+
+end else if (STYLE_INT == "LOOP") begin
+
+    // use nested loops
+    // very slow in iverilog
+    // slightly smaller than generated code with ISE
+    // same size as generated code with Quartus
+    // --> better for synthesis
+
+    reg [LFSR_WIDTH-1:0] state_out_reg = 0;
+    reg [DATA_WIDTH-1:0] data_out_reg = 0;
+
+    assign state_out = state_out_reg;
+    assign data_out = data_out_reg;
+
+    always @* begin
+        for (i = 0; i < LFSR_WIDTH; i = i + 1) begin
+            state_out_reg[i] = 0;
+            for (j = 0; j < LFSR_WIDTH; j = j + 1) begin
+                if (lfsr_mask_state[i][j]) begin
+                    state_out_reg[i] = state_out_reg[i] ^ state_in[j];
+                end
+            end
+            for (j = 0; j < DATA_WIDTH; j = j + 1) begin
+                if (lfsr_mask_data[i][j]) begin
+                    state_out_reg[i] = state_out_reg[i] ^ data_in[j];
+                end
+            end
+        end
+        for (i = 0; i < DATA_WIDTH; i = i + 1) begin
+            data_out_reg[i] = 0;
+            for (j = 0; j < LFSR_WIDTH; j = j + 1) begin
+                if (output_mask_state[i][j]) begin
+                    data_out_reg[i] = data_out_reg[i] ^ state_in[j];
+                end
+            end
+            for (j = 0; j < DATA_WIDTH; j = j + 1) begin
+                if (output_mask_data[i][j]) begin
+                    data_out_reg[i] = data_out_reg[i] ^ data_in[j];
+                end
+            end
+        end
+    end
+
+end else begin
+
+    initial begin
+        $error("Error: unknown style setting!");
+        $finish;
+    end
+
+end
+
+endgenerate
+
+endmodule
diff --git a/verilog/rtl/mii_phy_if.v b/verilog/rtl/mii_phy_if.v
new file mode 100644
index 0000000..22dff7b
--- /dev/null
+++ b/verilog/rtl/mii_phy_if.v
@@ -0,0 +1,144 @@
+/*
+
+Copyright (c) 2019 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
+
+`timescale 1ns / 1ps
+
+/*
+ * MII PHY interface
+ */
+module mii_phy_if #
+(
+    // target ("SIM", "GENERIC", "XILINX", "ALTERA")
+    parameter TARGET = "GENERIC",
+    // Clock input style ("BUFG", "BUFR", "BUFIO", "BUFIO2")
+    // Use BUFR for Virtex-5, Virtex-6, 7-series
+    // Use BUFG for Ultrascale
+    // Use BUFIO2 for Spartan-6
+    parameter CLOCK_INPUT_STYLE = "BUFIO2"
+)
+(
+    input  wire        rst,
+
+    /*
+     * MII interface to MAC
+     */
+    output wire        mac_mii_rx_clk,
+    output wire        mac_mii_rx_rst,
+    output wire [3:0]  mac_mii_rxd,
+    output wire        mac_mii_rx_dv,
+    output wire        mac_mii_rx_er,
+    output wire        mac_mii_tx_clk,
+    output wire        mac_mii_tx_rst,
+    input  wire [3:0]  mac_mii_txd,
+    input  wire        mac_mii_tx_en,
+    input  wire        mac_mii_tx_er,
+
+    /*
+     * MII interface to PHY
+     */
+    input  wire        phy_mii_rx_clk,
+    input  wire [3:0]  phy_mii_rxd,
+    input  wire        phy_mii_rx_dv,
+    input  wire        phy_mii_rx_er,
+    input  wire        phy_mii_tx_clk,
+    output wire [3:0]  phy_mii_txd,
+    output wire        phy_mii_tx_en,
+    output wire        phy_mii_tx_er
+);
+
+ssio_sdr_in #
+(
+    .TARGET(TARGET),
+    .CLOCK_INPUT_STYLE(CLOCK_INPUT_STYLE),
+    .WIDTH(6)
+)
+rx_ssio_sdr_inst (
+    .input_clk(phy_mii_rx_clk),
+    .input_d({phy_mii_rxd, phy_mii_rx_dv, phy_mii_rx_er}),
+    .output_clk(mac_mii_rx_clk),
+    .output_q({mac_mii_rxd, mac_mii_rx_dv, mac_mii_rx_er})
+);
+
+(* IOB = "TRUE" *)
+reg [3:0] phy_mii_txd_reg;
+(* IOB = "TRUE" *)
+reg phy_mii_tx_en_reg, phy_mii_tx_er_reg;
+
+assign phy_mii_txd = phy_mii_txd_reg;
+assign phy_mii_tx_en = phy_mii_tx_en_reg;
+assign phy_mii_tx_er = phy_mii_tx_er_reg;
+
+always @(posedge mac_mii_tx_clk) begin
+    if(rst) begin
+        phy_mii_txd_reg <= 4'd0;
+        phy_mii_tx_en_reg <= 1'b0;
+        phy_mii_tx_er_reg <= 1'b0;
+    end else begin
+        phy_mii_txd_reg <= mac_mii_txd;
+        phy_mii_tx_en_reg <= mac_mii_tx_en;
+        phy_mii_tx_er_reg <= mac_mii_tx_er;
+    end
+end
+
+generate
+
+if (TARGET == "XILINX") begin
+    BUFG
+    mii_bufg_inst (
+        .I(phy_mii_tx_clk),
+        .O(mac_mii_tx_clk)
+    );
+end else begin
+    assign mac_mii_tx_clk = phy_mii_tx_clk;
+end
+
+endgenerate
+
+// reset sync
+reg [3:0] tx_rst_reg;
+assign mac_mii_tx_rst = tx_rst_reg[0];
+
+always @(posedge mac_mii_tx_clk or posedge rst) begin
+    if (rst) begin
+        tx_rst_reg <= 4'hf;
+    end else begin
+        tx_rst_reg <= {1'b0, tx_rst_reg[3:1]};
+    end
+end
+
+reg [3:0] rx_rst_reg;
+assign mac_mii_rx_rst = rx_rst_reg[0];
+
+always @(posedge mac_mii_rx_clk or posedge rst) begin
+    if (rst) begin
+        rx_rst_reg <= 4'hf;
+    end else begin
+        rx_rst_reg <= {1'b0, rx_rst_reg[3:1]};
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/priority_encoder.v b/verilog/rtl/priority_encoder.v
new file mode 100644
index 0000000..dd59fa4
--- /dev/null
+++ b/verilog/rtl/priority_encoder.v
@@ -0,0 +1,88 @@
+/*
+
+Copyright (c) 2014-2021 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
+
+`timescale 1ns / 1ps
+
+/*
+ * Priority encoder module
+ */
+module priority_encoder #
+(
+    parameter WIDTH = 4,
+    // LSB priority selection
+    parameter LSB_HIGH_PRIORITY = 0
+)
+(
+    input  wire [WIDTH-1:0]         input_unencoded,
+    output wire                     output_valid,
+    output wire [$clog2(WIDTH)-1:0] output_encoded,
+    output wire [WIDTH-1:0]         output_unencoded
+);
+
+parameter LEVELS = WIDTH > 2 ? $clog2(WIDTH) : 1;
+parameter W = 2**LEVELS;
+
+// pad input to even power of two
+wire [W-1:0] input_padded = {{W-WIDTH{1'b0}}, input_unencoded};
+
+wire [W/2-1:0] stage_valid[LEVELS-1:0];
+wire [W/2-1:0] stage_enc[LEVELS-1:0];
+
+generate
+    genvar l, n;
+
+    // process input bits; generate valid bit and encoded bit for each pair
+    for (n = 0; n < W/2; n = n + 1) begin : loop_in
+        assign stage_valid[0][n] = |input_padded[n*2+1:n*2];
+        if (LSB_HIGH_PRIORITY) begin
+            // bit 0 is highest priority
+            assign stage_enc[0][n] = !input_padded[n*2+0];
+        end else begin
+            // bit 0 is lowest priority
+            assign stage_enc[0][n] = input_padded[n*2+1];
+        end
+    end
+
+    // compress down to single valid bit and encoded bus
+    for (l = 1; l < LEVELS; l = l + 1) begin : loop_levels
+        for (n = 0; n < W/(2*2**l); n = n + 1) begin : loop_compress
+            assign stage_valid[l][n] = |stage_valid[l-1][n*2+1:n*2];
+            if (LSB_HIGH_PRIORITY) begin
+                // bit 0 is highest priority
+                assign stage_enc[l][(n+1)*(l+1)-1:n*(l+1)] = stage_valid[l-1][n*2+0] ? {1'b0, stage_enc[l-1][(n*2+1)*l-1:(n*2+0)*l]} : {1'b1, stage_enc[l-1][(n*2+2)*l-1:(n*2+1)*l]};
+            end else begin
+                // bit 0 is lowest priority
+                assign stage_enc[l][(n+1)*(l+1)-1:n*(l+1)] = stage_valid[l-1][n*2+1] ? {1'b1, stage_enc[l-1][(n*2+2)*l-1:(n*2+1)*l]} : {1'b0, stage_enc[l-1][(n*2+1)*l-1:(n*2+0)*l]};
+            end
+        end
+    end
+endgenerate
+
+assign output_valid = stage_valid[LEVELS-1];
+assign output_encoded = stage_enc[LEVELS-1];
+assign output_unencoded = 1 << output_encoded;
+
+endmodule
diff --git a/verilog/rtl/ssio_sdr_in.v b/verilog/rtl/ssio_sdr_in.v
new file mode 100644
index 0000000..996ea91
--- /dev/null
+++ b/verilog/rtl/ssio_sdr_in.v
@@ -0,0 +1,164 @@
+/*
+
+Copyright (c) 2016-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * Generic source synchronous SDR input
+ */
+module ssio_sdr_in #
+(
+    // target ("SIM", "GENERIC", "XILINX", "ALTERA")
+    parameter TARGET = "GENERIC",
+    // Clock input style ("BUFG", "BUFR", "BUFIO", "BUFIO2")
+    // Use BUFR for Virtex-5, Virtex-6, 7-series
+    // Use BUFG for Ultrascale
+    // Use BUFIO2 for Spartan-6
+    parameter CLOCK_INPUT_STYLE = "BUFIO2",
+    // Width of register in bits
+    parameter WIDTH = 1
+)
+(
+    input  wire             input_clk,
+
+    input  wire [WIDTH-1:0] input_d,
+
+    output wire             output_clk,
+
+    output wire [WIDTH-1:0] output_q
+);
+
+wire clk_int;
+wire clk_io;
+
+generate
+
+if (TARGET == "XILINX") begin
+
+    // use Xilinx clocking primitives
+
+    if (CLOCK_INPUT_STYLE == "BUFG") begin
+
+        // buffer RX clock
+        BUFG
+        clk_bufg (
+            .I(input_clk),
+            .O(clk_int)
+        );
+
+        // pass through RX clock to logic and input buffers
+        assign clk_io = clk_int;
+        assign output_clk = clk_int;
+
+    end else if (CLOCK_INPUT_STYLE == "BUFR") begin
+
+        assign clk_int = input_clk;
+
+        // pass through RX clock to input buffers
+        BUFIO
+        clk_bufio (
+            .I(clk_int),
+            .O(clk_io)
+        );
+
+        // pass through RX clock to logic
+        BUFR #(
+            .BUFR_DIVIDE("BYPASS")
+        )
+        clk_bufr (
+            .I(clk_int),
+            .O(output_clk),
+            .CE(1'b1),
+            .CLR(1'b0)
+        );
+        
+    end else if (CLOCK_INPUT_STYLE == "BUFIO") begin
+
+        assign clk_int = input_clk;
+
+        // pass through RX clock to input buffers
+        BUFIO
+        clk_bufio (
+            .I(clk_int),
+            .O(clk_io)
+        );
+
+        // pass through RX clock to MAC
+        BUFG
+        clk_bufg (
+            .I(clk_int),
+            .O(output_clk)
+        );
+
+    end else if (CLOCK_INPUT_STYLE == "BUFIO2") begin
+
+        // pass through RX clock to input buffers
+        BUFIO2 #(
+            .DIVIDE(1),
+            .DIVIDE_BYPASS("TRUE"),
+            .I_INVERT("FALSE"),
+            .USE_DOUBLER("FALSE")
+        )
+        clk_bufio (
+            .I(input_clk),
+            .DIVCLK(clk_int),
+            .IOCLK(clk_io),
+            .SERDESSTROBE()
+        );
+
+        // pass through RX clock to MAC
+        BUFG
+        clk_bufg (
+            .I(clk_int),
+            .O(output_clk)
+        );
+
+    end
+
+end else begin
+
+    // pass through RX clock to input buffers
+    assign clk_io = input_clk;
+
+    // pass through RX clock to logic
+    assign clk_int = input_clk;
+    assign output_clk = clk_int;
+
+end
+
+endgenerate
+
+(* IOB = "TRUE" *)
+reg [WIDTH-1:0] output_q_reg;
+
+assign output_q = output_q_reg;
+
+always @(posedge clk_io) begin
+    output_q_reg <= input_d;
+end
+
+endmodule
+
diff --git a/verilog/rtl/udp.v b/verilog/rtl/udp.v
new file mode 100644
index 0000000..b38f42d
--- /dev/null
+++ b/verilog/rtl/udp.v
@@ -0,0 +1,340 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * UDP block, IP interface
+ */
+module udp #
+(
+    parameter CHECKSUM_GEN_ENABLE = 1,
+    parameter CHECKSUM_PAYLOAD_FIFO_DEPTH = 2048,
+    parameter CHECKSUM_HEADER_FIFO_DEPTH = 8
+)
+(
+    input  wire        clk,
+    input  wire        rst,
+    
+    /*
+     * IP frame input
+     */
+    input  wire        s_ip_hdr_valid,
+    output wire        s_ip_hdr_ready,
+    input  wire [47:0] s_ip_eth_dest_mac,
+    input  wire [47:0] s_ip_eth_src_mac,
+    input  wire [15:0] s_ip_eth_type,
+    input  wire [3:0]  s_ip_version,
+    input  wire [3:0]  s_ip_ihl,
+    input  wire [5:0]  s_ip_dscp,
+    input  wire [1:0]  s_ip_ecn,
+    input  wire [15:0] s_ip_length,
+    input  wire [15:0] s_ip_identification,
+    input  wire [2:0]  s_ip_flags,
+    input  wire [12:0] s_ip_fragment_offset,
+    input  wire [7:0]  s_ip_ttl,
+    input  wire [7:0]  s_ip_protocol,
+    input  wire [15:0] s_ip_header_checksum,
+    input  wire [31:0] s_ip_source_ip,
+    input  wire [31:0] s_ip_dest_ip,
+    input  wire [7:0]  s_ip_payload_axis_tdata,
+    input  wire        s_ip_payload_axis_tvalid,
+    output wire        s_ip_payload_axis_tready,
+    input  wire        s_ip_payload_axis_tlast,
+    input  wire        s_ip_payload_axis_tuser,
+    
+    /*
+     * IP frame output
+     */
+    output wire        m_ip_hdr_valid,
+    input  wire        m_ip_hdr_ready,
+    output wire [47:0] m_ip_eth_dest_mac,
+    output wire [47:0] m_ip_eth_src_mac,
+    output wire [15:0] m_ip_eth_type,
+    output wire [3:0]  m_ip_version,
+    output wire [3:0]  m_ip_ihl,
+    output wire [5:0]  m_ip_dscp,
+    output wire [1:0]  m_ip_ecn,
+    output wire [15:0] m_ip_length,
+    output wire [15:0] m_ip_identification,
+    output wire [2:0]  m_ip_flags,
+    output wire [12:0] m_ip_fragment_offset,
+    output wire [7:0]  m_ip_ttl,
+    output wire [7:0]  m_ip_protocol,
+    output wire [15:0] m_ip_header_checksum,
+    output wire [31:0] m_ip_source_ip,
+    output wire [31:0] m_ip_dest_ip,
+    output wire [7:0]  m_ip_payload_axis_tdata,
+    output wire        m_ip_payload_axis_tvalid,
+    input  wire        m_ip_payload_axis_tready,
+    output wire        m_ip_payload_axis_tlast,
+    output wire        m_ip_payload_axis_tuser,
+    
+    /*
+     * UDP frame input
+     */
+    input  wire        s_udp_hdr_valid,
+    output wire        s_udp_hdr_ready,
+    input  wire [47:0] s_udp_eth_dest_mac,
+    input  wire [47:0] s_udp_eth_src_mac,
+    input  wire [15:0] s_udp_eth_type,
+    input  wire [3:0]  s_udp_ip_version,
+    input  wire [3:0]  s_udp_ip_ihl,
+    input  wire [5:0]  s_udp_ip_dscp,
+    input  wire [1:0]  s_udp_ip_ecn,
+    input  wire [15:0] s_udp_ip_identification,
+    input  wire [2:0]  s_udp_ip_flags,
+    input  wire [12:0] s_udp_ip_fragment_offset,
+    input  wire [7:0]  s_udp_ip_ttl,
+    input  wire [15:0] s_udp_ip_header_checksum,
+    input  wire [31:0] s_udp_ip_source_ip,
+    input  wire [31:0] s_udp_ip_dest_ip,
+    input  wire [15:0] s_udp_source_port,
+    input  wire [15:0] s_udp_dest_port,
+    input  wire [15:0] s_udp_length,
+    input  wire [15:0] s_udp_checksum,
+    input  wire [7:0]  s_udp_payload_axis_tdata,
+    input  wire        s_udp_payload_axis_tvalid,
+    output wire        s_udp_payload_axis_tready,
+    input  wire        s_udp_payload_axis_tlast,
+    input  wire        s_udp_payload_axis_tuser,
+    
+    /*
+     * UDP frame output
+     */
+    output wire        m_udp_hdr_valid,
+    input  wire        m_udp_hdr_ready,
+    output wire [47:0] m_udp_eth_dest_mac,
+    output wire [47:0] m_udp_eth_src_mac,
+    output wire [15:0] m_udp_eth_type,
+    output wire [3:0]  m_udp_ip_version,
+    output wire [3:0]  m_udp_ip_ihl,
+    output wire [5:0]  m_udp_ip_dscp,
+    output wire [1:0]  m_udp_ip_ecn,
+    output wire [15:0] m_udp_ip_length,
+    output wire [15:0] m_udp_ip_identification,
+    output wire [2:0]  m_udp_ip_flags,
+    output wire [12:0] m_udp_ip_fragment_offset,
+    output wire [7:0]  m_udp_ip_ttl,
+    output wire [7:0]  m_udp_ip_protocol,
+    output wire [15:0] m_udp_ip_header_checksum,
+    output wire [31:0] m_udp_ip_source_ip,
+    output wire [31:0] m_udp_ip_dest_ip,
+    output wire [15:0] m_udp_source_port,
+    output wire [15:0] m_udp_dest_port,
+    output wire [15:0] m_udp_length,
+    output wire [15:0] m_udp_checksum,
+    output wire [7:0]  m_udp_payload_axis_tdata,
+    output wire        m_udp_payload_axis_tvalid,
+    input  wire        m_udp_payload_axis_tready,
+    output wire        m_udp_payload_axis_tlast,
+    output wire        m_udp_payload_axis_tuser,
+    
+    /*
+     * Status signals
+     */
+    output wire        rx_busy,
+    output wire        tx_busy,
+    output wire        rx_error_header_early_termination,
+    output wire        rx_error_payload_early_termination,
+    output wire        tx_error_payload_early_termination
+);
+
+wire        tx_udp_hdr_valid;
+wire        tx_udp_hdr_ready;
+wire [47:0] tx_udp_eth_dest_mac;
+wire [47:0] tx_udp_eth_src_mac;
+wire [15:0] tx_udp_eth_type;
+wire [3:0]  tx_udp_ip_version;
+wire [3:0]  tx_udp_ip_ihl;
+wire [5:0]  tx_udp_ip_dscp;
+wire [1:0]  tx_udp_ip_ecn;
+wire [15:0] tx_udp_ip_identification;
+wire [2:0]  tx_udp_ip_flags;
+wire [12:0] tx_udp_ip_fragment_offset;
+wire [7:0]  tx_udp_ip_ttl;
+wire [15:0] tx_udp_ip_header_checksum;
+wire [31:0] tx_udp_ip_source_ip;
+wire [31:0] tx_udp_ip_dest_ip;
+wire [15:0] tx_udp_source_port;
+wire [15:0] tx_udp_dest_port;
+wire [15:0] tx_udp_length;
+wire [15:0] tx_udp_checksum;
+wire [7:0]  tx_udp_payload_axis_tdata;
+wire        tx_udp_payload_axis_tvalid;
+wire        tx_udp_payload_axis_tready;
+wire        tx_udp_payload_axis_tlast;
+wire        tx_udp_payload_axis_tuser;
+
+udp_ip_rx
+udp_ip_rx_inst (
+    .clk(clk),
+    .rst(rst),
+    // IP frame input
+    .s_ip_hdr_valid(s_ip_hdr_valid),
+    .s_ip_hdr_ready(s_ip_hdr_ready),
+    .s_eth_dest_mac(s_ip_eth_dest_mac),
+    .s_eth_src_mac(s_ip_eth_src_mac),
+    .s_eth_type(s_ip_eth_type),
+    .s_ip_version(s_ip_version),
+    .s_ip_ihl(s_ip_ihl),
+    .s_ip_dscp(s_ip_dscp),
+    .s_ip_ecn(s_ip_ecn),
+    .s_ip_length(s_ip_length),
+    .s_ip_identification(s_ip_identification),
+    .s_ip_flags(s_ip_flags),
+    .s_ip_fragment_offset(s_ip_fragment_offset),
+    .s_ip_ttl(s_ip_ttl),
+    .s_ip_protocol(s_ip_protocol),
+    .s_ip_header_checksum(s_ip_header_checksum),
+    .s_ip_source_ip(s_ip_source_ip),
+    .s_ip_dest_ip(s_ip_dest_ip),
+    .s_ip_payload_axis_tdata(s_ip_payload_axis_tdata),
+    .s_ip_payload_axis_tvalid(s_ip_payload_axis_tvalid),
+    .s_ip_payload_axis_tready(s_ip_payload_axis_tready),
+    .s_ip_payload_axis_tlast(s_ip_payload_axis_tlast),
+    .s_ip_payload_axis_tuser(s_ip_payload_axis_tuser),
+    // UDP frame output
+    .m_udp_hdr_valid(m_udp_hdr_valid),
+    .m_udp_hdr_ready(m_udp_hdr_ready),
+    .m_eth_dest_mac(m_udp_eth_dest_mac),
+    .m_eth_src_mac(m_udp_eth_src_mac),
+    .m_eth_type(m_udp_eth_type),
+    .m_ip_version(m_udp_ip_version),
+    .m_ip_ihl(m_udp_ip_ihl),
+    .m_ip_dscp(m_udp_ip_dscp),
+    .m_ip_ecn(m_udp_ip_ecn),
+    .m_ip_length(m_udp_ip_length),
+    .m_ip_identification(m_udp_ip_identification),
+    .m_ip_flags(m_udp_ip_flags),
+    .m_ip_fragment_offset(m_udp_ip_fragment_offset),
+    .m_ip_ttl(m_udp_ip_ttl),
+    .m_ip_protocol(m_udp_ip_protocol),
+    .m_ip_header_checksum(m_udp_ip_header_checksum),
+    .m_ip_source_ip(m_udp_ip_source_ip),
+    .m_ip_dest_ip(m_udp_ip_dest_ip),
+    .m_udp_source_port(m_udp_source_port),
+    .m_udp_dest_port(m_udp_dest_port),
+    .m_udp_length(m_udp_length),
+    .m_udp_checksum(m_udp_checksum),
+    .m_udp_payload_axis_tdata(m_udp_payload_axis_tdata),
+    .m_udp_payload_axis_tvalid(m_udp_payload_axis_tvalid),
+    .m_udp_payload_axis_tready(m_udp_payload_axis_tready),
+    .m_udp_payload_axis_tlast(m_udp_payload_axis_tlast),
+    .m_udp_payload_axis_tuser(m_udp_payload_axis_tuser),
+    // Status signals
+    .busy(rx_busy),
+    .error_header_early_termination(rx_error_header_early_termination),
+    .error_payload_early_termination(rx_error_payload_early_termination)
+);
+
+    assign tx_udp_hdr_valid = s_udp_hdr_valid;
+    assign s_udp_hdr_ready = tx_udp_hdr_ready;
+    assign tx_udp_eth_dest_mac = s_udp_eth_dest_mac;
+    assign tx_udp_eth_src_mac = s_udp_eth_src_mac;
+    assign tx_udp_eth_type = s_udp_eth_type;
+    assign tx_udp_ip_version = s_udp_ip_version;
+    assign tx_udp_ip_ihl = s_udp_ip_ihl;
+    assign tx_udp_ip_dscp = s_udp_ip_dscp;
+    assign tx_udp_ip_ecn = s_udp_ip_ecn;
+    assign tx_udp_ip_identification = s_udp_ip_identification;
+    assign tx_udp_ip_flags = s_udp_ip_flags;
+    assign tx_udp_ip_fragment_offset = s_udp_ip_fragment_offset;
+    assign tx_udp_ip_ttl = s_udp_ip_ttl;
+    assign tx_udp_ip_header_checksum = s_udp_ip_header_checksum;
+    assign tx_udp_ip_source_ip = s_udp_ip_source_ip;
+    assign tx_udp_ip_dest_ip = s_udp_ip_dest_ip;
+    assign tx_udp_source_port = s_udp_source_port;
+    assign tx_udp_dest_port = s_udp_dest_port;
+    assign tx_udp_length = s_udp_length;
+    assign tx_udp_checksum = s_udp_checksum;
+    assign tx_udp_payload_axis_tdata = s_udp_payload_axis_tdata;
+    assign tx_udp_payload_axis_tvalid = s_udp_payload_axis_tvalid;
+    assign s_udp_payload_axis_tready = tx_udp_payload_axis_tready;
+    assign tx_udp_payload_axis_tlast = s_udp_payload_axis_tlast;
+    assign tx_udp_payload_axis_tuser = s_udp_payload_axis_tuser;
+
+udp_ip_tx
+udp_ip_tx_inst (
+    .clk(clk),
+    .rst(rst),
+    // UDP frame input
+    .s_udp_hdr_valid(tx_udp_hdr_valid),
+    .s_udp_hdr_ready(tx_udp_hdr_ready),
+    .s_eth_dest_mac(tx_udp_eth_dest_mac),
+    .s_eth_src_mac(tx_udp_eth_src_mac),
+    .s_eth_type(tx_udp_eth_type),
+    .s_ip_version(tx_udp_ip_version),
+    .s_ip_ihl(tx_udp_ip_ihl),
+    .s_ip_dscp(tx_udp_ip_dscp),
+    .s_ip_ecn(tx_udp_ip_ecn),
+    .s_ip_identification(tx_udp_ip_identification),
+    .s_ip_flags(tx_udp_ip_flags),
+    .s_ip_fragment_offset(tx_udp_ip_fragment_offset),
+    .s_ip_ttl(tx_udp_ip_ttl),
+    .s_ip_protocol(8'h11),
+    .s_ip_header_checksum(tx_udp_ip_header_checksum),
+    .s_ip_source_ip(tx_udp_ip_source_ip),
+    .s_ip_dest_ip(tx_udp_ip_dest_ip),
+    .s_udp_source_port(tx_udp_source_port),
+    .s_udp_dest_port(tx_udp_dest_port),
+    .s_udp_length(tx_udp_length),
+    .s_udp_checksum(tx_udp_checksum),
+    .s_udp_payload_axis_tdata(tx_udp_payload_axis_tdata),
+    .s_udp_payload_axis_tvalid(tx_udp_payload_axis_tvalid),
+    .s_udp_payload_axis_tready(tx_udp_payload_axis_tready),
+    .s_udp_payload_axis_tlast(tx_udp_payload_axis_tlast),
+    .s_udp_payload_axis_tuser(tx_udp_payload_axis_tuser),
+    // IP frame output
+    .m_ip_hdr_valid(m_ip_hdr_valid),
+    .m_ip_hdr_ready(m_ip_hdr_ready),
+    .m_eth_dest_mac(m_ip_eth_dest_mac),
+    .m_eth_src_mac(m_ip_eth_src_mac),
+    .m_eth_type(m_ip_eth_type),
+    .m_ip_version(m_ip_version),
+    .m_ip_ihl(m_ip_ihl),
+    .m_ip_dscp(m_ip_dscp),
+    .m_ip_ecn(m_ip_ecn),
+    .m_ip_length(m_ip_length),
+    .m_ip_identification(m_ip_identification),
+    .m_ip_flags(m_ip_flags),
+    .m_ip_fragment_offset(m_ip_fragment_offset),
+    .m_ip_ttl(m_ip_ttl),
+    .m_ip_protocol(m_ip_protocol),
+    .m_ip_header_checksum(m_ip_header_checksum),
+    .m_ip_source_ip(m_ip_source_ip),
+    .m_ip_dest_ip(m_ip_dest_ip),
+    .m_ip_payload_axis_tdata(m_ip_payload_axis_tdata),
+    .m_ip_payload_axis_tvalid(m_ip_payload_axis_tvalid),
+    .m_ip_payload_axis_tready(m_ip_payload_axis_tready),
+    .m_ip_payload_axis_tlast(m_ip_payload_axis_tlast),
+    .m_ip_payload_axis_tuser(m_ip_payload_axis_tuser),
+    // Status signals
+    .busy(tx_busy),
+    .error_payload_early_termination(tx_error_payload_early_termination)
+);
+
+endmodule
diff --git a/verilog/rtl/udp_complete.v b/verilog/rtl/udp_complete.v
new file mode 100644
index 0000000..16a4893
--- /dev/null
+++ b/verilog/rtl/udp_complete.v
@@ -0,0 +1,638 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * IPv4 and ARP block with UDP support, ethernet frame interface
+ */
+module udp_complete #(
+    parameter ARP_CACHE_ADDR_WIDTH = 9,
+    parameter ARP_REQUEST_RETRY_COUNT = 4,
+    parameter ARP_REQUEST_RETRY_INTERVAL = 125000000*2,
+    parameter ARP_REQUEST_TIMEOUT = 125000000*30,
+    parameter UDP_CHECKSUM_GEN_ENABLE = 1,
+    parameter UDP_CHECKSUM_PAYLOAD_FIFO_DEPTH = 2048,
+    parameter UDP_CHECKSUM_HEADER_FIFO_DEPTH = 8
+)
+(
+    input  wire        clk,
+    input  wire        rst,
+    
+    /*
+     * Ethernet frame input
+     */
+    input  wire        s_eth_hdr_valid,
+    output wire        s_eth_hdr_ready,
+    input  wire [47:0] s_eth_dest_mac,
+    input  wire [47:0] s_eth_src_mac,
+    input  wire [15:0] s_eth_type,
+    input  wire [7:0]  s_eth_payload_axis_tdata,
+    input  wire        s_eth_payload_axis_tvalid,
+    output wire        s_eth_payload_axis_tready,
+    input  wire        s_eth_payload_axis_tlast,
+    input  wire        s_eth_payload_axis_tuser,
+    
+    /*
+     * Ethernet frame output
+     */
+    output wire        m_eth_hdr_valid,
+    input  wire        m_eth_hdr_ready,
+    output wire [47:0] m_eth_dest_mac,
+    output wire [47:0] m_eth_src_mac,
+    output wire [15:0] m_eth_type,
+    output wire [7:0]  m_eth_payload_axis_tdata,
+    output wire        m_eth_payload_axis_tvalid,
+    input  wire        m_eth_payload_axis_tready,
+    output wire        m_eth_payload_axis_tlast,
+    output wire        m_eth_payload_axis_tuser,
+    
+    /*
+     * IP input
+     */
+    input  wire        s_ip_hdr_valid,
+    output wire        s_ip_hdr_ready,
+    input  wire [5:0]  s_ip_dscp,
+    input  wire [1:0]  s_ip_ecn,
+    input  wire [15:0] s_ip_length,
+    input  wire [7:0]  s_ip_ttl,
+    input  wire [7:0]  s_ip_protocol,
+    input  wire [31:0] s_ip_source_ip,
+    input  wire [31:0] s_ip_dest_ip,
+    input  wire [7:0]  s_ip_payload_axis_tdata,
+    input  wire        s_ip_payload_axis_tvalid,
+    output wire        s_ip_payload_axis_tready,
+    input  wire        s_ip_payload_axis_tlast,
+    input  wire        s_ip_payload_axis_tuser,
+    
+    /*
+     * IP output
+     */
+    output wire        m_ip_hdr_valid,
+    input  wire        m_ip_hdr_ready,
+    output wire [47:0] m_ip_eth_dest_mac,
+    output wire [47:0] m_ip_eth_src_mac,
+    output wire [15:0] m_ip_eth_type,
+    output wire [3:0]  m_ip_version,
+    output wire [3:0]  m_ip_ihl,
+    output wire [5:0]  m_ip_dscp,
+    output wire [1:0]  m_ip_ecn,
+    output wire [15:0] m_ip_length,
+    output wire [15:0] m_ip_identification,
+    output wire [2:0]  m_ip_flags,
+    output wire [12:0] m_ip_fragment_offset,
+    output wire [7:0]  m_ip_ttl,
+    output wire [7:0]  m_ip_protocol,
+    output wire [15:0] m_ip_header_checksum,
+    output wire [31:0] m_ip_source_ip,
+    output wire [31:0] m_ip_dest_ip,
+    output wire [7:0]  m_ip_payload_axis_tdata,
+    output wire        m_ip_payload_axis_tvalid,
+    input  wire        m_ip_payload_axis_tready,
+    output wire        m_ip_payload_axis_tlast,
+    output wire        m_ip_payload_axis_tuser,
+    
+    /*
+     * UDP input
+     */
+    input  wire        s_udp_hdr_valid,
+    output wire        s_udp_hdr_ready,
+    input  wire [5:0]  s_udp_ip_dscp,
+    input  wire [1:0]  s_udp_ip_ecn,
+    input  wire [7:0]  s_udp_ip_ttl,
+    input  wire [31:0] s_udp_ip_source_ip,
+    input  wire [31:0] s_udp_ip_dest_ip,
+    input  wire [15:0] s_udp_source_port,
+    input  wire [15:0] s_udp_dest_port,
+    input  wire [15:0] s_udp_length,
+    input  wire [15:0] s_udp_checksum,
+    input  wire [7:0]  s_udp_payload_axis_tdata,
+    input  wire        s_udp_payload_axis_tvalid,
+    output wire        s_udp_payload_axis_tready,
+    input  wire        s_udp_payload_axis_tlast,
+    input  wire        s_udp_payload_axis_tuser,
+    
+    /*
+     * UDP output
+     */
+    output wire        m_udp_hdr_valid,
+    input  wire        m_udp_hdr_ready,
+    output wire [47:0] m_udp_eth_dest_mac,
+    output wire [47:0] m_udp_eth_src_mac,
+    output wire [15:0] m_udp_eth_type,
+    output wire [3:0]  m_udp_ip_version,
+    output wire [3:0]  m_udp_ip_ihl,
+    output wire [5:0]  m_udp_ip_dscp,
+    output wire [1:0]  m_udp_ip_ecn,
+    output wire [15:0] m_udp_ip_length,
+    output wire [15:0] m_udp_ip_identification,
+    output wire [2:0]  m_udp_ip_flags,
+    output wire [12:0] m_udp_ip_fragment_offset,
+    output wire [7:0]  m_udp_ip_ttl,
+    output wire [7:0]  m_udp_ip_protocol,
+    output wire [15:0] m_udp_ip_header_checksum,
+    output wire [31:0] m_udp_ip_source_ip,
+    output wire [31:0] m_udp_ip_dest_ip,
+    output wire [15:0] m_udp_source_port,
+    output wire [15:0] m_udp_dest_port,
+    output wire [15:0] m_udp_length,
+    output wire [15:0] m_udp_checksum,
+    output wire [7:0]  m_udp_payload_axis_tdata,
+    output wire        m_udp_payload_axis_tvalid,
+    input  wire        m_udp_payload_axis_tready,
+    output wire        m_udp_payload_axis_tlast,
+    output wire        m_udp_payload_axis_tuser,
+
+    /*
+     * Status
+     */
+    output wire        ip_rx_busy,
+    output wire        ip_tx_busy,
+    output wire        udp_rx_busy,
+    output wire        udp_tx_busy,
+    output wire        ip_rx_error_header_early_termination,
+    output wire        ip_rx_error_payload_early_termination,
+    output wire        ip_rx_error_invalid_header,
+    output wire        ip_rx_error_invalid_checksum,
+    output wire        ip_tx_error_payload_early_termination,
+    output wire        ip_tx_error_arp_failed,
+    output wire        udp_rx_error_header_early_termination,
+    output wire        udp_rx_error_payload_early_termination,
+    output wire        udp_tx_error_payload_early_termination,
+
+    /*
+     * Configuration
+     */
+    input  wire [47:0] local_mac,
+    input  wire [31:0] local_ip,
+    input  wire [31:0] gateway_ip,
+    input  wire [31:0] subnet_mask,
+    input  wire        clear_arp_cache
+);
+
+wire ip_rx_ip_hdr_valid;
+wire ip_rx_ip_hdr_ready;
+wire [47:0] ip_rx_ip_eth_dest_mac;
+wire [47:0] ip_rx_ip_eth_src_mac;
+wire [15:0] ip_rx_ip_eth_type;
+wire [3:0] ip_rx_ip_version;
+wire [3:0] ip_rx_ip_ihl;
+wire [5:0] ip_rx_ip_dscp;
+wire [1:0] ip_rx_ip_ecn;
+wire [15:0] ip_rx_ip_length;
+wire [15:0] ip_rx_ip_identification;
+wire [2:0] ip_rx_ip_flags;
+wire [12:0] ip_rx_ip_fragment_offset;
+wire [7:0] ip_rx_ip_ttl;
+wire [7:0] ip_rx_ip_protocol;
+wire [15:0] ip_rx_ip_header_checksum;
+wire [31:0] ip_rx_ip_source_ip;
+wire [31:0] ip_rx_ip_dest_ip;
+wire [7:0] ip_rx_ip_payload_axis_tdata;
+wire ip_rx_ip_payload_axis_tvalid;
+wire ip_rx_ip_payload_axis_tlast;
+wire ip_rx_ip_payload_axis_tuser;
+wire ip_rx_ip_payload_axis_tready;
+
+wire ip_tx_ip_hdr_valid;
+wire ip_tx_ip_hdr_ready;
+wire [5:0] ip_tx_ip_dscp;
+wire [1:0] ip_tx_ip_ecn;
+wire [15:0] ip_tx_ip_length;
+wire [7:0] ip_tx_ip_ttl;
+wire [7:0] ip_tx_ip_protocol;
+wire [31:0] ip_tx_ip_source_ip;
+wire [31:0] ip_tx_ip_dest_ip;
+wire [7:0] ip_tx_ip_payload_axis_tdata;
+wire ip_tx_ip_payload_axis_tvalid;
+wire ip_tx_ip_payload_axis_tlast;
+wire ip_tx_ip_payload_axis_tuser;
+wire ip_tx_ip_payload_axis_tready;
+
+wire udp_rx_ip_hdr_valid;
+wire udp_rx_ip_hdr_ready;
+wire [47:0] udp_rx_ip_eth_dest_mac;
+wire [47:0] udp_rx_ip_eth_src_mac;
+wire [15:0] udp_rx_ip_eth_type;
+wire [3:0] udp_rx_ip_version;
+wire [3:0] udp_rx_ip_ihl;
+wire [5:0] udp_rx_ip_dscp;
+wire [1:0] udp_rx_ip_ecn;
+wire [15:0] udp_rx_ip_length;
+wire [15:0] udp_rx_ip_identification;
+wire [2:0] udp_rx_ip_flags;
+wire [12:0] udp_rx_ip_fragment_offset;
+wire [7:0] udp_rx_ip_ttl;
+wire [7:0] udp_rx_ip_protocol;
+wire [15:0] udp_rx_ip_header_checksum;
+wire [31:0] udp_rx_ip_source_ip;
+wire [31:0] udp_rx_ip_dest_ip;
+wire [7:0] udp_rx_ip_payload_axis_tdata;
+wire udp_rx_ip_payload_axis_tvalid;
+wire udp_rx_ip_payload_axis_tlast;
+wire udp_rx_ip_payload_axis_tuser;
+wire udp_rx_ip_payload_axis_tready;
+
+wire udp_tx_ip_hdr_valid;
+wire udp_tx_ip_hdr_ready;
+wire [5:0] udp_tx_ip_dscp;
+wire [1:0] udp_tx_ip_ecn;
+wire [15:0] udp_tx_ip_length;
+wire [7:0] udp_tx_ip_ttl;
+wire [7:0] udp_tx_ip_protocol;
+wire [31:0] udp_tx_ip_source_ip;
+wire [31:0] udp_tx_ip_dest_ip;
+wire [7:0] udp_tx_ip_payload_axis_tdata;
+wire udp_tx_ip_payload_axis_tvalid;
+wire udp_tx_ip_payload_axis_tlast;
+wire udp_tx_ip_payload_axis_tuser;
+wire udp_tx_ip_payload_axis_tready;
+
+/*
+ * Input classifier (ip_protocol)
+ */
+wire s_select_udp = (ip_rx_ip_protocol == 8'h11);
+wire s_select_ip = !s_select_udp;
+
+reg s_select_udp_reg;
+reg s_select_ip_reg;
+
+always @(posedge clk) begin
+    if (rst) begin
+        s_select_udp_reg <= 1'b0;
+        s_select_ip_reg <= 1'b0;
+    end else begin
+        if (ip_rx_ip_payload_axis_tvalid) begin
+            if ((!s_select_udp_reg && !s_select_ip_reg) ||
+                (ip_rx_ip_payload_axis_tvalid && ip_rx_ip_payload_axis_tready && ip_rx_ip_payload_axis_tlast)) begin
+                s_select_udp_reg <= s_select_udp;
+                s_select_ip_reg <= s_select_ip;
+            end
+        end else begin
+            s_select_udp_reg <= 1'b0;
+            s_select_ip_reg <= 1'b0;
+        end
+    end
+end
+
+// IP frame to UDP module
+assign udp_rx_ip_hdr_valid = s_select_udp && ip_rx_ip_hdr_valid;
+assign udp_rx_ip_eth_dest_mac = ip_rx_ip_eth_dest_mac;
+assign udp_rx_ip_eth_src_mac = ip_rx_ip_eth_src_mac;
+assign udp_rx_ip_eth_type = ip_rx_ip_eth_type;
+assign udp_rx_ip_version = ip_rx_ip_version;
+assign udp_rx_ip_ihl = ip_rx_ip_ihl;
+assign udp_rx_ip_dscp = ip_rx_ip_dscp;
+assign udp_rx_ip_ecn = ip_rx_ip_ecn;
+assign udp_rx_ip_length = ip_rx_ip_length;
+assign udp_rx_ip_identification = ip_rx_ip_identification;
+assign udp_rx_ip_flags = ip_rx_ip_flags;
+assign udp_rx_ip_fragment_offset = ip_rx_ip_fragment_offset;
+assign udp_rx_ip_ttl = ip_rx_ip_ttl;
+assign udp_rx_ip_protocol = 8'h11;
+assign udp_rx_ip_header_checksum = ip_rx_ip_header_checksum;
+assign udp_rx_ip_source_ip = ip_rx_ip_source_ip;
+assign udp_rx_ip_dest_ip = ip_rx_ip_dest_ip;
+assign udp_rx_ip_payload_axis_tdata = ip_rx_ip_payload_axis_tdata;
+assign udp_rx_ip_payload_axis_tvalid = s_select_udp_reg && ip_rx_ip_payload_axis_tvalid;
+assign udp_rx_ip_payload_axis_tlast = ip_rx_ip_payload_axis_tlast;
+assign udp_rx_ip_payload_axis_tuser = ip_rx_ip_payload_axis_tuser;
+
+// External IP frame output
+assign m_ip_hdr_valid = s_select_ip && ip_rx_ip_hdr_valid;
+assign m_ip_eth_dest_mac = ip_rx_ip_eth_dest_mac;
+assign m_ip_eth_src_mac = ip_rx_ip_eth_src_mac;
+assign m_ip_eth_type = ip_rx_ip_eth_type;
+assign m_ip_version = ip_rx_ip_version;
+assign m_ip_ihl = ip_rx_ip_ihl;
+assign m_ip_dscp = ip_rx_ip_dscp;
+assign m_ip_ecn = ip_rx_ip_ecn;
+assign m_ip_length = ip_rx_ip_length;
+assign m_ip_identification = ip_rx_ip_identification;
+assign m_ip_flags = ip_rx_ip_flags;
+assign m_ip_fragment_offset = ip_rx_ip_fragment_offset;
+assign m_ip_ttl = ip_rx_ip_ttl;
+assign m_ip_protocol = ip_rx_ip_protocol;
+assign m_ip_header_checksum = ip_rx_ip_header_checksum;
+assign m_ip_source_ip = ip_rx_ip_source_ip;
+assign m_ip_dest_ip = ip_rx_ip_dest_ip;
+assign m_ip_payload_axis_tdata = ip_rx_ip_payload_axis_tdata;
+assign m_ip_payload_axis_tvalid = s_select_ip_reg && ip_rx_ip_payload_axis_tvalid;
+assign m_ip_payload_axis_tlast = ip_rx_ip_payload_axis_tlast;
+assign m_ip_payload_axis_tuser = ip_rx_ip_payload_axis_tuser;
+
+assign ip_rx_ip_hdr_ready = (s_select_udp && udp_rx_ip_hdr_ready) ||
+                            (s_select_ip && m_ip_hdr_ready);
+
+assign ip_rx_ip_payload_axis_tready = (s_select_udp_reg && udp_rx_ip_payload_axis_tready) ||
+                                      (s_select_ip_reg && m_ip_payload_axis_tready);
+
+/*
+ * Output arbiter
+ */
+ip_arb_mux #(
+    .S_COUNT(2),
+    .DATA_WIDTH(8),
+    .KEEP_ENABLE(0),
+    .ID_ENABLE(0),
+    .DEST_ENABLE(0),
+    .USER_ENABLE(1),
+    .USER_WIDTH(1),
+    .ARB_TYPE_ROUND_ROBIN(0),
+    .ARB_LSB_HIGH_PRIORITY(1)
+)
+ip_arb_mux_inst (
+    .clk(clk),
+    .rst(rst),
+    // IP frame inputs
+    .s_ip_hdr_valid({s_ip_hdr_valid, udp_tx_ip_hdr_valid}),
+    .s_ip_hdr_ready({s_ip_hdr_ready, udp_tx_ip_hdr_ready}),
+    .s_eth_dest_mac(0),
+    .s_eth_src_mac(0),
+    .s_eth_type(0),
+    .s_ip_version(0),
+    .s_ip_ihl(0),
+    .s_ip_dscp({s_ip_dscp, udp_tx_ip_dscp}),
+    .s_ip_ecn({s_ip_ecn, udp_tx_ip_ecn}),
+    .s_ip_length({s_ip_length, udp_tx_ip_length}),
+    .s_ip_identification(0),
+    .s_ip_flags(0),
+    .s_ip_fragment_offset(0),
+    .s_ip_ttl({s_ip_ttl, udp_tx_ip_ttl}),
+    .s_ip_protocol({s_ip_protocol, udp_tx_ip_protocol}),
+    .s_ip_header_checksum(0),
+    .s_ip_source_ip({s_ip_source_ip, udp_tx_ip_source_ip}),
+    .s_ip_dest_ip({s_ip_dest_ip, udp_tx_ip_dest_ip}),
+    .s_ip_payload_axis_tdata({s_ip_payload_axis_tdata, udp_tx_ip_payload_axis_tdata}),
+    .s_ip_payload_axis_tkeep(0),
+    .s_ip_payload_axis_tvalid({s_ip_payload_axis_tvalid, udp_tx_ip_payload_axis_tvalid}),
+    .s_ip_payload_axis_tready({s_ip_payload_axis_tready, udp_tx_ip_payload_axis_tready}),
+    .s_ip_payload_axis_tlast({s_ip_payload_axis_tlast, udp_tx_ip_payload_axis_tlast}),
+    .s_ip_payload_axis_tid(0),
+    .s_ip_payload_axis_tdest(0),
+    .s_ip_payload_axis_tuser({s_ip_payload_axis_tuser, udp_tx_ip_payload_axis_tuser}),
+    // IP frame output
+    .m_ip_hdr_valid(ip_tx_ip_hdr_valid),
+    .m_ip_hdr_ready(ip_tx_ip_hdr_ready),
+    .m_eth_dest_mac(),
+    .m_eth_src_mac(),
+    .m_eth_type(),
+    .m_ip_version(),
+    .m_ip_ihl(),
+    .m_ip_dscp(ip_tx_ip_dscp),
+    .m_ip_ecn(ip_tx_ip_ecn),
+    .m_ip_length(ip_tx_ip_length),
+    .m_ip_identification(),
+    .m_ip_flags(),
+    .m_ip_fragment_offset(),
+    .m_ip_ttl(ip_tx_ip_ttl),
+    .m_ip_protocol(ip_tx_ip_protocol),
+    .m_ip_header_checksum(),
+    .m_ip_source_ip(ip_tx_ip_source_ip),
+    .m_ip_dest_ip(ip_tx_ip_dest_ip),
+    .m_ip_payload_axis_tdata(ip_tx_ip_payload_axis_tdata),
+    .m_ip_payload_axis_tkeep(),
+    .m_ip_payload_axis_tvalid(ip_tx_ip_payload_axis_tvalid),
+    .m_ip_payload_axis_tready(ip_tx_ip_payload_axis_tready),
+    .m_ip_payload_axis_tlast(ip_tx_ip_payload_axis_tlast),
+    .m_ip_payload_axis_tid(),
+    .m_ip_payload_axis_tdest(),
+    .m_ip_payload_axis_tuser(ip_tx_ip_payload_axis_tuser)
+);
+
+/*
+ * IP stack
+ */
+ip_complete #(
+    .ARP_CACHE_ADDR_WIDTH(ARP_CACHE_ADDR_WIDTH),
+    .ARP_REQUEST_RETRY_COUNT(ARP_REQUEST_RETRY_COUNT),
+    .ARP_REQUEST_RETRY_INTERVAL(ARP_REQUEST_RETRY_INTERVAL),
+    .ARP_REQUEST_TIMEOUT(ARP_REQUEST_TIMEOUT)
+)
+ip_complete_inst (
+    .clk(clk),
+    .rst(rst),
+    // Ethernet frame input
+    .s_eth_hdr_valid(s_eth_hdr_valid),
+    .s_eth_hdr_ready(s_eth_hdr_ready),
+    .s_eth_dest_mac(s_eth_dest_mac),
+    .s_eth_src_mac(s_eth_src_mac),
+    .s_eth_type(s_eth_type),
+    .s_eth_payload_axis_tdata(s_eth_payload_axis_tdata),
+    .s_eth_payload_axis_tvalid(s_eth_payload_axis_tvalid),
+    .s_eth_payload_axis_tready(s_eth_payload_axis_tready),
+    .s_eth_payload_axis_tlast(s_eth_payload_axis_tlast),
+    .s_eth_payload_axis_tuser(s_eth_payload_axis_tuser),
+    // Ethernet frame output
+    .m_eth_hdr_valid(m_eth_hdr_valid),
+    .m_eth_hdr_ready(m_eth_hdr_ready),
+    .m_eth_dest_mac(m_eth_dest_mac),
+    .m_eth_src_mac(m_eth_src_mac),
+    .m_eth_type(m_eth_type),
+    .m_eth_payload_axis_tdata(m_eth_payload_axis_tdata),
+    .m_eth_payload_axis_tvalid(m_eth_payload_axis_tvalid),
+    .m_eth_payload_axis_tready(m_eth_payload_axis_tready),
+    .m_eth_payload_axis_tlast(m_eth_payload_axis_tlast),
+    .m_eth_payload_axis_tuser(m_eth_payload_axis_tuser),
+    // IP frame input
+    .s_ip_hdr_valid(ip_tx_ip_hdr_valid),
+    .s_ip_hdr_ready(ip_tx_ip_hdr_ready),
+    .s_ip_dscp(ip_tx_ip_dscp),
+    .s_ip_ecn(ip_tx_ip_ecn),
+    .s_ip_length(ip_tx_ip_length),
+    .s_ip_ttl(ip_tx_ip_ttl),
+    .s_ip_protocol(ip_tx_ip_protocol),
+    .s_ip_source_ip(ip_tx_ip_source_ip),
+    .s_ip_dest_ip(ip_tx_ip_dest_ip),
+    .s_ip_payload_axis_tdata(ip_tx_ip_payload_axis_tdata),
+    .s_ip_payload_axis_tvalid(ip_tx_ip_payload_axis_tvalid),
+    .s_ip_payload_axis_tready(ip_tx_ip_payload_axis_tready),
+    .s_ip_payload_axis_tlast(ip_tx_ip_payload_axis_tlast),
+    .s_ip_payload_axis_tuser(ip_tx_ip_payload_axis_tuser),
+    // IP frame output
+    .m_ip_hdr_valid(ip_rx_ip_hdr_valid),
+    .m_ip_hdr_ready(ip_rx_ip_hdr_ready),
+    .m_ip_eth_dest_mac(ip_rx_ip_eth_dest_mac),
+    .m_ip_eth_src_mac(ip_rx_ip_eth_src_mac),
+    .m_ip_eth_type(ip_rx_ip_eth_type),
+    .m_ip_version(ip_rx_ip_version),
+    .m_ip_ihl(ip_rx_ip_ihl),
+    .m_ip_dscp(ip_rx_ip_dscp),
+    .m_ip_ecn(ip_rx_ip_ecn),
+    .m_ip_length(ip_rx_ip_length),
+    .m_ip_identification(ip_rx_ip_identification),
+    .m_ip_flags(ip_rx_ip_flags),
+    .m_ip_fragment_offset(ip_rx_ip_fragment_offset),
+    .m_ip_ttl(ip_rx_ip_ttl),
+    .m_ip_protocol(ip_rx_ip_protocol),
+    .m_ip_header_checksum(ip_rx_ip_header_checksum),
+    .m_ip_source_ip(ip_rx_ip_source_ip),
+    .m_ip_dest_ip(ip_rx_ip_dest_ip),
+    .m_ip_payload_axis_tdata(ip_rx_ip_payload_axis_tdata),
+    .m_ip_payload_axis_tvalid(ip_rx_ip_payload_axis_tvalid),
+    .m_ip_payload_axis_tready(ip_rx_ip_payload_axis_tready),
+    .m_ip_payload_axis_tlast(ip_rx_ip_payload_axis_tlast),
+    .m_ip_payload_axis_tuser(ip_rx_ip_payload_axis_tuser),
+    // Status
+    .rx_busy(ip_rx_busy),
+    .tx_busy(ip_tx_busy),
+    .rx_error_header_early_termination(ip_rx_error_header_early_termination),
+    .rx_error_payload_early_termination(ip_rx_error_payload_early_termination),
+    .rx_error_invalid_header(ip_rx_error_invalid_header),
+    .rx_error_invalid_checksum(ip_rx_error_invalid_checksum),
+    .tx_error_payload_early_termination(ip_tx_error_payload_early_termination),
+    .tx_error_arp_failed(ip_tx_error_arp_failed),
+    // Configuration
+    .local_mac(local_mac),
+    .local_ip(local_ip),
+    .gateway_ip(gateway_ip),
+    .subnet_mask(subnet_mask),
+    .clear_arp_cache(clear_arp_cache)
+);
+
+/*
+ * UDP interface
+ */
+udp #(
+    .CHECKSUM_GEN_ENABLE(UDP_CHECKSUM_GEN_ENABLE),
+    .CHECKSUM_PAYLOAD_FIFO_DEPTH(UDP_CHECKSUM_PAYLOAD_FIFO_DEPTH),
+    .CHECKSUM_HEADER_FIFO_DEPTH(UDP_CHECKSUM_HEADER_FIFO_DEPTH)
+)
+udp_inst (
+    .clk(clk),
+    .rst(rst),
+    // IP frame input
+    .s_ip_hdr_valid(udp_rx_ip_hdr_valid),
+    .s_ip_hdr_ready(udp_rx_ip_hdr_ready),
+    .s_ip_eth_dest_mac(udp_rx_ip_eth_dest_mac),
+    .s_ip_eth_src_mac(udp_rx_ip_eth_src_mac),
+    .s_ip_eth_type(udp_rx_ip_eth_type),
+    .s_ip_version(udp_rx_ip_version),
+    .s_ip_ihl(udp_rx_ip_ihl),
+    .s_ip_dscp(udp_rx_ip_dscp),
+    .s_ip_ecn(udp_rx_ip_ecn),
+    .s_ip_length(udp_rx_ip_length),
+    .s_ip_identification(udp_rx_ip_identification),
+    .s_ip_flags(udp_rx_ip_flags),
+    .s_ip_fragment_offset(udp_rx_ip_fragment_offset),
+    .s_ip_ttl(udp_rx_ip_ttl),
+    .s_ip_protocol(udp_rx_ip_protocol),
+    .s_ip_header_checksum(udp_rx_ip_header_checksum),
+    .s_ip_source_ip(udp_rx_ip_source_ip),
+    .s_ip_dest_ip(udp_rx_ip_dest_ip),
+    .s_ip_payload_axis_tdata(udp_rx_ip_payload_axis_tdata),
+    .s_ip_payload_axis_tvalid(udp_rx_ip_payload_axis_tvalid),
+    .s_ip_payload_axis_tready(udp_rx_ip_payload_axis_tready),
+    .s_ip_payload_axis_tlast(udp_rx_ip_payload_axis_tlast),
+    .s_ip_payload_axis_tuser(udp_rx_ip_payload_axis_tuser),
+    // IP frame output
+    .m_ip_hdr_valid(udp_tx_ip_hdr_valid),
+    .m_ip_hdr_ready(udp_tx_ip_hdr_ready),
+    .m_ip_eth_dest_mac(),
+    .m_ip_eth_src_mac(),
+    .m_ip_eth_type(),
+    .m_ip_version(),
+    .m_ip_ihl(),
+    .m_ip_dscp(udp_tx_ip_dscp),
+    .m_ip_ecn(udp_tx_ip_ecn),
+    .m_ip_length(udp_tx_ip_length),
+    .m_ip_identification(),
+    .m_ip_flags(),
+    .m_ip_fragment_offset(),
+    .m_ip_ttl(udp_tx_ip_ttl),
+    .m_ip_protocol(udp_tx_ip_protocol),
+    .m_ip_header_checksum(),
+    .m_ip_source_ip(udp_tx_ip_source_ip),
+    .m_ip_dest_ip(udp_tx_ip_dest_ip),
+    .m_ip_payload_axis_tdata(udp_tx_ip_payload_axis_tdata),
+    .m_ip_payload_axis_tvalid(udp_tx_ip_payload_axis_tvalid),
+    .m_ip_payload_axis_tready(udp_tx_ip_payload_axis_tready),
+    .m_ip_payload_axis_tlast(udp_tx_ip_payload_axis_tlast),
+    .m_ip_payload_axis_tuser(udp_tx_ip_payload_axis_tuser),
+    // UDP frame input
+    .s_udp_hdr_valid(s_udp_hdr_valid),
+    .s_udp_hdr_ready(s_udp_hdr_ready),
+    .s_udp_eth_dest_mac(48'd0),
+    .s_udp_eth_src_mac(48'd0),
+    .s_udp_eth_type(16'd0),
+    .s_udp_ip_version(4'd0),
+    .s_udp_ip_ihl(4'd0),
+    .s_udp_ip_dscp(s_udp_ip_dscp),
+    .s_udp_ip_ecn(s_udp_ip_ecn),
+    .s_udp_ip_identification(16'd0),
+    .s_udp_ip_flags(3'd0),
+    .s_udp_ip_fragment_offset(13'd0),
+    .s_udp_ip_ttl(s_udp_ip_ttl),
+    .s_udp_ip_header_checksum(16'd0),
+    .s_udp_ip_source_ip(s_udp_ip_source_ip),
+    .s_udp_ip_dest_ip(s_udp_ip_dest_ip),
+    .s_udp_source_port(s_udp_source_port),
+    .s_udp_dest_port(s_udp_dest_port),
+    .s_udp_length(s_udp_length),
+    .s_udp_checksum(s_udp_checksum),
+    .s_udp_payload_axis_tdata(s_udp_payload_axis_tdata),
+    .s_udp_payload_axis_tvalid(s_udp_payload_axis_tvalid),
+    .s_udp_payload_axis_tready(s_udp_payload_axis_tready),
+    .s_udp_payload_axis_tlast(s_udp_payload_axis_tlast),
+    .s_udp_payload_axis_tuser(s_udp_payload_axis_tuser),
+    // UDP frame output
+    .m_udp_hdr_valid(m_udp_hdr_valid),
+    .m_udp_hdr_ready(m_udp_hdr_ready),
+    .m_udp_eth_dest_mac(m_udp_eth_dest_mac),
+    .m_udp_eth_src_mac(m_udp_eth_src_mac),
+    .m_udp_eth_type(m_udp_eth_type),
+    .m_udp_ip_version(m_udp_ip_version),
+    .m_udp_ip_ihl(m_udp_ip_ihl),
+    .m_udp_ip_dscp(m_udp_ip_dscp),
+    .m_udp_ip_ecn(m_udp_ip_ecn),
+    .m_udp_ip_length(m_udp_ip_length),
+    .m_udp_ip_identification(m_udp_ip_identification),
+    .m_udp_ip_flags(m_udp_ip_flags),
+    .m_udp_ip_fragment_offset(m_udp_ip_fragment_offset),
+    .m_udp_ip_ttl(m_udp_ip_ttl),
+    .m_udp_ip_protocol(m_udp_ip_protocol),
+    .m_udp_ip_header_checksum(m_udp_ip_header_checksum),
+    .m_udp_ip_source_ip(m_udp_ip_source_ip),
+    .m_udp_ip_dest_ip(m_udp_ip_dest_ip),
+    .m_udp_source_port(m_udp_source_port),
+    .m_udp_dest_port(m_udp_dest_port),
+    .m_udp_length(m_udp_length),
+    .m_udp_checksum(m_udp_checksum),
+    .m_udp_payload_axis_tdata(m_udp_payload_axis_tdata),
+    .m_udp_payload_axis_tvalid(m_udp_payload_axis_tvalid),
+    .m_udp_payload_axis_tready(m_udp_payload_axis_tready),
+    .m_udp_payload_axis_tlast(m_udp_payload_axis_tlast),
+    .m_udp_payload_axis_tuser(m_udp_payload_axis_tuser),
+    // Status
+    .rx_busy(udp_rx_busy),
+    .tx_busy(udp_tx_busy),
+    .rx_error_header_early_termination(udp_rx_error_header_early_termination),
+    .rx_error_payload_early_termination(udp_rx_error_payload_early_termination),
+    .tx_error_payload_early_termination(udp_tx_error_payload_early_termination)
+);
+
+endmodule
+
diff --git a/verilog/rtl/udp_ip_rx.v b/verilog/rtl/udp_ip_rx.v
new file mode 100644
index 0000000..13755ca
--- /dev/null
+++ b/verilog/rtl/udp_ip_rx.v
@@ -0,0 +1,564 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * UDP ethernet frame receiver (IP frame in, UDP frame out)
+ */
+module udp_ip_rx
+(
+    input  wire        clk,
+    input  wire        rst,
+
+    /*
+     * IP frame input
+     */
+    input  wire        s_ip_hdr_valid,
+    output wire        s_ip_hdr_ready,
+    input  wire [47:0] s_eth_dest_mac,
+    input  wire [47:0] s_eth_src_mac,
+    input  wire [15:0] s_eth_type,
+    input  wire [3:0]  s_ip_version,
+    input  wire [3:0]  s_ip_ihl,
+    input  wire [5:0]  s_ip_dscp,
+    input  wire [1:0]  s_ip_ecn,
+    input  wire [15:0] s_ip_length,
+    input  wire [15:0] s_ip_identification,
+    input  wire [2:0]  s_ip_flags,
+    input  wire [12:0] s_ip_fragment_offset,
+    input  wire [7:0]  s_ip_ttl,
+    input  wire [7:0]  s_ip_protocol,
+    input  wire [15:0] s_ip_header_checksum,
+    input  wire [31:0] s_ip_source_ip,
+    input  wire [31:0] s_ip_dest_ip,
+    input  wire [7:0]  s_ip_payload_axis_tdata,
+    input  wire        s_ip_payload_axis_tvalid,
+    output wire        s_ip_payload_axis_tready,
+    input  wire        s_ip_payload_axis_tlast,
+    input  wire        s_ip_payload_axis_tuser,
+
+    /*
+     * UDP frame output
+     */
+    output wire        m_udp_hdr_valid,
+    input  wire        m_udp_hdr_ready,
+    output wire [47:0] m_eth_dest_mac,
+    output wire [47:0] m_eth_src_mac,
+    output wire [15:0] m_eth_type,
+    output wire [3:0]  m_ip_version,
+    output wire [3:0]  m_ip_ihl,
+    output wire [5:0]  m_ip_dscp,
+    output wire [1:0]  m_ip_ecn,
+    output wire [15:0] m_ip_length,
+    output wire [15:0] m_ip_identification,
+    output wire [2:0]  m_ip_flags,
+    output wire [12:0] m_ip_fragment_offset,
+    output wire [7:0]  m_ip_ttl,
+    output wire [7:0]  m_ip_protocol,
+    output wire [15:0] m_ip_header_checksum,
+    output wire [31:0] m_ip_source_ip,
+    output wire [31:0] m_ip_dest_ip,
+    output wire [15:0] m_udp_source_port,
+    output wire [15:0] m_udp_dest_port,
+    output wire [15:0] m_udp_length,
+    output wire [15:0] m_udp_checksum,
+    output wire [7:0]  m_udp_payload_axis_tdata,
+    output wire        m_udp_payload_axis_tvalid,
+    input  wire        m_udp_payload_axis_tready,
+    output wire        m_udp_payload_axis_tlast,
+    output wire        m_udp_payload_axis_tuser,
+
+    /*
+     * Status signals
+     */
+    output wire        busy,
+    output wire        error_header_early_termination,
+    output wire        error_payload_early_termination
+);
+
+/*
+
+UDP Frame
+
+ Field                       Length
+ Destination MAC address     6 octets
+ Source MAC address          6 octets
+ Ethertype (0x0800)          2 octets
+ Version (4)                 4 bits
+ IHL (5-15)                  4 bits
+ DSCP (0)                    6 bits
+ ECN (0)                     2 bits
+ length                      2 octets
+ identification (0?)         2 octets
+ flags (010)                 3 bits
+ fragment offset (0)         13 bits
+ time to live (64?)          1 octet
+ protocol                    1 octet
+ header checksum             2 octets
+ source IP                   4 octets
+ destination IP              4 octets
+ options                     (IHL-5)*4 octets
+
+ source port                 2 octets
+ desination port             2 octets
+ length                      2 octets
+ checksum                    2 octets
+
+ payload                     length octets
+
+This module receives an IP frame with header fields in parallel and payload on
+an AXI stream interface, decodes and strips the UDP header fields, then
+produces the header fields in parallel along with the UDP payload in a
+separate AXI stream.
+
+*/
+
+localparam [2:0]
+    STATE_IDLE = 3'd0,
+    STATE_READ_HEADER = 3'd1,
+    STATE_READ_PAYLOAD = 3'd2,
+    STATE_READ_PAYLOAD_LAST = 3'd3,
+    STATE_WAIT_LAST = 3'd4;
+
+reg [2:0] state_reg, state_next;
+
+// datapath control signals
+reg store_ip_hdr;
+reg store_udp_source_port_0;
+reg store_udp_source_port_1;
+reg store_udp_dest_port_0;
+reg store_udp_dest_port_1;
+reg store_udp_length_0;
+reg store_udp_length_1;
+reg store_udp_checksum_0;
+reg store_udp_checksum_1;
+reg store_last_word;
+
+reg [2:0] hdr_ptr_reg, hdr_ptr_next;
+reg [15:0] word_count_reg, word_count_next;
+
+reg [7:0] last_word_data_reg;
+
+reg m_udp_hdr_valid_reg, m_udp_hdr_valid_next;
+reg [47:0] m_eth_dest_mac_reg;
+reg [47:0] m_eth_src_mac_reg;
+reg [15:0] m_eth_type_reg;
+reg [3:0] m_ip_version_reg;
+reg [3:0] m_ip_ihl_reg;
+reg [5:0] m_ip_dscp_reg;
+reg [1:0] m_ip_ecn_reg;
+reg [15:0] m_ip_length_reg;
+reg [15:0] m_ip_identification_reg;
+reg [2:0] m_ip_flags_reg;
+reg [12:0] m_ip_fragment_offset_reg;
+reg [7:0] m_ip_ttl_reg;
+reg [7:0] m_ip_protocol_reg;
+reg [15:0] m_ip_header_checksum_reg;
+reg [31:0] m_ip_source_ip_reg;
+reg [31:0] m_ip_dest_ip_reg;
+reg [15:0] m_udp_source_port_reg;
+reg [15:0] m_udp_dest_port_reg;
+reg [15:0] m_udp_length_reg;
+reg [15:0] m_udp_checksum_reg;
+
+reg s_ip_hdr_ready_reg, s_ip_hdr_ready_next;
+reg s_ip_payload_axis_tready_reg, s_ip_payload_axis_tready_next;
+
+reg busy_reg;
+reg error_header_early_termination_reg, error_header_early_termination_next;
+reg error_payload_early_termination_reg, error_payload_early_termination_next;
+
+// internal datapath
+reg [7:0] m_udp_payload_axis_tdata_int;
+reg       m_udp_payload_axis_tvalid_int;
+reg       m_udp_payload_axis_tready_int_reg;
+reg       m_udp_payload_axis_tlast_int;
+reg       m_udp_payload_axis_tuser_int;
+wire      m_udp_payload_axis_tready_int_early;
+
+assign s_ip_hdr_ready = s_ip_hdr_ready_reg;
+assign s_ip_payload_axis_tready = s_ip_payload_axis_tready_reg;
+
+assign m_udp_hdr_valid = m_udp_hdr_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+assign m_ip_version = m_ip_version_reg;
+assign m_ip_ihl = m_ip_ihl_reg;
+assign m_ip_dscp = m_ip_dscp_reg;
+assign m_ip_ecn = m_ip_ecn_reg;
+assign m_ip_length = m_ip_length_reg;
+assign m_ip_identification = m_ip_identification_reg;
+assign m_ip_flags = m_ip_flags_reg;
+assign m_ip_fragment_offset = m_ip_fragment_offset_reg;
+assign m_ip_ttl = m_ip_ttl_reg;
+assign m_ip_protocol = m_ip_protocol_reg;
+assign m_ip_header_checksum = m_ip_header_checksum_reg;
+assign m_ip_source_ip = m_ip_source_ip_reg;
+assign m_ip_dest_ip = m_ip_dest_ip_reg;
+assign m_udp_source_port = m_udp_source_port_reg;
+assign m_udp_dest_port = m_udp_dest_port_reg;
+assign m_udp_length = m_udp_length_reg;
+assign m_udp_checksum = m_udp_checksum_reg;
+
+assign busy = busy_reg;
+assign error_header_early_termination = error_header_early_termination_reg;
+assign error_payload_early_termination = error_payload_early_termination_reg;
+
+always @* begin
+    state_next = STATE_IDLE;
+
+    s_ip_hdr_ready_next = 1'b0;
+    s_ip_payload_axis_tready_next = 1'b0;
+
+    store_ip_hdr = 1'b0;
+    store_udp_source_port_0 = 1'b0;
+    store_udp_source_port_1 = 1'b0;
+    store_udp_dest_port_0 = 1'b0;
+    store_udp_dest_port_1 = 1'b0;
+    store_udp_length_0 = 1'b0;
+    store_udp_length_1 = 1'b0;
+    store_udp_checksum_0 = 1'b0;
+    store_udp_checksum_1 = 1'b0;
+
+    store_last_word = 1'b0;
+
+    hdr_ptr_next = hdr_ptr_reg;
+    word_count_next = word_count_reg;
+
+    m_udp_hdr_valid_next = m_udp_hdr_valid_reg && !m_udp_hdr_ready;
+
+    error_header_early_termination_next = 1'b0;
+    error_payload_early_termination_next = 1'b0;
+
+    m_udp_payload_axis_tdata_int = 8'd0;
+    m_udp_payload_axis_tvalid_int = 1'b0;
+    m_udp_payload_axis_tlast_int = 1'b0;
+    m_udp_payload_axis_tuser_int = 1'b0;
+
+    case (state_reg)
+        STATE_IDLE: begin
+            // idle state - wait for header
+            hdr_ptr_next = 3'd0;
+            s_ip_hdr_ready_next = !m_udp_hdr_valid_next;
+
+            if (s_ip_hdr_ready && s_ip_hdr_valid) begin
+                s_ip_hdr_ready_next = 1'b0;
+                s_ip_payload_axis_tready_next = 1'b1;
+                store_ip_hdr = 1'b1;
+                state_next = STATE_READ_HEADER;
+            end else begin
+                state_next = STATE_IDLE;
+            end
+        end
+        STATE_READ_HEADER: begin
+            // read header state
+            s_ip_payload_axis_tready_next = 1'b1;
+            word_count_next = m_udp_length_reg - 16'd8;
+
+            if (s_ip_payload_axis_tready && s_ip_payload_axis_tvalid) begin
+                // word transfer in - store it
+                hdr_ptr_next = hdr_ptr_reg + 3'd1;
+                state_next = STATE_READ_HEADER;
+
+                case (hdr_ptr_reg)
+                    3'h0: store_udp_source_port_1 = 1'b1;
+                    3'h1: store_udp_source_port_0 = 1'b1;
+                    3'h2: store_udp_dest_port_1 = 1'b1;
+                    3'h3: store_udp_dest_port_0 = 1'b1;
+                    3'h4: store_udp_length_1 = 1'b1;
+                    3'h5: store_udp_length_0 = 1'b1;
+                    3'h6: store_udp_checksum_1 = 1'b1;
+                    3'h7: begin
+                        store_udp_checksum_0 = 1'b1;
+                        m_udp_hdr_valid_next = 1'b1;
+                        s_ip_payload_axis_tready_next = m_udp_payload_axis_tready_int_early;
+                        state_next = STATE_READ_PAYLOAD;
+                    end
+                endcase
+
+                if (s_ip_payload_axis_tlast) begin
+                    error_header_early_termination_next = 1'b1;
+                    m_udp_hdr_valid_next = 1'b0;
+                    s_ip_hdr_ready_next = !m_udp_hdr_valid_next;
+                    s_ip_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end
+
+            end else begin
+                state_next = STATE_READ_HEADER;
+            end
+        end
+        STATE_READ_PAYLOAD: begin
+            // read payload
+            s_ip_payload_axis_tready_next = m_udp_payload_axis_tready_int_early;
+
+            m_udp_payload_axis_tdata_int = s_ip_payload_axis_tdata;
+            m_udp_payload_axis_tvalid_int = s_ip_payload_axis_tvalid;
+            m_udp_payload_axis_tlast_int = s_ip_payload_axis_tlast;
+            m_udp_payload_axis_tuser_int = s_ip_payload_axis_tuser;
+
+            if (s_ip_payload_axis_tready && s_ip_payload_axis_tvalid) begin
+                // word transfer through
+                word_count_next = word_count_reg - 16'd1;
+                if (s_ip_payload_axis_tlast) begin
+                    if (word_count_reg != 16'd1) begin
+                        // end of frame, but length does not match
+                        m_udp_payload_axis_tuser_int = 1'b1;
+                        error_payload_early_termination_next = 1'b1;
+                    end
+                    s_ip_hdr_ready_next = !m_udp_hdr_valid_next;
+                    s_ip_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    if (word_count_reg == 16'd1) begin
+                        store_last_word = 1'b1;
+                        m_udp_payload_axis_tvalid_int = 1'b0;
+                        state_next = STATE_READ_PAYLOAD_LAST;
+                    end else begin
+                        state_next = STATE_READ_PAYLOAD;
+                    end
+                end
+            end else begin
+                state_next = STATE_READ_PAYLOAD;
+            end
+        end
+        STATE_READ_PAYLOAD_LAST: begin
+            // read and discard until end of frame
+            s_ip_payload_axis_tready_next = m_udp_payload_axis_tready_int_early;
+
+            m_udp_payload_axis_tdata_int = last_word_data_reg;
+            m_udp_payload_axis_tvalid_int = s_ip_payload_axis_tvalid && s_ip_payload_axis_tlast;
+            m_udp_payload_axis_tlast_int = s_ip_payload_axis_tlast;
+            m_udp_payload_axis_tuser_int = s_ip_payload_axis_tuser;
+
+            if (s_ip_payload_axis_tready && s_ip_payload_axis_tvalid) begin
+                if (s_ip_payload_axis_tlast) begin
+                    s_ip_hdr_ready_next = !m_udp_hdr_valid_next;
+                    s_ip_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_READ_PAYLOAD_LAST;
+                end
+            end else begin
+                state_next = STATE_READ_PAYLOAD_LAST;
+            end
+        end
+        STATE_WAIT_LAST: begin
+            // wait for end of frame; read and discard
+            s_ip_payload_axis_tready_next = 1'b1;
+
+            if (s_ip_payload_axis_tready && s_ip_payload_axis_tvalid) begin
+                if (s_ip_payload_axis_tlast) begin
+                    s_ip_hdr_ready_next = !m_udp_hdr_valid_next;
+                    s_ip_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_WAIT_LAST;
+                end
+            end else begin
+                state_next = STATE_WAIT_LAST;
+            end
+        end
+    endcase
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        state_reg <= STATE_IDLE;
+        s_ip_hdr_ready_reg <= 1'b0;
+        s_ip_payload_axis_tready_reg <= 1'b0;
+        last_word_data_reg <= 8'd0;
+        m_udp_hdr_valid_reg <= 1'b0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+        m_ip_version_reg <= 4'd0;
+        m_ip_ihl_reg <= 4'd0;
+        m_ip_dscp_reg <= 6'd0;
+        m_ip_ecn_reg <= 2'd0;
+        m_ip_length_reg <= 16'd0;
+        m_ip_identification_reg <= 16'd0;
+        m_ip_flags_reg <= 3'd0;
+        m_ip_fragment_offset_reg <= 13'd0;
+        m_ip_ttl_reg <= 8'd0;
+        m_ip_protocol_reg <= 8'd0;
+        m_ip_header_checksum_reg <= 16'd0;
+        m_ip_source_ip_reg <= 32'd0;
+        m_ip_dest_ip_reg <= 32'd0;
+        m_udp_source_port_reg <= 16'd0;
+        m_udp_dest_port_reg <= 16'd0;
+        m_udp_length_reg <= 16'd0;
+        m_udp_checksum_reg <= 16'd0;
+        busy_reg <= 1'b0;
+        error_header_early_termination_reg <= 1'b0;
+        error_payload_early_termination_reg <= 1'b0;
+        hdr_ptr_reg <= 3'd0;
+        word_count_reg <= 16'd0;
+    end else begin
+        state_reg <= state_next;
+
+
+        s_ip_hdr_ready_reg <= s_ip_hdr_ready_next;
+        s_ip_payload_axis_tready_reg <= s_ip_payload_axis_tready_next;
+
+        m_udp_hdr_valid_reg <= m_udp_hdr_valid_next;
+
+        error_header_early_termination_reg <= error_header_early_termination_next;
+        error_payload_early_termination_reg <= error_payload_early_termination_next;
+
+        busy_reg <= state_next != STATE_IDLE;
+        
+        hdr_ptr_reg <= hdr_ptr_next;
+        word_count_reg <= word_count_next;
+
+        // datapath
+        if (store_ip_hdr) begin
+            m_eth_dest_mac_reg <= s_eth_dest_mac;
+            m_eth_src_mac_reg <= s_eth_src_mac;
+            m_eth_type_reg <= s_eth_type;
+            m_ip_version_reg <= s_ip_version;
+            m_ip_ihl_reg <= s_ip_ihl;
+            m_ip_dscp_reg <= s_ip_dscp;
+            m_ip_ecn_reg <= s_ip_ecn;
+            m_ip_length_reg <= s_ip_length;
+            m_ip_identification_reg <= s_ip_identification;
+            m_ip_flags_reg <= s_ip_flags;
+            m_ip_fragment_offset_reg <= s_ip_fragment_offset;
+            m_ip_ttl_reg <= s_ip_ttl;
+            m_ip_protocol_reg <= s_ip_protocol;
+            m_ip_header_checksum_reg <= s_ip_header_checksum;
+            m_ip_source_ip_reg <= s_ip_source_ip;
+            m_ip_dest_ip_reg <= s_ip_dest_ip;
+        end
+    
+        if (store_last_word) begin
+            last_word_data_reg <= m_udp_payload_axis_tdata_int;
+        end
+    
+        if (store_udp_source_port_0) m_udp_source_port_reg[ 7: 0] <= s_ip_payload_axis_tdata;
+        if (store_udp_source_port_1) m_udp_source_port_reg[15: 8] <= s_ip_payload_axis_tdata;
+        if (store_udp_dest_port_0) m_udp_dest_port_reg[ 7: 0] <= s_ip_payload_axis_tdata;
+        if (store_udp_dest_port_1) m_udp_dest_port_reg[15: 8] <= s_ip_payload_axis_tdata;
+        if (store_udp_length_0) m_udp_length_reg[ 7: 0] <= s_ip_payload_axis_tdata;
+        if (store_udp_length_1) m_udp_length_reg[15: 8] <= s_ip_payload_axis_tdata;
+        if (store_udp_checksum_0) m_udp_checksum_reg[ 7: 0] <= s_ip_payload_axis_tdata;
+        if (store_udp_checksum_1) m_udp_checksum_reg[15: 8] <= s_ip_payload_axis_tdata;
+    end
+end
+
+// output datapath logic
+reg [7:0] m_udp_payload_axis_tdata_reg;
+reg       m_udp_payload_axis_tvalid_reg, m_udp_payload_axis_tvalid_next;
+reg       m_udp_payload_axis_tlast_reg;
+reg       m_udp_payload_axis_tuser_reg;
+
+reg [7:0] temp_m_udp_payload_axis_tdata_reg;
+reg       temp_m_udp_payload_axis_tvalid_reg, temp_m_udp_payload_axis_tvalid_next;
+reg       temp_m_udp_payload_axis_tlast_reg;
+reg       temp_m_udp_payload_axis_tuser_reg;
+
+// datapath control
+reg store_udp_payload_int_to_output;
+reg store_udp_payload_int_to_temp;
+reg store_udp_payload_axis_temp_to_output;
+
+assign m_udp_payload_axis_tdata = m_udp_payload_axis_tdata_reg;
+assign m_udp_payload_axis_tvalid = m_udp_payload_axis_tvalid_reg;
+assign m_udp_payload_axis_tlast = m_udp_payload_axis_tlast_reg;
+assign m_udp_payload_axis_tuser = m_udp_payload_axis_tuser_reg;
+
+// 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_udp_payload_axis_tready_int_early = m_udp_payload_axis_tready || (!temp_m_udp_payload_axis_tvalid_reg && (!m_udp_payload_axis_tvalid_reg || !m_udp_payload_axis_tvalid_int));
+
+always @* begin
+    // transfer sink ready state to source
+    m_udp_payload_axis_tvalid_next = m_udp_payload_axis_tvalid_reg;
+    temp_m_udp_payload_axis_tvalid_next = temp_m_udp_payload_axis_tvalid_reg;
+
+    store_udp_payload_int_to_output = 1'b0;
+    store_udp_payload_int_to_temp = 1'b0;
+    store_udp_payload_axis_temp_to_output = 1'b0;
+    
+    if (m_udp_payload_axis_tready_int_reg) begin
+        // input is ready
+        if (m_udp_payload_axis_tready || !m_udp_payload_axis_tvalid_reg) begin
+            // output is ready or currently not valid, transfer data to output
+            m_udp_payload_axis_tvalid_next = m_udp_payload_axis_tvalid_int;
+            store_udp_payload_int_to_output = 1'b1;
+        end else begin
+            // output is not ready, store input in temp
+            temp_m_udp_payload_axis_tvalid_next = m_udp_payload_axis_tvalid_int;
+            store_udp_payload_int_to_temp = 1'b1;
+        end
+    end else if (m_udp_payload_axis_tready) begin
+        // input is not ready, but output is ready
+        m_udp_payload_axis_tvalid_next = temp_m_udp_payload_axis_tvalid_reg;
+        temp_m_udp_payload_axis_tvalid_next = 1'b0;
+        store_udp_payload_axis_temp_to_output = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        m_udp_payload_axis_tready_int_reg <= 1'b0;
+        
+        m_udp_payload_axis_tdata_reg <= 8'd0;
+        m_udp_payload_axis_tvalid_reg <= 1'b0;
+        m_udp_payload_axis_tlast_reg <= 1'b0;
+        m_udp_payload_axis_tuser_reg <= 1'b0;
+        
+        temp_m_udp_payload_axis_tdata_reg <= 8'd0;
+        temp_m_udp_payload_axis_tvalid_reg <= 1'b0;
+        temp_m_udp_payload_axis_tlast_reg <= 1'b0;
+        temp_m_udp_payload_axis_tuser_reg <= 1'b0;
+    end else begin
+        m_udp_payload_axis_tvalid_reg <= m_udp_payload_axis_tvalid_next;
+        m_udp_payload_axis_tready_int_reg <= m_udp_payload_axis_tready_int_early;
+        temp_m_udp_payload_axis_tvalid_reg <= temp_m_udp_payload_axis_tvalid_next;
+    
+        // datapath
+        if (store_udp_payload_int_to_output) begin
+            m_udp_payload_axis_tdata_reg <= m_udp_payload_axis_tdata_int;
+            m_udp_payload_axis_tlast_reg <= m_udp_payload_axis_tlast_int;
+            m_udp_payload_axis_tuser_reg <= m_udp_payload_axis_tuser_int;
+        end else if (store_udp_payload_axis_temp_to_output) begin
+            m_udp_payload_axis_tdata_reg <= temp_m_udp_payload_axis_tdata_reg;
+            m_udp_payload_axis_tlast_reg <= temp_m_udp_payload_axis_tlast_reg;
+            m_udp_payload_axis_tuser_reg <= temp_m_udp_payload_axis_tuser_reg;
+        end
+    
+        if (store_udp_payload_int_to_temp) begin
+            temp_m_udp_payload_axis_tdata_reg <= m_udp_payload_axis_tdata_int;
+            temp_m_udp_payload_axis_tlast_reg <= m_udp_payload_axis_tlast_int;
+            temp_m_udp_payload_axis_tuser_reg <= m_udp_payload_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+
diff --git a/verilog/rtl/udp_ip_tx.v b/verilog/rtl/udp_ip_tx.v
new file mode 100644
index 0000000..48b9328
--- /dev/null
+++ b/verilog/rtl/udp_ip_tx.v
@@ -0,0 +1,525 @@
+/*
+
+Copyright (c) 2014-2018 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
+
+`timescale 1ns / 1ps
+
+/*
+ * UDP ethernet frame transmitter (UDP frame in, IP frame out)
+ */
+module udp_ip_tx
+(
+    input  wire        clk,
+    input  wire        rst,
+
+    /*
+     * UDP frame input
+     */
+    input  wire        s_udp_hdr_valid,
+    output wire        s_udp_hdr_ready,
+    input  wire [47:0] s_eth_dest_mac,
+    input  wire [47:0] s_eth_src_mac,
+    input  wire [15:0] s_eth_type,
+    input  wire [3:0]  s_ip_version,
+    input  wire [3:0]  s_ip_ihl,
+    input  wire [5:0]  s_ip_dscp,
+    input  wire [1:0]  s_ip_ecn,
+    input  wire [15:0] s_ip_identification,
+    input  wire [2:0]  s_ip_flags,
+    input  wire [12:0] s_ip_fragment_offset,
+    input  wire [7:0]  s_ip_ttl,
+    input  wire [7:0]  s_ip_protocol,
+    input  wire [15:0] s_ip_header_checksum,
+    input  wire [31:0] s_ip_source_ip,
+    input  wire [31:0] s_ip_dest_ip,
+    input  wire [15:0] s_udp_source_port,
+    input  wire [15:0] s_udp_dest_port,
+    input  wire [15:0] s_udp_length,
+    input  wire [15:0] s_udp_checksum,
+    input  wire [7:0]  s_udp_payload_axis_tdata,
+    input  wire        s_udp_payload_axis_tvalid,
+    output wire        s_udp_payload_axis_tready,
+    input  wire        s_udp_payload_axis_tlast,
+    input  wire        s_udp_payload_axis_tuser,
+
+    /*
+     * IP frame output
+     */
+    output wire        m_ip_hdr_valid,
+    input  wire        m_ip_hdr_ready,
+    output wire [47:0] m_eth_dest_mac,
+    output wire [47:0] m_eth_src_mac,
+    output wire [15:0] m_eth_type,
+    output wire [3:0]  m_ip_version,
+    output wire [3:0]  m_ip_ihl,
+    output wire [5:0]  m_ip_dscp,
+    output wire [1:0]  m_ip_ecn,
+    output wire [15:0] m_ip_length,
+    output wire [15:0] m_ip_identification,
+    output wire [2:0]  m_ip_flags,
+    output wire [12:0] m_ip_fragment_offset,
+    output wire [7:0]  m_ip_ttl,
+    output wire [7:0]  m_ip_protocol,
+    output wire [15:0] m_ip_header_checksum,
+    output wire [31:0] m_ip_source_ip,
+    output wire [31:0] m_ip_dest_ip,
+    output wire [7:0]  m_ip_payload_axis_tdata,
+    output wire        m_ip_payload_axis_tvalid,
+    input  wire        m_ip_payload_axis_tready,
+    output wire        m_ip_payload_axis_tlast,
+    output wire        m_ip_payload_axis_tuser,
+
+    /*
+     * Status signals
+     */
+    output wire        busy,
+    output wire        error_payload_early_termination
+);
+
+/*
+
+UDP Frame
+
+ Field                       Length
+ Destination MAC address     6 octets
+ Source MAC address          6 octets
+ Ethertype (0x0800)          2 octets
+ Version (4)                 4 bits
+ IHL (5-15)                  4 bits
+ DSCP (0)                    6 bits
+ ECN (0)                     2 bits
+ length                      2 octets
+ identification (0?)         2 octets
+ flags (010)                 3 bits
+ fragment offset (0)         13 bits
+ time to live (64?)          1 octet
+ protocol                    1 octet
+ header checksum             2 octets
+ source IP                   4 octets
+ destination IP              4 octets
+ options                     (IHL-5)*4 octets
+
+ source port                 2 octets
+ desination port             2 octets
+ length                      2 octets
+ checksum                    2 octets
+
+ payload                     length octets
+
+This module receives a UDP frame with header fields in parallel along with the
+payload in an AXI stream, combines the header with the payload, passes through
+the IP headers, and transmits the complete IP payload on an AXI interface.
+
+*/
+
+localparam [2:0]
+    STATE_IDLE = 3'd0,
+    STATE_WRITE_HEADER = 3'd1,
+    STATE_WRITE_PAYLOAD = 3'd2,
+    STATE_WRITE_PAYLOAD_LAST = 3'd3,
+    STATE_WAIT_LAST = 3'd4;
+
+reg [2:0] state_reg, state_next;
+
+// datapath control signals
+reg store_udp_hdr;
+reg store_last_word;
+
+reg [2:0] hdr_ptr_reg, hdr_ptr_next;
+reg [15:0] word_count_reg, word_count_next;
+
+reg [7:0] last_word_data_reg;
+
+reg [15:0] udp_source_port_reg;
+reg [15:0] udp_dest_port_reg;
+reg [15:0] udp_length_reg;
+reg [15:0] udp_checksum_reg;
+
+reg s_udp_hdr_ready_reg, s_udp_hdr_ready_next;
+reg s_udp_payload_axis_tready_reg, s_udp_payload_axis_tready_next;
+
+reg m_ip_hdr_valid_reg, m_ip_hdr_valid_next;
+reg [47:0] m_eth_dest_mac_reg;
+reg [47:0] m_eth_src_mac_reg;
+reg [15:0] m_eth_type_reg;
+reg [3:0] m_ip_version_reg;
+reg [3:0] m_ip_ihl_reg;
+reg [5:0] m_ip_dscp_reg;
+reg [1:0] m_ip_ecn_reg;
+reg [15:0] m_ip_length_reg;
+reg [15:0] m_ip_identification_reg;
+reg [2:0] m_ip_flags_reg;
+reg [12:0] m_ip_fragment_offset_reg;
+reg [7:0] m_ip_ttl_reg;
+reg [7:0] m_ip_protocol_reg;
+reg [15:0] m_ip_header_checksum_reg;
+reg [31:0] m_ip_source_ip_reg;
+reg [31:0] m_ip_dest_ip_reg;
+
+reg busy_reg;
+reg error_payload_early_termination_reg, error_payload_early_termination_next;
+
+// internal datapath
+reg [7:0] m_ip_payload_axis_tdata_int;
+reg       m_ip_payload_axis_tvalid_int;
+reg       m_ip_payload_axis_tready_int_reg;
+reg       m_ip_payload_axis_tlast_int;
+reg       m_ip_payload_axis_tuser_int;
+wire      m_ip_payload_axis_tready_int_early;
+
+assign s_udp_hdr_ready = s_udp_hdr_ready_reg;
+assign s_udp_payload_axis_tready = s_udp_payload_axis_tready_reg;
+
+assign m_ip_hdr_valid = m_ip_hdr_valid_reg;
+assign m_eth_dest_mac = m_eth_dest_mac_reg;
+assign m_eth_src_mac = m_eth_src_mac_reg;
+assign m_eth_type = m_eth_type_reg;
+assign m_ip_version = m_ip_version_reg;
+assign m_ip_ihl = m_ip_ihl_reg;
+assign m_ip_dscp = m_ip_dscp_reg;
+assign m_ip_ecn = m_ip_ecn_reg;
+assign m_ip_length = m_ip_length_reg;
+assign m_ip_identification = m_ip_identification_reg;
+assign m_ip_flags = m_ip_flags_reg;
+assign m_ip_fragment_offset = m_ip_fragment_offset_reg;
+assign m_ip_ttl = m_ip_ttl_reg;
+assign m_ip_protocol = m_ip_protocol_reg;
+assign m_ip_header_checksum = m_ip_header_checksum_reg;
+assign m_ip_source_ip = m_ip_source_ip_reg;
+assign m_ip_dest_ip = m_ip_dest_ip_reg;
+
+assign busy = busy_reg;
+assign error_payload_early_termination = error_payload_early_termination_reg;
+
+always @* begin
+    state_next = STATE_IDLE;
+
+    s_udp_hdr_ready_next = 1'b0;
+    s_udp_payload_axis_tready_next = 1'b0;
+
+    store_udp_hdr = 1'b0;
+
+    store_last_word = 1'b0;
+
+    hdr_ptr_next = hdr_ptr_reg;
+    word_count_next = word_count_reg;
+
+    m_ip_hdr_valid_next = m_ip_hdr_valid_reg && !m_ip_hdr_ready;
+
+    error_payload_early_termination_next = 1'b0;
+
+    m_ip_payload_axis_tdata_int = 8'd0;
+    m_ip_payload_axis_tvalid_int = 1'b0;
+    m_ip_payload_axis_tlast_int = 1'b0;
+    m_ip_payload_axis_tuser_int = 1'b0;
+
+    case (state_reg)
+        STATE_IDLE: begin
+            // idle state - wait for data
+            hdr_ptr_next = 3'd0;
+            s_udp_hdr_ready_next = !m_ip_hdr_valid_next;
+
+            if (s_udp_hdr_ready && s_udp_hdr_valid) begin
+                store_udp_hdr = 1'b1;
+                s_udp_hdr_ready_next = 1'b0;
+                m_ip_hdr_valid_next = 1'b1;
+                if (m_ip_payload_axis_tready_int_reg) begin
+                    m_ip_payload_axis_tvalid_int = 1'b1;
+                    m_ip_payload_axis_tdata_int = s_udp_source_port[15: 8];
+                    hdr_ptr_next = 3'd1;
+                end
+                state_next = STATE_WRITE_HEADER;
+            end else begin
+                state_next = STATE_IDLE;
+            end
+        end
+        STATE_WRITE_HEADER: begin
+            // write header state
+            word_count_next = udp_length_reg - 16'd8;
+
+            if (m_ip_payload_axis_tready_int_reg) begin
+                // word transfer out
+                hdr_ptr_next = hdr_ptr_reg + 3'd1;
+                m_ip_payload_axis_tvalid_int = 1'b1;
+                state_next = STATE_WRITE_HEADER;
+                case (hdr_ptr_reg)
+                    3'h0: m_ip_payload_axis_tdata_int = udp_source_port_reg[15: 8];
+                    3'h1: m_ip_payload_axis_tdata_int = udp_source_port_reg[ 7: 0];
+                    3'h2: m_ip_payload_axis_tdata_int = udp_dest_port_reg[15: 8];
+                    3'h3: m_ip_payload_axis_tdata_int = udp_dest_port_reg[ 7: 0];
+                    3'h4: m_ip_payload_axis_tdata_int = udp_length_reg[15: 8];
+                    3'h5: m_ip_payload_axis_tdata_int = udp_length_reg[ 7: 0];
+                    3'h6: m_ip_payload_axis_tdata_int = udp_checksum_reg[15: 8];
+                    3'h7: begin
+                        m_ip_payload_axis_tdata_int = udp_checksum_reg[ 7: 0];
+                        s_udp_payload_axis_tready_next = m_ip_payload_axis_tready_int_early;
+                        state_next = STATE_WRITE_PAYLOAD;
+                    end
+                endcase
+            end else begin
+                state_next = STATE_WRITE_HEADER;
+            end
+        end
+        STATE_WRITE_PAYLOAD: begin
+            // write payload
+            s_udp_payload_axis_tready_next = m_ip_payload_axis_tready_int_early;
+
+            m_ip_payload_axis_tdata_int = s_udp_payload_axis_tdata;
+            m_ip_payload_axis_tvalid_int = s_udp_payload_axis_tvalid;
+            m_ip_payload_axis_tlast_int = s_udp_payload_axis_tlast;
+            m_ip_payload_axis_tuser_int = s_udp_payload_axis_tuser;
+
+            if (s_udp_payload_axis_tready && s_udp_payload_axis_tvalid) begin
+                // word transfer through
+                word_count_next = word_count_reg - 16'd1;
+                if (s_udp_payload_axis_tlast) begin
+                    if (word_count_reg != 16'd1) begin
+                        // end of frame, but length does not match
+                        m_ip_payload_axis_tuser_int = 1'b1;
+                        error_payload_early_termination_next = 1'b1;
+                    end
+                    s_udp_hdr_ready_next = !m_ip_hdr_valid_next;
+                    s_udp_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    if (word_count_reg == 16'd1) begin
+                        store_last_word = 1'b1;
+                        m_ip_payload_axis_tvalid_int = 1'b0;
+                        state_next = STATE_WRITE_PAYLOAD_LAST;
+                    end else begin
+                        state_next = STATE_WRITE_PAYLOAD;
+                    end
+                end
+            end else begin
+                state_next = STATE_WRITE_PAYLOAD;
+            end
+        end
+        STATE_WRITE_PAYLOAD_LAST: begin
+            // read and discard until end of frame
+            s_udp_payload_axis_tready_next = m_ip_payload_axis_tready_int_early;
+
+            m_ip_payload_axis_tdata_int = last_word_data_reg;
+            m_ip_payload_axis_tvalid_int = s_udp_payload_axis_tvalid && s_udp_payload_axis_tlast;
+            m_ip_payload_axis_tlast_int = s_udp_payload_axis_tlast;
+            m_ip_payload_axis_tuser_int = s_udp_payload_axis_tuser;
+
+            if (s_udp_payload_axis_tready && s_udp_payload_axis_tvalid) begin
+                if (s_udp_payload_axis_tlast) begin
+                    s_udp_hdr_ready_next = !m_ip_hdr_valid_next;
+                    s_udp_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_WRITE_PAYLOAD_LAST;
+                end
+            end else begin
+                state_next = STATE_WRITE_PAYLOAD_LAST;
+            end
+        end
+        STATE_WAIT_LAST: begin
+            // wait for end of frame; read and discard
+            s_udp_payload_axis_tready_next = 1'b1;
+
+            if (s_udp_payload_axis_tvalid) begin
+                if (s_udp_payload_axis_tlast) begin
+                    s_udp_hdr_ready_next = !m_ip_hdr_valid_next;
+                    s_udp_payload_axis_tready_next = 1'b0;
+                    state_next = STATE_IDLE;
+                end else begin
+                    state_next = STATE_WAIT_LAST;
+                end
+            end else begin
+                state_next = STATE_WAIT_LAST;
+            end
+        end
+    endcase
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        state_reg <= STATE_IDLE;
+        s_udp_hdr_ready_reg <= 1'b0;
+        s_udp_payload_axis_tready_reg <= 1'b0;
+        m_ip_hdr_valid_reg <= 1'b0;
+        m_eth_dest_mac_reg <= 48'd0;
+        m_eth_src_mac_reg <= 48'd0;
+        m_eth_type_reg <= 16'd0;
+        m_ip_version_reg <= 4'd0;
+        m_ip_ihl_reg <= 4'd0;
+        m_ip_dscp_reg <= 6'd0;
+        m_ip_ecn_reg <= 2'd0;
+        m_ip_length_reg <= 16'd0;
+        m_ip_identification_reg <= 16'd0;
+        m_ip_flags_reg <= 3'd0;
+        m_ip_fragment_offset_reg <= 13'd0;
+        m_ip_ttl_reg <= 8'd0;
+        m_ip_protocol_reg <= 8'd0;
+        m_ip_header_checksum_reg <= 16'd0;
+        m_ip_source_ip_reg <= 32'd0;
+        m_ip_dest_ip_reg <= 32'd0;
+        busy_reg <= 1'b0;
+        error_payload_early_termination_reg <= 1'b0;
+        hdr_ptr_reg <= 3'd0;
+        word_count_reg <= 16'd0;
+        last_word_data_reg <= 8'd0;
+        udp_source_port_reg <= 16'd0;
+        udp_dest_port_reg <= 16'd0;
+        udp_length_reg <= 16'd0;
+        udp_checksum_reg <= 16'd0;
+    end else begin
+        state_reg <= state_next;
+
+        s_udp_hdr_ready_reg <= s_udp_hdr_ready_next;
+        s_udp_payload_axis_tready_reg <= s_udp_payload_axis_tready_next;
+
+        m_ip_hdr_valid_reg <= m_ip_hdr_valid_next;
+
+        busy_reg <= state_next != STATE_IDLE;
+
+        error_payload_early_termination_reg <= error_payload_early_termination_next;
+
+        hdr_ptr_reg <= hdr_ptr_next;
+        word_count_reg <= word_count_next;
+    
+        // datapath
+        if (store_udp_hdr) begin
+            m_eth_dest_mac_reg <= s_eth_dest_mac;
+            m_eth_src_mac_reg <= s_eth_src_mac;
+            m_eth_type_reg <= s_eth_type;
+            m_ip_version_reg <= s_ip_version;
+            m_ip_ihl_reg <= s_ip_ihl;
+            m_ip_dscp_reg <= s_ip_dscp;
+            m_ip_ecn_reg <= s_ip_ecn;
+            m_ip_length_reg <= s_udp_length + 20;
+            m_ip_identification_reg <= s_ip_identification;
+            m_ip_flags_reg <= s_ip_flags;
+            m_ip_fragment_offset_reg <= s_ip_fragment_offset;
+            m_ip_ttl_reg <= s_ip_ttl;
+            m_ip_protocol_reg <= s_ip_protocol;
+            m_ip_header_checksum_reg <= s_ip_header_checksum;
+            m_ip_source_ip_reg <= s_ip_source_ip;
+            m_ip_dest_ip_reg <= s_ip_dest_ip;
+            udp_source_port_reg <= s_udp_source_port;
+            udp_dest_port_reg <= s_udp_dest_port;
+            udp_length_reg <= s_udp_length;
+            udp_checksum_reg <= s_udp_checksum;
+        end
+    
+        if (store_last_word) begin
+            last_word_data_reg <= m_ip_payload_axis_tdata_int;
+        end
+    end
+end
+
+// output datapath logic
+reg [7:0] m_ip_payload_axis_tdata_reg;
+reg       m_ip_payload_axis_tvalid_reg, m_ip_payload_axis_tvalid_next;
+reg       m_ip_payload_axis_tlast_reg;
+reg       m_ip_payload_axis_tuser_reg;
+
+reg [7:0] temp_m_ip_payload_axis_tdata_reg;
+reg       temp_m_ip_payload_axis_tvalid_reg, temp_m_ip_payload_axis_tvalid_next;
+reg       temp_m_ip_payload_axis_tlast_reg;
+reg       temp_m_ip_payload_axis_tuser_reg;
+
+// datapath control
+reg store_ip_payload_int_to_output;
+reg store_ip_payload_int_to_temp;
+reg store_ip_payload_axis_temp_to_output;
+
+assign m_ip_payload_axis_tdata = m_ip_payload_axis_tdata_reg;
+assign m_ip_payload_axis_tvalid = m_ip_payload_axis_tvalid_reg;
+assign m_ip_payload_axis_tlast = m_ip_payload_axis_tlast_reg;
+assign m_ip_payload_axis_tuser = m_ip_payload_axis_tuser_reg;
+
+// 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_ip_payload_axis_tready_int_early = m_ip_payload_axis_tready || (!temp_m_ip_payload_axis_tvalid_reg && (!m_ip_payload_axis_tvalid_reg || !m_ip_payload_axis_tvalid_int));
+
+always @* begin
+    // transfer sink ready state to source
+    m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_reg;
+    temp_m_ip_payload_axis_tvalid_next = temp_m_ip_payload_axis_tvalid_reg;
+
+    store_ip_payload_int_to_output = 1'b0;
+    store_ip_payload_int_to_temp = 1'b0;
+    store_ip_payload_axis_temp_to_output = 1'b0;
+    
+    if (m_ip_payload_axis_tready_int_reg) begin
+        // input is ready
+        if (m_ip_payload_axis_tready || !m_ip_payload_axis_tvalid_reg) begin
+            // output is ready or currently not valid, transfer data to output
+            m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_int;
+            store_ip_payload_int_to_output = 1'b1;
+        end else begin
+            // output is not ready, store input in temp
+            temp_m_ip_payload_axis_tvalid_next = m_ip_payload_axis_tvalid_int;
+            store_ip_payload_int_to_temp = 1'b1;
+        end
+    end else if (m_ip_payload_axis_tready) begin
+        // input is not ready, but output is ready
+        m_ip_payload_axis_tvalid_next = temp_m_ip_payload_axis_tvalid_reg;
+        temp_m_ip_payload_axis_tvalid_next = 1'b0;
+        store_ip_payload_axis_temp_to_output = 1'b1;
+    end
+end
+
+always @(posedge clk) begin
+    if (rst) begin
+        m_ip_payload_axis_tready_int_reg <= 1'b0;
+        
+        m_ip_payload_axis_tdata_reg <= 8'd0;
+        m_ip_payload_axis_tvalid_reg <= 1'b0;
+        m_ip_payload_axis_tlast_reg <= 1'b0;
+        m_ip_payload_axis_tuser_reg <= 1'b0;
+        
+        temp_m_ip_payload_axis_tdata_reg <= 8'd0;
+        temp_m_ip_payload_axis_tvalid_reg <= 1'b0;
+        temp_m_ip_payload_axis_tlast_reg <= 1'b0;
+        temp_m_ip_payload_axis_tuser_reg <= 1'b0;
+    end else begin
+        m_ip_payload_axis_tvalid_reg <= m_ip_payload_axis_tvalid_next;
+        m_ip_payload_axis_tready_int_reg <= m_ip_payload_axis_tready_int_early;
+        temp_m_ip_payload_axis_tvalid_reg <= temp_m_ip_payload_axis_tvalid_next;
+
+        // datapath
+        if (store_ip_payload_int_to_output) begin
+            m_ip_payload_axis_tdata_reg <= m_ip_payload_axis_tdata_int;
+            m_ip_payload_axis_tlast_reg <= m_ip_payload_axis_tlast_int;
+            m_ip_payload_axis_tuser_reg <= m_ip_payload_axis_tuser_int;
+        end else if (store_ip_payload_axis_temp_to_output) begin
+            m_ip_payload_axis_tdata_reg <= temp_m_ip_payload_axis_tdata_reg;
+            m_ip_payload_axis_tlast_reg <= temp_m_ip_payload_axis_tlast_reg;
+            m_ip_payload_axis_tuser_reg <= temp_m_ip_payload_axis_tuser_reg;
+        end
+    
+        if (store_ip_payload_int_to_temp) begin
+            temp_m_ip_payload_axis_tdata_reg <= m_ip_payload_axis_tdata_int;
+            temp_m_ip_payload_axis_tlast_reg <= m_ip_payload_axis_tlast_int;
+            temp_m_ip_payload_axis_tuser_reg <= m_ip_payload_axis_tuser_int;
+        end
+    end
+end
+
+endmodule
+