---------------------------------------------------------------------------
-- File           : avalon_mem_master_bfm.vhd
-- Author(s)      : Nikola Avramovic
-- Created        : December 30th, 2018
---------------------------------------------------------------------------
-- Description    : BFM that emulates a master bfm with Avalon MM interface
--      
-- Basic function of this BFM:
--    - burst write
--    - burst read
--    - compare burst data
---------------------------------------------------------------------------

---------------------------------------------------------------------------

library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   use work.p_avalon_mem_master_bfm.all;
   use work.p_report_utils.all;
   use work.p_generic_bfm.all;
 
entity avalon_mem_master_bfm is
   generic (
      G_INST_NAME          : string;
      G_HANDLE_NO          : rAvalonMemMasterBfmHandleNo;
      G_CLOCK_CYCLE        : time     := 20 ns;
      G_ADDR_WIDTH         : positive := 4;
      G_DATA_WIDTH         : positive;
      G_BURSTCOUNT_WIDTH   : positive
   );
   port (
      clk               : in  std_logic;
      tb_stop           : in  boolean;
      ----------------------------------------------------------------
      -- Avalon MM interface (connect to slave)
      ----------------------------------------------------------------
      -- address signal
      address           : out std_logic_vector (G_ADDR_WIDTH-1 downto 0);
      -- write master
      write             : out std_logic;
      writedata         : out std_logic_vector (G_DATA_WIDTH-1 downto 0);
      -- read master
      read              : out std_logic;
      readdatavalid     : in  std_logic;
      readdata          : in  std_logic_vector (G_DATA_WIDTH-1 downto 0);
      -- burst signals
      beginbursttransfer: out std_logic;
      burstcount        : out std_logic_vector(G_BURSTCOUNT_WIDTH-1 downto 0);
      -- handshake signals
      byteenable        : out std_logic_vector(C_BYTES_PER_WORD-1 downto 0);
      waitrequest       : in  std_logic
   );
end entity;


