module axi_lite_regs #(
  /// The size of the register field in bytes.
  parameter int unsigned RegNumBytes = 32'd0,
  /// Address width of the AXI4-Lite port.
  ///
  /// The minimum value of this parameter is `$clog2(RegNumBytes)`.
  parameter int unsigned AxiAddrWidth = 32'd0,
  /// Data width of the AXI4-Lite port.
  parameter int unsigned AxiDataWidth = 32'd0,
  /// Only allow *privileged* accesses on the AXI4-Lite port.
  ///
  /// If this parameter is set to `1`, this module only allows reads and writes that have the
  /// `AxProt[0]` bit set.  If a transaction does not have the `AxProt[0]` bit set, this module
  /// replies with `SLVERR` and does not read or write register data.
  parameter bit PrivProtOnly = 1'b0,
  /// Only allow *secure* accesses on the AXI4-Lite port.
  ///
  /// If this parameter is set to `1`, this module only allows reads and writes that have the
  /// `AxProt[1]` bit set.  If a transaction does not have the `AxProt[1]` bit set, this module
  /// replies with `SLVERR` and does not read or write register data.
  parameter bit SecuProtOnly = 1'b0,
  /// Define individual bytes as *read-only from the AXI4-Lite port*.
  ///
  /// This parameter is an array with one bit for each byte.  If that bit is `0`, the byte can be
  /// read and written on the AXI4-Lite port; if that bit is `1`, the byte can only be read on the
  /// AXI4-Lite port.
  parameter logic [RegNumBytes-1:0] AxiReadOnly = {RegNumBytes{1'b0}},
  /// Constant (=**do not overwrite!**); type of a byte is 8 bit.
  parameter type byte_t = logic [7:0],
  /// Reset value for the whole register array.
  ///
  /// This parameter is an array with one byte value for each byte.  At reset, each byte is
  /// assigned its value from this array.
  parameter byte_t [RegNumBytes-1:0] RegRstVal = {RegNumBytes{8'h00}},
  /// Request struct of the AXI4-Lite port.
  parameter type req_lite_t = logic,
  /// Response struct of the AXI4-Lite port.
  parameter type resp_lite_t = logic
) (
  /// Rising-edge clock of all ports
  input  logic clk_i,
  /// Asynchronous reset, active low
  input  logic rst_ni,
  /// AXI4-Lite slave request
  input  req_lite_t axi_req_i,
  /// AXI4-Lite slave response
  output resp_lite_t axi_resp_o,
  /// Signals that a byte is being written from the AXI4-Lite port in the current clock cycle.  This
  /// signal is asserted regardless of the value of `AxiReadOnly` and can therefore be used by
  /// surrounding logic to react to write-on-read-only-byte errors.
  output logic [RegNumBytes-1:0] wr_active_o,
  /// Signals that a byte is being read from the AXI4-Lite port in the current clock cycle.
  output logic [RegNumBytes-1:0] rd_active_o,
  /// Input value of each byte.  If `reg_load_i` is `1` for a byte in the current clock cycle, the
  /// byte register in this module is set to the value of the byte in `reg_d_i` at the next clock
  /// edge.
  input  byte_t [RegNumBytes-1:0] reg_d_i,
  /// Load enable of each byte.
  ///
  /// If `reg_load_i` is `1` for a byte defined as non-read-only in a clock cycle, an AXI4-Lite
  /// write transaction is stalled when it tries to write the same byte.  That is, a write
  /// transaction is stalled if all of the following conditions are true for the byte at index `i`:
  /// - `AxiReadOnly[i]` is `0`,
  /// - `reg_load_i[i]` is `1`,
  /// - the bit in `axi_req_i.w.strb` that affects the byte is `1`.
  ///
  /// If unused, set this input to `'0`.
  input  logic  [RegNumBytes-1:0] reg_load_i,
  /// The registered value of each byte.
  output byte_t [RegNumBytes-1:0] reg_q_o
);

  // Define the number of register chunks needed to map all `RegNumBytes` to the AXI channel.
  // Eg: `AxiDataWidth == 32'd32`
  // AXI strb:                       3 2 1 0
  //                                 | | | |
  //             *---------*---------* | | |
  //             | *-------|-*-------|-* | |
  //             | | *-----|-|-*-----|-|-* |
  //             | | | *---|-|-|-*---|-|-|-*
  //             | | | |   | | | |   | | | |
  // Reg byte:   B A 9 8   7 6 5 4   3 2 1 0
  //           | chunk_2 | chunk_1 | chunk_0 |
  localparam int unsigned AxiStrbWidth  = AxiDataWidth / 32'd8;
  localparam int unsigned NumChunks     = cf_math_pkg::ceil_div(RegNumBytes, AxiStrbWidth);
  localparam int unsigned ChunkIdxWidth = (NumChunks > 32'd1) ? $clog2(NumChunks) : 32'd1;
  // Type of the index to identify a specific register chunk.
  typedef logic [ChunkIdxWidth-1:0] chunk_idx_t;

  // Find out how many bits of the address are applicable for this module.
  // Look at the `AddrWidth` number of LSBs to calculate the multiplexer index of the AXI.
  localparam int unsigned AddrWidth = (RegNumBytes > 32'd1) ? ($clog2(RegNumBytes)+1) : 32'd2;
  typedef logic [AddrWidth-1:0] addr_t;

  // Define the address map which maps each register chunk onto an AXI address.
  typedef struct packed {
    int unsigned idx;
    addr_t       start_addr;
    addr_t       end_addr;
  } axi_rule_t;
  axi_rule_t    [NumChunks-1:0] addr_map;
  for (genvar i = 0; i < NumChunks; i++) begin : gen_addr_map
    assign addr_map[i] = axi_rule_t'{
      idx:        i,
      start_addr: addr_t'( i   * AxiStrbWidth),
      end_addr:   addr_t'((i+1)* AxiStrbWidth)
    };
  end

  // Channel definitions for spill register
  typedef logic [AxiDataWidth-1:0] axi_data_t;
  `AXI_LITE_TYPEDEF_B_CHAN_T(b_chan_lite_t)
  `AXI_LITE_TYPEDEF_R_CHAN_T(r_chan_lite_t, axi_data_t)

  // Register array declarations
  byte_t [RegNumBytes-1:0] reg_q,        reg_d;
  logic  [RegNumBytes-1:0] reg_update;

  // Write logic
  chunk_idx_t              aw_chunk_idx;
  logic                    aw_dec_valid;
  b_chan_lite_t            b_chan;
  logic                    b_valid,      b_ready;
  logic                    aw_prot_ok;
  logic                    chunk_loaded, chunk_ro;

  // Flag for telling that the protection level is the right one.
  assign aw_prot_ok = (PrivProtOnly ? axi_req_i.aw.prot[0] : 1'b1) &
                      (SecuProtOnly ? axi_req_i.aw.prot[1] : 1'b1);
  // Have a flag which is true if any of the bytes inside a chunk are directly loaded.
  logic [AxiStrbWidth-1:0] load;
  logic [AxiStrbWidth-1:0] read_only;
  // Address of the lowest byte byte of a chunk accessed by an AXI write transaction.
  addr_t byte_w_addr;
  assign byte_w_addr = addr_t'(aw_chunk_idx * AxiStrbWidth);

  for (genvar i = 0; i < AxiStrbWidth; i++) begin : gen_load_assign
    // Indexed byte address
    addr_t reg_w_idx;
    assign reg_w_idx = byte_w_addr + addr_t'(i);
    // Only assert load flag for non read only bytes.
    assign load[i]      = (reg_w_idx < RegNumBytes) ?
        (reg_load_i[reg_w_idx] && !AxiReadOnly[reg_w_idx]) : 1'b0;
    // Flag to find out that all bytes of the chunk are read only.
    assign read_only[i] = (reg_w_idx < RegNumBytes) ? AxiReadOnly[reg_w_idx] : 1'b1;
  end
  // Only assert the loaded flag if there could be a load conflict between a strobe and load
  // signal.
  assign chunk_loaded = |(load & axi_req_i.w.strb);
  assign chunk_ro     = &read_only;


  // Register write logic.
  always_comb begin
    automatic addr_t reg_byte_idx = '0;
    // default assignments
    reg_d               = reg_q;
    reg_update          = '0;
    // Channel handshake
    axi_resp_o.aw_ready = 1'b0;
    axi_resp_o.w_ready  = 1'b0;
    // Response
    b_chan              = b_chan_lite_t'{resp: axi_pkg::RESP_SLVERR, default: '0};
    b_valid             = 1'b0;
    // write active flag
    wr_active_o         = '0;

    // Control
    // Handle all non AXI register loads.
    for (int unsigned i = 0; i < RegNumBytes; i++) begin
      if (reg_load_i[i]) begin
        reg_d[i]      = reg_d_i[i];
        reg_update[i] = 1'b1;
      end
    end

    // Handle load from AXI write.
    // `b_ready` is allowed to be a condition as it comes from a spill register.
    if (axi_req_i.aw_valid && axi_req_i.w_valid && b_ready) begin
      // The write can be performed when these conditions are true:
      // - AW decode is valid.
      // - `axi_req_i.aw.prot` has the right value.
      if (aw_dec_valid && aw_prot_ok) begin
        // Stall write as long as any direct load is going on in the current chunk.
        // Read-only bytes within a chunk have no influence on stalling.
        if (!chunk_loaded) begin
          // Go through all bytes on the W channel.
          for (int unsigned i = 0; i < AxiStrbWidth; i++) begin
            reg_byte_idx = byte_w_addr + addr_t'(i);
            // Only execute if the byte is mapped onto the register array.
            if (reg_byte_idx < RegNumBytes) begin
              // Only update the reg from an AXI write if it is not `ReadOnly`.
              // Only connect the data and load to the reg, if the byte is written from AXI.
              // This allows for simultaneous direct load onto unwritten bytes.
              if (!AxiReadOnly[reg_byte_idx] && axi_req_i.w.strb[i]) begin
                reg_d[reg_byte_idx]      = axi_req_i.w.data[8*i+:8];
                reg_update[reg_byte_idx] = 1'b1;
              end
              wr_active_o[reg_byte_idx] = axi_req_i.w.strb[i];
            end
          end
          b_chan.resp         = chunk_ro ? axi_pkg::RESP_SLVERR : axi_pkg::RESP_OKAY;
          b_valid             = 1'b1;
          axi_resp_o.aw_ready = 1'b1;
          axi_resp_o.w_ready  = 1'b1;
        end
      end else begin
        // Send default B error response on each not allowed write transaction.
        b_valid             = 1'b1;
        axi_resp_o.aw_ready = 1'b1;
        axi_resp_o.w_ready  = 1'b1;
      end
    end
  end

  // Read logic
  chunk_idx_t   ar_chunk_idx;
  logic         ar_dec_valid;
  r_chan_lite_t r_chan;
  logic         r_valid,      r_ready;
  logic         ar_prot_ok;
  assign ar_prot_ok = (PrivProtOnly ? axi_req_i.ar.prot[0] : 1'b1) &
                      (SecuProtOnly ? axi_req_i.ar.prot[1] : 1'b1);
  // Multiplexer to determine R channel
  always_comb begin
    automatic int unsigned reg_byte_idx = '0;
    // Default R channel throws an error.
    r_chan = r_chan_lite_t'{
      data: axi_data_t'(32'hBA5E1E55),
      resp: axi_pkg::RESP_SLVERR,
      default: '0
    };
    // Default nothing is reading the registers
    rd_active_o = '0;
    // Read is valid on a chunk
    if (ar_dec_valid && ar_prot_ok) begin
      // Calculate the corresponding byte index from `ar_chunk_idx`.
      for (int unsigned i = 0; i < AxiStrbWidth; i++) begin
        reg_byte_idx = unsigned'(ar_chunk_idx) * AxiStrbWidth + i;
        // Guard to not index outside the `reg_q_o` array.
        if (reg_byte_idx < RegNumBytes) begin
          r_chan.data[8*i+:8]       = reg_q_o[reg_byte_idx];
          rd_active_o[reg_byte_idx] = r_valid & r_ready;
        end else begin
          r_chan.data[8*i+:8] = 8'h00;
        end
      end
      r_chan.resp = axi_pkg::RESP_OKAY;
    end
  end

  assign r_valid             = axi_req_i.ar_valid; // to spill register
  assign axi_resp_o.ar_ready = r_ready;            // from spill register

  // Register array mapping, even read only register can be loaded over `reg_load_i`.
  for (genvar i = 0; i < RegNumBytes; i++) begin : gen_rw_regs
    `FFLARN(reg_q[i], reg_d[i], reg_update[i], RegRstVal[i], clk_i, rst_ni)
    assign reg_q_o[i] = reg_q[i];
  end

  addr_decode #(
    .NoIndices ( NumChunks  ),
    .NoRules   ( NumChunks  ),
    .addr_t    ( addr_t     ),
    .rule_t    ( axi_rule_t )
  ) i_aw_decode (
    .addr_i           ( addr_t'(axi_req_i.aw.addr) ), // Only look at the `AddrWidth` LSBs.
    .addr_map_i       ( addr_map                   ),
    .idx_o            ( aw_chunk_idx               ),
    .dec_valid_o      ( aw_dec_valid               ),
    .dec_error_o      ( /*not used*/               ),
    .en_default_idx_i ( '0                         ),
    .default_idx_i    ( '0                         )
  );

  addr_decode #(
    .NoIndices ( NumChunks  ),
    .NoRules   ( NumChunks  ),
    .addr_t    ( addr_t     ),
    .rule_t    ( axi_rule_t )
  ) i_ar_decode (
    .addr_i           ( addr_t'(axi_req_i.ar.addr) ), // Only look at the `AddrWidth` LSBs.
    .addr_map_i       ( addr_map                   ),
    .idx_o            ( ar_chunk_idx               ),
    .dec_valid_o      ( ar_dec_valid               ),
    .dec_error_o      ( /*not used*/               ),
    .en_default_idx_i ( '0                         ),
    .default_idx_i    ( '0                         )
  );

  // Add a cycle delay on AXI response, cut all comb paths between slave port inputs and outputs.
  spill_register #(
    .T      ( b_chan_lite_t ),
    .Bypass ( 1'b0          )
  ) i_b_spill_register (
    .clk_i,
    .rst_ni,
    .valid_i ( b_valid            ),
    .ready_o ( b_ready            ),
    .data_i  ( b_chan             ),
    .valid_o ( axi_resp_o.b_valid ),
    .ready_i ( axi_req_i.b_ready  ),
    .data_o  ( axi_resp_o.b       )
  );

  // Add a cycle delay on AXI response, cut all comb paths between slave port inputs and outputs.
  spill_register #(
    .T      ( r_chan_lite_t ),
    .Bypass ( 1'b0          )
  ) i_r_spill_register (
    .clk_i,
    .rst_ni,
    .valid_i ( r_valid            ),
    .ready_o ( r_ready            ),
    .data_i  ( r_chan             ),
    .valid_o ( axi_resp_o.r_valid ),
    .ready_i ( axi_req_i.r_ready  ),
    .data_o  ( axi_resp_o.r       )
  );

  // Validate parameters.
  // pragma translate_off
  `ifndef VERILATOR
    initial begin: p_assertions
      assert (RegNumBytes > 32'd0) else
          $fatal(1, "The number of bytes must be at least 1!");
      assert (AxiAddrWidth >= AddrWidth) else
          $fatal(1, "AxiAddrWidth is not wide enough, has to be at least %0d-bit wide!", AddrWidth);
      assert ($bits(axi_req_i.aw.addr) == AxiAddrWidth) else
          $fatal(1, "AddrWidth does not match req_i.aw.addr!");
      assert ($bits(axi_req_i.ar.addr) == AxiAddrWidth) else
          $fatal(1, "AddrWidth does not match req_i.ar.addr!");
      assert (AxiDataWidth == $bits(axi_req_i.w.data)) else
          $fatal(1, "AxiDataWidth has to be: AxiDataWidth == $bits(axi_req_i.w.data)!");
      assert (AxiDataWidth == $bits(axi_resp_o.r.data)) else
          $fatal(1, "AxiDataWidth has to be: AxiDataWidth == $bits(axi_resp_o.r.data)!");
      assert (RegNumBytes == $bits(AxiReadOnly)) else
          $fatal(1, "Each register needs a `ReadOnly` flag!");
    end
    default disable iff (~rst_ni);
    for (genvar i = 0; i < RegNumBytes; i++) begin
      assert property (@(posedge clk_i) (!reg_load_i[i] && AxiReadOnly[i] |=> $stable(reg_q_o[i])))
          else $fatal(1, "Read-only register at `byte_index: %0d` was changed by AXI!", i);
    end
  `endif
  // pragma translate_on
endmodule