[alert_handler/prim] Add differential sender/receiver primitives
diff --git a/hw/ip/prim/prim.core b/hw/ip/prim/prim.core
index f810c76..591dbf6 100644
--- a/hw/ip/prim/prim.core
+++ b/hw/ip/prim/prim.core
@@ -11,9 +11,14 @@
     depend:
       - lowrisc:prim:ram_2p # for prim_ram_2p_adv
       - lowrisc:prim:assert
+      - lowrisc:prim:diff_decode # for prim_alert_sender/receiver
     files:
       - rtl/prim_clock_inverter.sv
+      - rtl/prim_alert_receiver.sv
+      - rtl/prim_alert_sender.sv
       - rtl/prim_arbiter.sv
+      - rtl/prim_esc_receiver.sv
+      - rtl/prim_esc_sender.sv
       - rtl/prim_sram_arbiter.sv
       - rtl/prim_fifo_async.sv
       - rtl/prim_fifo_sync.sv
diff --git a/hw/ip/prim/prim_diff_decode.core b/hw/ip/prim/prim_diff_decode.core
new file mode 100644
index 0000000..eda0fe7
--- /dev/null
+++ b/hw/ip/prim/prim_diff_decode.core
@@ -0,0 +1,19 @@
+CAPI=2:
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+name: "lowrisc:prim:diff_decode"
+description: "prim"
+filesets:
+  files_rtl:
+    depend:
+      - lowrisc:prim:assert
+    files:
+      - rtl/prim_diff_decode.sv
+    file_type: systemVerilogSource
+
+targets:
+  default:
+    filesets:
+      - files_rtl
diff --git a/hw/ip/prim/rtl/prim_alert_receiver.sv b/hw/ip/prim/rtl/prim_alert_receiver.sv
new file mode 100644
index 0000000..3093eb5
--- /dev/null
+++ b/hw/ip/prim/rtl/prim_alert_receiver.sv
@@ -0,0 +1,206 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// The alert receiver primitive decodes alerts that have been differentially
+// encoded and transmitted via a handshake protocol on alert_pi/ni and
+// ack_po/no. In case an alert handshake is initiated, the output alert_o will
+// immediately be asserted (even before completion of the handshake).
+//
+// In case the differential input is not correctly encoded, this module will
+// raise an error by asserting integ_fail_o.
+//
+// Further, the module supports ping testing of the alert diff pair. In order to
+// initiate a ping test, ping_en_i shall be set to 1'b1 until ping_ok_o is
+// asserted for one cycle. The signal may be de-asserted (e.g. after a long)
+// timeout period. However note that all ping responses that come in after
+// deasserting ping_en_i will be treated as native alerts.
+//
+// The protocol works in both asynchronous and synchronous cases. In the
+// asynchronous case, the parameter AsyncOn must be set to 1'b1 in order to
+// instantiate additional synchronization logic. Further, it must be ensured
+// that the timing skew between all diff pairs is smaller than the shortest
+// clock period of the involved clocks.
+//
+// Note that in case of synchronous operation, alerts on the diffpair are
+// decoded combinationally and forwarded on alert_o within the same cycle.
+//
+// See also: prim_alert_sender, prim_diff_decode, alert_handler
+
+module prim_alert_receiver #(
+  // enables additional synchronization logic
+  parameter bit AsyncOn = 1'b0
+) (
+  input        clk_i,
+  input        rst_ni,
+  // this triggers a ping test. keep asserted
+  // until ping_ok_o is asserted.
+  input        ping_en_i,
+  output logic ping_ok_o,
+  // asserted if signal integrity issue detected
+  output logic integ_fail_o,
+  // alert output (pulsed high) if a handshake is initiated
+  // on alert_p/n and no ping request is outstanding
+  output logic alert_o,
+  // ping output diff pair
+  output logic ping_po,
+  output logic ping_no,
+  // ack output diff pair
+  output logic ack_po,
+  output logic ack_no,
+  // alert input diff pair
+  input        alert_pi,
+  input        alert_ni
+);
+
+  //////////////////////////////////////////////////////
+  // decode differential signals
+  //////////////////////////////////////////////////////
+  logic alert_level, alert_sigint;
+
+  prim_diff_decode #(
+    .AsyncOn(AsyncOn)
+  ) i_decode_alert (
+    .clk_i,
+    .rst_ni,
+    .diff_pi  ( alert_pi     ),
+    .diff_ni  ( alert_ni     ),
+    .level_o  ( alert_level  ),
+    .rise_o   (              ),
+    .fall_o   (              ),
+    .event_o  (              ),
+    .sigint_o ( alert_sigint )
+  );
+
+  //////////////////////////////////////////////////////
+  // main protocol FSM that drives the diff outputs
+  //////////////////////////////////////////////////////
+  typedef enum logic [1:0] {Idle, HsAckWait, Pause0, Pause1} state_e;
+  state_e state_d, state_q;
+  logic ping_rise;
+  logic ping_tog_d, ping_tog_q, ack_d, ack_q;
+  logic ping_en_d, ping_en_q;
+  logic ping_pending_d, ping_pending_q;
+
+  // signal ping request upon positive transition on ping_en_i
+  // signalling is performed by a level change event on the diff output
+  assign ping_en_d  = ping_en_i;
+  assign ping_rise  = ping_en_i && !ping_en_q;
+  assign ping_tog_d = (ping_rise) ? ~ping_tog_q : ping_tog_q;
+
+  // the ping pending signal is used to in the FSM to distinguish whether the
+  // incoming handshake shall be treated as an alert or a ping response.
+  // it is important that this is only set on a rising ping_en level change, since
+  // otherwise the ping enable signal could be abused to "mask" all native alerts
+  // as ping responses by constantly tying it to 1.
+  assign ping_pending_d = ping_rise | ((~ping_ok_o) & ping_en_i & ping_pending_q);
+
+  // diff pair outputs
+  assign ack_po  = ack_q;
+  assign ack_no  = ~ack_q;
+  assign ping_po = ping_tog_q;
+  assign ping_no = ~ping_tog_q;
+
+  // this FSM receives the four phase handshakes from the alert receiver
+  // note that the latency of the alert_p/n input diff pair is at least one
+  // cycle until it enters the receiver FSM. the same holds for the ack_* diff
+  // pair outputs.
+  always_comb begin : p_fsm
+    // default
+    state_d      = state_q;
+    ack_d        = 1'b0;
+    ping_ok_o    = 1'b0;
+    integ_fail_o = 1'b0;
+    alert_o      = 1'b0;
+
+    unique case (state_q)
+      Idle: begin
+        // wait for handshake to be initiated
+        if (alert_level) begin
+          state_d = HsAckWait;
+          ack_d   = 1'b1;
+          // signal either an alert or ping received on the output
+          if (ping_pending_q) begin
+            ping_ok_o = 1'b1;
+          end else begin
+            alert_o   = 1'b1;
+          end
+        end
+      end
+      // waiting for deassertion of alert to complete HS
+      HsAckWait: begin
+        if (!alert_level) begin
+          state_d  = Pause0;
+        end else begin
+          ack_d    = 1'b1;
+        end
+      end
+      // pause cycles between back-to-back handshakes
+      Pause0: state_d = Pause1;
+      Pause1: state_d = Idle;
+      default : ; // full case
+    endcase
+
+    // override in case of sigint
+    if (alert_sigint) begin
+      state_d      = Idle;
+      ack_d        = 1'b0;
+      ping_ok_o    = 1'b0;
+      integ_fail_o = 1'b1;
+      alert_o      = 1'b0;
+    end
+  end
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_reg
+    if (!rst_ni) begin
+      state_q        <= Idle;
+      ack_q          <= 1'b0;
+      ping_tog_q     <= 1'b0;
+      ping_en_q      <= 1'b0;
+      ping_pending_q <= 1'b0;
+    end else begin
+      state_q        <= state_d;
+      ack_q          <= ack_d;
+      ping_tog_q     <= ping_tog_d;
+      ping_en_q      <= ping_en_d;
+      ping_pending_q <= ping_pending_d;
+    end
+  end
+
+  //////////////////////////////////////////////////////
+  // assertions
+  //////////////////////////////////////////////////////
+
+  // shared assertions
+  // check encoding of outgoing diffpairs
+  `ASSERT(PingDiffOk_A, ping_po ^ ping_no, clk_i, !rst_ni)
+  `ASSERT(AckDiffOk_A, ack_po ^ ack_no, clk_i, !rst_ni)
+  // ping request at input -> need to see encoded ping request
+  `ASSERT(PingRequest0_A, ##1 $rose(ping_en_i) |=> $changed(ping_po), clk_i, !rst_ni)
+  // ping response implies it has been requested
+  `ASSERT(PingResponse0_A, ping_ok_o |-> ping_pending_q, clk_i, !rst_ni)
+  // correctly latch ping request
+  `ASSERT(PingPending_A, ##1 $rose(ping_en_i) |=> ping_pending_q, clk_i, !rst_ni)
+
+  if (AsyncOn) begin : gen_async_assert
+    // signal integrity check propagation
+    `ASSERT(SigInt_A, alert_pi == alert_ni [*2] |-> ##2 integ_fail_o, clk_i, !rst_ni)
+    // TODO: need to add skewed cases as well, the assertions below assume no skew at the moment
+    // ping response
+    `ASSERT(PingResponse1_A, ##1 $rose(alert_pi) && (alert_pi ^ alert_ni) ##2
+        state_q == Idle && ping_pending_q |-> ping_ok_o, clk_i, !rst_ni || integ_fail_o)
+    // alert
+    `ASSERT(Alert_A, ##1 $rose(alert_pi) && (alert_pi ^ alert_ni) ##2
+        state_q == Idle && !ping_pending_q |-> alert_o, clk_i, !rst_ni || integ_fail_o)
+  end else begin : gen_sync_assert
+    // signal integrity check propagation
+    `ASSERT(SigInt_A, alert_pi == alert_ni |-> integ_fail_o, clk_i, !rst_ni)
+    // ping response
+    `ASSERT(PingResponse1_A, ##1 $rose(alert_pi) && state_q == Idle && ping_pending_q |->
+        ping_ok_o, clk_i, !rst_ni || integ_fail_o)
+    // alert
+    `ASSERT(Alert_A, ##1 $rose(alert_pi) && state_q == Idle && !ping_pending_q |->
+        alert_o, clk_i, !rst_ni || integ_fail_o)
+  end
+
+endmodule : prim_alert_receiver
diff --git a/hw/ip/prim/rtl/prim_alert_sender.sv b/hw/ip/prim/rtl/prim_alert_sender.sv
new file mode 100644
index 0000000..814a5cc
--- /dev/null
+++ b/hw/ip/prim/rtl/prim_alert_sender.sv
@@ -0,0 +1,225 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// The alert sender primitive module differentially encodes and transmits an
+// alert signal to the prim_alert_receiver module. An alert will be signalled
+// by a full handshake on alert_po/no and ack_pi/ni. The alert_i signal may
+// be continuously asserted, in which case the alert signalling handshake
+// will be repeatedly initiated.
+//
+// Further, this module supports in-band ping testing, which means that a level
+// change on the ping_pi/ni diff pair will result in a full-handshake response
+// on alert_po/no and ack_pi/ni.
+//
+// The protocol works in both asynchronous and synchronous cases. In the
+// asynchronous case, the parameter AsyncOn must be set to 1'b1 in order to
+// instantiate additional synchronization logic. Further, it must be ensured
+// that the timing skew between all diff pairs is smaller than the shortest
+// clock period of the involved clocks.
+//
+// Incorrectly encoded diff inputs can be detected and will be signalled
+// to the receiver by placing an inconsistent diff value on the differential
+// output (and continuously toggling it).
+//
+// See also: prim_alert_receiver, prim_diff_decode, alert_handler
+
+module prim_alert_sender #(
+  // enables additional synchronization logic
+  parameter bit AsyncOn = 1'b1
+) (
+  input        clk_i,
+  input        rst_ni,
+  // native alert from the peripheral
+  input        alert_i,
+  // ping input diff pair
+  input        ping_pi,
+  input        ping_ni,
+  // alert input diff pair
+  input        ack_pi,
+  input        ack_ni,
+  // alert output diff pair
+  output logic alert_po,
+  output logic alert_no
+);
+
+  //////////////////////////////////////////////////////
+  // decode differential signals
+  //////////////////////////////////////////////////////
+  logic ping_sigint, ping_event;
+
+  prim_diff_decode #(
+    .AsyncOn(AsyncOn)
+  ) i_decode_ping (
+    .clk_i,
+    .rst_ni,
+    .diff_pi  ( ping_pi     ),
+    .diff_ni  ( ping_ni     ),
+    .level_o  (             ),
+    .rise_o   (             ),
+    .fall_o   (             ),
+    .event_o  ( ping_event  ),
+    .sigint_o ( ping_sigint )
+  );
+
+  logic ack_sigint, ack_level;
+
+  prim_diff_decode #(
+    .AsyncOn(AsyncOn)
+  ) i_decode_ack (
+    .clk_i,
+    .rst_ni,
+    .diff_pi  ( ack_pi      ),
+    .diff_ni  ( ack_ni      ),
+    .level_o  ( ack_level   ),
+    .rise_o   (             ),
+    .fall_o   (             ),
+    .event_o  (             ),
+    .sigint_o ( ack_sigint  )
+  );
+
+  //////////////////////////////////////////////////////
+  // main protocol FSM that drives the diff output
+  //////////////////////////////////////////////////////
+  typedef enum logic [2:0] {Idle, HsPhase1, HsPhase2, SigInt, Pause0, Pause1} state_e;
+  state_e state_d, state_q;
+  logic alert_pq, alert_nq, alert_pd, alert_nd;
+  logic sigint_detected;
+
+  assign sigint_detected = ack_sigint | ping_sigint;
+
+  // diff pair output
+  assign alert_po = alert_pq;
+  assign alert_no = alert_nq;
+
+  // alert and ping set regs
+  logic alert_set_d, alert_set_q, alert_clr;
+  logic ping_set_d, ping_set_q, ping_clr;
+  assign alert_set_d = (alert_clr) ? 1'b0 :  (alert_set_q | alert_i);
+  assign ping_set_d  = (ping_clr) ? 1'b0 : (ping_set_q | ping_event);
+
+  // this FSM performs a full four phase handshake upon a ping or alert trigger.
+  // note that the latency of the alert_p/n diff pair is at least one cycle
+  // until it enters the receiver FSM. the same holds for the ack_* diff pair
+  // input. in case a signal integrity issue is detected, the FSM bails out,
+  // sets the alert_p/n diff pair to the same value and toggles it in order to
+  // signal that condition over to the receiver.
+  always_comb begin : p_fsm
+    // default
+    state_d = state_q;
+    alert_pd   = 1'b0;
+    alert_nd   = 1'b1;
+    ping_clr   = 1'b0;
+    alert_clr  = 1'b0;
+
+    unique case (state_q)
+      Idle: begin
+        // alert always takes precedence
+        if (alert_i || alert_set_q || ping_event || ping_set_q) begin
+          state_d   = HsPhase1;
+          alert_pd  = 1'b1;
+          alert_nd  = 1'b0;
+          if (ping_event || ping_set_q) begin
+            ping_clr  = 1'b1;
+          end else begin
+            alert_clr = 1'b1;
+          end
+        end
+      end
+      // waiting for ack from receiver
+      HsPhase1: begin
+        if (ack_level) begin
+          state_d  = HsPhase2;
+        end else begin
+          alert_pd = 1'b1;
+          alert_nd = 1'b0;
+        end
+      end
+      // wait for deassertion of ack
+      HsPhase2: begin
+        if (!ack_level) begin
+          state_d = Pause0;
+        end
+      end
+      // pause cycles between back-to-back handshakes
+      Pause0: state_d = Pause1;
+      Pause1: state_d = Idle;
+      // we have a signal integrity issue at one of
+      // the incoming diff pairs. this condition is
+      // signalled by setting the output diffpair
+      // to the same value and continuously toggling
+      // them.
+      SigInt: begin
+        state_d  = Idle;
+        if (sigint_detected) begin
+          state_d  = SigInt;
+          alert_pd = ~alert_pq;
+          alert_nd = ~alert_pq;
+        end
+      end
+      // catch parasitic states
+      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;
+      alert_pd  = 1'b0;
+      alert_nd  = 1'b0;
+      ping_clr  = 1'b0;
+      alert_clr = 1'b0;
+    end
+  end
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_reg
+    if (!rst_ni) begin
+      state_q     <= Idle;
+      alert_pq    <= 1'b0;
+      alert_nq    <= 1'b1;
+      alert_set_q <= 1'b0;
+      ping_set_q  <= 1'b0;
+    end else begin
+      state_q     <= state_d;
+      alert_pq    <= alert_pd;
+      alert_nq    <= alert_nd;
+      alert_set_q <= alert_set_d;
+      ping_set_q  <= ping_set_d;
+    end
+  end
+
+  //////////////////////////////////////////////////////
+  // assertions
+  //////////////////////////////////////////////////////
+
+  if (AsyncOn) begin : gen_async_assert
+    // check propagation of sigint issues to output within three cycles
+    `ASSERT(SigIntPing_A, ping_pi == ping_ni [*2] |-> ##3 alert_po == alert_no, clk_i, !rst_ni)
+    `ASSERT(SigIntAck_A,  ack_pi == ack_ni   [*2] |-> ##3 alert_po == alert_no, clk_i, !rst_ni)
+    // output must be driven diff unless sigint issue detected
+    `ASSERT(DiffEncoding_A, (ack_pi ^ ack_ni) && (ping_pi ^ ping_ni) |->
+        ##3 alert_po ^ alert_no, clk_i, !rst_ni)
+
+    // handshakes can take indefinite time if blocked due to sigint on outgoing
+    // lines (which is not visible here). thus, we only check whether the handshake is correctly initiated
+    // and defer the full handshake checking to the testbench.
+    // TODO: add the staggered cases as well
+    `ASSERT(PingHs_A, ##1 $changed(ping_pi) && (ping_pi ^ ping_ni) ##2 state_q == Idle |=>
+        $rose(alert_po), clk_i, !rst_ni || (alert_po == alert_no))
+  end else begin : gen_sync_assert
+    // check propagation of sigint issues to output within one cycle
+    `ASSERT(SigIntPing_A, ping_pi == ping_ni |=> alert_po == alert_no, clk_i, !rst_ni)
+    `ASSERT(SigIntAck_A,  ack_pi == ack_ni   |=> alert_po == alert_no, clk_i, !rst_ni)
+    // output must be driven diff unless sigint issue detected
+    `ASSERT(DiffEncoding_A, (ack_pi ^ ack_ni) && (ping_pi ^ ping_ni) |=> alert_po ^ alert_no,
+        clk_i, !rst_ni)
+    // handshakes can take indefinite time if blocked due to sigint on outgoing
+    // lines (which is not visible here). thus, we only check whether the handshake is correctly initiated
+    // and defer the full handshake checking to the testbench.
+    `ASSERT(PingHs_A, ##1 $changed(ping_pi) && state_q == Idle |=> $rose(alert_po),
+        clk_i, !rst_ni || (alert_po == alert_no))
+  end
+
+  // if alert_i is true, handshakes should be continuously repeated
+  `ASSERT(AlertHs_A, alert_i && state_q == Idle |=> $rose(alert_po),
+      clk_i, !rst_ni || (alert_po == alert_no))
+
+endmodule : prim_alert_sender
diff --git a/hw/ip/prim/rtl/prim_diff_decode.sv b/hw/ip/prim/rtl/prim_diff_decode.sv
new file mode 100644
index 0000000..8efa703
--- /dev/null
+++ b/hw/ip/prim/rtl/prim_diff_decode.sv
@@ -0,0 +1,262 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// This module decodes a differentially encoded signal and detects
+// incorrectly encoded differential states.
+//
+// In case the differential pair crosses an asynchronous boundary, it has
+// to be re-synchronized to the local clock. This can be achieved by
+// setting the AsyncOn parameter to 1'b1. In that case, two additional
+// input registers are added (to counteract metastability), and
+// a pattern detector is instantiated that detects skewed level changes on
+// the differential pair (i.e., when level changes on the diff pair are
+// sampled one cycle apart due to a timing skew between the two wires).
+//
+// See also: prim_alert_sender, prim_alert_receiver, alert_handler
+
+module prim_diff_decode #(
+  // enables additional synchronization logic
+  parameter bit AsyncOn = 1'b0
+) (
+  input        clk_i,
+  input        rst_ni,
+  // input diff pair
+  input        diff_pi,
+  input        diff_ni,
+  // logical level and
+  // detected edges
+  output logic level_o,
+  output logic rise_o,
+  output logic fall_o,
+  // either rise or fall
+  output logic event_o,
+  //signal integrity issue detected
+  output logic sigint_o
+);
+
+  logic level_d, level_q;
+
+  //////////////////////////////////////////////////////
+  // synchronization regs for incoming diff pair
+  // (if required)
+  //////////////////////////////////////////////////////
+
+  if (AsyncOn) begin : gen_async
+
+    typedef enum logic [1:0] {IsStd, IsSkewed, SigInt} state_e;
+    state_e state_d, state_q;
+    logic diff_p_edge, diff_n_edge, diff_check_ok, level;
+
+    // 2 sync regs, one reg for edge detection
+    logic diff_pq, diff_nq, diff_pd, diff_nd;
+
+    prim_flop_2sync #(
+      .Width(1),
+      .ResetValue(0)
+    ) i_sync_p (
+      .clk_i,
+      .rst_ni,
+      .d(diff_pi),
+      .q(diff_pd)
+    );
+
+    prim_flop_2sync #(
+      .Width(1),
+      .ResetValue(1)
+    ) i_sync_n (
+      .clk_i,
+      .rst_ni,
+      .d(diff_ni),
+      .q(diff_nd)
+    );
+
+    // detect level transitions
+    assign diff_p_edge   = diff_pq ^ diff_pd;
+    assign diff_n_edge   = diff_nq ^ diff_nd;
+
+    // detect sigint issue
+    assign diff_check_ok = diff_pd ^ diff_nd;
+
+    // this is the current logical level
+    assign level         = diff_pd;
+
+    // outputs
+    assign level_o  = level_d;
+    assign event_o = rise_o | fall_o;
+
+    // sigint detection is a bit more involved in async case since
+    // we might have skew on the diff pair, which can result in a
+    // one cycle sampling delay between the two wires
+    // so we need a simple pattern matcher
+    // the following waves are legal
+    // clk    |   |   |   |   |   |   |   |
+    //           _______     _______
+    // p _______/        ...        \________
+    //   _______                     ________
+    // n        \_______ ... _______/
+    //              ____     ___
+    // p __________/     ...    \________
+    //   _______                     ________
+    // n        \_______ ... _______/
+    //
+    // i.e., level changes may be off by one cycle - which is permissible
+    // as long as this condition is only one cycle long.
+
+
+    always_comb begin : p_diff_fsm
+      // default
+      state_d  = state_q;
+      level_d  = level_q;
+      rise_o   = 1'b0;
+      fall_o   = 1'b0;
+      sigint_o = 1'b0;
+
+      unique case (state_q)
+        // we remain here as long as
+        // the diff pair is correctly encoded
+        IsStd: begin
+          if (diff_check_ok) begin
+            level_d = level;
+            if (diff_p_edge && diff_n_edge) begin
+              if (level) begin
+                rise_o = 1'b1;
+              end else begin
+                fall_o = 1'b1;
+              end
+            end
+          end else begin
+            if (diff_p_edge || diff_n_edge) begin
+              state_d = IsSkewed;
+            end else begin
+              state_d = SigInt;
+              sigint_o = 1'b1;
+            end
+          end
+        end
+        // diff pair must be correctly encoded, otherwise we got a sigint
+        IsSkewed: begin
+          if (diff_check_ok) begin
+            state_d = IsStd;
+            level_d = level;
+            if (level) rise_o = 1'b1;
+            else       fall_o = 1'b1;
+          end else begin
+            state_d  = SigInt;
+            sigint_o = 1'b1;
+          end
+        end
+        // Signal integrity issue detected, remain here
+        // until resolved
+        SigInt: begin
+          sigint_o = 1'b1;
+          if (diff_check_ok) begin
+            state_d  = IsStd;
+            sigint_o = 1'b0;
+          end
+        end
+        default : ;
+      endcase
+    end
+
+    always_ff @(posedge clk_i or negedge rst_ni) begin : p_sync_reg
+      if (!rst_ni) begin
+        state_q  <= IsStd;
+        diff_pq  <= 1'b0;
+        diff_nq  <= 1'b1;
+        level_q  <= 1'b0;
+      end else begin
+        state_q  <= state_d;
+        diff_pq  <= diff_pd;
+        diff_nq  <= diff_nd;
+        level_q  <= level_d;
+      end
+    end
+
+  //////////////////////////////////////////////////////
+  // fully synchronous case
+  // no skew present in this case
+  //////////////////////////////////////////////////////
+
+  end else begin : gen_no_async
+    logic diff_pq, diff_pd;
+
+    // one reg for edge detection
+    assign diff_pd = diff_pi;
+
+    // incorrect encoding -> signal integrity issue
+    assign sigint_o = ~(diff_pi ^ diff_ni);
+
+    assign level_o = (sigint_o) ? level_q : diff_pi;
+    assign level_d = level_o;
+
+    // detect level transitions
+    assign rise_o  = (~diff_pq &  diff_pi) & ~sigint_o;
+    assign fall_o  = ( diff_pq & ~diff_pi) & ~sigint_o;
+    assign event_o = rise_o | fall_o;
+
+    always_ff @(posedge clk_i or negedge rst_ni) begin : p_edge_reg
+      if (!rst_ni) begin
+        diff_pq  <= 1'b0;
+        level_q  <= 1'b0;
+      end else begin
+        diff_pq  <= diff_pd;
+        level_q  <= level_d;
+      end
+    end
+  end
+
+  //////////////////////////////////////////////////////
+  // assertions
+  //////////////////////////////////////////////////////
+
+  // shared assertions
+  // sigint -> level stays the same during sigint
+  // $isunknown is needed to avoid false assertion in first clock cycle
+  `ASSERT(SigintLevelCheck_A, ##1 sigint_o |-> $stable(level_o), clk_i, !rst_ni)
+  // sigint -> no additional events asserted at output
+  `ASSERT(SigintEventCheck_A, sigint_o |-> !event_o, clk_i, !rst_ni)
+  `ASSERT(SigintRiseCheck_A,  sigint_o |-> !rise_o, clk_i, !rst_ni)
+  `ASSERT(SigintFallCheck_A,  sigint_o |-> !fall_o, clk_i, !rst_ni)
+
+  if (AsyncOn) begin : gen_async_assert
+    // assertions for asynchronous case
+    // correctly detect sigint issue (only one transition cycle of permissible due to skew)
+    `ASSERT(SigintCheck0_A, diff_pi == diff_ni [*2] |-> ##[1:2] sigint_o, clk_i, !rst_ni)
+    // the synchronizer adds 2 cycles of latency
+    `ASSERT(SigintCheck1_A, ##1 (diff_pi ^ diff_ni) && $stable(diff_pi) && $stable(diff_ni) ##1
+        $rose(diff_pi) && $stable(diff_ni) ##1 $stable(diff_pi) && $fell(diff_ni) |->
+        ##2 rise_o, clk_i, !rst_ni)
+    `ASSERT(SigintCheck2_A, ##1 (diff_pi ^ diff_ni) && $stable(diff_pi) && $stable(diff_ni) ##1
+        $fell(diff_pi) && $stable(diff_ni) ##1 $stable(diff_pi) && $rose(diff_ni) |->
+        ##2 fall_o, clk_i, !rst_ni)
+    `ASSERT(SigintCheck3_A, ##1 (diff_pi ^ diff_ni) && $stable(diff_pi) && $stable(diff_ni) ##1
+        $rose(diff_ni) && $stable(diff_pi) ##1 $stable(diff_ni) && $fell(diff_pi) |->
+        ##2 fall_o, clk_i, !rst_ni)
+    `ASSERT(SigintCheck4_A, ##1 (diff_pi ^ diff_ni) && $stable(diff_pi) && $stable(diff_ni) ##1
+        $fell(diff_ni) && $stable(diff_pi) ##1 $stable(diff_ni) && $rose(diff_pi) |->
+        ##2 rise_o, clk_i, !rst_ni)
+    // correctly detect edges
+    `ASSERT(RiseCheck_A,  ##1 $rose(diff_pi)     && (diff_pi ^ diff_ni) |->
+        ##[2:3] rise_o,  clk_i, !rst_ni || sigint_o)
+    `ASSERT(FallCheck_A,  ##1 $fell(diff_pi)     && (diff_pi ^ diff_ni) |->
+        ##[2:3] fall_o,  clk_i, !rst_ni || sigint_o)
+    `ASSERT(EventCheck_A, ##1 $changed(diff_pi)  && (diff_pi ^ diff_ni) |->
+        ##[2:3] event_o, clk_i, !rst_ni || sigint_o)
+    // correctly detect level
+    `ASSERT(LevelCheck0_A, !sigint_o && (diff_pi ^ diff_ni) [*3] |=> $past(diff_pi, 2) == level_o,
+        clk_i, !rst_ni || sigint_o)
+
+  end else begin : gen_sync_assert
+    // assertions for synchronous case
+    // correctly detect sigint issue
+    `ASSERT(SigintCheck_A, diff_pi == diff_ni |-> sigint_o, clk_i, !rst_ni)
+    // correctly detect edges
+    `ASSERT(RiseCheck_A,  ##1 $rose(diff_pi)    && (diff_pi ^ diff_ni) |->  rise_o, clk_i, !rst_ni)
+    `ASSERT(FallCheck_A,  ##1 $fell(diff_pi)    && (diff_pi ^ diff_ni) |->  fall_o, clk_i, !rst_ni)
+    `ASSERT(EventCheck_A, ##1 $changed(diff_pi) && (diff_pi ^ diff_ni) |-> event_o, clk_i, !rst_ni)
+    // correctly detect level
+    `ASSERT(LevelCheck_A, (diff_pi ^ diff_ni) |-> diff_pi == level_o, clk_i, !rst_ni)
+  end
+
+endmodule : prim_diff_decode
diff --git a/hw/ip/prim/rtl/prim_esc_receiver.sv b/hw/ip/prim/rtl/prim_esc_receiver.sv
new file mode 100644
index 0000000..4c5ac7d
--- /dev/null
+++ b/hw/ip/prim/rtl/prim_esc_receiver.sv
@@ -0,0 +1,175 @@
+// 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
+
+module prim_esc_receiver (
+  input        clk_i,
+  input        rst_ni,
+  // escalation enable
+  output logic esc_en_o,
+  // escalation / ping response
+  output logic resp_po,
+  output logic resp_no,
+  // escalation output diff pair
+  input        esc_pi,
+  input        esc_ni
+);
+
+  //////////////////////////////////////////////////////
+  // decode differential signals
+  //////////////////////////////////////////////////////
+
+  logic esc_level, sigint_detected;
+
+  prim_diff_decode #(
+    .AsyncOn(1'b0)
+  ) i_decode_esc (
+    .clk_i,
+    .rst_ni,
+    .diff_pi  ( esc_pi          ),
+    .diff_ni  ( esc_ni          ),
+    .level_o  ( esc_level       ),
+    .rise_o   (                 ),
+    .fall_o   (                 ),
+    .event_o  (                 ),
+    .sigint_o ( sigint_detected )
+  );
+
+  //////////////////////////////////////////////////////
+  // 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, resp_nd, resp_nq;
+
+  assign resp_po = resp_pq;
+  assign resp_no = resp_nq;
+
+
+  always_comb begin : p_fsm
+    // default
+    state_d  = state_q;
+    resp_pd  = 1'b0;
+    resp_nd  = 1'b1;
+    esc_en_o = 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 = 1'b1;
+          resp_nd = 1'b0;
+        end
+      end
+      ///////////////////////////////////
+      // we decide here whether this is only a ping request or
+      // whether this is an escalation enable
+      Check: begin
+        state_d = PingResp;
+        if (esc_level) begin
+          state_d  = EscResp;
+          esc_en_o = 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 = 1'b1;
+        resp_nd = 1'b0;
+        if (esc_level) begin
+          state_d  = EscResp;
+          esc_en_o = 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_en_o = 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;
+        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
+
+  //////////////////////////////////////////////////////
+  // Flops
+  //////////////////////////////////////////////////////
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      state_q <= Idle;
+      resp_pq <= 1'b0;
+      resp_nq <= 1'b1;
+    end else begin
+      state_q <= state_d;
+      resp_pq <= resp_pd;
+      resp_nq <= resp_nd;
+    end
+  end
+
+  //////////////////////////////////////////////////////
+  // assertions
+  //////////////////////////////////////////////////////
+
+  `ASSERT(SigIntCheck0_A, esc_pi == esc_ni |=> resp_po == resp_no, clk_i, !rst_ni)
+  `ASSERT(SigIntCheck1_A, esc_pi == esc_ni |=> state_q == SigInt, clk_i, !rst_ni)
+  // correct diff encoding
+  `ASSERT(DiffEncCheck_A, esc_pi ^ esc_ni |=> resp_po ^ resp_no, clk_i, !rst_ni)
+  // disable in case of ping integrity issue
+  `ASSERT(PingRespCheck_A, $rose(esc_pi) |=> $fell(esc_pi) |-> $rose(resp_po) |=> $fell(resp_po),
+      clk_i, !rst_ni || (esc_pi == esc_ni))
+  // escalation response needs to continuously toggle
+  `ASSERT(EscRespCheck_A, esc_pi && $past(esc_pi) && (esc_pi ^ esc_ni) && $past(esc_pi ^ esc_ni)
+      |=> resp_po != $past(resp_po), clk_i, !rst_ni)
+  // detect escalation pulse
+  `ASSERT(EscEnCheck_A, esc_pi && (esc_pi ^ esc_ni) && state_q != SigInt |=>
+      esc_pi && (esc_pi ^ esc_ni) |-> esc_en_o, clk_i, !rst_ni )
+
+endmodule : prim_esc_receiver
diff --git a/hw/ip/prim/rtl/prim_esc_sender.sv b/hw/ip/prim/rtl/prim_esc_sender.sv
new file mode 100644
index 0000000..bf8affb
--- /dev/null
+++ b/hw/ip/prim/rtl/prim_esc_sender.sv
@@ -0,0 +1,241 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// This module differentially encodes an escalation enable pulse
+// of arbitrary width.
+//
+// The module supports in-band ping testing of the escalation
+// wires. This is accomplished by sending out a single, differentially
+// encoded pulse on esc_p/n which will be interpreted as a ping
+// request by the escalation receiver. Note that ping_en_i shall
+// be held high until either ping_ok_o or integ_fail_o is asserted.
+//
+// Native escalation enable pulses are differentiated from ping
+// requests by making sure that these pulses are always longer than 1 cycle.
+//
+// If there is a differential encoding error, integ_fail_o
+// will be asserted.
+//
+// See also: prim_esc_receiver, prim_diff_decode, alert_handler
+
+module prim_esc_sender (
+  input        clk_i,
+  input        rst_ni,
+  // this triggers a ping test. keep asserted
+  // until either ping_ok_o or ping_fail_o is asserted.
+  input        ping_en_i,
+  output logic ping_ok_o,
+  // asserted if signal integrity issue detected
+  output logic integ_fail_o,
+  // escalation enable signal
+  input        esc_en_i,
+  // escalation / ping response
+  input        resp_pi,
+  input        resp_ni,
+  // escalation output diff pair
+  output logic esc_po,
+  output logic esc_no
+);
+
+  //////////////////////////////////////////////////////
+  // decode differential signals
+  //////////////////////////////////////////////////////
+
+  logic resp, sigint_detected;
+
+  prim_diff_decode #(
+    .AsyncOn(1'b0)
+  ) i_decode_resp (
+    .clk_i,
+    .rst_ni,
+    .diff_pi  ( resp_pi         ),
+    .diff_ni  ( resp_ni         ),
+    .level_o  ( resp            ),
+    .rise_o   (                 ),
+    .fall_o   (                 ),
+    .event_o  (                 ),
+    .sigint_o ( sigint_detected )
+  );
+
+  //////////////////////////////////////////////////////
+  // TX Logic
+  //////////////////////////////////////////////////////
+
+  logic ping_en_d, ping_en_q;
+  logic esc_en_d, esc_en_q, esc_en_q1;
+
+  assign ping_en_d = ping_en_i;
+  assign esc_en_d  = esc_en_i;
+
+  // ping enable is 1 cycle pulse
+  // escalation pulse is always longer than 2 cycles
+  assign esc_po = esc_en_i | esc_en_q | ( ping_en_d & ~ping_en_q);
+  assign esc_no = ~esc_po;
+
+  //////////////////////////////////////////////////////
+  // RX Logic
+  //////////////////////////////////////////////////////
+
+  typedef enum logic [2:0] {Idle, CheckEscRespLo, CheckEscRespHi,
+    CheckPingResp0, CheckPingResp1, CheckPingResp2, CheckPingResp3} fsm_e;
+
+  fsm_e state_d, state_q;
+
+  always_comb begin : p_fsm
+    // default
+    state_d      = state_q;
+    ping_ok_o    = 1'b0;
+    integ_fail_o = sigint_detected;
+
+    unique case (state_q)
+      ///////////////////////////////////
+      // wait for ping or escalation enable
+      Idle: begin
+        if (esc_en_i) begin
+          state_d = CheckEscRespHi;
+        end else if (ping_en_i) begin
+          state_d = CheckPingResp0;
+        end
+        // any assertion of the response signal
+        // signal here will trigger a sigint error
+        if (resp) begin
+          integ_fail_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////
+      // check whether response is 0
+      CheckEscRespLo: begin
+        state_d      = CheckEscRespHi;
+        if (!esc_en_i || resp) begin
+          state_d = Idle;
+          integ_fail_o = sigint_detected | resp;
+        end
+      end
+      ///////////////////////////////////
+      // check whether response is 1
+      CheckEscRespHi: begin
+        state_d = CheckEscRespLo;
+        if (!esc_en_i || !resp) begin
+          state_d = Idle;
+          integ_fail_o = sigint_detected | ~resp;
+        end
+      end
+      ///////////////////////////////////
+      // start of ping response sequence
+      // we expect the sequence "1010"
+      CheckPingResp0: begin
+        state_d = CheckPingResp1;
+        // abort sequence immediately if escalation is signalled,
+        // jump to escalation response checking (lo state)
+        if (esc_en_i) begin
+          state_d = CheckEscRespLo;
+        // abort if response is wrong
+        end else if (!resp) begin
+          state_d = Idle;
+          integ_fail_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////
+      CheckPingResp1: begin
+        state_d = CheckPingResp2;
+        // abort sequence immediately if escalation is signalled,
+        // jump to escalation response checking (hi state)
+        if (esc_en_i) begin
+          state_d = CheckEscRespHi;
+        // abort if response is wrong
+        end else if (resp) begin
+          state_d = Idle;
+          integ_fail_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////
+      CheckPingResp2: begin
+        state_d = CheckPingResp3;
+        // abort sequence immediately if escalation is signalled,
+        // jump to escalation response checking (lo state)
+        if (esc_en_i) begin
+          state_d = CheckEscRespLo;
+        // abort if response is wrong
+        end else if (!resp) begin
+          state_d = Idle;
+          integ_fail_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////
+      CheckPingResp3: begin
+        state_d = Idle;
+        // abort sequence immediately if escalation is signalled,
+        // jump to escalation response checking (hi state)
+        if (esc_en_i) begin
+          state_d = CheckEscRespHi;
+        // abort if response is wrong
+        end else if (resp) begin
+          integ_fail_o = 1'b1;
+        end else begin
+          ping_ok_o = ping_en_i;
+        end
+      end
+      ///////////////////////////////////
+      default : state_d = Idle;
+    endcase
+
+    // escalation takes precedence,
+    // immediately return ok in that case
+    if ((esc_en_i || esc_en_q || esc_en_q1) && ping_en_i) begin
+      ping_ok_o = 1'b1;
+    end
+
+    // a sigint error will reset the state machine
+    // and have it pause for two cycles to let the
+    // receiver recover
+    if (sigint_detected) begin
+      ping_ok_o = 1'b0;
+      state_d = Idle;
+    end
+  end
+
+  //////////////////////////////////////////////////////
+  // Flops
+  //////////////////////////////////////////////////////
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      state_q   <= Idle;
+      esc_en_q  <= 1'b0;
+      esc_en_q1 <= 1'b0;
+      ping_en_q <= 1'b0;
+    end else begin
+      state_q   <= state_d;
+      esc_en_q  <= esc_en_d;
+      esc_en_q1 <= esc_en_q;
+      ping_en_q <= ping_en_d;
+    end
+  end
+
+  //////////////////////////////////////////////////////
+  // assertions
+  //////////////////////////////////////////////////////
+
+  // diff encoding of output
+  `ASSERT(DiffEncCheck_A, esc_po ^ esc_no, clk_i, !rst_ni)
+  // signal integrity check propagation
+  `ASSERT(SigIntCheck0_A, resp_pi == resp_ni  |-> integ_fail_o, clk_i, !rst_ni)
+  // this happens in case we did not get a correct escalation response
+  `ASSERT(SigIntCheck1_A, ##1 $rose(esc_en_i) &&
+      state_q inside {Idle, CheckPingResp1, CheckPingResp3} ##1 !resp_pi |->
+      integ_fail_o, clk_i, !rst_ni || (resp_pi == resp_ni) || (state_q == Idle && resp))
+  `ASSERT(SigIntCheck2_A, ##1 $rose(esc_en_i) &&
+      state_q inside {CheckPingResp0, CheckPingResp2} ##1 resp_pi |->
+      integ_fail_o, clk_i, !rst_ni || (resp_pi == resp_ni) || (state_q == Idle && resp))
+  // unexpected response
+  `ASSERT(SigIntCheck3_A, state_q == Idle && resp |-> integ_fail_o, clk_i, !rst_ni)
+  // check that escalation signal is at least 2 cycles high
+  `ASSERT(EscCheck_A, esc_en_i |-> esc_po [*2] , clk_i, !rst_ni)
+  // escalation / ping collision
+  `ASSERT(EscPingCheck_A, esc_en_i && ping_en_i |-> ping_ok_o, clk_i, !rst_ni || integ_fail_o)
+  // check that ping request results in only a single cycle pulse
+  `ASSERT(PingCheck_A, ##1 $rose(ping_en_i) |-> esc_po ##1 !esc_po , clk_i,
+      !rst_ni || esc_en_i || integ_fail_o)
+
+endmodule : prim_esc_sender