-- file Sprsaxisfwd_v1_0.vhd
-- Sprsaxisfwd_v1_0 module implementation
-- copyright: (C) 2024 MPSI Technologies GmbH
-- author: Alexander Wirthmueller
-- date created: 17 Nov 2024
-- IP header --- ABOVE

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity Sprsaxisfwd_v1_0 is
	generic (
		wSize: natural range 3 to 16 := 12;
		wIxHshk: natural range 4 to 8 := 4;
		wIxData: natural range 2 to 6 := 4;
		wData: natural range 8 to 256 := 64;
		wUser: natural range 1 to 96 := 6
	);
	port (
		resetIgrclk: in std_logic; -- in mclk domain
		igrclk: in std_logic;

		reset: in std_logic;
		mclk: in std_logic;

		igrAXIS_tready: out std_logic; -- always 1
		igrAXIS_tvalid: in std_logic;
		igrAXIS_tdata: in std_logic_vector(wData-1 downto 0);
		igrAXIS_tkeep: in std_logic_vector(wData/8-1 downto 0);
		igrAXIS_tlast: in std_logic;
		igrAXIS_tuser: in std_logic_vector(wUser-1 downto 0);

		enEgr: in std_logic;

		egrAXIS_tready: in std_logic;
		egrAXIS_tvalid: out std_logic;
		egrAXIS_tdata: out std_logic_vector(wData-1 downto 0);
		egrAXIS_tkeep: out std_logic_vector(wData/8-1 downto 0);
		egrAXIS_tlast: out std_logic;
		egrAXIS_tuser: out std_logic_vector(wUser-1 downto 0)
	);
end Sprsaxisfwd_v1_0;

architecture Rtl of Sprsaxisfwd_v1_0 is

	------------------------------------------------------------------------
	-- signal declarations
	------------------------------------------------------------------------

	constant size: natural := 2**wSize;
	constant sizeIxHshk: natural := 2**wIxHshk;
	constant sizeIxData: natural := 2**wIxData;

	---- AXI stream egress (egr)
	type stateEgr_t is (
		stateEgrInit,
		stateEgrIdle,
		stateEgrWait,
		stateEgrRun,
		stateEgrDone
	);
	signal stateEgr: stateEgr_t := stateEgrInit;

	signal egrAXIS_tvalid_sig: std_logic;
	signal egrAXIS_tdata_sig: std_logic_vector(wData-1 downto 0);
	signal egrAXIS_tkeep_sig: std_logic_vector(wData/8-1 downto 0);
	signal egrAXIS_tlast_sig: std_logic;
	signal egrAXIS_tuser_sig: std_logic_vector(wUser-1 downto 0);

	signal ixDataRd: natural range 0 to sizeIxData-1; -- next position to read from
	signal lenHshkRd: std_logic_vector(wSize-1 downto 0); -- last wIxHshk bits: next position to read from

	signal strbEgrDone: std_logic;

	---- AXI stream ingress (igr)
	type stateIgr_t is (
		stateIgrInit,
		stateIgrIdle,
		stateIgrRun,
		stateIgrDone
	);
	signal stateIgr: stateIgr_t := stateIgrInit;

	type dataitem_t is record
		tdata: std_logic_vector(wData-1 downto 0);
		tkeep: std_logic_vector(wData/8-1 downto 0);
		tuser: std_logic_vector(wUser-1 downto 0);
	end record dataitem_t;

	type data_t is array (0 to sizeIxData-1) of dataitem_t;
	signal data: data_t;

	type hshkitem_t is record
		tvalid: std_logic;
		tlast: std_logic;
	end record hshkitem_t;

	type hshk_t is array (0 to sizeIxHshk-1) of hshkitem_t;
	signal hshk: hshk_t;

	signal ixDataWr: natural range 0 to sizeIxData-1; -- next position to write to
	signal lenHshkWr: std_logic_vector(wSize-1 downto 0); -- last wIxHshk bits: next position to write to

	signal strbIgrStart: std_logic;

	---- handshake

	---- igrclk to mclk CDC sampling (igrclkToMclkSample)

	signal strbIgrStart_mclk: std_logic;
	signal strbIgrStart_to_mclkm1: std_logic;

	---- igrclk to mclk CDC stretching (igrclkToMclkStretch)

	signal strbIgrStart_to_mclk: std_logic;

	---- mclk to igrclk CDC sampling (mclkToIgrclkSample)

	signal strbEgrDone_igrclk: std_logic;
	signal strbEgrDone_to_igrclkm1: std_logic;

	---- mclk to igrclk CDC stretching (mclkToIgrclkStretch)

	signal strbEgrDone_to_igrclk: std_logic;

	------------------------------------------------------------------------
	-- documentation
	------------------------------------------------------------------------

	-- unstable ingress sparse AXI stream forwarder with no back-pressure allowed

	-- assumes fIgrclk approx. fMclk with unreliable igrclk including loss of igrclk or sudden fIgrclk change
	-- sizeHshk to be chosen such that +/- sizeIxHshk clocks delta during transaction can be compensated

	-- for fIgrclk approx. fIgrclk / 2,3,4, ..., egrAXIS_tready can be used to throttle egress

	-- sparsity: fewer data slots (sizeIxData) than handshake slots (sizeIxHshk, each clock) available

	-- egr (mclk) side assumes role of stable monitor:
	-- a. triggers (non-conflicting) read sizeIxHshk/2 clocks behind write
	-- b. invalidates transaction and triggers reset if igr restarts while egr is still running
	-- c. faulty transaction for fMclk < fIgrclk cannot be avoided

