// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
//
// This module decodes escalation enable pulses that have been encoded using
// the prim_esc_sender module.
//
// The module supports in-band ping testing of the escalation
// wires. This is accomplished by the sender module that places a single-cycle,
// differentially encoded pulse on esc_p/n which will be interpreted as a ping
// request by the receiver module. The receiver module responds by sending back
// the response pattern "1010".
//
// Native escalation enable pulses are differentiated from ping
// requests by making sure that these pulses are always longer than 1 cycle.
//
// See also: prim_esc_sender, prim_diff_decode, alert_handler

`include "prim_assert.sv"

module prim_esc_receiver
  import prim_esc_pkg::*;
#(
  // The number of escalation severities. Should be set to the Alert Handler's N_ESC_SEV when this
  // primitive is instantiated.
  parameter int N_ESC_SEV = 4,

  // The width of the Alert Handler's ping counter. Should be set to the Alert Handler's PING_CNT_DW
  // when this primitive is instantiated.
  parameter int PING_CNT_DW = 16,

  // This counter monitors incoming ping requests and auto-escalates if the alert handler
  // ceases to send them regularly. The maximum number of cycles between subsequent ping requests
  // is N_ESC_SEV x (2 x 2 x 2**PING_CNT_DW), see also implementation of the ping timer
  // (alert_handler_ping_timer.sv). The timeout counter below uses a timeout that is 4x larger than
  // that in order to incorporate some margin.
  //
  // Do NOT modify this counter value, when instantiating it in the design. It is only exposed to
  // reduce the state space in the FPV testbench.
  localparam int MarginFactor = 4,
  localparam int NumWaitCounts = 2,
  localparam int NumTimeoutCounts = 2,
  parameter int TimeoutCntDw = $clog2(MarginFactor) +
                               $clog2(N_ESC_SEV) +
                               $clog2(NumWaitCounts) +
                               $clog2(NumTimeoutCounts) +
                               PING_CNT_DW
) (
  input           clk_i,
  input           rst_ni,
  // escalation enable
  output logic    esc_req_o,
  // escalation / ping response
  output esc_rx_t esc_rx_o,
  // escalation output diff pair
  input esc_tx_t  esc_tx_i
);

  /////////////////////////////////
  // decode differential signals //
  /////////////////////////////////

  logic esc_level, esc_p, esc_n, sigint_detected;

  // This prevents further tool optimizations of the differential signal.
  prim_buf #(
    .Width(2)
  ) u_prim_buf_esc (
    .in_i({esc_tx_i.esc_n,
           esc_tx_i.esc_p}),
    .out_o({esc_n,
            esc_p})
  );

  prim_diff_decode #(
    .AsyncOn(1'b0)
  ) u_decode_esc (
    .clk_i,
    .rst_ni,
    .diff_pi  ( esc_p           ),
    .diff_ni  ( esc_n           ),
    .level_o  ( esc_level       ),
    .rise_o   (                 ),
    .fall_o   (                 ),
    .event_o  (                 ),
    .sigint_o ( sigint_detected )
  );

  ////////////////////////////////////////////
  // Ping Monitor Counter / Auto Escalation //
  ////////////////////////////////////////////

  // The timeout counter is kicked off when the first ping occurs, and subsequent pings reset
  // the counter to 1. The counter keeps on counting when it is nonzero, and saturates when it
  // has reached its maximum (this state is terminal).
  logic ping_en, timeout_cnt_error;
  logic timeout_cnt_set, timeout_cnt_en;
  logic [TimeoutCntDw-1:0] timeout_cnt;
  assign timeout_cnt_set = (ping_en && !(&timeout_cnt));
  assign timeout_cnt_en = ((timeout_cnt > '0) && !(&timeout_cnt));

  prim_count #(
    .Width(TimeoutCntDw),
    // The escalation receiver behaves differently than other comportable IP. I.e., instead of
    // sending out an alert signal, this condition is handled internally in the alert handler.
    .EnableAlertTriggerSVA(0)
  ) u_prim_count (
    .clk_i,
    .rst_ni,
    .clr_i(1'b0),
    .set_i(timeout_cnt_set),
    .set_cnt_i(TimeoutCntDw'(1)),
    .incr_en_i(timeout_cnt_en),
    .decr_en_i(1'b0),
    .step_i(TimeoutCntDw'(1)),
    .commit_i(1'b1),
    .cnt_o(timeout_cnt),
    .cnt_after_commit_o(),
    .err_o(timeout_cnt_error)
  );

  // Escalation is asserted if
  // - requested via the escalation sender/receiver path,
  // - the ping monitor timeout is reached,
  // - the two ping monitor counters are in an inconsistent state.
  logic esc_req;
  prim_sec_anchor_buf #(
    .Width(1)
  ) u_prim_buf_esc_req (
    .in_i(esc_req || (&timeout_cnt) || timeout_cnt_error),
    .out_o(esc_req_o)
  );

  /////////////////
  // RX/TX Logic //
  /////////////////

  typedef enum logic [2:0] {Idle, Check, PingResp, EscResp, SigInt} state_e;
  state_e state_d, state_q;
  logic resp_pd, resp_pq;
  logic resp_nd, resp_nq;

  // This prevents further tool optimizations of the differential signal.
  prim_sec_anchor_flop #(
    .Width(2),
    .ResetValue(2'b10)
  ) u_prim_flop_esc (
    .clk_i,
    .rst_ni,
    .d_i({resp_nd, resp_pd}),
    .q_o({resp_nq, resp_pq})
  );

  assign esc_rx_o.resp_p = resp_pq;
  assign esc_rx_o.resp_n = resp_nq;

  always_comb begin : p_fsm
    // default
    state_d = state_q;
    resp_pd = 1'b0;
    resp_nd = 1'b1;
    esc_req = 1'b0;
    ping_en = 1'b0;

    unique case (state_q)
      // wait for the esc_p/n diff pair
      Idle: begin
        if (esc_level) begin
          state_d = Check;
          resp_pd = ~resp_pq;
          resp_nd = resp_pq;
        end
      end
      // we decide here whether this is only a ping request or
      // whether this is an escalation enable
      Check: begin
        state_d = PingResp;
        resp_pd = ~resp_pq;
        resp_nd = resp_pq;
        if (esc_level) begin
          state_d = EscResp;
          esc_req = 1'b1;
        end
      end
      // finish ping response. in case esc_level is again asserted,
      // we got an escalation signal (pings cannot occur back to back)
      PingResp: begin
        state_d = Idle;
        resp_pd = ~resp_pq;
        resp_nd = resp_pq;
        ping_en = 1'b1;
        if (esc_level) begin
          state_d = EscResp;
          esc_req = 1'b1;
        end
      end
      // we have got an escalation enable pulse,
      // keep on toggling the outputs
      EscResp: begin
        state_d = Idle;
        if (esc_level) begin
          state_d = EscResp;
          resp_pd = ~resp_pq;
          resp_nd = resp_pq;
          esc_req = 1'b1;
        end
      end
      // we have a signal integrity issue at one of
      // the incoming diff pairs. this condition is
      // signalled to the sender by setting the resp
      // diffpair to the same value and continuously
      // toggling them.
      SigInt: begin
        state_d = Idle;
        esc_req = 1'b1;
        if (sigint_detected) begin
          state_d = SigInt;
          resp_pd = ~resp_pq;
          resp_nd = ~resp_pq;
        end
      end
      default: state_d = Idle;
    endcase

    // bail out if a signal integrity issue has been detected
    if (sigint_detected && (state_q != SigInt)) begin
      state_d = SigInt;
      resp_pd = 1'b0;
      resp_nd = 1'b0;
    end
  end


  ///////////////
  // Registers //
  ///////////////

  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
    if (!rst_ni) begin
      state_q <= Idle;
    end else begin
      state_q <= state_d;
    end
  end

  ////////////////
  // assertions //
  ////////////////

  // check whether all outputs have a good known state after reset
  `ASSERT_KNOWN(EscEnKnownO_A, esc_req_o)
  `ASSERT_KNOWN(RespPKnownO_A, esc_rx_o)

  `ASSERT(SigIntCheck0_A, esc_tx_i.esc_p == esc_tx_i.esc_n |=> esc_rx_o.resp_p == esc_rx_o.resp_n)
  `ASSERT(SigIntCheck1_A, esc_tx_i.esc_p == esc_tx_i.esc_n |=> state_q == SigInt)
  // auto-escalate in case of signal integrity issue
  `ASSERT(SigIntCheck2_A, esc_tx_i.esc_p == esc_tx_i.esc_n |=> esc_req_o)
  // correct diff encoding
  `ASSERT(DiffEncCheck_A, esc_tx_i.esc_p ^ esc_tx_i.esc_n |=> esc_rx_o.resp_p ^ esc_rx_o.resp_n)
  // disable in case of signal integrity issue
  `ASSERT(PingRespCheck_A, state_q == Idle ##1 $rose(esc_tx_i.esc_p) ##1 $fell(esc_tx_i.esc_p) |->
      $rose(esc_rx_o.resp_p) ##1 $fell(esc_rx_o.resp_p),
      clk_i, !rst_ni || (esc_tx_i.esc_p == esc_tx_i.esc_n))
  // escalation response needs to continuously toggle
  `ASSERT(EscRespCheck_A, ##1 esc_tx_i.esc_p && $past(esc_tx_i.esc_p) &&
      (esc_tx_i.esc_p ^ esc_tx_i.esc_n) && $past(esc_tx_i.esc_p ^ esc_tx_i.esc_n)
      |=> esc_rx_o.resp_p != $past(esc_rx_o.resp_p))
  // detect escalation pulse
  `ASSERT(EscEnCheck_A,
          esc_tx_i.esc_p && (esc_tx_i.esc_p ^ esc_tx_i.esc_n) && state_q != SigInt
      ##1 esc_tx_i.esc_p && (esc_tx_i.esc_p ^ esc_tx_i.esc_n) |-> esc_req_o)
  // make sure the counter does not wrap around
  `ASSERT(EscCntWrap_A, &timeout_cnt |=> timeout_cnt != 0)
  // if the counter expires, escalation should be asserted
  `ASSERT(EscCntEsc_A, &timeout_cnt |-> esc_req_o)

endmodule : prim_esc_receiver
