-- #################################################################################################
-- # << NEORV32 CPU - Physical Memory Protection Unit >>                                           #
-- # ********************************************************************************************* #
-- # Compatible to the RISC-V PMP privilege architecture specifications.                           #
-- # ********************************************************************************************* #
-- # BSD 3-Clause License                                                                          #
-- #                                                                                               #
-- # The NEORV32 RISC-V Processor, https://github.com/stnolting/neorv32                            #
-- # Copyright (c) 2024, Stephan Nolting. All rights reserved.                                     #
-- #                                                                                               #
-- # Redistribution and use in source and binary forms, with or without modification, are          #
-- # permitted provided that the following conditions are met:                                     #
-- #                                                                                               #
-- # 1. Redistributions of source code must retain the above copyright notice, this list of        #
-- #    conditions and the following disclaimer.                                                   #
-- #                                                                                               #
-- # 2. Redistributions in binary form must reproduce the above copyright notice, this list of     #
-- #    conditions and the following disclaimer in the documentation and/or other materials        #
-- #    provided with the distribution.                                                            #
-- #                                                                                               #
-- # 3. Neither the name of the copyright holder nor the names of its contributors may be used to  #
-- #    endorse or promote products derived from this software without specific prior written      #
-- #    permission.                                                                                #
-- #                                                                                               #
-- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS   #
-- # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF               #
-- # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE    #
-- # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,     #
-- # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE #
-- # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED    #
-- # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING     #
-- # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED  #
-- # OF THE POSSIBILITY OF SUCH DAMAGE.                                                            #
-- #################################################################################################

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

library neorv32;
use neorv32.neorv32_package.all;

entity neorv32_cpu_pmp is
  generic (
    NUM_REGIONS : natural range 0 to 16; -- number of regions (0..16)
    GRANULARITY : natural range 4 to natural'high; -- minimal region granularity in bytes, has to be a power of 2, min 4 bytes
    TOR_EN      : boolean; -- implement TOR mode
    NAP_EN      : boolean  -- implement NAPOT/NA4 modes
  );
  port (
    -- global control --
    clk_i       : in  std_ulogic; -- global clock, rising edge
    rstn_i      : in  std_ulogic; -- global reset, low-active, async
    ctrl_i      : in  ctrl_bus_t; -- main control bus
    -- CSR interface --
    csr_we_i    : in  std_ulogic; -- global write enable
    csr_addr_i  : in  std_ulogic_vector(11 downto 0); -- address
    csr_wdata_i : in  std_ulogic_vector(XLEN-1 downto 0); -- write data
    csr_rdata_o : out std_ulogic_vector(XLEN-1 downto 0); -- read data
    -- address input --
    addr_if_i   : in  std_ulogic_vector(XLEN-1 downto 0); -- instruction fetch address
    addr_ls_i   : in  std_ulogic_vector(XLEN-1 downto 0); -- load/store address
    -- faults --
    fault_ex_o  : out std_ulogic; -- instruction fetch fault
    fault_rw_o  : out std_ulogic  -- read/write access fault
  );
end neorv32_cpu_pmp;