begin

	------------------------------------------------------------------------
	-- implementation: AXI stream egress (egr)
	------------------------------------------------------------------------

	egrAXIS_tvalid <= egrAXIS_tvalid_sig;
	egrAXIS_tdata <= egrAXIS_tdata_sig;
	egrAXIS_tkeep <= egrAXIS_tkeep_sig;
	egrAXIS_tlast <= egrAXIS_tlast_sig;
	egrAXIS_tuser <= egrAXIS_tuser_sig;

	process (reset, mclk)
		variable ixHshkRd: natural range 0 to sizeIxHshk-1;

		constant imax: natural := sizeIxHshk/2;
		variable i: natural range 0 to imax;

	begin
		if reset='1' then
			stateEgr <= stateEgrInit;
			egrAXIS_tvalid_sig <= '0';
			egrAXIS_tdata_sig <= (others => '0');
			egrAXIS_tkeep_sig <= (others => '0');
			egrAXIS_tlast_sig <= '1';
			egrAXIS_tuser_sig <= (others => '0');

			ixDataRd <= 0;
			lenHshkRd <= (others => '0');

			strbEgrDone <= '0';

			ixHshkRd := 0;
			i := 0;
		
		elsif rising_edge(mclk) then
			if stateEgr=stateEgrInit then
				egrAXIS_tvalid_sig <= '0';
				egrAXIS_tdata_sig <= (others => '0');
				egrAXIS_tkeep_sig <= (others => '0');
				egrAXIS_tlast_sig <= '1';
				egrAXIS_tuser_sig <= (others => '0');

				ixDataRd <= 0;
				lenHshkRd <= (others => '0');

				strbEgrDone <= '0';

				ixHshkRd := 0;
				i := 0;

				if resetIgrclk='0' then
					stateEgr <= stateEgrIdle;
				else
					stateEgr <= stateEgrInit;
				end if;

			elsif stateEgr=stateEgrIdle then
				if strbIgrStart_mclk='1' then
					stateEgr <= stateEgrWait;
				end if;

			elsif stateEgr=stateEgrWait then
				-- account for strbIgrStart CDC and other delays 
				if i=imax-4-2 then
					if enEgr='1' then
						stateEgr <= stateEgrRun;
					end if;

				else
					i := i + 1;

					stateEgr <= stateEgrWait;
				end if;

			elsif stateEgr=stateEgrRun then
				ixHshkRd := to_integer(unsigned(lenHshkRd(wIxHshk-1 downto 0)));

				if hshk(ixHshkRd).tvalid='1' then
					egrAXIS_tdata_sig <= data(ixDataRd).tdata;
					egrAXIS_tkeep_sig <= data(ixDataRd).tkeep;
					egrAXIS_tuser_sig <= data(ixDataRd).tuser;

					if enEgr='1' then
						if ixDataRd=sizeIxData-1 then
							ixDataRd <= 0;
						else
							ixDataRd <= ixDataRd + 1;
						end if;
					end if;
				end if;

				if hshk(ixHshkRd).tlast='1' or unsigned(lenHshkRd)=size-1 then
					egrAXIS_tvalid_sig <= hshk(ixHshkRd).tvalid;
					egrAXIS_tlast_sig <= hshk(ixHshkRd).tlast;

					strbEgrDone <= '1';

					stateEgr <= stateEgrDone;

				elsif resetIgrclk='1' or strbIgrStart_mclk='1' then
					egrAXIS_tvalid_sig <= '0';
					egrAXIS_tlast_sig <= '1';

					strbEgrDone <= '1';

					stateEgr <= stateEgrDone;

				else
					egrAXIS_tvalid_sig <= hshk(ixHshkRd).tvalid;
					egrAXIS_tlast_sig <= hshk(ixHshkRd).tlast;

					if enEgr='1' then
						lenHshkRd <= std_logic_vector(unsigned(lenHshkRd) + 1);
					end if;

					stateEgr <= stateEgrRun;
				end if;

			elsif stateEgr=stateEgrDone then
				strbEgrDone <= '0';

				if enEgr='1' then
					egrAXIS_tvalid_sig <= '0';

					stateEgr <= stateEgrInit;
				end if;
			end if;
		end if;
	end process;

	------------------------------------------------------------------------
	-- implementation: AXI stream ingress (igr)
	------------------------------------------------------------------------

	igrAXIS_tready <= '1';

	process (resetIgrclk, igrclk)
		variable ixHshkWr: natural range 0 to sizeIxHshk-1;
		
		constant imax: natural := sizeIxHshk/2;
		variable i: natural range 0 to imax;

	begin
		if resetIgrclk='1' then
			stateIgr <= stateIgrInit;
			for i in 0 to sizeIxData-1 loop
				data(i).tdata <= (others => '0');
				data(i).tkeep <= (others => '0');
				data(i).tuser <= (others => '0');
			end loop;
			for i in 0 to sizeIxHshk-1 loop
				hshk(i).tvalid <= '0';
				hshk(i).tlast <= '1';
			end loop;
			ixDataWr <= 0;
			lenHshkWr <= (others => '0');
			strbIgrStart <= '0';

			ixHshkWr := 0;
			i := 0;

		elsif rising_edge(igrclk) then
			if stateIgr=stateIgrInit or strbEgrDone_igrclk='1' then
				for i in 0 to sizeIxData-1 loop
					data(i).tdata <= (others => '0');
					data(i).tkeep <= (others => '0');
					data(i).tuser <= (others => '0');
				end loop;
				for i in 0 to sizeIxHshk-1 loop
					hshk(i).tvalid <= '0';
					hshk(i).tlast <= '1';
				end loop;
				ixDataWr <= 0;
				lenHshkWr <= (others => '0');
				strbIgrStart <= '0';

				ixHshkWr := 0;
				i := 0;

				if igrAXIS_tvalid='0' and igrAXIS_tlast='1' then
					stateIgr <= stateIgrIdle;
				else
					stateIgr <= stateIgrInit;
				end if;

			elsif stateIgr=stateIgrIdle then
				if igrAXIS_tvalid='1' then
					data(0).tdata <= igrAXIS_tdata;
					data(0).tkeep <= igrAXIS_tkeep;
					data(0).tuser <= igrAXIS_tuser;
					hshk(0).tvalid <= igrAXIS_tvalid;
					hshk(0).tlast <= igrAXIS_tlast;

					ixDataWr <= ixDataWr + 1;
					lenHshkWr <= std_logic_vector(unsigned(lenHshkWr) + 1);

					strbIgrStart <= '1';

					if igrAXIS_tlast='0' then
						stateIgr <= stateIgrRun;

					else
						i := 0;

						stateIgr <= stateIgrDone;
					end if;
				end if;

			elsif stateIgr=stateIgrRun then
				strbIgrStart <= '0';

				ixHshkWr := to_integer(unsigned(lenHshkWr(wIxHshk-1 downto 0)));

				hshk(ixHshkWr).tvalid <= igrAXIS_tvalid;
				hshk(ixHshkWr).tlast <= igrAXIS_tlast;

				if igrAXIS_tvalid='1' then
					data(ixDataWr).tdata <= igrAXIS_tdata;
					data(ixDataWr).tkeep <= igrAXIS_tkeep;
					data(ixDataWr).tuser <= igrAXIS_tuser;

					if ixDataWr=sizeIxData-1 then
						ixDataWr <= 0;
					else
						ixDataWr <= ixDataWr + 1;
					end if;
				end if;

				if igrAXIS_tlast='1' or unsigned(lenHshkWr)=size-1 then
					i := 0;

					stateIgr <= stateIgrDone;

				else
					lenHshkWr <= std_logic_vector(unsigned(lenHshkWr) + 1);

					stateIgr <= stateIgrRun;
				end if;

			elsif stateIgr=stateIgrDone then
				strbIgrStart <= '0';

				if i=imax then
					-- timeout; else leave via strbEgrDone_igrclk
					stateIgr <= stateIgrInit;

				else
					i := i + 1;

					stateIgr <= stateIgrDone;
				end if;
			end if;
		end if;
	end process;

	------------------------------------------------------------------------
	-- implementation: igrclk to mclk CDC sampling (igrclkToMclkSample)
	------------------------------------------------------------------------

	process (reset, mclk)
		variable strbIgrStart_wait: boolean;

	begin
		if reset ='1' then
			strbIgrStart_mclk <= '1';
			strbIgrStart_to_mclkm1 <= '1';

			strbIgrStart_wait := false;

		elsif rising_edge(mclk) then
			if strbIgrStart_mclk='1' then
				strbIgrStart_mclk <= '0';
			elsif strbIgrStart_to_mclkm1='1' and not strbIgrStart_wait then
				strbIgrStart_mclk <= '1';
				strbIgrStart_wait := true;
			end if;
			if strbIgrStart_to_mclkm1='0' and strbIgrStart_wait then
				strbIgrStart_wait := false;
			end if;
			strbIgrStart_to_mclkm1 <= strbIgrStart_to_mclk;

		end if;
	end process;

	------------------------------------------------------------------------
	-- implementation: igrclk to mclk CDC stretching (igrclkToMclkStretch)
	------------------------------------------------------------------------

	process (resetIgrclk, igrclk)
		constant imax: natural := 1;

		variable strbIgrStart_i: natural range 0 to imax;

	begin
		if resetIgrclk ='1' then
			strbIgrStart_to_mclk <= '1';

			strbIgrStart_i := 0;

		elsif rising_edge(igrclk) then
			if strbIgrStart='1' then
				strbIgrStart_to_mclk <= '1';
				strbIgrStart_i := 0;
			elsif strbIgrStart_i=imax then
				strbIgrStart_to_mclk <= '0';
			else
				strbIgrStart_i := strbIgrStart_i + 1;
			end if;

		end if;
	end process;

	------------------------------------------------------------------------
	-- implementation: mclk to igrclk CDC sampling (mclkToIgrclkSample)
	------------------------------------------------------------------------

	process (resetIgrclk, igrclk)
		variable strbEgrDone_wait: boolean;

	begin
		if resetIgrclk='1' then
			strbEgrDone_igrclk <= '1';
			strbEgrDone_to_igrclkm1 <= '1';

			strbEgrDone_wait := false;

		elsif rising_edge(igrclk) then
			if strbEgrDone_igrclk='1' then
				strbEgrDone_igrclk <= '0';
			elsif strbEgrDone_to_igrclkm1='1' and not strbEgrDone_wait then
				strbEgrDone_igrclk <= '1';
				strbEgrDone_wait := true;
			end if;
			if strbEgrDone_to_igrclkm1='0' and strbEgrDone_wait then
				strbEgrDone_wait := false;
			end if;
			strbEgrDone_to_igrclkm1 <= strbEgrDone_to_igrclk;

		end if;
	end process;

	------------------------------------------------------------------------
	-- implementation: mclk to igrclk CDC stretching (mclkToIgrclkStretch)
	------------------------------------------------------------------------

	process (reset, mclk)
		constant imax: natural := 1;

		variable strbEgrDone_i: natural range 0 to imax;

	begin
		if reset='1' then
			strbEgrDone_to_igrclk <= '1';

			strbEgrDone_i := 0;

		elsif rising_edge(mclk) then
			if strbEgrDone='1' then
				strbEgrDone_to_igrclk <= '1';
				strbEgrDone_i := 0;
			elsif strbEgrDone_i=imax then
				strbEgrDone_to_igrclk <= '0';
			else
				strbEgrDone_i := strbEgrDone_i + 1;
			end if;
		end if;
	end process;

end Rtl;
