[alert_handler] Add main alert_handler RTL files
diff --git a/hw/ip/alert_handler/rtl/alert_handler.sv b/hw/ip/alert_handler/rtl/alert_handler.sv
new file mode 100644
index 0000000..a72da64
--- /dev/null
+++ b/hw/ip/alert_handler/rtl/alert_handler.sv
@@ -0,0 +1,208 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Alert handler top.
+//
+// Note that the alert_pkg, the regfile and alert_handler_reg_wrap
+// have to be generated using the reg_alert_handler.py script.
+//
+
+module alert_handler (
+  input                      clk_i,
+  input                      rst_ni,
+  // Bus Interface (device)
+  input  tlul_pkg::tl_h2d_t  tl_i,
+  output tlul_pkg::tl_d2h_t  tl_o,
+  // Interrupt Requests
+  output logic [alert_pkg::N_CLASSES-1:0] irq_o,
+  // Entropy Input from TRNG
+  input                                   entropy_i,
+  // Alert Sources
+  input        [alert_pkg::NAlerts-1:0]   alert_pi,
+  input        [alert_pkg::NAlerts-1:0]   alert_ni,
+  output logic [alert_pkg::NAlerts-1:0]   ack_po,
+  output logic [alert_pkg::NAlerts-1:0]   ack_no,
+  output logic [alert_pkg::NAlerts-1:0]   ping_po,
+  output logic [alert_pkg::NAlerts-1:0]   ping_no,
+  // Escalation outputs
+  output logic [alert_pkg::N_ESC_SEV-1:0] esc_po,
+  output logic [alert_pkg::N_ESC_SEV-1:0] esc_no,
+  input        [alert_pkg::N_ESC_SEV-1:0] resp_pi,
+  input        [alert_pkg::N_ESC_SEV-1:0] resp_ni
+);
+
+  //////////////////////////////////////////////////////
+  // Regfile Breakout and Mapping
+  //////////////////////////////////////////////////////
+
+  alert_pkg::hw2reg_wrap_t hw2reg_wrap;
+  alert_pkg::reg2hw_wrap_t reg2hw_wrap;
+
+  alert_handler_reg_wrap i_reg_wrap (
+    .clk_i       ,
+    .rst_ni      ,
+    .tl_i        ,
+    .tl_o        ,
+    .irq_o       ,
+    .hw2reg_wrap ,
+    .reg2hw_wrap
+  );
+
+  //////////////////////////////////////////////////////
+  // Ping Timer
+  //////////////////////////////////////////////////////
+
+  logic [alert_pkg::N_LOC_ALERT-1:0] loc_alert_trig;
+
+  logic [alert_pkg::NAlerts-1:0]   alert_ping_en;
+  logic [alert_pkg::NAlerts-1:0]   alert_ping_ok;
+  logic [alert_pkg::N_ESC_SEV-1:0] esc_ping_en;
+  logic [alert_pkg::N_ESC_SEV-1:0] esc_ping_ok;
+
+  alert_handler_ping_timer i_ping_timer (
+    .clk_i,
+    .rst_ni,
+    .entropy_i,
+    // we enable ping testing as soon as the config
+    // regs have been locked
+    .en_i               ( reg2hw_wrap.config_locked     ),
+    .alert_en_i         ( reg2hw_wrap.alert_en          ),
+    .ping_timeout_cyc_i ( reg2hw_wrap.ping_timeout_cyc  ),
+    .alert_ping_en_o    ( alert_ping_en                 ),
+    .esc_ping_en_o      ( esc_ping_en                   ),
+    .alert_ping_ok_i    ( alert_ping_ok                 ),
+    .esc_ping_ok_i      ( esc_ping_ok                   ),
+    .alert_ping_fail_o  ( loc_alert_trig[0]             ),
+    .esc_ping_fail_o    ( loc_alert_trig[1]             )
+  );
+
+  //////////////////////////////////////////////////////
+  // Alert Receivers
+  //////////////////////////////////////////////////////
+
+  logic [alert_pkg::NAlerts-1:0] alert_integfail;
+  logic [alert_pkg::NAlerts-1:0] alert_trig;
+
+  // Target interrupt notification
+  for (genvar k = 0 ; k < alert_pkg::NAlerts ; k++) begin : gen_alerts
+    prim_alert_receiver #(
+      .AsyncOn(alert_pkg::AsyncOn[k])
+    ) i_alert_receiver (
+      .clk_i                               ,
+      .rst_ni                              ,
+      .ping_en_i    ( alert_ping_en[k]    ),
+      .ping_ok_o    ( alert_ping_ok[k]    ),
+      .integ_fail_o ( alert_integfail[k]  ),
+      .alert_o      ( alert_trig[k]       ),
+      .ping_po      ( ping_po[k]          ),
+      .ping_no      ( ping_no[k]          ),
+      .ack_po       ( ack_po[k]           ),
+      .ack_no       ( ack_no[k]           ),
+      .alert_pi     ( alert_pi[k]         ),
+      .alert_ni     ( alert_ni[k]         )
+    );
+  end
+
+  assign loc_alert_trig[2] = |(reg2hw_wrap.alert_en & alert_integfail);
+
+  //////////////////////////////////////////////////////
+  // Set alert cause bits and classify
+  //////////////////////////////////////////////////////
+
+  alert_handler_class i_class (
+    .alert_trig_i      ( alert_trig                  ),
+    .loc_alert_trig_i  ( loc_alert_trig              ),
+    .alert_en_i        ( reg2hw_wrap.alert_en        ),
+    .loc_alert_en_i    ( reg2hw_wrap.loc_alert_en    ),
+    .alert_class_i     ( reg2hw_wrap.alert_class     ),
+    .loc_alert_class_i ( reg2hw_wrap.loc_alert_class ),
+    .class_en_i        ( reg2hw_wrap.class_en        ),
+    .alert_cause_o     ( hw2reg_wrap.alert_cause     ),
+    .loc_alert_cause_o ( hw2reg_wrap.loc_alert_cause ),
+    .class_trig_o      ( hw2reg_wrap.class_trig      )
+  );
+
+  //////////////////////////////////////////////////////
+  // Escalation Handling of Classes
+  //////////////////////////////////////////////////////
+
+  logic [alert_pkg::N_CLASSES-1:0] class_accum_trig;
+  logic [alert_pkg::N_CLASSES-1:0][alert_pkg::N_ESC_SEV-1:0] class_esc_sig_en;
+
+  for (genvar k = 0; k < alert_pkg::N_CLASSES; k++) begin : gen_classes
+    alert_handler_accu i_accu (
+      .clk_i,
+      .rst_ni,
+      .clr_i        ( reg2hw_wrap.class_clr[k]          ),
+      .trig_i       ( hw2reg_wrap.class_trig[k]         ),
+      .thresh_i     ( reg2hw_wrap.class_accum_thresh[k] ),
+      .cnt_o        ( hw2reg_wrap.class_accum_cnt[k]    ),
+      .trig_o       ( class_accum_trig[k]               )
+    );
+
+    alert_handler_esc_timer i_esc_timer (
+      .clk_i,
+      .rst_ni,
+      .en_i             ( reg2hw_wrap.class_en[k]          ),
+      // this clear does not apply to interrupts
+      .clr_i            ( reg2hw_wrap.class_clr[k]         ),
+      // an interrupt enables the timeout
+      .timeout_en_i     ( irq_o[k]                         ),
+      .accum_trig_i     ( class_accum_trig[k]              ),
+      .timeout_cyc_i    ( reg2hw_wrap.class_timeout_cyc[k] ),
+      .esc_en_i         ( reg2hw_wrap.class_esc_en[k]      ),
+      .esc_map_i        ( reg2hw_wrap.class_esc_map[k]     ),
+      .phase_cyc_i      ( reg2hw_wrap.class_phase_cyc[k]   ),
+      .esc_trig_o       ( hw2reg_wrap.class_esc_trig[k]    ),
+      .esc_cnt_o        ( hw2reg_wrap.class_esc_cnt[k]     ),
+      .esc_state_o      ( hw2reg_wrap.class_esc_state[k]   ),
+      .esc_sig_en_o     ( class_esc_sig_en[k]              )
+    );
+  end
+
+  //////////////////////////////////////////////////////
+  // Escalation Senders
+  //////////////////////////////////////////////////////
+
+  logic [alert_pkg::N_ESC_SEV-1:0] esc_sig_en;
+  logic [alert_pkg::N_ESC_SEV-1:0] esc_integfail;
+  logic [alert_pkg::N_ESC_SEV-1:0][alert_pkg::N_CLASSES-1:0] esc_sig_en_trsp;
+
+  for (genvar k = 0; k < alert_pkg::N_ESC_SEV; k++) begin : gen_esc_sev
+    for (genvar j = 0; j < alert_pkg::N_CLASSES; j++) begin : gen_transp
+      assign esc_sig_en_trsp[k][j] = class_esc_sig_en[j][k];
+    end
+
+    assign esc_sig_en[k] = |esc_sig_en_trsp[k];
+
+    prim_esc_sender i_esc_sender (
+      .clk_i,
+      .rst_ni,
+      .ping_en_i    ( esc_ping_en[k]   ),
+      .ping_ok_o    ( esc_ping_ok[k]   ),
+      .integ_fail_o ( esc_integfail[k] ),
+      .esc_en_i     ( esc_sig_en[k]    ),
+      .resp_pi      ( resp_pi[k]       ),
+      .resp_ni      ( resp_ni[k]       ),
+      .esc_po       ( esc_po[k]        ),
+      .esc_no       ( esc_no[k]        )
+    );
+  end
+
+  assign loc_alert_trig[3] = |esc_integfail;
+
+  //////////////////////////////////////////////////////
+  // Assertions
+  //////////////////////////////////////////////////////
+
+  // this restriction is due to specifics in the ping selection mechanism
+  `ASSERT_INIT(CheckNAlerts,
+      alert_pkg::NAlerts  < (256 - alert_pkg::N_CLASSES))
+  `ASSERT_INIT(CheckEscCntDw,  alert_pkg::EscCntDw  <= 32)
+  `ASSERT_INIT(CheckAccuCntDw, alert_pkg::AccuCntDw <= 32)
+  `ASSERT_INIT(CheckNClasses,  alert_pkg::N_CLASSES <= 8)
+  `ASSERT_INIT(CheckNEscSev,   alert_pkg::N_ESC_SEV <= 8)
+
+endmodule
+
diff --git a/hw/ip/alert_handler/rtl/alert_handler_accu.sv b/hw/ip/alert_handler/rtl/alert_handler_accu.sv
new file mode 100644
index 0000000..74671f4
--- /dev/null
+++ b/hw/ip/alert_handler/rtl/alert_handler_accu.sv
@@ -0,0 +1,41 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// This module accumulates incoming alert triggers. Once the current accumulator
+// value is greater or equal the accumulator threshold, the next occurence of
+// trig_i will trigger escalation.
+//
+// Note that the accumulator is implemented using a saturation counter which
+// does not wrap around.
+//
+
+module alert_handler_accu (
+  input                                   clk_i,
+  input                                   rst_ni,
+  input                                   clr_i,     // clear the accumulator
+  input                                   trig_i,    // increments the accu
+  input        [alert_pkg::AccuCntDw-1:0] thresh_i,  // escalation trigger threshold
+  output logic [alert_pkg::AccuCntDw-1:0] cnt_o,     // output of current accu value
+  output logic                            trig_o     // escalation trigger output
+);
+
+  logic [alert_pkg::AccuCntDw-1:0] accu_d, accu_q;
+
+  assign accu_d = (clr_i)                ? '0            : // clear
+                  (trig_i && !(&accu_q)) ? accu_q + 1'b1 : // saturate counter at maximum
+                                           accu_q;
+
+  assign trig_o = (accu_q >= thresh_i) & trig_i;
+
+  assign cnt_o = accu_q;
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      accu_q <= '0;
+    end else begin
+      accu_q <= accu_d;
+    end
+  end
+
+endmodule : alert_handler_accu
diff --git a/hw/ip/alert_handler/rtl/alert_handler_class.sv b/hw/ip/alert_handler/rtl/alert_handler_class.sv
new file mode 100644
index 0000000..28020aa
--- /dev/null
+++ b/hw/ip/alert_handler/rtl/alert_handler_class.sv
@@ -0,0 +1,56 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// This module gates the alert triggers with their enable bits, and correctly bins
+// the enabled alerts into the class that they have been assigned to. The module
+// produces the alert cause and class trigger signals.
+//
+
+module alert_handler_class (
+  input [alert_pkg::NAlerts-1:0]                              alert_trig_i,      // alert trigger
+  input [alert_pkg::N_LOC_ALERT-1:0]                          loc_alert_trig_i,  // alert trigger
+  input [alert_pkg::NAlerts-1:0]                              alert_en_i,        // alert enable
+  input [alert_pkg::N_LOC_ALERT-1:0]                          loc_alert_en_i,    // alert enable
+  input [alert_pkg::NAlerts-1:0]    [alert_pkg::CLASS_DW-1:0] alert_class_i,     // class assignment
+  input [alert_pkg::N_LOC_ALERT-1:0][alert_pkg::CLASS_DW-1:0] loc_alert_class_i, // class assignment
+  input [alert_pkg::N_CLASSES-1:0]                            class_en_i,        // class enables
+
+  output logic [alert_pkg::NAlerts-1:0]                       alert_cause_o,     // alert cause
+  output logic [alert_pkg::N_LOC_ALERT-1:0]                   loc_alert_cause_o, // alert cause
+  output logic [alert_pkg::N_CLASSES-1:0]                     class_trig_o       // class triggered
+);
+
+  // assign alert cause
+  assign alert_cause_o     = alert_en_i     & alert_trig_i;
+  assign loc_alert_cause_o = loc_alert_en_i & loc_alert_trig_i;
+
+  // classification mapping
+  logic [alert_pkg::N_CLASSES-1:0][alert_pkg::NAlerts-1:0]     class_masks;
+  logic [alert_pkg::N_CLASSES-1:0][alert_pkg::N_LOC_ALERT-1:0] loc_class_masks;
+
+  // this is basically an address to onehot0 decoder
+  always_comb begin : p_class_mask
+    class_masks = '0;
+    loc_class_masks = '0;
+    for (int unsigned kk = 0; kk < alert_pkg::NAlerts; kk++) begin
+      class_masks[alert_class_i[kk]][kk] = 1'b1;
+    end
+    for (int unsigned kk = 0; kk < alert_pkg::N_LOC_ALERT; kk++) begin
+      loc_class_masks[loc_alert_class_i[kk]][kk] = 1'b1;
+    end
+  end
+
+  // mask and OR reduction, followed by class enable gating
+  for (genvar k = 0; k < alert_pkg::N_CLASSES; k++) begin : gen_classifier
+    assign class_trig_o[k] = class_en_i[k]         &
+                             (|{ alert_cause_o     & class_masks[k],
+                                 loc_alert_cause_o & loc_class_masks[k] });
+  end
+
+endmodule : alert_handler_class
+
+
+
+
+
diff --git a/hw/ip/alert_handler/rtl/alert_handler_esc_timer.sv b/hw/ip/alert_handler/rtl/alert_handler_esc_timer.sv
new file mode 100644
index 0000000..229ff84
--- /dev/null
+++ b/hw/ip/alert_handler/rtl/alert_handler_esc_timer.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 implements the escalation timer, which times the four escalation
+// phases. There are two mechanisms that can trigger the escalation protocol:
+//
+// 1) via accum_trigger_i, which will be asserted once the accumulator value
+//    exceeds a programmable amount of alert occurences.
+//
+// 2) via an interrupt timeout, if this is enabled. If this functionality is
+//    enabled, the internal escalation counter is reused to check whether the
+//    interrupt times out. If it does time out, the outcome is the same as if
+//    accum_trigger_i where asserted.
+//
+// Note that escalation always takes precedence over the interrupt timeout.
+//
+
+module alert_handler_esc_timer (
+  input                                   clk_i,
+  input                                   rst_ni,
+  input                                   en_i,           // enables timeout/escalation
+  input                                   clr_i,          // aborts escalation
+  input                                   accum_trig_i,   // this will trigger escalation
+  input                                   timeout_en_i,   // enables timeout
+  input        [alert_pkg::EscCntDw-1:0]  timeout_cyc_i,  // interrupt timeout. 0 = disabled
+  input        [alert_pkg::N_ESC_SEV-1:0] esc_en_i,       // escalation signal enables a
+  input        [alert_pkg::N_ESC_SEV-1:0]
+               [alert_pkg::PHASE_DW-1:0]  esc_map_i,      // escalation signal / phase map
+  input        [alert_pkg::N_PHASES-1:0]
+               [alert_pkg::EscCntDw-1:0]  phase_cyc_i,    // cycle counts of individual phases
+  output logic                            esc_trig_o,     // asserted if escalation triggers
+  output logic [alert_pkg::EscCntDw-1:0]  esc_cnt_o,      // current timeout / escalation count
+  output logic [alert_pkg::N_ESC_SEV-1:0] esc_sig_en_o,   // escalation signal outputs
+  // current state output
+  // 000: idle, 001: irq timeout counting 100: phase0, 101: phase1, 110: phase2, 111: phase3
+  output alert_pkg::cstate_e              esc_state_o
+);
+
+  //////////////////////////////////////////////////////
+  // Counter
+  //////////////////////////////////////////////////////
+
+  alert_pkg::cstate_e state_d, state_q;
+
+  logic cnt_en, cnt_clr, cnt_ge;
+  logic [alert_pkg::EscCntDw-1:0] cnt_d, cnt_q;
+
+  // escalation counter, used for all phases and the timeout
+  assign cnt_d = (cnt_en && cnt_clr) ? alert_pkg::EscCntDw'(1'b1) :
+                 (cnt_clr)           ? '0                         :
+                 (cnt_en)            ? cnt_q + 1'b1               :
+                                       cnt_q;
+
+  // current state output
+  assign esc_state_o = state_q;
+  assign esc_cnt_o   = cnt_q;
+
+  // threshold test, mux the thresholds depending on the current state
+  logic [alert_pkg::EscCntDw-1:0] thresh;
+  logic esc_is_on;
+  logic [1:0] phase_idx;
+  assign esc_is_on = state_q[2];
+  assign phase_idx = state_q[1:0];
+  assign thresh    = (esc_is_on) ? phase_cyc_i[phase_idx] : timeout_cyc_i;
+  assign cnt_ge    = (cnt_q >= thresh);
+
+  //////////////////////////////////////////////////////
+  // Main FSM
+  //////////////////////////////////////////////////////
+
+  always_comb begin : p_fsm
+    // default
+    state_d    = state_q;
+    cnt_en     = 1'b0;
+    cnt_clr    = 1'b0;
+    esc_trig_o = 1'b0;
+
+    unique case (state_q)
+      ////////////////////////////////////
+      // wait for an escalation trigger or an alert trigger
+      // the latter will trigger an interrupt timeout
+      alert_pkg::Idle: begin
+        if (accum_trig_i && en_i) begin
+          state_d    = alert_pkg::Phase0;
+          cnt_en     = 1'b1;
+          esc_trig_o = 1'b1;
+        // the counter is zero in this state. so if the
+        // timeout count is zero (==disabled), cnt_ge will be true.
+        end else if (timeout_en_i && !cnt_ge && en_i) begin
+          cnt_en  = 1'b1;
+          state_d = alert_pkg::Timeout;
+        end else begin
+          cnt_clr = 1'b1;
+        end
+      end
+      ////////////////////////////////////
+      // we are in interrupt timeout state
+      // in case an escalation comes in, we immediately have to
+      // switch over to the first escalation phase.
+      // in case the interrupt timeout hits it's cycle count, we
+      // also enter escalation phase0.
+      // ongoing timeouts can always be cleared.
+      alert_pkg::Timeout: begin
+        if (accum_trig_i || (cnt_ge && timeout_en_i)) begin
+          state_d    = alert_pkg::Phase0;
+          cnt_en     = 1'b1;
+          cnt_clr    = 1'b1;
+          esc_trig_o = 1'b1;
+        // the timeout enable is connected to the irq state
+        // if that is cleared, stop the timeout counter
+        end else if (timeout_en_i) begin
+          cnt_en  = 1'b1;
+        end else begin
+          state_d = alert_pkg::Idle;
+          cnt_clr = 1'b1;
+        end
+      end
+      ////////////////////////////////////
+      // note: autolocking the clear signal is done in the regfile
+      alert_pkg::Phase0: begin
+        if (clr_i) begin
+          state_d = alert_pkg::Idle;
+          cnt_clr = 1'b1;
+        end else if (cnt_ge) begin
+          state_d = alert_pkg::Phase1;
+          cnt_clr = 1'b1;
+          cnt_en  = 1'b1;
+        end else begin
+          cnt_en = 1'b1;
+        end
+      end
+      ////////////////////////////////////
+      alert_pkg::Phase1: begin
+        if (clr_i) begin
+          state_d = alert_pkg::Idle;
+          cnt_clr = 1'b1;
+        end else if (cnt_ge) begin
+          state_d = alert_pkg::Phase2;
+          cnt_clr = 1'b1;
+          cnt_en  = 1'b1;
+        end else begin
+          cnt_en = 1'b1;
+        end
+      end
+      ////////////////////////////////////
+      alert_pkg::Phase2: begin
+        if (clr_i) begin
+          state_d = alert_pkg::Idle;
+          cnt_clr = 1'b1;
+        end else if (cnt_ge) begin
+          state_d = alert_pkg::Phase3;
+          cnt_clr = 1'b1;
+          cnt_en  = 1'b1;
+        end else begin
+          cnt_en = 1'b1;
+        end
+      end
+      ////////////////////////////////////
+      alert_pkg::Phase3: begin
+        if (clr_i) begin
+          state_d = alert_pkg::Idle;
+          cnt_clr = 1'b1;
+        end else if (cnt_ge) begin
+          state_d = alert_pkg::Terminal;
+          cnt_clr = 1'b1;
+        end else begin
+          cnt_en = 1'b1;
+        end
+      end
+      ////////////////////////////////////
+      // final, terminal state after escalation.
+      // if clr is locked down, only a system reset
+      // will get us out of this state
+      alert_pkg::Terminal: begin
+        if (clr_i) begin
+          state_d = alert_pkg::Idle;
+        end
+      end
+      ////////////////////////////////////
+      default : state_d = alert_pkg::Idle;
+    endcase
+  end
+
+  for (genvar k = 0; k < alert_pkg::N_ESC_SEV; k++) begin : gen_esc_en
+    // check whether we are in the corresponding phase and whether
+    // the signal is enabled
+    assign esc_sig_en_o[k] = esc_en_i[k] &
+                             (alert_pkg::cstate_e'({1'b1, esc_map_i[k]}) == state_q);
+  end
+
+  //////////////////////////////////////////////////////
+  // Regs
+  //////////////////////////////////////////////////////
+
+  // switch interrupt / escalation mode
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      state_q <= alert_pkg::Idle;
+      cnt_q   <= '0;
+    end else begin
+      state_q <= state_d;
+      cnt_q   <= cnt_d;
+    end
+  end
+
+  //////////////////////////////////////////////////////
+  // Assertions
+  //////////////////////////////////////////////////////
+
+  // a clear should always bring us back to idle
+  `ASSERT(CheckClr, clr_i |=> state_q == alert_pkg::Idle, clk_i, !rst_ni)
+  // if currently in idle and not enabled, must remain here
+  `ASSERT(CheckEn,  state_q == alert_pkg::Idle && !en_i |=>
+      state_q == alert_pkg::Idle, clk_i, !rst_ni)
+  // Check if accumulation trigger correctly captured
+  `ASSERT(CheckAccumTrig0,  accum_trig_i && state_q == alert_pkg::Idle && !en_i |=>
+      state_q == alert_pkg::Phase0, clk_i, !rst_ni)
+  `ASSERT(CheckAccumTrig1,  accum_trig_i && state_q == alert_pkg::Timeout       |=>
+    state_q == alert_pkg::Phase0, clk_i, !rst_ni)
+  // Check if timeout correctly captured
+  `ASSERT(CheckTimeout0, !accum_trig_i && state_q == alert_pkg::Idle && timeout_en_i |=>
+      state_q == alert_pkg::Timeout, clk_i, !rst_ni)
+  `ASSERT(CheckTimeout1, !accum_trig_i && state_q == alert_pkg::Timeout && timeout_en_i |=>
+      state_q == alert_pkg::Timeout, clk_i, !rst_ni)
+  `ASSERT(CheckTimeout2, !accum_trig_i && state_q == alert_pkg::Timeout && !timeout_en_i |=>
+      state_q == alert_pkg::Idle, clk_i, !rst_ni)
+  // Check if timeout correctly triggers escalation
+  `ASSERT(CheckTimeoutTrig, state_q == alert_pkg::Timeout && timeout_en_i &&
+      cnt_q == timeout_cyc_i |=> state_q == alert_pkg::Phase0, clk_i, !rst_ni)
+  // Check whether escalation phases are correctly switched
+  `ASSERT(CheckPhase0, state_q == alert_pkg::Phase0 && !clr_i && cnt_q == phase_cyc_i[0] |=>
+      state_q == alert_pkg::Phase1, clk_i, !rst_ni)
+  `ASSERT(CheckPhase1, state_q == alert_pkg::Phase1 && !clr_i && cnt_q == phase_cyc_i[1] |=>
+      state_q == alert_pkg::Phase2, clk_i, !rst_ni)
+  `ASSERT(CheckPhase2, state_q == alert_pkg::Phase2 && !clr_i && cnt_q == phase_cyc_i[2] |=>
+      state_q == alert_pkg::Phase3, clk_i, !rst_ni)
+  `ASSERT(CheckPhase3, state_q == alert_pkg::Phase3 && !clr_i && cnt_q == phase_cyc_i[3] |=>
+      state_q == alert_pkg::Terminal, clk_i, !rst_ni)
+
+endmodule : alert_handler_esc_timer
diff --git a/hw/ip/alert_handler/rtl/alert_handler_ping_timer.sv b/hw/ip/alert_handler/rtl/alert_handler_ping_timer.sv
new file mode 100644
index 0000000..1196089
--- /dev/null
+++ b/hw/ip/alert_handler/rtl/alert_handler_ping_timer.sv
@@ -0,0 +1,222 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// This module implements the ping mechanism. Once enabled, this module uses an
+// LFSR-based PRNG to
+//
+// a) determine the next peripheral index to be pinged (can be an alert receiver or an
+//    escalation sender). it it is detected that this particular peripheral is disabled,
+//    another index will be drawn from the PRNG.
+//
+// b) determine the amount of pause cycles to wait before pinging the peripheral selected in a).
+//
+// Once the ping timer waited for the amount of pause cycles determined in b), it asserts
+// the ping enable signal of the peripheral determined in a). If that peripheral does
+// not respond within the ping timeout window, an internal alert will be raised.
+//
+// Further, if a spurious ping_ok signal is detected (i.e., a ping ok that has not been
+// requested), the ping timer will also raise an internal alert.
+//
+
+module alert_handler_ping_timer (
+  input                                     clk_i,
+  input                                     rst_ni,
+  input                                     entropy_i,          // from TRNG
+  input                                     en_i,               // enable ping testing
+  input        [alert_pkg::NAlerts-1:0]     alert_en_i,         // determines which alerts to ping
+  input        [alert_pkg::PING_CNT_DW-1:0] ping_timeout_cyc_i, // timeout in cycles
+  output logic [alert_pkg::NAlerts-1:0]     alert_ping_en_o,    // enable to alert receivers
+  output logic [alert_pkg::N_ESC_SEV-1:0]   esc_ping_en_o,      // enable to esc senders
+  input        [alert_pkg::NAlerts-1:0]     alert_ping_ok_i,    // response from alert receivers
+  input        [alert_pkg::N_ESC_SEV-1:0]   esc_ping_ok_i,      // response from esc senders
+  output logic                              alert_ping_fail_o,  // any of the alert receivers failed
+  output logic                              esc_ping_fail_o     // any of the esc senders failed
+);
+
+  localparam int unsigned NModsToPing = alert_pkg::NAlerts + alert_pkg::N_ESC_SEV;
+  localparam int unsigned IdDw        = $clog2(NModsToPing);
+
+  // this defines a random permutation
+  localparam int unsigned perm [0:31] = '{ 4, 11, 25,  3,
+                                          15, 16,  1, 10,
+                                           2, 22,  7,  0,
+                                          23, 28, 30, 19,
+                                          27, 12, 24, 26,
+                                          14, 21, 18,  5,
+                                          13,  8, 29, 31,
+                                          20,  6,  9, 17};
+
+  //////////////////////////////////////////////////////
+  // PRNG
+  //////////////////////////////////////////////////////
+
+  logic lfsr_en;
+  logic [31:0] lfsr_state, perm_state;
+
+  prim_lfsr #(
+    .LfsrDw ( 32                  ),
+    .InDw   ( 1                   ),
+    .OutDw  ( 32                  ),
+    .Seed   ( alert_pkg::LfsrSeed )
+  ) i_prim_lfsr (
+    .clk_i,
+    .rst_ni,
+    .en_i   ( lfsr_en    ),
+    .data_i ( entropy_i  ),
+    .data_o ( lfsr_state )
+  );
+
+  for (genvar k = 0; k < 32; k++) begin : gen_perm
+    assign perm_state[k] = lfsr_state[perm[k]];
+  end
+
+  logic [IdDw-1:0] id_to_ping;
+  logic [alert_pkg::PING_CNT_DW-1:0] wait_cyc;
+  // we only use bits up to 23, as IdDw is 8bit maximum
+  assign id_to_ping = perm_state[16 +: IdDw];
+  // concatenate with constant offset, introduce some stagger
+  // by concatenating the lower bits below
+  assign wait_cyc   = {perm_state[15:2], 8'h01, perm_state[1:0]};
+
+  logic [2**IdDw-1:0] enable_mask;
+  always_comb begin : p_enable_mask
+    enable_mask                                   = '0;         // tie off unused
+    enable_mask[alert_pkg::NAlerts-1:0]           = alert_en_i; // alerts
+    enable_mask[NModsToPing-1:alert_pkg::NAlerts] = '1;         // escalation senders
+  end
+
+  logic id_vld;
+  // check if the randomly drawn ID is actually valid and the alert is enabled
+  assign id_vld  = enable_mask[id_to_ping];
+
+  //////////////////////////////////////////////////////
+  // Counter
+  //////////////////////////////////////////////////////
+
+  logic [alert_pkg::PING_CNT_DW-1:0] cnt_d, cnt_q;
+  logic cnt_en, cnt_clr;
+  logic wait_ge, timeout_ge;
+
+  assign cnt_d = (cnt_clr) ? '0           :
+                 (cnt_en)  ? cnt_q + 1'b1 :
+                             cnt_q;
+
+  assign wait_ge    = (cnt_q >= wait_cyc);
+  assign timeout_ge = (cnt_q >= ping_timeout_cyc_i);
+
+  //////////////////////////////////////////////////////
+  // Ping and Timeout Logic
+  //////////////////////////////////////////////////////
+
+  typedef enum logic [1:0] {Init, RespWait, DoPing} state_e;
+  state_e state_d, state_q;
+  logic ping_en, ping_ok;
+  logic [NModsToPing-1:0] ping_sel;
+  logic [NModsToPing-1:0] spurious_ping;
+  logic spurious_alert_ping, spurious_esc_ping;
+
+  // generate ping enable vector
+  assign ping_sel        = (alert_pkg::NAlerts+alert_pkg::N_ESC_SEV)'(ping_en) << id_to_ping;
+  assign alert_ping_en_o = ping_sel[alert_pkg::NAlerts-1:0];
+  assign esc_ping_en_o   = ping_sel[NModsToPing-1:alert_pkg::NAlerts];
+
+  // mask out response
+  assign ping_ok             = |({esc_ping_ok_i, alert_ping_ok_i} & ping_sel);
+  assign spurious_ping       = ({esc_ping_ok_i, alert_ping_ok_i} & ~ping_sel);
+  // under normal operation, these signals should never be asserted.
+  // double check that these signals are not optimized away during synthesis.
+  // this may need "don't touch" or "no boundary optimization" constraints
+  assign spurious_alert_ping = |spurious_ping[alert_pkg::NAlerts-1:0];
+  assign spurious_esc_ping   = |spurious_ping[NModsToPing-1:alert_pkg::NAlerts];
+
+  always_comb begin : p_fsm
+    // default
+    state_d = state_q;
+    cnt_en  = 1'b0;
+    cnt_clr = 1'b0;
+    lfsr_en = 1'b0;
+    ping_en = 1'b0;
+    // this captures spurious
+    alert_ping_fail_o = spurious_alert_ping;
+    esc_ping_fail_o   = spurious_esc_ping;
+
+    unique case (state_q)
+      ///////////////////////////
+      // wait until activiated
+      // we never return to this state
+      // once activated!
+      Init: begin
+        cnt_clr = 1'b0;
+        if (en_i) begin
+          state_d = RespWait;
+        end
+      end
+      ///////////////////////////
+      // wait for random amount of cycles
+      // draw another ID/wait count if the
+      // peripheral ID is not valid
+      RespWait: begin
+        if (!id_vld) begin
+          lfsr_en = 1'b1;
+          cnt_clr = 1'b1;
+        end else if (wait_ge) begin
+          state_d = DoPing;
+          lfsr_en = 1'b1;
+          cnt_clr = 1'b1;
+        end else begin
+          cnt_en = 1'b1;
+        end
+      end
+      ///////////////////////////
+      // send out ping request and wait for a ping
+      // response or a ping timeout (whatever comes first)
+      DoPing: begin
+        cnt_en  = 1'b1;
+        ping_en = 1'b1;
+        if (timeout_ge || ping_ok) begin
+          state_d = RespWait;
+          cnt_clr = 1'b1;
+          if (timeout_ge) begin
+            if (id_to_ping < alert_pkg::NAlerts) begin
+              alert_ping_fail_o = 1'b1;
+            end else begin
+              esc_ping_fail_o   = 1'b1;
+            end
+          end
+        end
+      end
+      ///////////////////////////
+      default : state_d = Init;
+    endcase
+
+  end
+
+  //////////////////////////////////////////////////////
+  // Flops
+  //////////////////////////////////////////////////////
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      state_q <= Init;
+      cnt_q   <= '0;
+    end else begin
+      state_q <= state_d;
+      cnt_q   <= cnt_d;
+    end
+  end
+
+  //////////////////////////////////////////////////////
+  // Assertions
+  //////////////////////////////////////////////////////
+
+  // internals
+  `ASSERT(PingOH0, $onehot0(ping_sel), clk_i, !rst_ni)
+  // we should never get into the ping state without knowing
+  // which module to ping
+  `ASSERT(PingOH, ping_en |-> $onehot(ping_sel), clk_i, !rst_ni)
+
+  // TODO: add some cover metrics to check whether all devices
+  // are pinged eventually.
+
+endmodule : alert_handler_ping_timer