architecture neorv32_cpu_pmp_rtl of neorv32_cpu_pmp is

  -- auto-configuration --
  constant granularity_valid_c : boolean := is_power_of_two_f(GRANULARITY);
  constant granularity_c       : natural := cond_sel_natural_f(granularity_valid_c, GRANULARITY, 2**index_size_f(GRANULARITY));

  -- PMP configuration register bits --
  constant cfg_r_c  : natural := 0; -- read permit
  constant cfg_w_c  : natural := 1; -- write permit
  constant cfg_x_c  : natural := 2; -- execute permit
  constant cfg_al_c : natural := 3; -- mode bit low
  constant cfg_ah_c : natural := 4; -- mode bit high
  constant cfg_rl_c : natural := 5; -- reserved
  constant cfg_rh_c : natural := 6; -- reserved
  constant cfg_l_c  : natural := 7; -- locked entry

  -- PMP modes --
  constant mode_off_c   : std_ulogic_vector(1 downto 0) := "00"; -- null region (disabled)
  constant mode_tor_c   : std_ulogic_vector(1 downto 0) := "01"; -- top of range
  constant mode_na4_c   : std_ulogic_vector(1 downto 0) := "10"; -- naturally aligned four-byte region
  constant mode_napot_c : std_ulogic_vector(1 downto 0) := "11"; -- naturally aligned power-of-two region (>= 8 bytes)

  -- PMP helpers --
  constant pmp_lsb_c : natural := index_size_f(granularity_c); -- min = 2

  -- PMP CSRs --
  type csr_cfg_t      is array (0 to NUM_REGIONS-1) of std_ulogic_vector(7 downto 0);
  type csr_addr_t     is array (0 to NUM_REGIONS-1) of std_ulogic_vector(XLEN-1 downto 0);
  type csr_cfg_rd_t   is array (0 to 15) of std_ulogic_vector(7 downto 0);
  type csr_cfg_rd32_t is array (0 to 03) of std_ulogic_vector(XLEN-1 downto 0);
  type csr_addr_rd_t  is array (0 to 15) of std_ulogic_vector(XLEN-1 downto 0);
  type csr_t is record
    we_cfg  : std_ulogic_vector(03 downto 0);
    we_addr : std_ulogic_vector(15 downto 0);
    cfg     : csr_cfg_t;
    addr    : csr_addr_t;
  end record;
  signal csr      : csr_t;
  signal cfg_rd   : csr_cfg_rd_t;
  signal cfg_rd32 : csr_cfg_rd32_t;
  signal addr_rd  : csr_addr_rd_t;

  -- PMP address extension to 34 bit --
  type xaddr_t is array (0 to NUM_REGIONS-1) of std_ulogic_vector(XLEN+1 downto 0);
  signal xaddr : xaddr_t;

  -- region access logic --
  type addr_mask_t is array (0 to NUM_REGIONS-1) of std_ulogic_vector(XLEN-1 downto pmp_lsb_c);
  signal addr_mask_napot, addr_mask : addr_mask_t;
  type region_t is record
    i_cmp_mm, d_cmp_mm : std_ulogic_vector(NUM_REGIONS-1 downto 0); -- masked match
    i_cmp_ge, d_cmp_ge : std_ulogic_vector(NUM_REGIONS-1 downto 0); -- greater or equal
    i_cmp_lt, d_cmp_lt : std_ulogic_vector(NUM_REGIONS-1 downto 0); -- less than
    i_match,  d_match  : std_ulogic_vector(NUM_REGIONS-1 downto 0); -- region address match
    perm_ex,  perm_rw  : std_ulogic_vector(NUM_REGIONS-1 downto 0); -- region's permission
  end record;
  signal region : region_t;

  -- permission check violation --
  signal fail_ex, fail_rw : std_ulogic_vector(NUM_REGIONS downto 0);

