LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.NUMERIC_STD.ALL;

library work;
use work.mips_types.all;

ENTITY dcache IS
	Generic
	(
		cache_size	: natural := 2048;	-- words
		line_size	: natural := 8		-- words
	);
	Port
	(
		RST_I		: in  STD_LOGIC;
		CLK_I		: in  STD_LOGIC;
		ACK_I		: in  STD_LOGIC;
		SRDY_I		: in  STD_LOGIC;
		ADDR_O		: out word_t;
		DAT_I		: in  word_t;
		STB_O		: out STD_LOGIC;
		CYC_O		: out STD_LOGIC;
		en			: in  STD_LOGIC;
		cpu_en		: in  STD_LOGIC;
		cpu_we		: in  STD_LOGIC;
		cpu_be		: in  unsigned(3 downto 0);
		cpu_addr	: in  word_t;
		cpu_din		: in  word_t;
		cpu_dout	: out word_t;
		cpu_busy	: out STD_LOGIC
	);
END dcache;

ARCHITECTURE behavior OF dcache IS

COMPONENT dpram_1w1r
	GENERIC
	(
		addr_width : integer := 3;
		data_width : integer := 8
	);
	PORT (
		clka		: in  STD_LOGIC;
		clkb		: in  STD_LOGIC;
		en_a		: in  STD_LOGIC;
		en_b		: in  STD_LOGIC;
		we_a		: in  STD_LOGIC;
		addr_a		: in  unsigned (addr_width-1 downto 0);
		addr_b		: in  unsigned (addr_width-1 downto 0);
		din_a		: in  unsigned (data_width-1 downto 0);
		dout_b		: out unsigned (data_width-1 downto 0)
	);
END COMPONENT;

COMPONENT dpram_2w2r is
	GENERIC 
	(
		addr_width : integer := 3;
		data_width : integer := 8
	);
	PORT
	(
		clk_a		: in  STD_LOGIC;
		clk_b		: in  STD_LOGIC;
		en_a		: in  STD_LOGIC;
		en_b		: in  STD_LOGIC;
		we_a		: in  STD_LOGIC;
		we_b		: in  STD_LOGIC;
		addr_a		: in  unsigned (addr_width-1 downto 0);
		addr_b		: in  unsigned (addr_width-1 downto 0);
		din_a		: in  unsigned (data_width-1 downto 0);
		din_b		: in  unsigned (data_width-1 downto 0);
		dout_a		: out unsigned (data_width-1 downto 0);
		dout_b		: out unsigned (data_width-1 downto 0)
	);
END COMPONENT;

	constant word_index_width	: natural := lg2(line_size);
	constant cache_index_width	: natural := lg2(cache_size) - word_index_width;
	constant tag_width			: natural := 32 - word_index_width - cache_index_width - 2;
	constant tag_parity_width	: natural := 3;
	constant tram_data_width	: natural := 1 + tag_parity_width + tag_width;
	constant tram_addr_width	: natural := cache_index_width;
	
	subtype	tram_data_t is unsigned (tram_data_width-1 downto 0);

	type dcache_entry_t is record
		valid	: std_logic;
		tv_p	: unsigned(tag_parity_width-1 downto 0);
		tag		: unsigned(tag_width-1 downto 0);
	end record;
	
	alias cpu_word_index is cpu_addr(word_index_width+1 downto 2);
	alias cpu_cache_index is cpu_addr(cache_index_width+word_index_width+1 downto word_index_width+2);
	alias cpu_tag is cpu_addr(tag_width+cache_index_width+word_index_width+1 downto cache_index_width+word_index_width+2);
	
	function to_dcache_entry(x : tram_data_t) return dcache_entry_t is
	variable result : dcache_entry_t;
	begin
		result.valid	:= x(0);
		result.tv_p		:= x(3 downto 1);
		result.tag		:= x(tag_width+3 downto 4);
		
		return result;
	end to_dcache_entry;

	function to_tram_data(x : dcache_entry_t) return tram_data_t is
	variable result : tram_data_t;
	begin
		result(0)										:= x.valid;
		result(3 downto 1)								:= x.tv_p;
		result(tag_width+3 downto 4)					:= x.tag;
		
		return result;
	end to_tram_data;

	type cache_state_t is (init, ready, flush, mem_request, mem_access, mem_data, rd_cache);
	signal s, sn : cache_state_t;
	
	signal	cache_busy			: std_logic;
	signal	cache_hit			: std_logic;
	signal	tag_match			: std_logic;
	signal  word_index_reg		: unsigned(word_index_width-1 downto 0);
	signal  addr_windex_reg		: unsigned(word_index_width-1 downto 0);
	signal  cache_index_reg		: unsigned(cache_index_width-1 downto 0);
	signal  tag_index_reg		: unsigned(tag_width-1 downto 0);
	
	signal	cache_entry_in		: dcache_entry_t;
	signal	cache_entry_out		: dcache_entry_t;
	signal  cpu_dram_addr		: unsigned(lg2(cache_size)-1 downto 0);
	signal  cpu_dram_dout		: word_t;
	signal  cpu_dram_din		: word_t;
	signal  cpu_data_reg		: word_t;
	signal  cpu_be_reg			: unsigned(3 downto 0);
	signal  cpu_we_reg			: std_logic;
	signal	ctrl_dram_en		: std_logic;
	signal  ctrl_dram_addr		: unsigned(lg2(cache_size)-1 downto 0);
	signal  ctrl_dram_din		: word_t;
	signal	ctrl_dram_we		: unsigned(3 downto 0);
	signal	cpu_dram_we			: unsigned(3 downto 0);
	signal	cpu_dram_en			: std_logic;
	signal	cpu_en2				: std_logic;
	signal	cpu_we2				: std_logic;

	signal  tram_addr_rd		: unsigned(cache_index_width-1 downto 0);
	signal  tram_dout			: tram_data_t;
	signal  tram_addr_wr		: unsigned(cache_index_width-1 downto 0);
	signal  tram_din			: tram_data_t;
	signal	tram_we				: std_logic;
	
	signal	fill_count			: natural range 0 to 2**word_index_width-1;
	signal	fill_count_en		: std_logic;
	signal	fill_count_rdy		: std_logic;
	signal	flush_count			: natural range 0 to 2**cache_index_width-1;
	signal	flush_count_en		: std_logic;
	signal	flush_count_rdy		: std_logic;
	signal	request_count		: natural range 0 to 2**word_index_width-1;
	signal	request_count_en	: std_logic;
	signal	request_count_rdy	: std_logic;
	signal	cpu_reg_en			: std_logic;
	signal	was_miss			: std_logic;
	signal	cpu_hit_we			: std_logic;
	signal	instant_raw			: std_logic;

