[otbn,dv] Add a check for the INSN_CNT register

Since we want a cycle-by-cycle check that the dut and model match, it
seemed easiest just to use an assertion in an interface. This also
makes it easy to wire up a cover property to check we've seen the
counter saturate.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/doc/dv/index.md b/hw/ip/otbn/doc/dv/index.md
index 6303f70..95f6bc3 100644
--- a/hw/ip/otbn/doc/dv/index.md
+++ b/hw/ip/otbn/doc/dv/index.md
@@ -159,6 +159,12 @@
 > The covergroup contains eight coverpoints (for each flag set and cleared).
 > These are then crossed with the flag group.
 
+#### Instruction counter
+
+See the instruction counter saturate.
+
+> This is tracked in the `insn_cnt_if` interface with the `InsnCntSaturated_C` cover property.
+
 ### Instruction-based coverage
 
 As a processor, much of OTBN's coverage points are described in terms of instructions being executed.
diff --git a/hw/ip/otbn/dv/uvm/env/otbn_env.core b/hw/ip/otbn/dv/uvm/env/otbn_env.core
index af06087..f23c345 100644
--- a/hw/ip/otbn/dv/uvm/env/otbn_env.core
+++ b/hw/ip/otbn/dv/uvm/env/otbn_env.core
@@ -20,6 +20,7 @@
       - otbn_alu_bignum_if.sv
       - otbn_mac_bignum_if.sv
       - otbn_rf_base_if.sv
+      - otbn_insn_cnt_if.sv
       - otbn_trace_item.sv: {is_include_file: true}
       - otbn_trace_monitor.sv: {is_include_file: true}
       - otbn_env_cfg.sv: {is_include_file: true}
diff --git a/hw/ip/otbn/dv/uvm/env/otbn_insn_cnt_if.sv b/hw/ip/otbn/dv/uvm/env/otbn_insn_cnt_if.sv
new file mode 100644
index 0000000..da0db16
--- /dev/null
+++ b/hw/ip/otbn/dv/uvm/env/otbn_insn_cnt_if.sv
@@ -0,0 +1,47 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Used to track instruction counts
+
+`include "prim_assert.sv"
+
+interface otbn_insn_cnt_if (
+  input              clk_i,
+  input              rst_ni,
+
+  input logic [31:0] insn_cnt_i,
+  input logic        insn_executing_i,
+  input logic        stall_i,
+
+  input bit [31:0]   model_insn_cnt_i
+);
+
+  // Check that the model and OTBN have matching instruction counters
+  `ASSERT(InsnCntMatches_A, model_insn_cnt_i == insn_cnt_i)
+
+  // As well as exposing the count itself, we've also exposed the "increment me" signal
+  // (insn_executing_i && !stall_i). This means we can see when an instruction counter of all ones
+  // would have incremented but saturated instead. Make sure we've exposed it properly by asserting
+  // that the count works as we expect the rest of the time.
+  logic [31:0] insn_cnt_r;
+  logic        increment_me_r;
+  always_ff @(posedge clk_i or negedge rst_ni) begin
+    if (!rst_ni) begin
+      insn_cnt_r     <= 0;
+      increment_me_r <= 1'b0;
+    end else begin
+      insn_cnt_r     <= insn_cnt_i;
+      increment_me_r <= insn_executing_i & ~stall_i;
+    end
+  end
+  `ASSERT(IncWhenTold_A,
+          (increment_me_r && (insn_cnt_r < '1)) |-> (insn_cnt_i == insn_cnt_r + 32'd1))
+
+  // Now we know that the insn_executing_i and stall_i signals have the behaviour we expect, check
+  // for insn_cnt saturating by expecting to see a cycle where increment_me_r is true but insn_cnt_r
+  // and insn_cnt both equal '1.
+  `COVER(InsnCntSaturated_C,
+         increment_me_r && (insn_cnt_i == '1) && (insn_cnt_r == '1))
+
+endinterface
diff --git a/hw/ip/otbn/dv/uvm/tb.sv b/hw/ip/otbn/dv/uvm/tb.sv
index ccff183..06fde1f 100644
--- a/hw/ip/otbn/dv/uvm/tb.sv
+++ b/hw/ip/otbn/dv/uvm/tb.sv
@@ -129,6 +129,8 @@
   assign edn_rnd_data_valid = dut.edn_rnd_req & dut.edn_rnd_ack;
   assign edn_urnd_data_valid = dut.edn_urnd_req & dut.edn_urnd_ack;
 
+  bit [31:0] model_insn_cnt;
+
   otbn_core_model #(
     .DmemSizeByte (otbn_reg_pkg::OTBN_DMEM_SIZE),
     .ImemSizeByte (otbn_reg_pkg::OTBN_IMEM_SIZE),
@@ -141,16 +143,29 @@
     .start_i      (model_if.start),
     .done_o       (model_if.done),
     .start_addr_i (model_if.start_addr),
-    .err_o        (model_if.err),
 
     .edn_rnd_data_valid_i  (edn_rnd_data_valid),
     .edn_rnd_data_i        (dut.edn_rnd_data),
-    .edn_urnd_data_valid_i (edn_urnd_data_valid)
+    .edn_urnd_data_valid_i (edn_urnd_data_valid),
+
+    .insn_cnt_o   (model_insn_cnt),
+    .err_o        (model_if.err)
   );
 
   // Pull the final PC out of the DUT
   assign model_if.stop_pc = u_model.stop_pc_q;
 
+  otbn_insn_cnt_if insn_cnt_if (
+   .clk_i            (clk),
+   .rst_ni           (rst_n),
+
+   .insn_cnt_i       (dut.insn_cnt),
+   .insn_executing_i (dut.u_otbn_core.u_otbn_controller.insn_executing),
+   .stall_i          (dut.u_otbn_core.u_otbn_controller.stall),
+
+   .model_insn_cnt_i (model_insn_cnt)
+  );
+
   initial begin
     // drive clk and rst_n from clk_if
     clk_rst_if.set_active();