/* Copyright 2018 ETH Zurich and University of Bologna.
* Copyright and related rights are licensed under the Solderpad Hardware
* License, Version 0.51 (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-0.51. 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.
*
* File:   dm_mem.sv
* Author: Florian Zaruba <zarubaf@iis.ee.ethz.ch>
* Date:   11.7.2018
*
* Description: Memory module for execution-based debug clients
*
*/

module dm_mem #(
  parameter int unsigned        NrHarts          =  1,
  parameter int unsigned        BusWidth         = 32,
  parameter logic [NrHarts-1:0] SelectableHarts  = {NrHarts{1'b1}},
  parameter int unsigned        DmBaseAddress    = '0
) (
  input  logic                             clk_i,       // Clock
  input  logic                             rst_ni,      // debug module reset
  input  logic                             ndmreset_i,

  output logic [NrHarts-1:0]               debug_req_o,
  input  logic [19:0]                      hartsel_i,
  // from Ctrl and Status register
  input  logic [NrHarts-1:0]               haltreq_i,
  input  logic [NrHarts-1:0]               resumereq_i,
  input  logic                             clear_resumeack_i,

  // state bits
  output logic [NrHarts-1:0]               halted_o,    // hart acknowledge halt
  output logic [NrHarts-1:0]               resuming_o,  // hart is resuming

  input  logic [dm::ProgBufSize-1:0][31:0] progbuf_i,    // program buffer to expose

  input  logic [dm::DataCount-1:0][31:0]   data_i,       // data in
  output logic [dm::DataCount-1:0][31:0]   data_o,       // data out
  output logic                             data_valid_o, // data out is valid
  // abstract command interface
  input  logic                             cmd_valid_i,
  input  dm::command_t                     cmd_i,
  output logic                             cmderror_valid_o,
  output dm::cmderr_e                      cmderror_o,
  output logic                             cmdbusy_o,
  // data interface

  // SRAM interface
  input  logic                             req_i,
  input  logic                             we_i,
  input  logic [BusWidth-1:0]              addr_i,
  input  logic [BusWidth-1:0]              wdata_i,
  input  logic [BusWidth/8-1:0]            be_i,
  output logic [BusWidth-1:0]              rdata_o
);
  localparam int unsigned DbgAddressBits = 12;
  localparam int unsigned HartSelLen     = (NrHarts == 1) ? 1 : $clog2(NrHarts);
  localparam int unsigned NrHartsAligned = 2**HartSelLen;
  localparam int unsigned MaxAar         = (BusWidth == 64) ? 4 : 3;
  localparam bit          HasSndScratch  = (DmBaseAddress != 0);
  // Depending on whether we are at the zero page or not we either use `x0` or `x10/a0`
  localparam logic [4:0]  LoadBaseAddr   = (DmBaseAddress == 0) ? 5'd0 : 5'd10;

  localparam logic [DbgAddressBits-1:0] DataBaseAddr        = (dm::DataAddr);
  localparam logic [DbgAddressBits-1:0] DataEndAddr         = (dm::DataAddr + 4*dm::DataCount - 1);
  localparam logic [DbgAddressBits-1:0] ProgBufBaseAddr     = (dm::DataAddr - 4*dm::ProgBufSize);
  localparam logic [DbgAddressBits-1:0] ProgBufEndAddr      = (dm::DataAddr - 1);
  localparam logic [DbgAddressBits-1:0] AbstractCmdBaseAddr = (ProgBufBaseAddr - 4*10);
  localparam logic [DbgAddressBits-1:0] AbstractCmdEndAddr  = (ProgBufBaseAddr - 1);

  localparam logic [DbgAddressBits-1:0] WhereToAddr   = 'h300;
  localparam logic [DbgAddressBits-1:0] FlagsBaseAddr = 'h400;
  localparam logic [DbgAddressBits-1:0] FlagsEndAddr  = 'h7FF;

  localparam logic [DbgAddressBits-1:0] HaltedAddr    = 'h100;
  localparam logic [DbgAddressBits-1:0] GoingAddr     = 'h104;
  localparam logic [DbgAddressBits-1:0] ResumingAddr  = 'h108;
  localparam logic [DbgAddressBits-1:0] ExceptionAddr = 'h10C;

  logic [dm::ProgBufSize/2-1:0][63:0]   progbuf;
  logic [7:0][63:0]   abstract_cmd;
  logic [NrHarts-1:0] halted_d, halted_q;
  logic [NrHarts-1:0] resuming_d, resuming_q;
  logic               resume, go, going;

  logic exception;
  logic unsupported_command;

  logic [63:0] rom_rdata;
  logic [63:0] rdata_d, rdata_q;
  logic        word_enable32_q;

  // this is needed to avoid lint warnings related to array indexing
  // resize hartsel to valid range
  logic [HartSelLen-1:0] hartsel, wdata_hartsel;

  assign hartsel       = hartsel_i[HartSelLen-1:0];
  assign wdata_hartsel = wdata_i[HartSelLen-1:0];

  logic [NrHartsAligned-1:0] resumereq_aligned, haltreq_aligned,
                             halted_d_aligned, halted_q_aligned,
                             halted_aligned, resumereq_wdata_aligned,
                             resuming_d_aligned, resuming_q_aligned;

  assign resumereq_aligned       = NrHartsAligned'(resumereq_i);
  assign haltreq_aligned         = NrHartsAligned'(haltreq_i);
  assign resumereq_wdata_aligned = NrHartsAligned'(resumereq_i);

  assign halted_q_aligned        = NrHartsAligned'(halted_q);
  assign halted_d                = ndmreset_i ? '0 : NrHarts'(halted_d_aligned);
  assign resuming_q_aligned      = NrHartsAligned'(resuming_q);
  assign resuming_d              = ndmreset_i ? '0 : NrHarts'(resuming_d_aligned);

  // distinguish whether we need to forward data from the ROM or the FSM
  // latch the address for this
  logic fwd_rom_d, fwd_rom_q;
  dm::ac_ar_cmd_t ac_ar;

  // Abstract Command Access Register
  assign ac_ar       = dm::ac_ar_cmd_t'(cmd_i.control);
  assign debug_req_o = haltreq_i;
  assign halted_o    = halted_q;
  assign resuming_o  = resuming_q;

  // reshape progbuf
  assign progbuf = progbuf_i;

  typedef enum logic [1:0] { Idle, Go, Resume, CmdExecuting } state_e;
  state_e state_d, state_q;

  // hart ctrl queue
  always_comb begin : p_hart_ctrl_queue
    cmderror_valid_o = 1'b0;
    cmderror_o       = dm::CmdErrNone;
    state_d          = state_q;
    go               = 1'b0;
    resume           = 1'b0;
    cmdbusy_o        = 1'b1;

    unique case (state_q)
      Idle: begin
        cmdbusy_o = 1'b0;
        if (cmd_valid_i && halted_q_aligned[hartsel] && !unsupported_command) begin
          // give the go signal
          state_d = Go;
        end else if (cmd_valid_i) begin
          // hart must be halted for all requests
          cmderror_valid_o = 1'b1;
          cmderror_o = dm::CmdErrorHaltResume;
        end
        // CSRs want to resume, the request is ignored when the hart is
        // requested to halt or it didn't clear the resuming_q bit before
        if (resumereq_aligned[hartsel] && !resuming_q_aligned[hartsel] &&
            !haltreq_aligned[hartsel] && halted_q_aligned[hartsel]) begin
          state_d = Resume;
        end
      end

      Go: begin
        // we are already busy here since we scheduled the execution of a program
        cmdbusy_o = 1'b1;
        go        = 1'b1;
        // the thread is now executing the command, track its state
        if (going) begin
            state_d = CmdExecuting;
        end
      end

      Resume: begin
        cmdbusy_o = 1'b1;
        resume = 1'b1;
        if (resuming_q_aligned[hartsel]) begin
          state_d = Idle;
        end
      end

      CmdExecuting: begin
        cmdbusy_o = 1'b1;
        go        = 1'b0;
        // wait until the hart has halted again
        if (halted_aligned[hartsel]) begin
          state_d = Idle;
        end
      end

      default: ;
    endcase

    // only signal once that cmd is unsupported so that we can clear cmderr
    // in subsequent writes to abstractcs
    if (unsupported_command && cmd_valid_i) begin
      cmderror_valid_o = 1'b1;
      cmderror_o = dm::CmdErrNotSupported;
    end

    if (exception) begin
      cmderror_valid_o = 1'b1;
      cmderror_o = dm::CmdErrorException;
    end
  end

  // word mux for 32bit and 64bit buses
  logic [63:0] word_mux;
  assign word_mux = (fwd_rom_q) ? rom_rdata : rdata_q;

  if (BusWidth == 64) begin : gen_word_mux64
    assign rdata_o = word_mux;
  end else begin : gen_word_mux32
    assign rdata_o = (word_enable32_q) ? word_mux[32 +: 32] : word_mux[0 +: 32];
  end

  // read/write logic
  logic [63:0] data_bits;
  logic [7:0][7:0] rdata;
  always_comb begin : p_rw_logic

    halted_d_aligned   = NrHartsAligned'(halted_q);
    resuming_d_aligned = NrHartsAligned'(resuming_q);
    rdata_d        = rdata_q;
    // convert the data in bits representation
    data_bits      = data_i;
    rdata          = '0;

    // write data in csr register
    data_valid_o   = 1'b0;
    exception      = 1'b0;
    halted_aligned     = '0;
    going          = 1'b0;

    // The resume ack signal is lowered when the resume request is deasserted
    if (clear_resumeack_i) begin
      resuming_d_aligned[hartsel] = 1'b0;
    end
    // we've got a new request
    if (req_i) begin
      // this is a write
      if (we_i) begin
        unique case (addr_i[DbgAddressBits-1:0]) inside
          HaltedAddr: begin
            halted_aligned[wdata_hartsel] = 1'b1;
            halted_d_aligned[wdata_hartsel] = 1'b1;
          end
          GoingAddr: begin
            going = 1'b1;
          end
          ResumingAddr: begin
            // clear the halted flag as the hart resumed execution
            halted_d_aligned[wdata_hartsel] = 1'b0;
            // set the resuming flag which needs to be cleared by the debugger
            resuming_d_aligned[wdata_hartsel] = 1'b1;
          end
          // an exception occurred during execution
          ExceptionAddr: exception = 1'b1;
          // core can write data registers
          [DataBaseAddr:DataEndAddr]: begin
            data_valid_o = 1'b1;
            for (int i = 0; i < $bits(be_i); i++) begin
              if (be_i[i]) begin
                data_bits[i*8+:8] = wdata_i[i*8+:8];
              end
            end
          end
          default ;
        endcase

      // this is a read
      end else begin
        unique case (addr_i[DbgAddressBits-1:0]) inside
          // variable ROM content
          WhereToAddr: begin
            // variable jump to abstract cmd, program_buffer or resume
            if (resumereq_wdata_aligned[wdata_hartsel]) begin
              rdata_d = {32'b0, dm::jal('0, 21'(dm::ResumeAddress[11:0])-21'(WhereToAddr))};
            end

            // there is a command active so jump there
            if (cmdbusy_o) begin
              // transfer not set is shortcut to the program buffer if postexec is set
              // keep this statement narrow to not catch invalid commands
              if (cmd_i.cmdtype == dm::AccessRegister &&
                  !ac_ar.transfer && ac_ar.postexec) begin
                rdata_d = {32'b0, dm::jal('0, 21'(ProgBufBaseAddr)-21'(WhereToAddr))};
              // this is a legit abstract cmd -> execute it
              end else begin
                rdata_d = {32'b0, dm::jal('0, 21'(AbstractCmdBaseAddr)-21'(WhereToAddr))};
              end
            end
          end

          [DataBaseAddr:DataEndAddr]: begin
            rdata_d = {
                      data_i[$clog2(dm::ProgBufSize)'(addr_i[DbgAddressBits-1:3] -
                          DataBaseAddr[DbgAddressBits-1:3] + 1'b1)],
                      data_i[$clog2(dm::ProgBufSize)'(addr_i[DbgAddressBits-1:3] -
                          DataBaseAddr[DbgAddressBits-1:3])]
                      };
          end

          [ProgBufBaseAddr:ProgBufEndAddr]: begin
            rdata_d = progbuf[$clog2(dm::ProgBufSize)'(addr_i[DbgAddressBits-1:3] -
                          ProgBufBaseAddr[DbgAddressBits-1:3])];
          end

          // two slots for abstract command
          [AbstractCmdBaseAddr:AbstractCmdEndAddr]: begin
            // return the correct address index
            rdata_d = abstract_cmd[3'(addr_i[DbgAddressBits-1:3] -
                           AbstractCmdBaseAddr[DbgAddressBits-1:3])];
          end
          // harts are polling for flags here
          [FlagsBaseAddr:FlagsEndAddr]: begin
            // release the corresponding hart
            if (({addr_i[DbgAddressBits-1:3], 3'b0} - FlagsBaseAddr[DbgAddressBits-1:0]) ==
              (DbgAddressBits'(hartsel) & {{(DbgAddressBits-3){1'b1}}, 3'b0})) begin
              rdata[DbgAddressBits'(hartsel) & DbgAddressBits'(3'b111)] = {6'b0, resume, go};
            end
            rdata_d = rdata;
          end
          default: ;
        endcase
      end
    end

    data_o = data_bits;
  end

  always_comb begin : p_abstract_cmd_rom
    // this abstract command is currently unsupported
    unsupported_command = 1'b0;
    // default memory
    // if ac_ar.transfer is not set then we can take a shortcut to the program buffer
    abstract_cmd[0][31:0]  = dm::illegal();
    // load debug module base address into a0, this is shared among all commands
    abstract_cmd[0][63:32] = HasSndScratch ? dm::auipc(5'd10, '0) : dm::nop();
    // clr lowest 12b -> DM base offset
    abstract_cmd[1][31:0]  = HasSndScratch ? dm::srli(5'd10, 5'd10, 6'd12) : dm::nop();
    abstract_cmd[1][63:32] = HasSndScratch ? dm::slli(5'd10, 5'd10, 6'd12) : dm::nop();
    abstract_cmd[2][31:0]  = dm::nop();
    abstract_cmd[2][63:32] = dm::nop();
    abstract_cmd[3][31:0]  = dm::nop();
    abstract_cmd[3][63:32] = dm::nop();
    abstract_cmd[4][31:0]  = HasSndScratch ? dm::csrr(dm::CSR_DSCRATCH1, 5'd10) : dm::nop();
    abstract_cmd[4][63:32] = dm::ebreak();
    abstract_cmd[7:5]      = '0;

    // this depends on the command being executed
    unique case (cmd_i.cmdtype)
      // --------------------
      // Access Register
      // --------------------
      dm::AccessRegister: begin
        if (32'(ac_ar.aarsize) < MaxAar && ac_ar.transfer && ac_ar.write) begin
          // store a0 in dscratch1
          abstract_cmd[0][31:0] = HasSndScratch ? dm::csrw(dm::CSR_DSCRATCH1, 5'd10) : dm::nop();
          // this range is reserved
          if (ac_ar.regno[15:14] != '0) begin
            abstract_cmd[0][31:0] = dm::ebreak(); // we leave asap
            unsupported_command = 1'b1;
          // A0 access needs to be handled separately, as we use A0 to load
          // the DM address offset need to access DSCRATCH1 in this case
          end else if (HasSndScratch && ac_ar.regno[12] && (!ac_ar.regno[5]) &&
                      (ac_ar.regno[4:0] == 5'd10)) begin
            // store s0 in dscratch
            abstract_cmd[2][31:0]  = dm::csrw(dm::CSR_DSCRATCH0, 5'd8);
            // load from data register
            abstract_cmd[2][63:32] = dm::load(ac_ar.aarsize, 5'd8, LoadBaseAddr, dm::DataAddr);
            // and store it in the corresponding CSR
            abstract_cmd[3][31:0]  = dm::csrw(dm::CSR_DSCRATCH1, 5'd8);
            // restore s0 again from dscratch
            abstract_cmd[3][63:32] = dm::csrr(dm::CSR_DSCRATCH0, 5'd8);
          // GPR/FPR access
          end else if (ac_ar.regno[12]) begin
            // determine whether we want to access the floating point register or not
            if (ac_ar.regno[5]) begin
              abstract_cmd[2][31:0] =
                  dm::float_load(ac_ar.aarsize, ac_ar.regno[4:0], LoadBaseAddr, dm::DataAddr);
            end else begin
              abstract_cmd[2][31:0] =
                  dm::load(ac_ar.aarsize, ac_ar.regno[4:0], LoadBaseAddr, dm::DataAddr);
            end
          // CSR access
          end else begin
            // data register to CSR
            // store s0 in dscratch
            abstract_cmd[2][31:0]  = dm::csrw(dm::CSR_DSCRATCH0, 5'd8);
            // load from data register
            abstract_cmd[2][63:32] = dm::load(ac_ar.aarsize, 5'd8, LoadBaseAddr, dm::DataAddr);
            // and store it in the corresponding CSR
            abstract_cmd[3][31:0]  = dm::csrw(dm::csr_reg_t'(ac_ar.regno[11:0]), 5'd8);
            // restore s0 again from dscratch
            abstract_cmd[3][63:32] = dm::csrr(dm::CSR_DSCRATCH0, 5'd8);
          end
        end else if (32'(ac_ar.aarsize) < MaxAar && ac_ar.transfer && !ac_ar.write) begin
          // store a0 in dscratch1
          abstract_cmd[0][31:0]  = HasSndScratch ?
                                   dm::csrw(dm::CSR_DSCRATCH1, LoadBaseAddr) :
                                   dm::nop();
          // this range is reserved
          if (ac_ar.regno[15:14] != '0) begin
              abstract_cmd[0][31:0] = dm::ebreak(); // we leave asap
              unsupported_command = 1'b1;
          // A0 access needs to be handled separately, as we use A0 to load
          // the DM address offset need to access DSCRATCH1 in this case
          end else if (HasSndScratch && ac_ar.regno[12] && (!ac_ar.regno[5]) &&
                      (ac_ar.regno[4:0] == 5'd10)) begin
            // store s0 in dscratch
            abstract_cmd[2][31:0]  = dm::csrw(dm::CSR_DSCRATCH0, 5'd8);
            // read value from CSR into s0
            abstract_cmd[2][63:32] = dm::csrr(dm::CSR_DSCRATCH1, 5'd8);
            // and store s0 into data section
            abstract_cmd[3][31:0]  = dm::store(ac_ar.aarsize, 5'd8, LoadBaseAddr, dm::DataAddr);
            // restore s0 again from dscratch
            abstract_cmd[3][63:32] = dm::csrr(dm::CSR_DSCRATCH0, 5'd8);
          // GPR/FPR access
          end else if (ac_ar.regno[12]) begin
            // determine whether we want to access the floating point register or not
            if (ac_ar.regno[5]) begin
              abstract_cmd[2][31:0] =
                  dm::float_store(ac_ar.aarsize, ac_ar.regno[4:0], LoadBaseAddr, dm::DataAddr);
            end else begin
              abstract_cmd[2][31:0] =
                  dm::store(ac_ar.aarsize, ac_ar.regno[4:0], LoadBaseAddr, dm::DataAddr);
            end
          // CSR access
          end else begin
            // CSR register to data
            // store s0 in dscratch
            abstract_cmd[2][31:0]  = dm::csrw(dm::CSR_DSCRATCH0, 5'd8);
            // read value from CSR into s0
            abstract_cmd[2][63:32] = dm::csrr(dm::csr_reg_t'(ac_ar.regno[11:0]), 5'd8);
            // and store s0 into data section
            abstract_cmd[3][31:0]  = dm::store(ac_ar.aarsize, 5'd8, LoadBaseAddr, dm::DataAddr);
            // restore s0 again from dscratch
            abstract_cmd[3][63:32] = dm::csrr(dm::CSR_DSCRATCH0, 5'd8);
          end
        end else if (32'(ac_ar.aarsize) >= MaxAar || ac_ar.aarpostincrement == 1'b1) begin
          // this should happend when e.g. ac_ar.aarsize >= MaxAar
          // Openocd will try to do an access with aarsize=64 bits
          // first before falling back to 32 bits.
          abstract_cmd[0][31:0] = dm::ebreak(); // we leave asap
          unsupported_command = 1'b1;
        end

        // Check whether we need to execute the program buffer. When we
        // get an unsupported command we really should abort instead of
        // still trying to execute the program buffer, makes it easier
        // for the debugger to recover
        if (ac_ar.postexec && !unsupported_command) begin
          // issue a nop, we will automatically run into the program buffer
          abstract_cmd[4][63:32] = dm::nop();
        end
      end
      // not supported at the moment
      // dm::QuickAccess:;
      // dm::AccessMemory:;
      default: begin
        abstract_cmd[0][31:0] = dm::ebreak();
        unsupported_command = 1'b1;
      end
    endcase
  end

  logic [63:0] rom_addr;
  assign rom_addr = 64'(addr_i);

  // Depending on whether the debug module is located
  // at the zero page we can instantiate a simplified version
  // which only requires one scratch register per hart.
  // For all other cases we need to set aside
  // two registers per hart, hence we also need
  // two scratch registers.
  if (HasSndScratch) begin : gen_rom_snd_scratch
    debug_rom i_debug_rom (
      .clk_i,
      .req_i,
      .addr_i  ( rom_addr  ),
      .rdata_o ( rom_rdata )
    );
  end else begin : gen_rom_one_scratch
    // It uses the zero register (`x0`) as the base
    // for its loads. The zero register does not need to
    // be saved.
    debug_rom_one_scratch i_debug_rom (
      .clk_i,
      .req_i,
      .addr_i  ( rom_addr  ),
      .rdata_o ( rom_rdata )
    );
  end

  // ROM starts at the HaltAddress of the core e.g.: it immediately jumps to
  // the ROM base address
  assign fwd_rom_d = logic'(addr_i[DbgAddressBits-1:0] >= dm::HaltAddress[DbgAddressBits-1:0]);

  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
    if (!rst_ni) begin
      fwd_rom_q       <= 1'b0;
      rdata_q         <= '0;
      state_q         <= Idle;
      word_enable32_q <= 1'b0;
    end else begin
      fwd_rom_q       <= fwd_rom_d;
      rdata_q         <= rdata_d;
      state_q         <= state_d;
      word_enable32_q <= addr_i[2];
    end
  end

  always_ff @(posedge clk_i or negedge rst_ni) begin
    if (!rst_ni) begin
      halted_q   <= 1'b0;
      resuming_q <= 1'b0;
    end else begin
      halted_q   <= SelectableHarts & halted_d;
      resuming_q <= SelectableHarts & resuming_d;
    end
  end

endmodule : dm_mem