architecture behav of avalon_mem_master_bfm is
   ----------------------------------------------------------------------------
   -- all outputs need to be registered, because of mixed signals
   ----------------------------------------------------------------------------

   -- read record
   type tReadOutput is record
      address            : std_logic_vector (G_ADDR_WIDTH-1 downto 0);
      beginbursttransfer : std_logic;
      burstcount         : std_logic_vector(G_BURSTCOUNT_WIDTH-1 downto 0);
      byteenable         : std_logic_vector(C_BYTES_PER_WORD-1 downto 0);
      read               : std_logic;
   end record;

   -- write recored
   type tWriteOutput is record
      address            : std_logic_vector (G_ADDR_WIDTH-1 downto 0);
      beginbursttransfer : std_logic;
      burstcount         : std_logic_vector(G_BURSTCOUNT_WIDTH-1 downto 0);
      byteenable         : std_logic_vector(C_BYTES_PER_WORD-1 downto 0);
      writedata          : std_logic_vector (G_DATA_WIDTH-1 downto 0);
      write              : std_logic; 
   end record;

   ----------------------------------------------------------------------------

   type tParams is protected
      procedure set_verbosity(v : tVerbosity);
      impure function get_verbosity return tVerbosity;
      procedure debug(msg: string);
   end protected;

   type tParams is protected body
      variable verbosity : tVerbosity := INFO;

      procedure set_verbosity(v : tVerbosity) is
      begin
         verbosity := v;
      end procedure;

      impure function get_verbosity return tVerbosity is
      begin
         return verbosity;
      end function;

      procedure debug(msg: string) is
      begin
         report_info(G_INST_NAME, msg, DEBUG, verbosity);
      end procedure;       
   end protected body;

   shared variable params : tParams;

   ----------------------------------------------------------------------------

   type tReadMemory is protected
      procedure       read_data (data : tData);
      procedure       set_read(addr : tAddr; byteenable: tByteEnable; burstcount : tBurstcount);
      procedure       set_wait_request(wait_request : boolean);
      impure function get_read_request return std_logic;
      impure function get_burst_data return tDataArray;
      impure function get_begin_burst_transfer return std_logic;
      impure function burst_data_ready return boolean;
      impure function get_read_address return std_logic_vector;
      impure function get_burst_count return std_logic_vector;
      impure function get_byte_enable return std_logic_vector;
   end protected;

   type tReadMemory is protected body
      variable read_mem        : tDataArray(tAddr);
      variable data_ready      : boolean;
      variable start_reading   : boolean;
      variable burst_count     : tBurstcount;
      variable byte_enable     : tByteEnable; 
      variable address         : tAddr;
      variable burst_addr      : tAddr;
      variable read_request    : std_logic;

      procedure set_read(addr : tAddr; byteenable: tByteEnable; burstcount : tBurstcount) is
      begin
         byte_enable     := byteenable;
         burst_count     := burstcount;
         address         := addr;
         start_reading   := true;
         read_request    := '1';
      end procedure;

      procedure set_wait_request(wait_request : boolean) is
      begin
         -- reset  bigin burst transfer         
         if start_reading then
            start_reading  := false;
            burst_addr     := address;
         end if;

         if wait_request then
            read_request  := '1';
         elsif read_request = '1' and not wait_request then
            read_request  := '0';
         end if;

      end procedure;

      procedure read_data (data : tData) is
      begin
         -- init value(s)
         data_ready        := false;

         -- reset  bigin burst transfer
         if start_reading then
            start_reading  := false;
            burst_addr     := address;
         end if;

         if burst_addr < address and burst_addr > (address + burst_count) then
            report_error(G_INST_NAME, " Master read from unsuported address " & to_string(burst_addr));

         elsif burst_addr >= address then

            -- execute a single read access for the enabled bytes
            for i in byte_enable'range loop
               if byte_enable(i) = '1' then
                  read_mem(burst_addr)(i*8+7 downto i*8) := data(i*8+7 downto i*8);
               end if;
            end loop;

            params.debug(" Master READ data " &  to_hstring(data) & ", from address " & to_string(burst_addr));

            burst_addr  := burst_addr + 1;
         end if;

         if burst_addr = address + burst_count then
            data_ready  := true;

            params.debug(" All burst data have been received");

         end if;
      end procedure;

      impure function get_burst_data return tDataArray is
      begin

         if not data_ready then
            report_error(G_INST_NAME, " Burst data is not ready.");
         else
            data_ready     := false;
         end if;

         return read_mem;
      end function;

      impure function get_begin_burst_transfer return std_logic is
         variable begin_burst_transfer : std_logic;
      begin
         if start_reading then
            begin_burst_transfer := '1';
         else
            begin_burst_transfer := '0';
         end if;

         return begin_burst_transfer;
      end function;

      impure function burst_data_ready return boolean is
      begin
         return data_ready;
      end function;

      impure function get_read_request return std_logic is
      begin
         return read_request;
      end function;

      impure function get_read_address return std_logic_vector is
         variable read_address : std_logic_vector (G_ADDR_WIDTH-1 downto 0);
      begin
         read_address      := std_logic_vector(to_unsigned(address,G_ADDR_WIDTH));

         return read_address;
      end function;

      impure function get_burst_count return std_logic_vector is
         variable read_burst_count : std_logic_vector(G_BURSTCOUNT_WIDTH-1 downto 0);
      begin
         read_burst_count  := std_logic_vector(to_unsigned(burst_count,G_BURSTCOUNT_WIDTH));

         return read_burst_count;
      end function;

      impure function get_byte_enable return std_logic_vector is
      begin
         return byte_enable;
      end function;

   end protected body;
  
   ----------------------------------------------------------------------------

   type tWriteMemory is protected
      procedure       set_burst_data(addr : tAddr; burstcount : tBurstcount; byteenable: tByteEnable; data : tDataArray);
      procedure       prepare_data(wait_request : boolean);
      impure function write_data return tData;
      impure function get_address return std_logic_vector;
      impure function get_burst_count return std_logic_vector;
      impure function get_byte_enable return std_logic_vector;
      impure function get_begin_burst_transfer return std_logic;
      impure function get_write_request return std_logic;
   end protected;

   type tWriteMemory is protected body
      variable write_mem       : tDataArray(tAddr);
      variable write_burst_count : tBurstcount;
      variable byte_enable     : tByteEnable;
      variable address         : tAddr;
      variable previous_addr   : tAddr;
      variable next_addr       : tAddr;
      variable begin_burst_transfer : std_logic;
      variable write_request   : std_logic;
      variable data_out        : tData;
      variable first_wait_request : boolean := false;

      procedure set_burst_data(addr : tAddr; burstcount : tBurstcount; byteenable: tByteEnable; data : tDataArray) is
      begin
         -- public variables, which will be used for output signal
         byte_enable          := byteenable;
         write_burst_count    := burstcount;
         data_out             := data(addr);
         address              := addr;
         write_request        := '1';
         begin_burst_transfer := '1';

         -- set previous and next address
         previous_addr        := address;
         next_addr            := address + 1;

         params.debug(" Set begin burst transfer, address, write, burstcount, and writedata." );
         params.debug(" Write Data " & to_hstring(data_out) & " to address " & to_string(address));

         for i in addr to addr + burstcount loop
            write_mem(i) := data(i);

            params.debug(" Set data "   & to_hstring(data(i)) &
                         " on address " & to_string(i));
         end loop;

      end procedure;

      procedure prepare_data(wait_request : boolean) is
      begin

         if next_addr < address and next_addr > (address + write_burst_count + 1) then
            report_error(G_INST_NAME, " Master write to unsuported address " & to_string(next_addr));
            write_request        := '0';

         -- if next address is updated send new data
         elsif next_addr > previous_addr and next_addr < (address + write_burst_count + 1) then
            write_request      := '1';

            -- begin burst transfer is set just first clock cycle
            -- first data is prepared in the procedure set burst data
            begin_burst_transfer := '0';

            -- execute a single write access for the enabled bytes
            for i in byte_enable'range loop
               if byte_enable(i) = '1' then
                  data_out(i*8+7 downto i*8) := write_mem(next_addr)(i*8+7 downto i*8);
               end if;
            end loop;
   
            params.debug(" Write Data " & to_hstring(data_out) &
                         " to address " & to_string(next_addr));

         elsif next_addr = previous_addr then
            write_request   := '1';

         else
            write_request   := '0';
   
         end if;

         previous_addr      := next_addr;

         -- if salve has not stuck update next address
         if not wait_request and next_addr <= (address + write_burst_count) then
            first_wait_request := false;
            write_request   := '1';
            next_addr       := next_addr + 1;
            params.debug(" Write request is true and next address is " & to_string(next_addr));
         end if;

         -- all burst data have been received
         if next_addr > (address + write_burst_count ) then
            write_request   := '0';
         end if;

      end procedure;

      impure function write_data return tData is
      begin
         return data_out;

      end function;

      impure function get_address return std_logic_vector is
         variable write_address : std_logic_vector (G_ADDR_WIDTH-1 downto 0);
      begin
         write_address      := std_logic_vector(to_unsigned(address,G_ADDR_WIDTH));

         return write_address;
      end function;

      impure function get_burst_count return std_logic_vector is
         variable burstcount : std_logic_vector(G_BURSTCOUNT_WIDTH-1 downto 0);
      begin
         burstcount  := std_logic_vector(to_unsigned(write_burst_count,G_BURSTCOUNT_WIDTH));

         return burstcount;
      end function;

      impure function get_byte_enable return std_logic_vector is
      begin
         return byte_enable;
      end function;

      impure function get_begin_burst_transfer return std_logic is
      begin
         return begin_burst_transfer;
      end function;

      impure function get_write_request return std_logic is
      begin
         return write_request;
      end function;

   end protected body;

   ----------------------------------------------------------------------------
   shared variable read_memory   : tReadMemory;
   shared variable write_memory  : tWriteMemory;

