diff --git a/libraries/io/eth/hdllib.cfg b/libraries/io/eth/hdllib.cfg
index 11d97855c2d2c66fc69d0db9120c34e4d56095d3..9a89f41a6d3a32cba4dce40a87163adc436e6a16 100644
--- a/libraries/io/eth/hdllib.cfg
+++ b/libraries/io/eth/hdllib.cfg
@@ -22,6 +22,7 @@ synth_files =
     src/vhdl/eth_control.vhd
     src/vhdl/eth_ihl_to_20.vhd
     src/vhdl/eth.vhd
+    src/vhdl/eth_stream.vhd
     src/vhdl/eth_tester_pkg.vhd
     src/vhdl/eth_tester_tx.vhd
     src/vhdl/eth_tester_rx.vhd
@@ -35,8 +36,10 @@ test_bench_files =
     tb/vhdl/tb_eth.vhd
     tb/vhdl/tb_eth_tester_pkg.vhd
     tb/vhdl/tb_eth_tester.vhd
+    tb/vhdl/tb_eth_stream.vhd
     tb/vhdl/tb_tb_eth.vhd
     tb/vhdl/tb_tb_eth_tester.vhd
+    tb/vhdl/tb_tb_eth_stream.vhd
     tb/vhdl/tb_eth_udp_offload.vhd
     tb/vhdl/tb_eth_ihl_to_20.vhd
     tb/vhdl/tb_tb_tb_eth_regression.vhd
@@ -49,6 +52,7 @@ regression_test_vhdl =
     tb/vhdl/tb_eth_ihl_to_20.vhd
     tb/vhdl/tb_tb_eth.vhd
     tb/vhdl/tb_tb_eth_tester.vhd
+    tb/vhdl/tb_tb_eth_stream.vhd
 
 
 [modelsim_project_file]
