--------------------------------------------------------------------------------
-- CERN BE-CO-HT
-- Mock Turtle
-- https://gitlab.cern.ch/coht/mockturtle
--------------------------------------------------------------------------------
--
-- unit name:   hydra_ram
--
-- description: a 32b RAM for rad-tol systems (with ECC and scrubing)
--   2 ports: 1 ro, 1 wo.
--
--------------------------------------------------------------------------------
-- Copyright CERN 2014-2018
--------------------------------------------------------------------------------
-- Copyright and related rights are licensed under the Solderpad Hardware
-- 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://solderpad.org/licenses/SHL-2.0.
-- Unless required by applicable law or agreed to in writing, software,
-- hardware and materials distributed under this 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.
--------------------------------------------------------------------------------

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

use work.secded_32b_pkg.all;

entity hydra_iram is
  generic(
    g_RAM_LOG_SIZE : natural := 12; --  In bytes
    g_SIM_SEU_PERIOD : natural := 0);  
  port(
    --  Note: only writes are allowed during reset.
    clk_i            : in  std_logic;
    rst_n_i          : in  std_logic;

    --  Enable scrubber
    scrub_en_i       : in  std_logic;

    --  Port 1: read access (high priority)
    --  RDONE_O is a pulse.
    r1_addr_i      : in  std_logic_vector(g_RAM_LOG_SIZE - 1 downto 2);
    r1_en_i        : in  std_logic;
    r1_data_o      : out std_logic_vector(31 downto 0);
    r1_done_o      : out std_logic;
    r1_err_o       : out std_logic;

    --  Port 2: read access (low priority)
    --  RDONE_O is a pulse.
    r2_addr_i      : in  std_logic_vector(g_RAM_LOG_SIZE - 1 downto 2);
    r2_en_i        : in  std_logic;
    r2_data_o      : out std_logic_vector(31 downto 0);
    r2_done_o      : out std_logic;
    r2_err_o       : out std_logic;

    --  Write access
    --  WFORCE_I are raw bits modified after ECC.  Can be used to force an ECC error.
    waddr_i      : in  std_logic_vector(g_RAM_LOG_SIZE - 1 downto 2);
    we_i         : in  std_logic;
    wecc_i     : in  std_logic_vector(6 downto 0);
    wdata_i      : in  std_logic_vector(31 downto 0);
    wdone_o      : out std_logic;

    --  For statistics
    ecc_one_o        : out std_logic;
    ecc_fatal_o      : out std_logic;
    ecc_addr_o       : out std_logic_vector(g_RAM_LOG_SIZE - 1 downto 2);
    scrub_se_o       : out std_logic;
    scrub_de_o       : out std_logic;
    scrub_cycle_o    : out std_logic;

    --  Fault injection
    err_addr_i       : in std_logic_vector(g_RAM_LOG_SIZE - 1 downto 2);
    err_addr_en_i    : in std_logic;
    err_addr_done_o  : out std_logic;

    --  Scrubber
    scrubber_period_i : in  std_logic_vector(15 downto 0)
  );
end hydra_iram;

architecture arch of hydra_iram is
  constant RAM_WSIZE : natural := 2 ** (g_RAM_LOG_SIZE - 2);
  type t_ram39_type is array(natural range <>) of std_logic_vector(38 downto 0);

  signal raddr, waddr : std_logic_vector(g_RAM_LOG_SIZE - 1 downto 2);
  signal rdata_ecc, wdata_ecc : std_logic_vector(38 downto 0);
  signal wen, ren, ren_d : std_logic;

  signal recc, rsyndrome : std_logic_vector(6 downto 0);
  signal rerr, rerr_one : std_logic;
  signal r1_done, n_r1_done, r1_done_d : std_logic;
  signal r2_done, n_r2_done, r2_done_d : std_logic;
  signal wdone, n_wdone : std_logic;

  signal n_ecc_one, n_ecc_fatal : std_logic;
  signal last_raddr, n_last_raddr : std_logic_vector(g_RAM_LOG_SIZE - 1 downto 2);

  --  The scrubber counter has an extra bit to signal a new cycle.
  signal scrub_addr_ext : std_logic_vector(g_RAM_LOG_SIZE - 1 + 1 downto 2);
  alias scrub_cycle : std_logic is scrub_addr_ext (g_RAM_LOG_SIZE);
  alias scrub_addr : std_logic_vector(g_RAM_LOG_SIZE - 1 downto 2) is scrub_addr_ext (g_RAM_LOG_SIZE - 1 downto 2);
  signal scrub_cycle_d1, scrub_cycle_d2 : std_logic;
  
  signal scrub_counter : unsigned(15 downto 0);
  signal scrub_rd, scrub_done, n_scrub_done : std_logic;
  signal n_scrub_se, n_scrub_de : std_logic;

  signal err_addr_hit : std_logic;

  type state_t is (S_READ, S_REWRITE);
  signal state, n_state : state_t;
