// Copyright 2018-2019 ETH Zurich and University of Bologna.
// Solderpad Hardware License, Version 0.51, see LICENSE for details.
// SPDX-License-Identifier: SHL-0.51
//
// File:   axi_adapter.sv
// Author: Florian Zaruba <zarubaf@iis.ee.ethz.ch>
// Date:   1.8.2018
//
// Description: Manages communication with the AXI Bus

module snitch_axi_adapter #(
  parameter int unsigned WriteFIFODepth = 2,
  parameter int unsigned ReadFIFODepth = 2,
  parameter type addr_t = logic,
  parameter type data_t = logic,
  parameter type strb_t = logic,
  parameter type axi_mst_req_t  = logic,
  parameter type axi_mst_resp_t = logic
) (
  input  logic          clk_i,
  input  logic          rst_ni,
  // AXI port
  input  axi_mst_resp_t axi_resp_i,
  output axi_mst_req_t  axi_req_o,

  input  addr_t         slv_qaddr_i,
  input  logic          slv_qwrite_i,
  input  logic [3:0]    slv_qamo_i,
  input  data_t         slv_qdata_i,
  input  logic [2:0]    slv_qsize_i,
  input  strb_t         slv_qstrb_i,
  input  logic [7:0]    slv_qrlen_i,
  input  logic          slv_qvalid_i,
  output logic          slv_qready_o,
  output data_t         slv_pdata_o,
  output logic          slv_pwrite_o,
  output logic          slv_perror_o,
  output logic          slv_plast_o,
  output logic          slv_pvalid_o,
  input  logic          slv_pready_i
);

  localparam DataWidth     = $bits(data_t);
  localparam StrbWidth     = $bits(strb_t);
  localparam SlvByteOffset = $clog2($bits(strb_t));
  localparam AxiByteOffset = $clog2($bits(axi_req_o.w.strb));

  typedef enum logic [3:0] {
    AMONone = 4'h0,
    AMOSwap = 4'h1,
    AMOAdd  = 4'h2,
    AMOAnd  = 4'h3,
    AMOOr   = 4'h4,
    AMOXor  = 4'h5,
    AMOMax  = 4'h6,
    AMOMaxu = 4'h7,
    AMOMin  = 4'h8,
    AMOMinu = 4'h9,
    AMOLR   = 4'hA,
    AMOSC   = 4'hB
  } amo_op_t;

  typedef struct packed {
    data_t data;
    strb_t strb;
  } write_t;

  typedef struct packed {
    data_t data;
    logic  write;
    logic  error;
    logic  last;
  } resp_t;

  logic   write_full;
  logic   write_empty;
  logic   read_full;
  write_t write_data_in;
  write_t write_data_out;
  write_t r_data;

  assign axi_req_o.aw.addr   = slv_qaddr_i;
  assign axi_req_o.aw.prot   = 3'b0;
  assign axi_req_o.aw.region = 4'b0;
  assign axi_req_o.aw.size   = slv_qsize_i;
  assign axi_req_o.aw.len    = '0;
  assign axi_req_o.aw.burst  = axi_pkg::BURST_INCR;
  assign axi_req_o.aw.lock   = 1'b0;
  assign axi_req_o.aw.cache  = axi_pkg::CACHE_MODIFIABLE;
  assign axi_req_o.aw.qos    = 4'b0;
  assign axi_req_o.aw.id     = '0;
  assign axi_req_o.aw.user   = '0;
  assign axi_req_o.aw_valid  = ~write_full & slv_qvalid_i & slv_qwrite_i;

  always_comb begin
    write_data_in.data = slv_qdata_i;
    write_data_in.strb = slv_qstrb_i;
    unique case (amo_op_t'(slv_qamo_i))
      // RISC-V atops have a load semantic
      AMOSwap: axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_ATOMICSWAP};
      AMOAdd:  axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_ADD};
      AMOAnd:  begin
        // in this case we need to invert the data to get a "CLR"
        write_data_in.data = ~slv_qdata_i;
        axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_CLR};
      end
      AMOOr:   axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_SET};
      AMOXor:  axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_EOR};
      AMOMax:  axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_SMAX};
      AMOMaxu: axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_UMAX};
      AMOMin:  axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_SMIN};
      AMOMinu: axi_req_o.aw.atop  = {axi_pkg::ATOP_ATOMICLOAD, axi_pkg::ATOP_LITTLE_END, axi_pkg::ATOP_UMIN};
      default: axi_req_o.aw.atop = '0;
    endcase
  end

  localparam int unsigned ShiftWidth = (SlvByteOffset == AxiByteOffset) ? 1 : AxiByteOffset - SlvByteOffset;
  typedef logic [ShiftWidth-1:0] shift_t;
  typedef struct packed {
    write_t data;
    shift_t shift;
  } write_ext_t;

  if (SlvByteOffset == AxiByteOffset) begin : gen_w_data
    // Write
    fifo_v3 #(
      .DEPTH      ( WriteFIFODepth                             ),
      .dtype      ( write_t                                    )
    ) i_fifo_w_data (
      .clk_i,
      .rst_ni,
      .flush_i    ( 1'b0                                       ),
      .testmode_i ( 1'b0                                       ),
      .full_o     ( write_full                                 ),
      .empty_o    ( write_empty                                ),
      .usage_o    ( /* NC */                                   ),
      .data_i     ( write_data_in                              ),
      .push_i     ( slv_qvalid_i & slv_qready_o & slv_qwrite_i ),
      .data_o     ( write_data_out                             ),
      .pop_i      ( axi_req_o.w_valid & axi_resp_i.w_ready     )
    );
    assign axi_req_o.w.data = write_data_out.data;
    assign axi_req_o.w.strb = write_data_out.strb;

    // Read
    assign read_full = 1'b0;
    assign r_data = axi_resp_i.r.data;
  end else begin  : gen_w_data
    // Write
    write_ext_t write_data_ext_in, write_data_ext_out;

    fifo_v3 #(
      .DEPTH      ( WriteFIFODepth                             ),
      .dtype      ( write_ext_t                                )
    ) i_fifo_w_data (
      .clk_i,
      .rst_ni,
      .flush_i    ( 1'b0                                       ),
      .testmode_i ( 1'b0                                       ),
      .full_o     ( write_full                                 ),
      .empty_o    ( write_empty                                ),
      .usage_o    ( /* NC */                                   ),
      .data_i     ( write_data_ext_in                          ),
      .push_i     ( slv_qvalid_i & slv_qready_o & slv_qwrite_i ),
      .data_o     ( write_data_ext_out                         ),
      .pop_i      ( axi_req_o.w_valid & axi_resp_i.w_ready     )
    );

    assign write_data_ext_in.data  = write_data_in;
    assign write_data_ext_in.shift = slv_qaddr_i[AxiByteOffset-1:SlvByteOffset];
    assign axi_req_o.w.data  = {'0, write_data_ext_out.data.data} << ($bits(data_t) * write_data_ext_out.shift);
    assign axi_req_o.w.strb  = {'0, write_data_ext_out.data.strb} << ($bits(strb_t) * write_data_ext_out.shift);

    // Read
    shift_t read_shift;

    fifo_v3 #(
      .DEPTH      ( ReadFIFODepth                               ),
      .DATA_WIDTH ( AxiByteOffset-SlvByteOffset                 )
    ) i_fifo_r_shift (
      .clk_i,
      .rst_ni,
      .flush_i    ( 1'b0                                        ),
      .testmode_i ( 1'b0                                        ),
      .full_o     ( read_full                                   ),
      .empty_o    ( /* NC */                                    ),
      .usage_o    ( /* NC */                                    ),
      .data_i     ( slv_qaddr_i[AxiByteOffset-1:SlvByteOffset]  ),
      .push_i     ( slv_qvalid_i & slv_qready_o & ~slv_qwrite_i ),
      .data_o     ( read_shift                                  ),
      .pop_i      ( axi_resp_i.r_valid & axi_req_o.r_ready      )
    );

    assign r_data = axi_resp_i.r.data >> ($bits(data_t) * read_shift);
  end
  assign axi_req_o.w.last    = 1'b1;
  assign axi_req_o.w.user    = '0;
  assign axi_req_o.w_valid   = ~write_empty;

  assign axi_req_o.ar.addr   = slv_qaddr_i;
  assign axi_req_o.ar.prot   = 3'b0;
  assign axi_req_o.ar.region = 4'b0;
  assign axi_req_o.ar.size   = slv_qsize_i;
  assign axi_req_o.ar.len    = slv_qrlen_i;
  assign axi_req_o.ar.burst  = axi_pkg::BURST_INCR;
  assign axi_req_o.ar.lock   = 1'b0;
  assign axi_req_o.ar.cache  = axi_pkg::CACHE_MODIFIABLE;
  assign axi_req_o.ar.qos    = 4'b0;
  assign axi_req_o.ar.id     = '0;
  assign axi_req_o.ar.user   = '0;
  assign axi_req_o.ar_valid  = ~read_full & slv_qvalid_i & ~slv_qwrite_i;

  // Response arbitration because we can get an R and B response simultaneously
  resp_t r_resp, b_resp, slv_resp;
  logic r_error, b_error;

  assign r_error = (axi_resp_i.r.resp inside {axi_pkg::RESP_EXOKAY, axi_pkg::RESP_OKAY}) ? 1'b0 : 1'b1;
  assign b_error = (axi_resp_i.b.resp inside {axi_pkg::RESP_EXOKAY, axi_pkg::RESP_OKAY}) ? 1'b0 : 1'b1;

  assign r_resp = '{
    data: r_data,
    write: 1'b0,
    error: r_error,
    last: axi_resp_i.r.last
  };

  assign b_resp = '{
    data: r_data,
    write: 1'b1,
    error: b_error,
    last: 1'b1
  };

  rr_arb_tree #(
    .NumIn      (2),
    .DataType   (resp_t),
    .ExtPrio    (1'b1),
    .AxiVldRdy  (1'b1),
    .LockIn     (1'b0)
  ) i_response_arbiter (
    .clk_i  (clk_i                                  ),
    .rst_ni (rst_ni                                 ),
    .flush_i('0                                     ),
    .rr_i   ('0                                     ),
    .req_i  ({axi_resp_i.b_valid,axi_resp_i.r_valid}),
    .gnt_o  ({axi_req_o.b_ready,axi_req_o.r_ready}  ),
    .data_i ({b_resp,r_resp}                        ),
    .gnt_i  (slv_pready_i                           ),
    .req_o  (slv_pvalid_o                           ),
    .data_o (slv_resp                               ),
    .idx_o  (                                       )
  );

  assign slv_pdata_o       = slv_resp.data;
  assign slv_pwrite_o      = slv_resp.write;
  assign slv_perror_o      = slv_resp.error;
  assign slv_plast_o       = slv_resp.last;

  assign slv_qready_o = (axi_resp_i.ar_ready & axi_req_o.ar_valid)
                      | (axi_resp_i.aw_ready & axi_req_o.aw_valid);

  `ifndef VERILATOR
  // pragma translate_off
  hot_one : assert property (
    @(posedge clk_i) disable iff (!rst_ni) (slv_qvalid_i & slv_qwrite_i & slv_qready_o) |-> (slv_qrlen_i == 0))
      else $warning("Bursts are not supported for write transactions");
  // pragma translate_on
  `endif
endmodule