begin

	cpu_hit_we	<= cpu_we2 and cache_hit;
	
cpu_index_register:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			if fill_count_en = '1' then
				if ACK_I = '1' then
					word_index_reg	<= word_index_reg + 1;
				end if;	
			elsif cpu_reg_en = '1' and en = '1' and instant_raw = '0' then
				word_index_reg	<= cpu_word_index;
				cache_index_reg	<= cpu_cache_index;
				tag_index_reg	<= cpu_tag;
			end if;
		end if;
	end process;	

addr_windex_register:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			if request_count_en = '1'then
				if SRDY_I = '1' then
					addr_windex_reg	<= addr_windex_reg + 1;
				end if;	
			elsif cpu_reg_en = '1' and en = '1' and instant_raw = '0' then
				addr_windex_reg	<= cpu_word_index;
			end if;
		end if;
	end process;	

cpu_data_register:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			if RST_I = '1' then
				cpu_we_reg	<= '0';
			elsif cpu_reg_en = '1' and en = '1' then
				cpu_data_reg	<= cpu_din;
				cpu_be_reg		<= cpu_be;
				cpu_we_reg		<= cpu_we;
			end if;
		end if;
	end process;	

cpu_was_wr_register:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			cpu_en2	<= '0';
			cpu_we2		<= '0';
			if cpu_en = '1' and en = '1' then
				cpu_we2			<= cpu_we;
				cpu_en2			<= '1';
				cpu_dram_din	<= cpu_din;
			end if;
		end if;
	end process;	

instant_raw_logic:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			instant_raw <= '0';
			if cpu_word_index = word_index_reg then
				instant_raw	<= cpu_hit_we and cpu_en and en and not cpu_we;
			end if;	
		end if;
	end process;	

inst_tag_ram : dpram_1w1r
	GENERIC MAP 
	(
		addr_width	=> tram_addr_width,
		data_width	=> tram_data_width
	)
	PORT MAP 
	(
		clka	=> CLK_I,
		clkb	=> CLK_I,
		en_a	=> '1',
		en_b	=> '1',
		we_a	=> tram_we,
		addr_a	=> tram_addr_wr,
		addr_b	=> tram_addr_rd,
		din_a	=> tram_din,
		dout_b	=> tram_dout
	);

gen_data_ram:
	for i in 0 to 3 generate
	begin
	
	inst_data_ram : dpram_2w2r
		GENERIC MAP 
		(
			addr_width	=> lg2(cache_size),
			data_width	=> word_t'length/4
		)
		PORT MAP 
		(
			clk_a	=> CLK_I,
			clk_b	=> CLK_I,
			en_a	=> '1',
			en_b	=> '1',
			we_a	=> cpu_dram_we(i),
			we_b	=> ctrl_dram_we(i),
			addr_a	=> ctrl_dram_addr,
			addr_b	=> cpu_dram_addr,
			din_a	=> cpu_data_reg(8*(i+1)-1 downto 8*i),
			din_b	=> DAT_I(8*(i+1)-1 downto 8*i),
			dout_a	=> open,
			dout_b	=> cpu_dram_dout(8*(i+1)-1 downto 8*i)
		);
	end generate;	
	