begin

   -- -------------------------------------------------------------------------
   -- Memory interface
   -- -------------------------------------------------------------------------
   mem_p : process (clk)
      variable wait_request  : boolean := true;
      variable read_out      : tReadOutput;
      variable write_out     : tWriteOutput;

   begin
      if rising_edge(clk) then
         -------------------------------------------------------------------------
         -- if wait request is not set, send data from next address
         -------------------------------------------------------------------------
         if waitrequest = '1' then
            wait_request                := true;
            params.debug(" Wait request is true");
         else
            wait_request                := false;
         end if;

         ------------------------------------------------------------------------
         -- write data handshake
         ------------------------------------------------------------------------
         -- set output variables
         write_out.write                := write_memory.get_write_request;
         write_out.beginbursttransfer   := write_memory.get_begin_burst_transfer;
         write_out.burstcount           := write_memory.get_burst_count;
         write_out.byteenable           := write_memory.get_byte_enable;
         write_out.writedata            := write_memory.write_data;
         write_out.address              := write_memory.get_address;

         -- reset begin burst transfer affter set of write 
         if beginbursttransfer = '1' and write = '1' and waitrequest = '0' then
            beginbursttransfer <= '0';
            params.debug(" Deassert bigin burst transfer.");
         end if;

         -- prepare data for next cycle
         write_memory.prepare_data(wait_request);

         -- Write can not be set, when data has wrong value
         if write_memory.write_data = X"UU" then
            write_out.write             := '0';
         end if;

         -------------------------------------------------------------------------
         -- read data handshake 
         -------------------------------------------------------------------------
         -- set output variables
         read_out.read                  := read_memory.get_read_request;
         read_out.beginbursttransfer    := read_memory.get_begin_burst_transfer;
         read_out.burstcount            := read_memory.get_burst_count;
         read_out.byteenable            := read_memory.get_byte_enable;
         read_out.address               := read_memory.get_read_address;
   
         read_memory.set_wait_request(wait_request);
   
         -- reset begin burst transfer affter set of write 
         if beginbursttransfer = '1' and read = '1' and waitrequest = '0' then
            beginbursttransfer <= '0';
            params.debug(" Deassert bigin burst transfer.");
         end if;

         -- read data if read data valid is asserted
         if readdatavalid = '1' then
            read_memory.read_data(data => readdata);
         end if;

         -------------------------------------------------------------------------
         -- this contion is used for not mixing of read/write function
         -------------------------------------------------------------------------

         -- error state; master can not read and write in the same time
         if read_out.read = '1' and write_out.write = '1' then
            report_error(G_INST_NAME, " Reading is not allowed during write request and vice verse!");
         end if; 

         -- set all write output signals
         if write_out.write = '1' then
            write                       <=  write_out.write;
            address                     <=  write_out.address;
            beginbursttransfer          <=  write_out.beginbursttransfer;
            burstcount                  <=  write_out.burstcount;
            byteenable                  <=  write_out.byteenable;
            writedata                   <=  write_out.writedata;
         -- set non mixed signals
         else
            write                       <=  write_out.write;
            writedata                   <=  write_out.writedata;
         end if; 

         if read_out.read = '1' then
            read                        <= read_out.read;
            address                     <= read_out.address;
            beginbursttransfer          <= read_out.beginbursttransfer;
            burstcount                  <= read_out.burstcount;
            byteenable                  <= read_out.byteenable;
         -- set non mixed signals
         else
            read                        <= read_out.read;
         end if; 

      end if;
   end process mem_p;

   -- -------------------------------------------------------------------------
   -- Command Process
   -- -------------------------------------------------------------------------
   command_p : process
      variable cmd         : tBfmCmd;
      variable data        : tDataArray(tAddr);
   begin
      -- Initial output state
      hAvalonMemMasterBfm(G_HANDLE_NO).ack <= '0';
      params.set_verbosity(INFO);

      loop
         -- Get request
         bfm_wait_for_request(hAvalonMemMasterBfm(G_HANDLE_NO));
         cmd := get_bfm_cmd(G_HANDLE_NO);
         bfm_ack_request(hAvalonMemMasterBfm(G_HANDLE_NO));
         wait for 0 ns;   -- Wait for delta cycle

         -- Process Request
         case cmd.op is
            -- write data 
            when SET_BURST_DATA =>
               write_memory.set_burst_data(addr       => cmd.waddr, 
                                           burstcount => cmd.burstcount, 
                                           byteenable => cmd.byteenable,
                                           data       => cmd.data);
               wait for 0 ns;   -- Wait for delta cycle

            -- read burst data
            when GET_BURST_DATA =>
               -- set read request
               read_memory.set_read(addr       => cmd.waddr, 
                                    byteenable => cmd.byteenable,
                                    burstcount => cmd.burstcount);

               -- wait to receive all burst data
               while not read_memory.burst_data_ready loop
                  wait for G_CLOCK_CYCLE;
               end loop;

               -- read burst data (read data array <address to address + burstcount>)
               cmd.read_data_burst  := read_memory.get_burst_data;

               setBfmCmd(G_HANDLE_NO, cmd);
               wait for 0 ns;   -- Wait for delta cycle

            when VERIFY =>
               -- set read request
               read_memory.set_read(addr       => cmd.waddr, 
                                    byteenable => cmd.byteenable,
                                    burstcount => cmd.burstcount);

               -- wait to receive all burst data
               while not read_memory.burst_data_ready loop
                  wait for G_CLOCK_CYCLE;
               end loop;

               -- compare data from given addresses 
               data := read_memory.get_burst_data;
               if cmd.data(cmd.waddr to (cmd.waddr + cmd.burstcount - 1)) /= data(cmd.waddr to (cmd.waddr + cmd.burstcount - 1)) then
                  for i in cmd.waddr to (cmd.waddr + cmd.burstcount - 1) loop 
                     report_error(G_INST_NAME,
                                 "Verify failed at address " & to_hstring(to_unsigned(i,4)) &
                                 "; Expecting " & to_hstring(cmd.data(i)) & 
                                 ", received " & to_hstring(data(i)));
                  end loop;
               end if;

               wait for 0 ns;   -- Wait for delta cycle

            when SET_VERBOSITY => -- Set Verbosity Level
               params.set_verbosity(cmd.verbosity);

            when others => 
               NULL;
         end case;

         -- Testbench Stop
         if tb_stop then
            wait;
         end if;
      end loop;
   end process;

end behav;