begin

  -- Sanity Checks --------------------------------------------------------------------------
  -- -------------------------------------------------------------------------------------------
  assert not (granularity_valid_c = false) report
    "[NEORV32] Auto-adjusting invalid PMP granularity configuration." severity warning;


  -- CSR Write Access -----------------------------------------------------------------------
  -- -------------------------------------------------------------------------------------------
  csr_we: process(csr_we_i, csr_addr_i) -- write enable decoder
  begin
    -- Configuration registers --
    csr.we_cfg <= (others => '0');
    if (csr_addr_i(11 downto 2) = csr_pmpcfg0_c(11 downto 2)) and (csr_we_i = '1') then
      csr.we_cfg(to_integer(unsigned(csr_addr_i(1 downto 0)))) <= '1';
    end if;
    -- Address registers --
    csr.we_addr <= (others => '0');
    if (csr_addr_i(11 downto 4) = csr_pmpaddr0_c(11 downto 4)) and (csr_we_i = '1') then
      csr.we_addr(to_integer(unsigned(csr_addr_i(3 downto 0)))) <= '1';
    end if;
  end process csr_we;

  -- PMP CSR registers --
  csr_reg_gen:
  for i in 0 to NUM_REGIONS-1 generate
    csr_reg: process(rstn_i, clk_i)
      variable mode_v : std_ulogic_vector(1 downto 0);
    begin
      if (rstn_i = '0') then
        csr.cfg(i)  <= (others => '0');
        csr.addr(i) <= (others => '0');
      elsif rising_edge(clk_i) then

        -- configuration --
        if (csr.we_cfg(i/4) = '1') and (csr.cfg(i)(7) = '0') then -- unlocked write access
          csr.cfg(i)(cfg_r_c) <= csr_wdata_i((i mod 4)*8+0); -- R (read)
          csr.cfg(i)(cfg_w_c) <= csr_wdata_i((i mod 4)*8+1); -- W (write)
          csr.cfg(i)(cfg_x_c) <= csr_wdata_i((i mod 4)*8+2); -- X (execute)
          -- A (mode) --
          mode_v := csr_wdata_i((i mod 4)*8+4 downto (i mod 4)*8+3);
          if ((mode_v = mode_tor_c)   and (TOR_EN = false)) or -- TOR mode not implemented
             ((mode_v = mode_na4_c)   and (NAP_EN = false)) or -- NA4 mode not implemented
             ((mode_v = mode_napot_c) and (NAP_EN = false)) or -- NAPOT mode not implemented
             ((mode_v = mode_na4_c)   and (granularity_c > 4)) then -- NA4 not available
            csr.cfg(i)(cfg_ah_c downto cfg_al_c) <= mode_off_c;
          else -- valid configuration
            csr.cfg(i)(cfg_ah_c downto cfg_al_c) <= mode_v;
          end if;
          --
          csr.cfg(i)(cfg_rl_c) <= '0'; -- reserved
          csr.cfg(i)(cfg_rh_c) <= '0'; -- reserved
          csr.cfg(i)(cfg_l_c)  <= csr_wdata_i((i mod 4)*8+7); -- L (locked)
        end if;

        -- address --
        if (csr.we_addr(i) = '1') and (csr.cfg(i)(cfg_l_c) = '0') then -- unlocked write access
          if (i < NUM_REGIONS-1) then
            if (csr.cfg(i+1)(cfg_l_c) = '0') or (csr.cfg(i+1)(cfg_ah_c downto cfg_al_c) /= mode_tor_c) then -- cfg(i+1) not "LOCKED TOR"
              csr.addr(i) <= "00" & csr_wdata_i(XLEN-3 downto 0);
            end if;
          else -- very last entry
            csr.addr(i) <= "00" & csr_wdata_i(XLEN-3 downto 0);
          end if;
        end if;

      end if;
    end process csr_reg;
  end generate;


  -- CSR Read Access ------------------------------------------------------------------------
  -- -------------------------------------------------------------------------------------------
  csr_read_access: process(csr_addr_i, cfg_rd32, addr_rd)
  begin
    if (csr_addr_i(11 downto 5) = csr_pmpcfg0_c(11 downto 5)) then -- PMP CSR
      if (csr_addr_i(4) = '0') then -- PMP configuration CSR
        csr_rdata_o <= cfg_rd32(to_integer(unsigned(csr_addr_i(1 downto 0))));
      else -- PMP address CSR
        csr_rdata_o <= addr_rd(to_integer(unsigned(csr_addr_i(3 downto 0))));
      end if;
    else
      csr_rdata_o <= (others => '0');
    end if;
  end process csr_read_access;

  -- CSR read-back --
  csr_read_back_gen:
  for i in 0 to NUM_REGIONS-1 generate
    -- configuration --
    cfg_rd(i) <= csr.cfg(i);
    -- address --
    address_read_back: process(csr)
    begin
      addr_rd(i) <= (others => '0');
      addr_rd(i)(XLEN-1 downto pmp_lsb_c-2) <= csr.addr(i)(XLEN-1 downto pmp_lsb_c-2);
      if (granularity_c = 8) and TOR_EN then -- bit G-1 reads as zero in TOR or OFF mode
        if (csr.cfg(i)(cfg_ah_c) = '0') then -- TOR/OFF mode
          addr_rd(i)(pmp_lsb_c) <= '0';
        end if;
      elsif (granularity_c > 8) then
        if NAP_EN then
          addr_rd(i)(pmp_lsb_c-2 downto 0) <= (others => '1'); -- in NAPOT mode bits G-2:0 must read as one
        end if;
        if TOR_EN then
          if (csr.cfg(i)(cfg_ah_c) = '0') then -- TOR/OFF mode
            addr_rd(i)(pmp_lsb_c-1 downto 0) <= (others => '0'); -- in TOR or OFF mode bits G-1:0 must read as zero
          end if;
        end if;
      end if;
    end process address_read_back;
  end generate;

  -- terminate unused CSR read-backs --
  csr_read_back_terminate:
  for i in NUM_REGIONS to 15 generate
    cfg_rd(i)  <= (others => '0');
    addr_rd(i) <= (others => '0');
  end generate;

  -- pack configuration read-back --
  csr_read_back_pack:
  for i in 0 to 3 generate
    cfg_rd32(i) <= cfg_rd(i*4+3) & cfg_rd(i*4+2) & cfg_rd(i*4+1) & cfg_rd(i*4+0);
  end generate;


  -- Region Access Logic --------------------------------------------------------------------
  -- -------------------------------------------------------------------------------------------
  region_gen:
  for r in 0 to NUM_REGIONS-1 generate

    -- extend region addresses to 34-bit --
    xaddr(r) <= csr.addr(r) & "00"; -- mask byte offset


    nap_mode_enable:
    if NAP_EN generate

      -- compute address masks for NAPOT mode --
      addr_mask_napot(r)(pmp_lsb_c) <= '0';
      addr_mask_napot_gen:
      for i in pmp_lsb_c+1 to XLEN-1 generate
        addr_mask_napot(r)(i) <= addr_mask_napot(r)(i-1) or (not xaddr(r)(i-1));
      end generate;

      -- address mask select --
      addr_masking: process(rstn_i, clk_i)
      begin
        if (rstn_i = '0') then
          addr_mask(r) <= (others => '0');
        elsif rising_edge(clk_i) then
          if (csr.cfg(r)(cfg_al_c) = '1') then -- NAPOT
            addr_mask(r) <= addr_mask_napot(r);
          else -- NA4
            addr_mask(r) <= (others => '1');
          end if;
        end if;
      end process addr_masking;

    end generate;


    -- check region address match --
    -- NA4 and NAPOT --
    region.i_cmp_mm(r) <= '1' when ((addr_if_i(XLEN-1 downto pmp_lsb_c) and addr_mask(r)) = (xaddr(r)(XLEN-1 downto pmp_lsb_c) and addr_mask(r))) and NAP_EN else '0';
    region.d_cmp_mm(r) <= '1' when ((addr_ls_i(XLEN-1 downto pmp_lsb_c) and addr_mask(r)) = (xaddr(r)(XLEN-1 downto pmp_lsb_c) and addr_mask(r))) and NAP_EN else '0';
    -- TOR region 0 --
    addr_match_r0_gen:
    if (r = 0) generate -- first entry: use ZERO as base and current entry as bound
      region.i_cmp_ge(r) <= '1' when TOR_EN else '0'; -- address is always greater than or equal to zero (and TOR mode enabled)
      region.i_cmp_lt(r) <= '0'; -- unused
      region.d_cmp_ge(r) <= '1' when TOR_EN else '0'; -- address is always greater than or equal to zero (and TOR mode enabled)
      region.d_cmp_lt(r) <= '0'; -- unused
    end generate;
    -- TOR region any --
    addr_match_rx_gen:
    if (r > 0) generate -- use previous entry as base and current entry as bound
      region.i_cmp_ge(r) <= '1' when (unsigned(addr_if_i(XLEN-1 downto pmp_lsb_c)) >= unsigned(xaddr(r-1)(XLEN-1 downto pmp_lsb_c))) and TOR_EN else '0';
      region.i_cmp_lt(r) <= '1' when (unsigned(addr_if_i(XLEN-1 downto pmp_lsb_c)) <  unsigned(xaddr(r  )(XLEN-1 downto pmp_lsb_c))) and TOR_EN else '0';
      region.d_cmp_ge(r) <= '1' when (unsigned(addr_ls_i(XLEN-1 downto pmp_lsb_c)) >= unsigned(xaddr(r-1)(XLEN-1 downto pmp_lsb_c))) and TOR_EN else '0';
      region.d_cmp_lt(r) <= '1' when (unsigned(addr_ls_i(XLEN-1 downto pmp_lsb_c)) <  unsigned(xaddr(r  )(XLEN-1 downto pmp_lsb_c))) and TOR_EN else '0';
    end generate;


    -- check region match according to configured mode --
    match_gen: process(csr, region)
    begin
      case csr.cfg(r)(cfg_ah_c downto cfg_al_c) is
        when mode_off_c => -- entry disabled
          region.i_match(r) <= '0';
          region.d_match(r) <= '0';
        when mode_tor_c => -- top of region
          if TOR_EN then -- TOR mode implemented?
            if (r = (NUM_REGIONS-1)) then -- very last entry
              region.i_match(r) <= region.i_cmp_ge(r) and region.i_cmp_lt(r);
              region.d_match(r) <= region.d_cmp_ge(r) and region.d_cmp_lt(r);
            else -- this saves a LOT of comparators
              region.i_match(r) <= region.i_cmp_ge(r) and (not region.i_cmp_ge(r+1));
              region.d_match(r) <= region.d_cmp_ge(r) and (not region.d_cmp_ge(r+1));
            end if;
          else
            region.i_match(r) <= '0';
            region.d_match(r) <= '0';
          end if;
        when others => -- naturally-aligned region
          if NAP_EN then -- NAPOT/NA4 modes implemented?
            region.i_match(r) <= region.i_cmp_mm(r);
            region.d_match(r) <= region.d_cmp_mm(r);
          else
            region.i_match(r) <= '0';
            region.d_match(r) <= '0';
          end if;
        end case;
    end process match_gen;


    -- compute region permissions --
    perm_gen: process(csr.cfg, ctrl_i)
    begin
      -- execute (X) --
      if (ctrl_i.cpu_priv = priv_mode_m_c) then -- M mode: always allow if lock bit
        region.perm_ex(r) <= csr.cfg(r)(cfg_x_c) or (not csr.cfg(r)(cfg_l_c));
      else -- U mode: check actual permission
        region.perm_ex(r) <= csr.cfg(r)(cfg_x_c);
      end if;
      -- read (R) --
      if (ctrl_i.lsu_rw = '0') then
        if (ctrl_i.lsu_priv = priv_mode_m_c) then -- M mode: always allow if lock bit
          region.perm_rw(r) <= csr.cfg(r)(cfg_r_c) or (not csr.cfg(r)(cfg_l_c));
        else -- U mode: check actual permission
          region.perm_rw(r) <= csr.cfg(r)(cfg_r_c);
        end if;
      -- write (W) --
      else
        if (ctrl_i.lsu_priv = priv_mode_m_c) then -- M mode: always allow if lock bit
          region.perm_rw(r) <= csr.cfg(r)(cfg_w_c) or (not csr.cfg(r)(cfg_l_c));
        else -- U mode: check actual permission
          region.perm_rw(r) <= csr.cfg(r)(cfg_w_c);
        end if;
      end if;
    end process perm_gen;

  end generate;


  -- Access Permission Check ----------------------------------------------------------------
  -- -------------------------------------------------------------------------------------------

  -- check for access fault (using static prioritization) --
  fail_ex(NUM_REGIONS) <= '1' when (ctrl_i.cpu_priv /= priv_mode_m_c) else '0'; -- default (if not match): fault if not M-mode
  fail_rw(NUM_REGIONS) <= '1' when (ctrl_i.lsu_priv /= priv_mode_m_c) else '0'; -- default (if not match): fault if not M-mode
  -- this is a *structural* description of a prioritization logic implemented as a multiplexer chain --
  fault_check_gen:
  for r in NUM_REGIONS-1 downto 0 generate -- start with lowest priority
    fail_ex(r) <= not region.perm_ex(r) when (region.i_match(r) = '1') else fail_ex(r+1);
    fail_rw(r) <= not region.perm_rw(r) when (region.d_match(r) = '1') else fail_rw(r+1);
  end generate;


  -- final access check --
  access_check: process(rstn_i, clk_i)
  begin
    if (rstn_i = '0') then
      fault_ex_o <= '0';
      fault_rw_o <= '0';
    elsif rising_edge(clk_i) then
      fault_ex_o <= (not ctrl_i.cpu_debug) and fail_ex(0); -- ignore PMP rules when in debug mode
      fault_rw_o <= (not ctrl_i.cpu_debug) and fail_rw(0);
    end if;
  end process access_check;


end neorv32_cpu_pmp_rtl;
