[otbn,rtl] Pass an escalation signal to start/stop controller

Otherwise, an escalation that happens when we're in e.g. state
OtbnStartStopStateUrndRefresh leaves us in a weird half-running state.
The rest of OTBN knows that we're stopping immediately in this case:
make sure that the start/stop controller knows too!

The other change with this PR is that we change the
OtbnStartStopStateError state to OtbnStartStopStateLocked. An FSM
glitch will still ping state_error_o for a cycle, causing a fatal
alert, but now we have a nice terminal state for local escalation.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/rtl/otbn_core.sv b/hw/ip/otbn/rtl/otbn_core.sv
index 9a641e2..005da02 100644
--- a/hw/ip/otbn/rtl/otbn_core.sv
+++ b/hw/ip/otbn/rtl/otbn_core.sv
@@ -214,7 +214,7 @@
   logic [ImemAddrWidth:0]   prefetch_loop_end_addr;
   logic [ImemAddrWidth-1:0] prefetch_loop_jump_addr;
 
-  mubi4_t               controller_escalate_en;
+  mubi4_t               controller_escalate_en, start_stop_escalate_en;
   controller_err_bits_t controller_err_bits;
 
   core_err_bits_t err_bits_q, err_bits_d;
@@ -232,6 +232,7 @@
     .rst_ni,
 
     .start_i,
+    .escalate_en_i(start_stop_escalate_en),
 
     .controller_start_o(controller_start),
 
@@ -438,6 +439,12 @@
   `ASSERT(InsnDataStableInStall, u_otbn_controller.state_q == OtbnStateStall |->
                                  insn_fetch_resp_data == $past(insn_fetch_resp_data))
 
+  // Spot the fatal error bits from the controller
+  logic controller_fatal_err;
+  assign controller_fatal_err = |{controller_err_bits.fatal_software,
+                                  controller_err_bits.bad_internal_state,
+                                  controller_err_bits.reg_intg_violation};
+
   // Generate an err_bits output by combining errors from all the blocks in otbn_core
   assign err_bits_d = '{
     fatal_software:      controller_err_bits.fatal_software,
@@ -472,12 +479,16 @@
   // Pass an "escalation" signal down to the controller by ORing in error signals from the other
   // modules in otbn_core. Note that each error signal except escalate_en_i that appears here also
   // appears somewhere in err_bits_o above (checked in ErrBitsIfControllerEscalate_A)
-  assign controller_escalate_en = mubi4_or_hi(
-      escalate_en_i, mubi4_bool_to_mubi(
-          |{start_stop_state_error, urnd_all_zero, rf_base_rd_data_err, lsu_rdata_err,
-            insn_fetch_err}
-      )
-  );
+  assign controller_escalate_en =
+      mubi4_or_hi(escalate_en_i,
+                  mubi4_bool_to_mubi(|{start_stop_state_error, urnd_all_zero,
+                                       rf_base_rd_data_err, lsu_rdata_err, insn_fetch_err}));
+
+  // Similarly for the start/stop controller
+  assign start_stop_escalate_en =
+      mubi4_or_hi(escalate_en_i,
+                  mubi4_bool_to_mubi(|{urnd_all_zero, rf_base_rd_data_err,
+                                       lsu_rdata_err, insn_fetch_err, controller_fatal_err}));
 
   assign insn_cnt_o = insn_cnt;
 
@@ -734,4 +745,10 @@
           mubi4_test_true_loose(controller_escalate_en) && mubi4_test_false_strict(escalate_en_i)
           |=> err_bits_q)
 
+  // Similarly, if we pass an escalation signal down to the start/stop controller then we should
+  // also be setting an error flag, unless the signal came from above.
+  `ASSERT(ErrBitsIfStartStopEscalate_A,
+          mubi4_test_true_loose(start_stop_escalate_en) && mubi4_test_false_strict(escalate_en_i)
+          |=> err_bits_q)
+
 endmodule
diff --git a/hw/ip/otbn/rtl/otbn_pkg.sv b/hw/ip/otbn/rtl/otbn_pkg.sv
index 3eb6515..7d4529d 100644
--- a/hw/ip/otbn/rtl/otbn_pkg.sv
+++ b/hw/ip/otbn/rtl/otbn_pkg.sv
@@ -481,7 +481,7 @@
     OtbnStartStopSecureWipeAccModBaseUrnd = 6'b100101,
     OtbnStartStopSecureWipeAllZero        = 6'b111110,
     OtbnStartStopSecureWipeComplete       = 6'b001011,
-    OtbnStartStopStateError               = 6'b000110
+    OtbnStartStopStateLocked              = 6'b000110
   } otbn_start_stop_state_e;
 
 // Encoding generated with:
diff --git a/hw/ip/otbn/rtl/otbn_start_stop_control.sv b/hw/ip/otbn/rtl/otbn_start_stop_control.sv
index 44054e3..1e61e1f 100644
--- a/hw/ip/otbn/rtl/otbn_start_stop_control.sv
+++ b/hw/ip/otbn/rtl/otbn_start_stop_control.sv
@@ -24,14 +24,16 @@
 
 module otbn_start_stop_control
   import otbn_pkg::*;