cache_state_next:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			if RST_I = '1' then
				s	<= init;
			else
				s	<= sn;
			end if;
		end if;
	end process;	

	cpu_busy			<= cache_busy or instant_raw;
	cpu_dout			<= cpu_dram_dout;
	
	tag_match			<= '1' when tag_index_reg = cache_entry_out.tag else '0';
	cache_hit			<= tag_match and cache_entry_out.valid;
	tram_din			<= to_tram_data(cache_entry_in);
	tram_addr_wr		<= to_unsigned(flush_count, cache_index_width) when flush_count_en = '1' else cache_index_reg;
	tram_addr_rd		<= cpu_cache_index when was_miss = '0' else cache_index_reg;
	cache_entry_out		<= to_dcache_entry(tram_dout);
	
	cpu_dram_addr		<= (cpu_cache_index & cpu_word_index) when (was_miss = '0' and instant_raw = '0' and fill_count_en = '0') else (cache_index_reg & word_index_reg);
	ADDR_O				<= tag_index_reg & cache_index_reg & addr_windex_reg & "00";
	ctrl_dram_addr		<= cache_index_reg & word_index_reg;
	ctrl_dram_we		<= (others => fill_count_en and ACK_I);
	cpu_dram_we			<= cpu_be_reg when (cpu_hit_we = '1') else (others => '0');
	
cache_state:
	process(s, instant_raw, cache_hit, flush_count_rdy, fill_count_rdy, request_count_rdy, tag_index_reg, cpu_en, SRDY_I, cpu_en2, cpu_we_reg)
	begin
		cpu_reg_en				<= '0';
		cache_busy 				<= '1';
		tram_we					<= '0';
		flush_count_en			<= '0';
		request_count_en		<= '0';
		fill_count_en			<= '0';
		CYC_O					<= '0';
		STB_O					<= '0';
		was_miss				<= '0';
		cache_entry_in.tv_p		<= (others => '0');
		cache_entry_in.tag		<= tag_index_reg;
		cache_entry_in.valid	<= '0';
		sn	<= s;
		
		case s is
		when init =>
			sn <= flush;
		when ready =>
			cache_busy 		<= '0';
			cpu_reg_en		<= '1';
			if cpu_en2 = '1' then
				if cache_hit = '0' and cpu_we_reg = '0' then
					sn						<= mem_request;
					cpu_reg_en				<= '0';
					cache_busy 				<= '1';
					CYC_O					<= '1';
				end if;	
			end if;	
		when flush =>
			flush_count_en			<= '1';
			tram_we					<= '1';
			cache_entry_in.valid	<= '0';
			cache_entry_in.tag		<= (others => '0');
			if flush_count_rdy = '1' then
				tram_we				<= '0';
				sn	<= ready;
				if cpu_en = '1' then
					cpu_reg_en			<= '1';
				end if;
			end if;
		when mem_request =>
			CYC_O		<= '1';
			if SRDY_I = '1' then
				sn <= mem_access;
			end if;	
		when mem_access =>
			fill_count_en		<= '1';
			request_count_en	<= '1';
			CYC_O				<= '1';
			STB_O				<= '1';
			if request_count_rdy = '1' then
				STB_O			<= '0';
				sn 				<= mem_data;
			end if;	
		when mem_data =>
			CYC_O			<= '1';
			fill_count_en	<= '1';
			if fill_count_rdy = '1' then
				tram_we					<= '1';
				cache_entry_in.valid	<= '1';
				sn						<= rd_cache;
			end if;	
		when rd_cache =>
			was_miss				<= '1';
			sn	<= ready;

		when others => 
			sn	<= ready;
		end case;

	end process;

flush_counter:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			if flush_count_en = '0' then
				flush_count_rdy	<= '0';
				flush_count		<= 2**cache_index_width-1;
			else
				if flush_count /= 0 then
					flush_count	<= flush_count - 1;
				else
					flush_count_rdy	<= '1';
				end if;	
			end if;
		end if;
	end process;	
		
request_counter:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			if request_count_en = '0' then
				request_count_rdy	<= '0';
				request_count		<= 2**word_index_width-1;
			else	
				if SRDY_I = '1' then
					if request_count /= 0 then
						request_count 	<= request_count - 1;
					else
						request_count_rdy	<= '1';
					end if;
				end if;	
			end if;	
		end if;
	end process;	

fill_counter:
	process(CLK_I)
	begin
		if rising_edge(CLK_I) then
			if fill_count_en = '0' then
				fill_count_rdy	<= '0';
				fill_count		<= 2**word_index_width-1;
			else	
				if ACK_I = '1' then
					if fill_count /= 0 then
						fill_count 	<= fill_count - 1;
					else
						fill_count_rdy	<= '1';
					end if;
				end if;
			end if;	
		end if;
	end process;	

end behavior;
