// Copyright lowRISC contributors (OpenTitan project).
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
//
// Generic Asynchronous SRAM FIFO (Dual port SRAM)

`include "prim_assert.sv"

module prim_fifo_async_sram_adapter #(
  parameter int unsigned Width = 32,
  parameter int unsigned Depth = 16,

  // SRAM parameters
  parameter int unsigned       SramAw = 16,

  // If SramDw > Width, upper data bits are 0.
  parameter int unsigned       SramDw = 32,
  parameter logic [SramAw-1:0] SramBaseAddr = 'h 0,

  // derived
  localparam int unsigned DepthW = $clog2(Depth+1)
) (
  // Write port
  input                     clk_wr_i,
  input                     rst_wr_ni,
  input                     wvalid_i,
  output logic              wready_o,
  input        [Width-1:0]  wdata_i,
  output logic [DepthW-1:0] wdepth_o,

  // Read port
  input                     clk_rd_i,
  input                     rst_rd_ni,
  output logic              rvalid_o,
  input                     rready_i,
  output logic [Width-1:0]  rdata_o,
  output logic [DepthW-1:0] rdepth_o,

  output logic r_full_o,
  output logic r_notempty_o,

  output logic w_full_o,

  // TODO: watermark(threshold) ?

  // SRAM interface
  // Write SRAM port
  output logic              w_sram_req_o,
  input                     w_sram_gnt_i,
  output logic              w_sram_write_o,
  output logic [SramAw-1:0] w_sram_addr_o,
  output logic [SramDw-1:0] w_sram_wdata_o,
  output logic [SramDw-1:0] w_sram_wmask_o,
  input                     w_sram_rvalid_i, // not used
  input        [SramDw-1:0] w_sram_rdata_i,  // not used
  input        [1:0]        w_sram_rerror_i, // not used

  // Read SRAM port
  output logic              r_sram_req_o,
  input                     r_sram_gnt_i,
  output logic              r_sram_write_o,
  output logic [SramAw-1:0] r_sram_addr_o,
  output logic [SramDw-1:0] r_sram_wdata_o, // not used
  output logic [SramDw-1:0] r_sram_wmask_o, // not used
  input                     r_sram_rvalid_i,
  input        [SramDw-1:0] r_sram_rdata_i,
  input        [1:0]        r_sram_rerror_i
);

  ////////////////
  // Definition //
  ////////////////
  // w_: write clock domain signals
  // r_: read clock domain signals

  // PtrVW: Pointer Value (without msb, flip) width
  localparam int unsigned PtrVW = $clog2(Depth);
  // PtrW: Read/Write pointer with flip bit
  localparam int unsigned PtrW  = PtrVW+1;

  ////////////
  // Signal //
  ////////////

  logic [PtrW-1:0]  w_wptr_q, w_wptr_d, w_wptr_gray_d, w_wptr_gray_q;
  logic [PtrW-1:0]  r_wptr_gray, r_wptr;
  logic [PtrVW-1:0] w_wptr_v, r_wptr_v;
  logic             w_wptr_p, r_wptr_p; // phase

  logic [PtrW-1:0]  r_rptr_q, r_rptr_d, r_rptr_gray_d, r_rptr_gray_q;
  logic [PtrW-1:0]  w_rptr_gray, w_rptr;
  logic [PtrVW-1:0] r_rptr_v, w_rptr_v;
  logic             r_rptr_p, w_rptr_p; // phase

  logic w_wptr_inc, r_rptr_inc;

  logic w_full, r_full, r_empty;

  // SRAM response one clock delayed. So store the value into read clock
  // domain
  logic             stored;
  logic [Width-1:0] rdata_q, rdata_d;

  // SRAM has another read pointer (for address of SRAM req)
  // It is -1 of r_rptr if stored, else same to r_rptr
  logic            r_sram_rptr_inc;
  logic [PtrW-1:0] r_sram_rptr;

  // r_sram_rptr == r_wptr
  // Used to determine r_sram_req
  logic r_sramrptr_empty;

  logic rfifo_ack; // Used to check if FIFO read interface consumes a data
  logic rsram_ack;

  //////////////
  // Datapath //
  //////////////

  // Begin: Write pointer sync to read clock ========================
  assign w_wptr_inc = wvalid_i & wready_o;

  assign w_wptr_d = w_wptr_q + PtrW'(1);

  always_ff @(posedge clk_wr_i or negedge rst_wr_ni) begin
    if (!rst_wr_ni) begin
      w_wptr_q      <= PtrW'(0);
      w_wptr_gray_q <= PtrW'(0);
    end else if (w_wptr_inc) begin
      w_wptr_q      <= w_wptr_d;
      w_wptr_gray_q <= w_wptr_gray_d;
    end
  end

  assign w_wptr_v = w_wptr_q[0+:PtrVW];
  assign w_wptr_p = w_wptr_q[PtrW-1];

  assign w_wptr_gray_d = dec2gray(w_wptr_d);

  prim_flop_2sync #(
    .Width (PtrW)
  ) u_sync_wptr_gray (
    .clk_i  (clk_rd_i),
    .rst_ni (rst_rd_ni),
    .d_i    (w_wptr_gray_q),
    .q_o    (r_wptr_gray)
  );

  assign r_wptr   = gray2dec(r_wptr_gray);
  assign r_wptr_p = r_wptr[PtrW-1];
  assign r_wptr_v = r_wptr[0+:PtrVW];

  assign wdepth_o = (w_wptr_p == w_rptr_p)
                  ? DepthW'(w_wptr_v - w_rptr_v)
                  : DepthW'({1'b1, w_wptr_v} - {1'b 0, w_rptr_v});
  // End:   Write pointer sync to read clock ------------------------

  // Begin: Read pointer sync to write clock ========================
  //assign r_rptr_inc = rvalid_o & rready_i;
  //assign r_rptr_inc = r_sram_req_o && r_sram_gnt_i;
  // Increase the read pointer (crossing the clock domain) only when the
  // reader acked.
  assign r_rptr_inc = rfifo_ack;

  assign r_rptr_d = r_rptr_q + PtrW'(1);

  always_ff @(posedge clk_rd_i or negedge rst_rd_ni) begin
    if (!rst_rd_ni) begin
      r_rptr_q      <= PtrW'(0);
      r_rptr_gray_q <= PtrW'(0);
    end else if (r_rptr_inc) begin
      r_rptr_q      <= r_rptr_d;
      r_rptr_gray_q <= r_rptr_gray_d;
    end
  end

  assign r_rptr_v = r_rptr_q[0+:PtrVW];
  assign r_rptr_p = r_rptr_q[PtrW-1];

  assign r_rptr_gray_d = dec2gray(r_rptr_d);

  prim_flop_2sync #(
    .Width (PtrW)
  ) u_sync_rptr_gray (
    .clk_i  (clk_wr_i),
    .rst_ni (rst_wr_ni),
    .d_i    (r_rptr_gray_q),
    .q_o    (w_rptr_gray)
  );

  assign w_rptr = gray2dec(w_rptr_gray);
  assign w_rptr_p = w_rptr[PtrW-1];
  assign w_rptr_v = w_rptr[0+:PtrVW];

  assign rdepth_o = (r_wptr_p == r_rptr_p)
                  ? DepthW'(r_wptr_v - r_rptr_v)
                  : DepthW'({1'b1, r_wptr_v} - {1'b 0, r_rptr_v});
  // End:   Read pointer sync to write clock ------------------------

  // Begin: SRAM Read pointer
  assign r_sram_rptr_inc = rsram_ack;

  always_ff @(posedge clk_rd_i or negedge rst_rd_ni) begin
    if (!rst_rd_ni) begin
      r_sram_rptr <= PtrW'(0);
    end else if (r_sram_rptr_inc) begin
      r_sram_rptr <= r_sram_rptr + PtrW'(1);
    end
  end

  assign r_sramrptr_empty = (r_wptr == r_sram_rptr);
  // End:   SRAM Read pointer

  // Full/ Empty
  // Lint complains PtrW'(1) << (PtrW-1). So changed as below
  localparam logic [PtrW-1:0] XorMask = {1'b 1, {PtrW-1{1'b0}}};
  assign w_full  = (w_wptr_q == (w_rptr   ^ XorMask));
  assign r_full  = (r_wptr   == (r_rptr_q ^ XorMask));
  assign r_empty = (r_wptr   == r_rptr_q);

  logic  unused_r_empty;
  assign unused_r_empty = r_empty;

  assign r_full_o     = r_full;
  assign w_full_o     = w_full;

  // The notempty status !(wptr == rptr) assert one clock earlier than the
  // actual `rvalid` signals.
  //
  // The reason is due to the SRAM read latency. The module uses SRAM FIFO
  // interface. When the logic in producer domain pushes entries, the pointer
  // is increased. This triggers the FIFO logic in the consumer clock domain
  // fetches data from SRAM.
  //
  // The pointer crosses the clock boundary. It takes usually two cycles (in
  // the consumer side). Then, as the read and write pointer in the read clock
  // domain has a gap by 1, the FIFO not empty status is raised.
  //
  // At this time, the logic just sent the read request to the SRAM. The data
  // is not yet read. The `rvalid` asserts when it receives data from the
  // SRAM.
  //
  // So, if the consumer reads data at the same cycle when notempty status is
  // raised, it reads incorrect data.
  assign r_notempty_o = rvalid_o;

  assign rsram_ack = r_sram_req_o && r_sram_gnt_i;
  assign rfifo_ack = rvalid_o     && rready_i;

  // SRAM Write Request
  assign w_sram_req_o   = wvalid_i && !w_full;
  assign wready_o       = !w_full && w_sram_gnt_i;
  assign w_sram_write_o = 1'b 1; // Always write
  assign w_sram_addr_o  = SramBaseAddr + SramAw'(w_wptr_v);

  assign w_sram_wdata_o = SramDw'(wdata_i);
  assign w_sram_wmask_o = SramDw'({Width{1'b1}});

  logic unused_w_sram;
  assign unused_w_sram = ^{w_sram_rvalid_i, w_sram_rdata_i, w_sram_rerror_i};

  // SRAM Read Request
  // Request Scenario (!r_empty):
  //  - storage empty: Send request if
  //               !r_sram_rvalid_i || (rfifo_ack && r_sram_rvalid_i);
  //  - storage !empty: depends on the rfifo_ack:
  //    - r_rptr_inc: Can request more
  //    - !r_rptr_inc: Can't request
  always_comb begin : r_sram_req
    // Karnaugh Map (!empty): sram_req
    // {sram_rv, rfifo_ack} | 00 | 01          | 11 | 10
    // ----------------------------------------------------------
    // stored          | 0  |  1 |  impossible |  1 |  0
    //                 | 1  |  0 |  1          |  X |  impossible
    //
    // req_o = r_ptr_inc || (!stored && !r_sram_rvalid_i)

    if (stored) begin
      // storage has data. depends on rfifo_ack
      // rfifo_ack can be replaced to rready_i as `rvalid_o` is 1
      r_sram_req_o = !r_sramrptr_empty && rfifo_ack;
    end else begin
      // storage has no data.
      // Can send request only when the reader accept the request or no
      // previous request sent out.
      r_sram_req_o = !r_sramrptr_empty && !(r_sram_rvalid_i ^ rfifo_ack);
    end
  end : r_sram_req

  assign rvalid_o       = stored || r_sram_rvalid_i;
  assign r_sram_write_o = 1'b 0; // always read
  assign r_sram_wdata_o = '0;
  assign r_sram_wmask_o = '0;

  // Send SRAM request with sram read pointer.
  assign r_sram_addr_o  = SramBaseAddr + SramAw'(r_sram_rptr[0+:PtrVW]);

  assign rdata_d = (r_sram_rvalid_i) ? r_sram_rdata_i[0+:Width] : Width'(0);

  assign rdata_o = (stored) ? rdata_q : rdata_d;

  logic unused_rsram;
  assign unused_rsram = ^{r_sram_rerror_i};

  if (Width < SramDw) begin : g_unused_rdata
    logic unused_rdata;
    assign unused_rdata = ^r_sram_rdata_i[SramDw-1:Width];
  end : g_unused_rdata

  // read clock domain rdata storage
  logic store_en;

  // Karnaugh Map (r_sram_rvalid_i):
  // rfifo_ack   | 0 | 1 |
  // ---------------------
  // stored    0 | 1 | 0 |
  //           1 | 0 | 1 |
  //
  // stored = s.r.v && XNOR(stored, rptr_inc)
  assign store_en = r_sram_rvalid_i && !(stored ^ rfifo_ack);

  always_ff @(posedge clk_rd_i or negedge rst_rd_ni) begin
    if (!rst_rd_ni) begin
      stored <= 1'b 0;
      rdata_q <= Width'(0);
    end else if (store_en) begin
      stored <= 1'b 1;
      rdata_q <= rdata_d;
    end else if (!r_sram_rvalid_i && rfifo_ack) begin
      // No request sent, host reads the data
      stored <= 1'b 0;
      rdata_q <= Width'(0);
    end
  end

  //////////////
  // Function //
  //////////////

  // dec2gray / gray2dec copied from prim_fifo_async.sv
  function automatic [PtrW-1:0] dec2gray(input logic [PtrW-1:0] decval);
    logic [PtrW-1:0] decval_sub;
    logic [PtrW-1:0] decval_in;
    logic            unused_decval_msb;

    decval_sub = (PtrW)'(Depth) - {1'b0, decval[PtrW-2:0]} - 1'b1;

    decval_in = decval[PtrW-1] ? decval_sub : decval;

    // We do not care about the MSB, hence we mask it out
    unused_decval_msb = decval_in[PtrW-1];
    decval_in[PtrW-1] = 1'b0;

    // Perform the XOR conversion
    dec2gray = decval_in;
    dec2gray ^= (decval_in >> 1);

    // Override the MSB
    dec2gray[PtrW-1] = decval[PtrW-1];
  endfunction

  // Algorithm walks up from 0..N-1 then flips the upper bit and walks down from N-1 to 0.
  function automatic [PtrW-1:0] gray2dec(input logic [PtrW-1:0] grayval);
    logic [PtrW-1:0] dec_tmp, dec_tmp_sub;
    logic            unused_decsub_msb;

    dec_tmp = '0;
    for (int i = PtrW-2; i >= 0; i--) begin
      dec_tmp[i] = dec_tmp[i+1] ^ grayval[i];
    end
    dec_tmp_sub = (PtrW)'(Depth) - dec_tmp - 1'b1;
    if (grayval[PtrW-1]) begin
      gray2dec = dec_tmp_sub;
      // Override MSB
      gray2dec[PtrW-1] = 1'b1;
      unused_decsub_msb = dec_tmp_sub[PtrW-1];
    end else begin
      gray2dec = dec_tmp;
    end
  endfunction

  ///////////////
  // Assertion //
  ///////////////

  `ASSERT_INIT(ParamCheckDepth_A, (Depth == 2**$clog2(Depth)))

  // Use FF if less than 4.
  `ASSERT_INIT(MinDepth_A, Depth >= 4)

  // SramDw greather than or equal to Width
  `ASSERT_INIT(WidthMatch_A, SramDw >= Width)

  // Not stored, Not read valid, but rptr_inc case is impossible
  `ASSERT(RptrIncDataValid_A,
          r_rptr_inc |-> stored || r_sram_rvalid_i,
          clk_rd_i, !rst_rd_ni)
  `ASSERT(SramRvalid_A,
          r_sram_rvalid_i |-> !stored || r_rptr_inc,
          clk_rd_i, !rst_rd_ni)

  // FIFO interface
  `ASSERT(NoWAckInFull_A, w_wptr_inc |-> !w_full,
          clk_wr_i, !rst_wr_ni)

  // If a valid/ready handshake happens for the write pointer, that pointer should increment by one
  `ASSERT(WptrIncrease_A,
          w_wptr_inc |=> w_wptr_q == $past(w_wptr_q) + PtrW'(1),
          clk_wr_i, !rst_wr_ni)
  // Check that the Gray coding works correctly, so the increment to w_wptr_gray_q changes exactly
  // one bit.
  `ASSERT(WptrGrayOneBitAtATime_A,
          w_wptr_inc |=> $countones(w_wptr_gray_q ^ $past(w_wptr_gray_q)) == 1,
          clk_wr_i, !rst_wr_ni)

  `ASSERT(NoRAckInEmpty_A, r_rptr_inc |-> !r_empty,
          clk_rd_i, !rst_rd_ni)

  `ASSERT(RptrIncrease_A,
          r_rptr_inc |=> PtrVW'($past(r_rptr_v) + 1) == r_rptr_v,
          clk_rd_i, !rst_rd_ni)
  `ASSERT(RptrGrayOneBitAtATime_A,
          r_rptr_inc |=> $countones(r_rptr_gray_q ^ $past(r_rptr_gray_q)) == 1,
          clk_rd_i, !rst_rd_ni)

  // SRAM interface
  `ASSERT(WSramRvalid_A, !w_sram_rvalid_i, clk_wr_i, !rst_wr_ni)
  `ASSUME_FPV(WSramRdataError_M, w_sram_rdata_i == '0 && w_sram_rerror_i == '0,
              clk_wr_i, !rst_wr_ni)

  `ASSUME(RSramRvalidOneCycle_M,
          r_sram_req_o && r_sram_gnt_i |=> r_sram_rvalid_i,
          clk_rd_i, !rst_rd_ni)
  `ASSUME_FPV(RErrorTied_M, r_sram_rerror_i == '0,
              clk_rd_i, !rst_rd_ni)


  // FPV coverage
  `COVER_FPV(WFull_C, w_full, clk_wr_i, !rst_wr_ni)

endmodule : prim_fifo_async_sram_adapter