-  #(
+  import prim_mubi_pkg::*;
+#(
   // Enable internal secure wipe
   parameter bit                SecWipeEn  = 1'b0
 )(
   input  logic clk_i,
   input  logic rst_ni,
 
-  input  logic start_i,
+  input  logic  start_i,
+  input mubi4_t escalate_en_i,
 
   output logic controller_start_o,
 
@@ -62,6 +64,27 @@
   logic addr_cnt_inc;
   logic [4:0] addr_cnt_q, addr_cnt_d;
 
+  // There are two ways in which the start/stop controller can be told to stop. Either
+  // start_secure_wipe_i comes from the controller (which means "I've run some instructions and I've
+  // hit an ECALL or error"). Or escalate_en_i can be asserted (which means "Someone else has told
+  // us to stop immediately"). If running, both can be true at once.
+  //
+  // An escalation signal gets latched into should_lock. If we were running some instructions, we'll
+  // go through the secure wipe process, but we'll see the should_lock_q signal when done and go
+  // into the local locked state.
+  logic esc_request, should_lock_d, should_lock_q, stop;
+  assign esc_request   = mubi4_test_true_loose(escalate_en_i);
+  assign stop          = esc_request | start_secure_wipe_i;
+  assign should_lock_d = should_lock_q | esc_request;
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin
+    if (!rst_ni) begin
+      should_lock_q <= 1'b0;
+    end else begin
+      should_lock_q <= should_lock_d;
+    end
+  end
+
   // SEC_CM: START_STOP_CTRL.FSM.SPARSE
   `PRIM_FLOP_SPARSE_FSM(u_state_regs, state_d, state_q,
       otbn_start_stop_state_e, OtbnStartStopStateHalt)
@@ -85,7 +108,9 @@
 
     unique case (state_q)
       OtbnStartStopStateHalt: begin
-        if (start_i) begin
+        if (stop) begin
+          state_d = OtbnStartStopStateLocked;
+        end else if (start_i) begin
           urnd_reseed_req_o = 1'b1;
           ispr_init_o       = 1'b1;
           state_reset_o     = 1'b1;
@@ -93,13 +118,15 @@
         end
       end
       OtbnStartStopStateUrndRefresh: begin
-        if (!urnd_reseed_busy_i) begin
+        if (stop) begin
+          state_d = OtbnStartStopStateLocked;
+        end else if (!urnd_reseed_busy_i) begin
           state_d     = OtbnStartStopStateRunning;
         end
       end
       OtbnStartStopStateRunning: begin
         urnd_advance_o = 1'b1;
-        if (start_secure_wipe_i) begin
+        if (stop) begin
           if (SecWipeEn) begin
             state_d = OtbnStartStopSecureWipeWdrUrnd;
           end
@@ -148,20 +175,21 @@
           state_d = OtbnStartStopSecureWipeComplete;
         end
       end
-       OtbnStartStopSecureWipeComplete: begin
+      OtbnStartStopSecureWipeComplete: begin
         urnd_advance_o = 1'b1;
         secure_wipe_running_o = 1'b1;
-        state_d = OtbnStartStopStateHalt;
+        state_d = should_lock_d ? OtbnStartStopStateLocked : OtbnStartStopStateHalt;
       end
-      OtbnStartStopStateError: begin
+      OtbnStartStopStateLocked: begin
         // SEC_CM: START_STOP_CTRL.FSM.LOCAL_ESC
-        // Terminal error state
-        state_error_o = 1'b1;
+        //
+        // Terminal state. This is either accessed by glitching state_q (and going through the
+        // default case below) or by getting an escalation signal
       end
       default: begin
         // We should never get here. If we do (e.g. via a malicious glitch), error out immediately.
         state_error_o = 1'b1;
-        state_d = OtbnStartStopStateError;
+        state_d = OtbnStartStopStateLocked;
       end
     endcase
   end
@@ -182,13 +210,17 @@
 
   assign sec_wipe_addr_o = addr_cnt_q;
 
-  `ASSERT(StartStopStateValid,
+  `ASSERT(StartStopStateValid_A,
       state_q inside {OtbnStartStopStateHalt,
                       OtbnStartStopStateUrndRefresh,
                       OtbnStartStopStateRunning,
                       OtbnStartStopSecureWipeWdrUrnd,
                       OtbnStartStopSecureWipeAccModBaseUrnd,
                       OtbnStartStopSecureWipeAllZero,
-                      OtbnStartStopSecureWipeComplete})
+                      OtbnStartStopSecureWipeComplete,
+                      OtbnStartStopStateLocked})
+
+  `ASSERT(StartSecureWipeImpliesRunning_A,
+          start_secure_wipe_i |-> (state_q == OtbnStartStopStateRunning))
 
 endmodule