begin
  --  The raw ram.
  p_ram: process (clk_i)
  is
    variable iram : t_ram39_type(0 to RAM_WSIZE - 1) := (others => (others => '1'));
    variable d : std_logic_vector(38 downto 0);
    variable err : std_logic_vector(38 downto 0) := (0 => '1', others => '0');
    variable sim_cnt : natural;
  begin
    if rising_edge(clk_i) then
      err_addr_hit <= '0';

      if wen = '1' then
        iram (to_integer(unsigned(waddr))) := wdata_ecc;
      end if;
      if ren = '1' then
        d := iram (to_integer(unsigned(raddr)));

        if raddr = err_addr_i and err_addr_en_i = '1' then
          err_addr_hit <= '1';
        end if;

        --  Simulate errors.
        if g_SIM_SEU_PERIOD /= 0 then
          if sim_cnt < g_SIM_SEU_PERIOD then
            sim_cnt := sim_cnt + 1;
          else
            sim_cnt := 0;
            d := d xor err;
            err := err(err'left - 1 downto 0) & err(err'left);
          end if;
        end if;

        rdata_ecc <= d;
      else
        rdata_ecc <= (others => 'X');
      end if;
      ren_d <= ren;
    end if;
  end process;

  r1_data_o <= rdata_ecc (31 downto 0);
  r2_data_o <= rdata_ecc (31 downto 0);

  recc <= f_calc_ecc(rdata_ecc (31 downto 0));
  rsyndrome <= recc xor rdata_ecc(38 downto 32);
  rerr <= (f_ecc_errors(rsyndrome) or err_addr_hit) and ren_d;
  rerr_one <= (f_ecc_one_error(rsyndrome) or err_addr_hit) and ren_d;

  --  Note: only single errors are reported, double errors are fatal.
  r1_err_o <= rerr_one;
  r1_done_o <= r1_done;
  r2_err_o <= rerr_one;
  r2_done_o <= r2_done;
  wdone_o <= wdone;

  err_addr_done_o <= err_addr_hit;

  --  We need to delay scrub_cycle_o so that it happens after a possible
  --  error on the last address.
  scrub_cycle_o <= scrub_cycle_d2;

  p_scrub: process (clk_i)
  begin
    if rising_edge(clk_i) then
      if rst_n_i = '0' or scrub_cycle_d2 = '1' then
        scrub_counter <= unsigned(scrubber_period_i);
        scrub_addr_ext <= (others => '0');
        scrub_rd <= '0';
        scrub_cycle_d2 <= '0';
        scrub_cycle_d1 <= '0';
      else
        scrub_cycle_d1 <= scrub_cycle;
        scrub_cycle_d2 <= scrub_cycle_d1;
        if n_scrub_done = '1' then
          scrub_addr_ext <= std_logic_vector(unsigned(scrub_addr_ext) + 1);
          scrub_rd <= '0';
        elsif scrub_en_i = '1' then
          if scrub_counter = (scrub_counter'range => '0') then
            scrub_rd <= '1';
          else
            scrub_counter <= scrub_counter - 1;
          end if;
        end if;
      end if;
    end if;
  end process;

  p_ctrl: process (state, r1_done, r2_done, rerr, rerr_one, rdata_ecc, rsyndrome, last_raddr,
                   r1_en_i, r1_addr_i, we_i, waddr_i, wdata_i, wecc_i, r1_done_d, r2_done_d,
                   r2_en_i, r2_addr_i, scrub_rd, scrub_addr, scrub_done, scrub_en_i)
  begin
    wen <= '0';
    waddr <= (others => 'X');
    wdata_ecc <= (others => 'X');
    n_wdone <= '0';
    ren <= '0';
    raddr <= (others => 'X');
    n_r1_done <= '0';
    n_r2_done <= '0';
    n_scrub_done <= '0';

    n_ecc_one <= '0';
    n_ecc_fatal <= '0';
    n_scrub_se <= '0';
    n_scrub_de <= '0';
    n_state <= state;
    n_last_raddr <= (others => 'X');

    case state is
      when S_READ =>
        if (r2_done = '1' or r1_done = '1' or scrub_done = '1') and rerr = '1' then
          --  There was an error on the last access.
          --  Write to fix it.
          wdata_ecc <= f_fix_error(rsyndrome, rdata_ecc(38 downto 32), rdata_ecc(31 downto 0));
          waddr <= last_raddr;
          n_last_raddr <= last_raddr;
          if rerr_one = '1' then
            --  Correctable: correct it.
            n_scrub_se <= scrub_done;
            n_ecc_one <= not scrub_done;
            wen <= '1';
            n_state <= S_REWRITE;
          else
            --  Uncorrectable.  Do not fix.  Either it is a fatal error, or it will be when
            --  accessed by the cpu.  A double-error detected by the scrubber is not fatal.
            n_scrub_de <= scrub_done;
            n_ecc_fatal <= not scrub_done;
          end if;
        elsif we_i = '1' then
          --  Write
          waddr <= waddr_i;
          wen <= '1';
          wdata_ecc <= wecc_i & wdata_i;
          n_wdone <= '1';
          n_state <= S_READ;
        elsif scrub_rd = '1' and scrub_done = '0' then
          -- scrubber
          raddr <= scrub_addr;
          ren <= '1';
          n_scrub_done <= '1';
          n_last_raddr <= scrub_addr;
        elsif r1_en_i = '1' then
          --  Read (as data).
          raddr <= r1_addr_i;
          ren <= '1';
          n_r1_done <= '1';
          n_last_raddr <= r1_addr_i;
        elsif r2_en_i = '1' then
          --  Read (as insn).
          raddr <= r2_addr_i;
          ren <= '1';
          n_r2_done <= '1';
          n_last_raddr <= r2_addr_i;
        elsif scrub_en_i = '1' and scrub_cycle = '0' then
          -- scrub if idle (but not during reset, as write is high priority
          -- and is a single pulse).
          raddr <= scrub_addr;
          ren <= '1';
          n_scrub_done <= '1';
          n_last_raddr <= scrub_addr;
        end if;
      when S_REWRITE =>
        --  Reread
        raddr <= last_raddr;
        ren <= '1';
        n_r1_done <= r1_done_d;
        if r2_done_d = '1' and r2_en_i = '1' and last_raddr = r2_addr_i then
          --  If the same instruction is being read, return the corrected data.
          n_r2_done <= '1';
        end if;
        n_last_raddr <= last_raddr;
        n_state <= S_READ;
    end case;
  end process;

  p_ctrl_reg: process (clk_i)
  begin
    if rising_edge(clk_i) then
      if rst_n_i = '0' then
        r1_done <= '0';
        r1_done_d <= '0';
        r2_done <= '0';
        r2_done_d <= '0';
        scrub_done <= '0';
        state <= S_READ;
        last_raddr <= (others => 'X');

        ecc_one_o <= '0';
        ecc_fatal_o <= '0';
        scrub_se_o <= '0';
        scrub_de_o <= '0';
      else
        r1_done_d <= r1_done;
        r1_done <= n_r1_done;
        r2_done_d <= r2_done;
        r2_done <= n_r2_done;
        scrub_done <= n_scrub_done;

        state <= n_state;
        last_raddr <= n_last_raddr;

        ecc_one_o <= n_ecc_one;
        ecc_fatal_o <= n_ecc_fatal;
        if (n_ecc_one or n_scrub_se or n_ecc_fatal) = '1' then
          ecc_addr_o <= n_last_raddr;
        end if;
        scrub_se_o <= n_scrub_se;
        scrub_de_o <= n_scrub_de;
      end if;

      wdone <= n_wdone;
    end if;
  end process;
end arch;