diff --git a/libraries/io/eth/src/vhdl/eth_stream.vhd b/libraries/io/eth/src/vhdl/eth_stream.vhd
new file mode 100644
index 0000000000000000000000000000000000000000..8f3921ac2eb1823b5bcaaf4e368313fe44f2c5ed
--- /dev/null
+++ b/libraries/io/eth/src/vhdl/eth_stream.vhd
@@ -0,0 +1,185 @@
+-------------------------------------------------------------------------------
+--
+-- Copyright (C) 2022
+-- ASTRON (Netherlands Institute for Radio Astronomy) <http://www.astron.nl/>
+-- JIVE (Joint Institute for VLBI in Europe) <http://www.jive.nl/>
+-- P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
+--
+-------------------------------------------------------------------------------
+
+-- Author: Eric Kooistra
+-- Purpose:
+--   Provide Ethernet access to a node for one UDP stream.
+-- Description:
+-- * This eth_stream.vhd is a stripped down version of eth.vhd.
+-- * This eth_stream only contains the components that are needed to send or
+--   receive an UDP stream via 1GbE.
+--   The IP checksum is filled in for Tx and checked or Rx.
+--   The Tx only contains UDP stream data, so no need for a dp_mux.
+--   The Rx may contain other packet types, because the 1GbE connects to a
+--   network. All Rx packets that are not UDP for g_rx_udp_port are discarded.
+-- References:
+-- [1] https://support.astron.nl/confluence/display/L2M/L6+FWLIB+Design+Document%3A+ETH+tester+unit+for+1GbE
+
+LIBRARY IEEE, common_lib, dp_lib;
+USE IEEE.std_logic_1164.ALL;
+USE common_lib.common_pkg.ALL;
+USE dp_lib.dp_stream_pkg.ALL;
+USE work.eth_pkg.ALL;
+
+ENTITY eth_stream IS
+  GENERIC (
+    g_rx_udp_port : NATURAL
+  );
+  PORT (
+    -- Clocks and reset
+    st_rst        : IN  STD_LOGIC;
+    st_clk        : IN  STD_LOGIC;
+    
+    -- User UDP interface
+    -- . Tx
+    udp_tx_sosi   : IN  t_dp_sosi;
+    udp_tx_siso   : OUT t_dp_siso;
+    -- . Rx
+    udp_rx_sosi   : OUT t_dp_sosi;
+    udp_rx_siso   : IN  t_dp_siso := c_dp_siso_rdy;
+
+    -- PHY interface
+    -- . Tx
+    tse_tx_sosi   : OUT t_dp_sosi;
+    tse_tx_siso   : IN  t_dp_siso;
+    -- . Rx
+    tse_rx_sosi   : IN  t_dp_sosi;
+    tse_rx_siso   : OUT t_dp_siso
+  );
+END eth_stream;
+
+
+ARCHITECTURE str OF eth_stream IS
+
+  -- ETH Tx
+  SIGNAL eth_tx_siso            : t_dp_siso;
+  SIGNAL eth_tx_sosi            : t_dp_sosi;
+
+  -- ETH Rx
+  SIGNAL rx_adapt_siso          : t_dp_siso;
+  SIGNAL rx_adapt_sosi          : t_dp_sosi;
+  
+  SIGNAL rx_hdr_status          : t_eth_hdr_status;
+  SIGNAL rx_hdr_status_complete : STD_LOGIC;
+
+  SIGNAL rx_eth_discard         : STD_LOGIC;
+  SIGNAL rx_eth_discard_val     : STD_LOGIC;
+
+BEGIN
+
+  ------------------------------------------------------------------------------
+  -- TX
+  ------------------------------------------------------------------------------
+
+  -- Insert IP header checksum
+  u_tx_ip : ENTITY work.eth_hdr
+  GENERIC MAP (
+    g_header_store_and_forward     => TRUE,
+    g_ip_header_checksum_calculate => TRUE
+  )
+  PORT MAP (
+    -- Clocks and reset
+    rst             => st_rst,
+    clk             => st_clk,
+
+    -- Streaming Sink
+    snk_in          => udp_tx_sosi,
+    snk_out         => udp_tx_siso,
+
+    -- Streaming Source
+    src_in          => tse_tx_siso,
+    src_out         => tse_tx_sosi   -- with err field value 0 for OK
+  );
+
+  ------------------------------------------------------------------------------
+  -- RX
+  ------------------------------------------------------------------------------
+
+  -- Adapt the TSE RX source ready latency from 2 to 1
+  u_adapt : ENTITY dp_lib.dp_latency_adapter
+  GENERIC MAP (
+    g_in_latency  => c_eth_rx_ready_latency,  -- = 2
+    g_out_latency => c_eth_ready_latency      -- = 1
+  )
+  PORT MAP (
+    rst     => st_rst,
+    clk     => st_clk,
+    -- ST sink
+    snk_out => tse_rx_siso,
+    snk_in  => tse_rx_sosi,
+    -- ST source
+    src_in  => rx_adapt_siso,
+    src_out => rx_adapt_sosi
+  );
+  
+  -- Pass on UDP stream for g_rx_udp_port
+  -- . Verify IP header checksum for IP
+  u_rx_udp : ENTITY work.eth_hdr
+  GENERIC MAP (
+    g_header_store_and_forward     => TRUE,
+    g_ip_header_checksum_calculate => TRUE
+  )
+  PORT MAP (
+    -- Clocks and reset
+    rst             => st_rst,
+    clk             => st_clk,
+    
+    -- Streaming Sink
+    snk_in          => rx_adapt_sosi,
+    snk_out         => rx_adapt_siso,
+    
+    -- Streaming Source
+    src_in          => udp_rx_siso,
+    src_out         => udp_rx_sosi,
+
+    -- Frame control
+    frm_discard     => rx_eth_discard,
+    frm_discard_val => rx_eth_discard_val,
+    
+    -- Header info
+    hdr_status          => rx_hdr_status,
+    hdr_status_complete => rx_hdr_status_complete
+  );
+
+  -- Discard all Rx data that is not UDP for g_rx_udp_port
+  p_rx_discard : PROCESS(st_rst, st_clk)
+  BEGIN
+    IF st_rst = '1' THEN
+      rx_eth_discard <= '1';  -- default discard
+      rx_eth_discard_val <= '0';
+    ELSIF rising_edge(st_clk) THEN
+      -- Default keep rx_eth_discard status (instead of '1'), to more clearly
+      -- see when a change occurs
+      IF rx_hdr_status_complete = '1' THEN
+        rx_eth_discard <= '1';  -- default discard
+        IF rx_hdr_status.is_ip = '1' AND
+           rx_hdr_status.is_udp = '1' AND
+           TO_UINT(rx_hdr_status.udp_port) = g_rx_udp_port THEN
+          rx_eth_discard <= '0';  -- pass on IP/UDP stream for g_rx_udp_port
+        END IF;
+      END IF;
+
+      rx_eth_discard_val <= rx_hdr_status_complete;
+    END IF;
+  END PROCESS;
+
+END str;
diff --git a/libraries/io/eth/tb/vhdl/tb_eth_stream.vhd b/libraries/io/eth/tb/vhdl/tb_eth_stream.vhd
new file mode 100644
index 0000000000000000000000000000000000000000..0907bc075707e40f7193ac67858283e86ac0f6b3
--- /dev/null
+++ b/libraries/io/eth/tb/vhdl/tb_eth_stream.vhd
@@ -0,0 +1,436 @@
+-------------------------------------------------------------------------------
+--
+-- Copyright 2022
+-- ASTRON (Netherlands Institute for Radio Astronomy) <http://www.astron.nl/>
+-- P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+-------------------------------------------------------------------------------
+-- AUthor: E. Kooistra
+-- Purpose: Test bench for eth_stream using eth_tester
+-- Description:
+--   Similar as tb_eth_tester.vhd, but for only one stream and using streaming
+--   interface loop back.
+--
+-- Usage:
+-- > as 8
+-- > run -a
+--
+-- References:
+-- [1] https://support.astron.nl/confluence/display/L2M/L6+FWLIB+Design+Document%3A+ETH+tester+unit+for+1GbE
+
+LIBRARY IEEE, common_lib, dp_lib, diag_lib;
+USE IEEE.std_logic_1164.ALL;
+USE IEEE.numeric_std.ALL;
+USE common_lib.common_pkg.ALL;
+USE common_lib.common_mem_pkg.ALL;
+USE common_lib.common_str_pkg.ALL;
+USE common_lib.tb_common_pkg.ALL;
+USE common_lib.tb_common_mem_pkg.ALL;
+USE common_lib.common_network_layers_pkg.ALL;
+USE dp_lib.dp_stream_pkg.ALL;
+USE diag_lib.diag_pkg.ALL;
+USE work.eth_pkg.ALL;
+USE work.eth_tester_pkg.ALL;
+USE work.tb_eth_tester_pkg.ALL;
+
+ENTITY tb_eth_stream IS
+  GENERIC (
+    g_tb_index         : NATURAL := 0;  -- use to incremental delay logging from tb instances in tb_tb
+    g_nof_sync         : NATURAL := 2;  -- number of BG sync intervals to set c_run_time
+    g_udp_port_match   : BOOLEAN := TRUE;
+
+    -- t_diag_block_gen_integer =
+    --   sl:  enable
+    --   sl:  enable_sync
+    --   nat: samples_per_packet
+    --   nat: blocks_per_sync
+    --   nat: gapsize
+    --   nat: mem_low_adrs
+    --   nat: mem_high_adrs
+    --   nat: bsn_init
+    g_bg_ctrl    : t_diag_block_gen_integer := ('1', '1', 50, 3, 200, 0, c_diag_bg_mem_max_adr, 0)  -- for first stream
+  );
+END tb_eth_stream;
+
+
+ARCHITECTURE tb OF tb_eth_stream IS
+
+  CONSTANT c_tb_str                : STRING := "tb-" & NATURAL'IMAGE(g_tb_index) & " : ";  -- use to distinguish logging from tb instances in tb_tb
+  CONSTANT mm_clk_period           : TIME := 10 ns;  -- 100 MHz
+  CONSTANT c_nof_st_clk_per_s      : NATURAL := 200 * 10**6;
+  CONSTANT st_clk_period           : TIME :=  (10**9 / c_nof_st_clk_per_s) * 1 ns;  -- 5 ns, 200 MHz
+
+  CONSTANT c_bg_block_len    : NATURAL := g_bg_ctrl.samples_per_packet;
+  CONSTANT c_bg_slot_len     : NATURAL := c_bg_block_len + g_bg_ctrl.gapsize;
+  CONSTANT c_eth_packet_len  : NATURAL := func_eth_tester_eth_packet_length(c_bg_block_len);
+
+  -- Use REAL to avoid NATURAL overflow in bps calculation
+  CONSTANT c_bg_nof_bps      : REAL := REAL(c_bg_block_len * c_octet_w) * REAL(c_nof_st_clk_per_s) / REAL(c_bg_slot_len);
+  CONSTANT c_bg_sync_period  : NATURAL := c_bg_slot_len * g_bg_ctrl.blocks_per_sync;
+
+  CONSTANT c_run_time        : NATURAL := g_nof_sync * c_bg_sync_period;
+  CONSTANT c_nof_sync        : NATURAL := c_run_time / c_bg_sync_period;
+
+  -- Destination UDP port
+  CONSTANT c_rx_udp_port     : NATURAL := TO_UINT(c_eth_tester_udp_dst_port);
+  CONSTANT c_dst_udp_port    : NATURAL := sel_a_b(g_udp_port_match, c_rx_udp_port, 17);
+
+  -- Expected Tx --> Rx latency values obtained from a tb run
+  CONSTANT c_tx_exp_latency          : NATURAL := 0;
+  CONSTANT c_rx_exp_latency_en       : BOOLEAN := c_bg_block_len >= 50;
+  CONSTANT c_rx_exp_latency_st       : NATURAL := sel_a_b(g_udp_port_match, 58, 0);
+
+  CONSTANT c_nof_valid_per_packet    : NATURAL := c_bg_block_len;
+
+  CONSTANT c_total_count_nof_valid_per_sync  : NATURAL := g_bg_ctrl.blocks_per_sync * c_nof_valid_per_packet;
+
+  CONSTANT c_mon_nof_sop_tx    : NATURAL := g_bg_ctrl.blocks_per_sync;
+  CONSTANT c_mon_nof_sop_rx    : NATURAL := sel_a_b(g_udp_port_match, c_mon_nof_sop_tx, 0);
+  CONSTANT c_mon_nof_valid_tx  : NATURAL := c_mon_nof_sop_tx * ceil_div(c_bg_block_len * c_octet_w, c_word_w);
+  CONSTANT c_mon_nof_valid_rx  : NATURAL := c_mon_nof_sop_rx * c_nof_valid_per_packet;
+
+  -- Use sim default src MAC, IP, UDP port from eth_tester_pkg.vhd and based on c_gn_index
+  CONSTANT c_gn_index           : NATURAL := 17;  -- global node index
+  CONSTANT c_gn_eth_src_mac     : STD_LOGIC_VECTOR(c_network_eth_mac_addr_w-1 DOWNTO 0) := c_eth_tester_eth_src_mac_47_16 & func_eth_tester_gn_index_to_mac_15_0(c_gn_index);
+  CONSTANT c_gn_ip_src_addr     : STD_LOGIC_VECTOR(c_network_ip_addr_w-1 DOWNTO 0) := c_eth_tester_ip_src_addr_31_16 & func_eth_tester_gn_index_to_ip_15_0(c_gn_index);
+  CONSTANT c_gn_udp_src_port    : STD_LOGIC_VECTOR(c_network_udp_port_w-1 DOWNTO 0) := c_eth_tester_udp_src_port_15_8 & TO_UVEC(c_gn_index, 8);
+
+  -- Clocks and reset
+  SIGNAL mm_rst              : STD_LOGIC := '1';
+  SIGNAL mm_clk              : STD_LOGIC := '1';
+  SIGNAL st_rst              : STD_LOGIC := '1';
+  SIGNAL st_clk              : STD_LOGIC := '1';
+  SIGNAL st_pps              : STD_LOGIC := '0';
+  SIGNAL stimuli_end         : STD_LOGIC := '0';
+  SIGNAL tb_end              : STD_LOGIC := '0';
+
+  SIGNAL tx_fifo_rd_emp      : STD_LOGIC;
+
+  -- ETH UDP data path interface
+  SIGNAL tx_udp_sosi         : t_dp_sosi;
+  SIGNAL tx_udp_siso         : t_dp_siso := c_dp_siso_rdy;
+  SIGNAL rx_udp_sosi         : t_dp_sosi;
+
+  -- MM interface
+  -- . Tx
+  SIGNAL reg_bg_ctrl_copi                : t_mem_copi := c_mem_copi_rst;
+  SIGNAL reg_bg_ctrl_cipo                : t_mem_cipo;
+  SIGNAL reg_hdr_dat_copi                : t_mem_copi := c_mem_copi_rst;
+  SIGNAL reg_hdr_dat_cipo                : t_mem_cipo;
+  SIGNAL reg_bsn_monitor_v2_tx_copi      : t_mem_copi := c_mem_copi_rst;
+  SIGNAL reg_bsn_monitor_v2_tx_cipo      : t_mem_cipo;
+  SIGNAL reg_strobe_total_count_tx_copi  : t_mem_copi := c_mem_copi_rst;
+  SIGNAL reg_strobe_total_count_tx_cipo  : t_mem_cipo;
+  -- . Rx
+  SIGNAL reg_bsn_monitor_v2_rx_copi      : t_mem_copi := c_mem_copi_rst;
+  SIGNAL reg_bsn_monitor_v2_rx_cipo      : t_mem_cipo;
+  SIGNAL reg_strobe_total_count_rx_copi  : t_mem_copi := c_mem_copi_rst;
+  SIGNAL reg_strobe_total_count_rx_cipo  : t_mem_cipo;
+
+  -- . reg_strobe_total_count
+  SIGNAL tx_total_count_nof_packet       : NATURAL;
+  SIGNAL rx_total_count_nof_packet       : NATURAL;
+  SIGNAL tx_exp_total_count_nof_packet   : NATURAL;
+  SIGNAL rx_exp_total_count_nof_packet   : NATURAL;
+
+  SIGNAL rx_total_count_nof_valid        : NATURAL;
+  SIGNAL rx_exp_total_count_nof_valid    : NATURAL;
+
+  -- . reg_bsn_monitor_v2
+  SIGNAL tx_mon_nof_sop      : NATURAL;
+  SIGNAL tx_mon_nof_valid    : NATURAL;
+  SIGNAL tx_mon_latency      : NATURAL;
+  SIGNAL rx_mon_nof_sop      : NATURAL;
+  SIGNAL rx_mon_nof_valid    : NATURAL;
+  SIGNAL rx_mon_latency      : NATURAL;
+
+  -- ETH stream
+  SIGNAL tse_tx_sosi         : t_dp_sosi;
+  SIGNAL tse_tx_siso         : t_dp_siso;
+  SIGNAL tse_rx_sosi         : t_dp_sosi;
+  SIGNAL tse_rx_siso         : t_dp_siso;
+
+  -- View in Wave window
+  SIGNAL dbg_g_bg_ctrl            : t_diag_block_gen_integer := g_bg_ctrl;
+  SIGNAL dbg_c_run_time           : NATURAL := c_run_time;
+  SIGNAL dbg_c_mon_nof_sop_tx     : NATURAL := c_mon_nof_sop_tx;
+  SIGNAL dbg_c_mon_nof_sop_rx     : NATURAL := c_mon_nof_sop_rx;
+  SIGNAL dbg_c_mon_nof_valid_tx   : NATURAL := c_mon_nof_valid_tx;
+  SIGNAL dbg_c_mon_nof_valid_rx   : NATURAL := c_mon_nof_valid_rx;
+
+BEGIN
+
+  mm_clk <= (NOT mm_clk) OR tb_end AFTER mm_clk_period/2;
+  st_clk <= (NOT st_clk) OR tb_end AFTER st_clk_period/2;
+  mm_rst <= '1', '0' AFTER mm_clk_period*5;
+  st_rst <= '1', '0' AFTER st_clk_period*5;
+
+  tx_exp_total_count_nof_packet <= c_nof_sync * g_bg_ctrl.blocks_per_sync;
+  rx_exp_total_count_nof_packet <= sel_a_b(g_udp_port_match, tx_exp_total_count_nof_packet, 0);
+
+  rx_exp_total_count_nof_valid <= sel_a_b(g_udp_port_match, c_nof_sync * c_total_count_nof_valid_per_sync, 0);
+
+  -----------------------------------------------------------------------------
+  -- MM control and monitoring
+  -----------------------------------------------------------------------------
+  p_mm : PROCESS
+  BEGIN
+    tb_end <= '0';
+
+    proc_common_wait_until_low(mm_clk, mm_rst);
+    proc_common_wait_some_cycles(mm_clk, 10);
+
+    ---------------------------------------------------------------------------
+    -- Rx UDP offload port
+    ---------------------------------------------------------------------------
+    -- Set destination MAC/IP/UDP port in tx header
+    -- The MM addresses follow from byte address_offset // 4 in eth.peripheral.yaml
+    proc_mem_mm_bus_wr(16#7#, c_dst_udp_port, mm_clk, reg_hdr_dat_cipo, reg_hdr_dat_copi);
+    proc_mem_mm_bus_wr(16#10#, TO_SINT(c_eth_tester_ip_dst_addr), mm_clk, reg_hdr_dat_cipo, reg_hdr_dat_copi);  -- use signed to fit 32 b in INTEGER
+    proc_mem_mm_bus_wr(16#18#, TO_SINT(c_eth_tester_eth_dst_mac(31 DOWNTO 0)), mm_clk, reg_hdr_dat_cipo, reg_hdr_dat_copi);  -- use signed to fit 32 b in INTEGER
+    proc_mem_mm_bus_wr(16#19#, TO_UINT(c_eth_tester_eth_dst_mac(47 DOWNTO 32)), mm_clk, reg_hdr_dat_cipo, reg_hdr_dat_copi);
+
+    ---------------------------------------------------------------------------
+    -- Stimuli
+    ---------------------------------------------------------------------------
+    -- Prepare the BG
+    proc_mem_mm_bus_wr(1, g_bg_ctrl.samples_per_packet, mm_clk, reg_bg_ctrl_copi);
+    proc_mem_mm_bus_wr(2, g_bg_ctrl.blocks_per_sync,    mm_clk, reg_bg_ctrl_copi);
+    proc_mem_mm_bus_wr(3, g_bg_ctrl.gapsize,            mm_clk, reg_bg_ctrl_copi);
+    proc_mem_mm_bus_wr(4, g_bg_ctrl.mem_low_adrs,       mm_clk, reg_bg_ctrl_copi);
+    proc_mem_mm_bus_wr(5, g_bg_ctrl.mem_high_adrs,      mm_clk, reg_bg_ctrl_copi);
+    proc_mem_mm_bus_wr(6, g_bg_ctrl.bsn_init,           mm_clk, reg_bg_ctrl_copi);  -- low part
+    proc_mem_mm_bus_wr(7, 0,                            mm_clk, reg_bg_ctrl_copi);  -- high part
+    -- Enable the BG at st_pps pulse.
+    proc_mem_mm_bus_wr(0, 3, mm_clk, reg_bg_ctrl_copi);
+    proc_common_wait_some_cycles(mm_clk, 10);
+    -- Issue an st_pps pulse to start the enabled BG
+    proc_common_gen_pulse(st_clk, st_pps);
+
+    -- Run test
+    proc_common_wait_some_cycles(st_clk, c_run_time);
+
+    -- Disable the BG
+    proc_mem_mm_bus_wr(0, 0, mm_clk, reg_bg_ctrl_copi);
+
+    -- Wait until Tx FIFO has emptied for the stream
+    WHILE tx_fifo_rd_emp /= '1' LOOP
+      proc_common_wait_some_cycles(st_clk, 1);
+    END LOOP;
+    proc_common_wait_some_cycles(st_clk, c_bg_sync_period);
+    stimuli_end <= '1';
+
+    -- Delay logging between different tb instances
+    proc_common_wait_some_cycles(st_clk, g_tb_index * 100);
+
+    -- Print logging
+    print_str("");   -- log empty line between tb results
+    print_str(c_tb_str &
+        "ETH bit rate :" &
+        " c_bg_nof_bps = " & REAL'IMAGE(c_bg_nof_bps) & " bps");
+    ASSERT c_bg_nof_bps < 10.0**9 REPORT "Tx flow control will keep ETH bitrate < 1Gbps." SEVERITY NOTE;
+
+    -------------------------------------------------------------------------
+    -- Verification: Total counts
+    -------------------------------------------------------------------------
+    -- . read low part, ignore high part (= 0) of two word total counts
+    -- Tx total nof packets
+    proc_mem_mm_bus_rd(0, mm_clk, reg_strobe_total_count_tx_cipo, reg_strobe_total_count_tx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    tx_total_count_nof_packet <= TO_UINT(reg_strobe_total_count_tx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    -- Rx total nof packets
+    proc_mem_mm_bus_rd(0, mm_clk, reg_strobe_total_count_rx_cipo, reg_strobe_total_count_rx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    rx_total_count_nof_packet <= TO_UINT(reg_strobe_total_count_rx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    -- Rx total nof valids
+    proc_mem_mm_bus_rd(2, mm_clk, reg_strobe_total_count_rx_cipo, reg_strobe_total_count_rx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    rx_total_count_nof_valid <= TO_UINT(reg_strobe_total_count_rx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    proc_common_wait_some_cycles(mm_clk, 1);
+
+    -- Print logging
+    print_str(c_tb_str &
+        "Tx total counts monitor :" &
+        " nof_packet = " & NATURAL'IMAGE(tx_total_count_nof_packet));
+
+    print_str(c_tb_str &
+        "Rx total counts monitor :" &
+        " nof_packet = " & NATURAL'IMAGE(rx_total_count_nof_packet) &
+        ", nof_valid  = " & NATURAL'IMAGE(rx_total_count_nof_valid));
+
+    -- Verify, only log when wrong
+    IF c_bg_nof_bps < 10.0**9 THEN
+      ASSERT tx_total_count_nof_packet = tx_exp_total_count_nof_packet REPORT c_tb_str &
+          "Wrong Tx total nof packets count, Tx count = " & NATURAL'IMAGE(tx_total_count_nof_packet) &
+          " /= " & NATURAL'IMAGE(tx_exp_total_count_nof_packet) &
+          " = Expected count" SEVERITY ERROR;
+
+      ASSERT rx_total_count_nof_packet = rx_exp_total_count_nof_packet REPORT c_tb_str &
+          "Wrong Rx total nof packets count, Rx count = " & NATURAL'IMAGE(rx_total_count_nof_packet) &
+          " /= " & NATURAL'IMAGE(rx_exp_total_count_nof_packet) &
+          " = Expected count" SEVERITY ERROR;
+
+      ASSERT rx_total_count_nof_valid = rx_exp_total_count_nof_valid REPORT c_tb_str &
+          "Wrong Rx total nof valids count, Rx count = " & NATURAL'IMAGE(rx_total_count_nof_valid) &
+          " /= " & NATURAL'IMAGE(rx_exp_total_count_nof_valid) &
+          " = Expected count" SEVERITY ERROR;
+    END IF;
+
+    -------------------------------------------------------------------------
+    -- Verification: BSN monitors (yield same values in every sync interval)
+    -------------------------------------------------------------------------
+    -- 3 = nof_sop
+    -- 4 = nof_valid
+    -- 6 = latency
+    -- . Tx
+    proc_mem_mm_bus_rd(3, mm_clk, reg_bsn_monitor_v2_tx_cipo, reg_bsn_monitor_v2_tx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    tx_mon_nof_sop <= TO_UINT(reg_bsn_monitor_v2_tx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    proc_mem_mm_bus_rd(4, mm_clk, reg_bsn_monitor_v2_tx_cipo, reg_bsn_monitor_v2_tx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    tx_mon_nof_valid <= TO_UINT(reg_bsn_monitor_v2_tx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    proc_mem_mm_bus_rd(6, mm_clk, reg_bsn_monitor_v2_tx_cipo, reg_bsn_monitor_v2_tx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    tx_mon_latency <= TO_UINT(reg_bsn_monitor_v2_tx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    -- . Rx
+    proc_mem_mm_bus_rd(3, mm_clk, reg_bsn_monitor_v2_rx_cipo, reg_bsn_monitor_v2_rx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    rx_mon_nof_sop <= TO_UINT(reg_bsn_monitor_v2_rx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    proc_mem_mm_bus_rd(4, mm_clk, reg_bsn_monitor_v2_rx_cipo, reg_bsn_monitor_v2_rx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    rx_mon_nof_valid <= TO_UINT(reg_bsn_monitor_v2_rx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    proc_mem_mm_bus_rd(6, mm_clk, reg_bsn_monitor_v2_rx_cipo, reg_bsn_monitor_v2_rx_copi);
+    proc_mem_mm_bus_rd_latency(1, mm_clk);
+    rx_mon_latency <= TO_UINT(reg_bsn_monitor_v2_rx_cipo.rddata(c_word_w-1 DOWNTO 0));
+    proc_common_wait_some_cycles(mm_clk, 1);
+
+    -- Print logging
+    print_str(c_tb_str &
+        "Tx BSN monitor :" &
+        " nof_sop = " & NATURAL'IMAGE(tx_mon_nof_sop) &
+        ", nof_valid = " & NATURAL'IMAGE(tx_mon_nof_valid) &
+        ", latency = " & NATURAL'IMAGE(tx_mon_latency));
+
+    print_str(c_tb_str &
+        "Rx BSN monitor :" &
+        " nof_sop = " & NATURAL'IMAGE(rx_mon_nof_sop) &
+        ", nof_valid = " & NATURAL'IMAGE(rx_mon_nof_valid) &
+        ", latency = " & NATURAL'IMAGE(rx_mon_latency));
+
+    IF c_bg_nof_bps < 10.0**9 THEN
+      -- Verify BSN monitors only when the BG sync interval is stable, so
+      -- the ETH data rate < 1 Gbps and no BG block flow control.
+      -- Verify, only log when wrong
+      ASSERT tx_mon_nof_sop = c_mon_nof_sop_tx REPORT c_tb_str & "Wrong tx nof_sop" SEVERITY ERROR;
+      ASSERT rx_mon_nof_sop = c_mon_nof_sop_rx REPORT c_tb_str & "Wrong rx nof_sop" SEVERITY ERROR;
+      ASSERT tx_mon_nof_valid = c_mon_nof_valid_tx REPORT c_tb_str & "Wrong tx nof_valid" SEVERITY ERROR;
+      ASSERT rx_mon_nof_valid = c_mon_nof_valid_rx REPORT c_tb_str & "Wrong rx nof_valid" SEVERITY ERROR;
+      ASSERT tx_mon_latency = c_tx_exp_latency REPORT c_tb_str & "Wrong tx latency" SEVERITY ERROR;
+
+      -- For short block lengths the Rx latency appears to become less, the
+      -- exact Rx latency is therefore hard to predetermine. The actual
+      -- latency is not critical, therefore it is sufficient to only very
+      -- the latency when it is more or less fixed.
+      IF c_rx_exp_latency_en THEN
+        ASSERT almost_equal(rx_mon_latency, c_rx_exp_latency_st, 0) REPORT
+            c_tb_str & "Wrong rx latency using st interface" SEVERITY ERROR;
+      END IF;
+    END IF;
+
+    -------------------------------------------------------------------------
+    -- End of test
+    -------------------------------------------------------------------------
+    proc_common_wait_some_cycles(mm_clk, 100);
+    tb_end <= '1';
+    WAIT;
+  END PROCESS;
+
+  u_eth_tester : ENTITY work.eth_tester
+  GENERIC MAP (
+    g_nof_streams      => 1,
+    g_bg_sync_timeout  => c_eth_tester_sync_timeout,
+    g_remove_crc       => FALSE  -- no CRC with streaming loopback
+  )
+  PORT MAP (
+    -- Clocks and reset
+    mm_rst             => mm_rst,
+    mm_clk             => mm_clk,
+    st_rst             => st_rst,
+    st_clk             => st_clk,
+    st_pps             => st_pps,
+
+    -- UDP transmit interface
+    eth_src_mac        => c_gn_eth_src_mac,
+    ip_src_addr        => c_gn_ip_src_addr,
+    udp_src_port       => c_gn_udp_src_port,
+
+    sl(tx_fifo_rd_emp_arr) => tx_fifo_rd_emp,
+
+    TO_DP_ONE(tx_udp_sosi_arr) => tx_udp_sosi,
+    tx_udp_siso_arr    => TO_DP_ARR(tx_udp_siso),
+
+    -- UDP receive interface
+    rx_udp_sosi_arr    => TO_DP_ARR(rx_udp_sosi),
+
+    -- Memory Mapped Slaves (one per stream)
+    -- . Tx
+    reg_bg_ctrl_copi               => reg_bg_ctrl_copi,
+    reg_bg_ctrl_cipo               => reg_bg_ctrl_cipo,
+    reg_hdr_dat_copi               => reg_hdr_dat_copi,
+    reg_hdr_dat_cipo               => reg_hdr_dat_cipo,
+    reg_bsn_monitor_v2_tx_copi     => reg_bsn_monitor_v2_tx_copi,
+    reg_bsn_monitor_v2_tx_cipo     => reg_bsn_monitor_v2_tx_cipo,
+    reg_strobe_total_count_tx_copi => reg_strobe_total_count_tx_copi,
+    reg_strobe_total_count_tx_cipo => reg_strobe_total_count_tx_cipo,
+    -- . Rx
+    reg_bsn_monitor_v2_rx_copi     => reg_bsn_monitor_v2_rx_copi,
+    reg_bsn_monitor_v2_rx_cipo     => reg_bsn_monitor_v2_rx_cipo,
+    reg_strobe_total_count_rx_copi => reg_strobe_total_count_rx_copi,
+    reg_strobe_total_count_rx_cipo => reg_strobe_total_count_rx_cipo
+  );
+  
+
+  -- ETH stream
+  u_dut : ENTITY work.eth_stream
+  GENERIC MAP (
+    g_rx_udp_port => c_rx_udp_port
+  )
+  PORT MAP (
+    -- Clocks and reset
+    st_rst        => st_rst,
+    st_clk        => st_clk,
+
+    -- User UDP interface
+    -- . Tx
+    udp_tx_sosi   => tx_udp_sosi,
+    udp_tx_siso   => tx_udp_siso,
+    -- . Rx
+    udp_rx_sosi   => rx_udp_sosi,
+    udp_rx_siso   => c_dp_siso_rdy,
+
+    -- PHY interface
+    -- . Tx
+    tse_tx_sosi   => tse_tx_sosi,
+    tse_tx_siso   => tse_tx_siso,
+    -- . Rx
+    tse_rx_sosi   => tse_rx_sosi,
+    tse_rx_siso   => tse_rx_siso
+  );
+
+  -- Loopback wire Tx to Rx, register to increasy ready latency from 1 to 2
+  tse_rx_sosi <= tse_tx_sosi WHEN rising_edge(st_clk);
+  tse_tx_siso <= tse_rx_siso;
+
+END tb;
diff --git a/libraries/io/eth/tb/vhdl/tb_tb_eth_stream.vhd b/libraries/io/eth/tb/vhdl/tb_tb_eth_stream.vhd
new file mode 100644
index 0000000000000000000000000000000000000000..f5018374593c6e553cd7816dbd0d0470656d9503
--- /dev/null
+++ b/libraries/io/eth/tb/vhdl/tb_tb_eth_stream.vhd
@@ -0,0 +1,80 @@
+-------------------------------------------------------------------------------
+--
+-- Copyright 2022
+-- ASTRON (Netherlands Institute for Radio Astronomy) <http://www.astron.nl/>
+-- P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+-------------------------------------------------------------------------------
+-- Author: E. Kooistra
+-- Purpose: Multi test bench for eth_stream
+-- Description:
+--
+-- Usage:
+--   > as 8
+--   > run -all
+
+LIBRARY IEEE, diag_lib;
+USE IEEE.std_logic_1164.ALL;
+USE diag_lib.diag_pkg.ALL;
+USE work.tb_eth_tester_pkg.ALL;
+
+ENTITY tb_tb_eth_stream IS
+END tb_tb_eth_stream;
+
+ARCHITECTURE tb OF tb_tb_eth_stream IS
+
+  -- Tb
+  CONSTANT c_eth_clk_MHz   : NATURAL := 125;
+  CONSTANT c_st_clk_MHz    : NATURAL := 200;
+  CONSTANT c_nof_sync      : NATURAL := 2;
+  CONSTANT c_nof_blk       : NATURAL := 3;   -- nof_blk per sync
+
+  -- Tx packet size and gap size in octets
+  CONSTANT c_block_len     : NATURAL := 50;
+  CONSTANT c_link_len      : NATURAL := func_eth_tester_eth_packet_on_link_length(c_block_len);
+
+  -- For near maximum 1Gbps link rate the c_block_len + c_gap_len_min time
+  -- in the st_clk domain equals c_link_len time in eth_clk domain.
+  CONSTANT c_gap_len_min   : NATURAL := c_link_len * c_st_clk_MHz / c_eth_clk_MHz - c_block_len;
+
+  -- Choose c_gap_len somewhat larger to have packet link rate < 1 Gbps
+  CONSTANT c_gap_len       : NATURAL := c_gap_len_min * 2;   -- for g_nof_streams = 1
+
+  -- BG ctrl
+  CONSTANT c_high          : NATURAL := c_diag_bg_mem_max_adr;  -- = 2**24
+
+  CONSTANT c_bg_ctrl       : t_diag_block_gen_integer := ('1', '1', c_block_len,  c_nof_blk, c_gap_len, 0, c_high, 0);  -- for first stream
+
+BEGIN
+
+--  g_tb_index         : NATURAL := 0;  -- use to incremental delay logging from tb instances in tb_tb
+--  g_nof_sync         : NATURAL := 2;  -- number of BG sync intervals to set c_run_time
+--  g_udp_port_match   : BOOLEAN := TRUE;
+--
+--  -- t_diag_block_gen_integer =
+--  --   sl:  enable
+--  --   sl:  enable_sync
+--  --   nat: samples_per_packet
+--  --   nat: blocks_per_sync
+--  --   nat: gapsize
+--  --   nat: mem_low_adrs
+--  --   nat: mem_high_adrs
+--  --   nat: bsn_init
+--  g_bg_ctrl    : t_diag_block_gen_integer := ('1', '1', 50, 3, 200, 0, c_diag_bg_mem_max_adr, 0)  -- for first stream
+
+  u_udp          : ENTITY work.tb_eth_stream GENERIC MAP (0, c_nof_sync,  TRUE, c_bg_ctrl);
+  u_udp_mismatch : ENTITY work.tb_eth_stream GENERIC MAP (1, c_nof_sync, FALSE, c_bg_ctrl);
+
+END tb;