[otbn] Report errors properly from ISS to RTL

Most of the plumbing is pretty obvious. The only challenging bit is
getting multiple values back from the C++ model wrapper to the SV code
in otbn_core_model.sv. We were already passing 3 flags as the bottom 3
bits of a uint32_t. Now we pass a 16-bit error code as the top 3 bits.
If we ever need more than that many bits, we might have to do
something cleverer with out parameters, but I thought the quick and
dirty solution was probably best for now.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/model/iss_wrapper.cc b/hw/ip/otbn/dv/model/iss_wrapper.cc
index b71fe98..910741e 100644
--- a/hw/ip/otbn/dv/model/iss_wrapper.cc
+++ b/hw/ip/otbn/dv/model/iss_wrapper.cc
@@ -189,6 +189,35 @@
   return strtoul(buf, nullptr, 16);
 }
 
+// Read through trace output (in the lines argument) to pick up any write to
+// the named CSR register. If there is no such write, return default_val.
+static uint32_t read_ext_reg(const std::string &reg_name,
+                             const std::vector<std::string> &lines,
+                             uint32_t default_val) {
+  // We're interested in lines that show an update to the external register
+  // called reg_name. These look something like this:
+  //
+  //   otbn.$REG_NAME &= ~ 0x000001 (from HW) (now 0x000000)
+  //
+  // The \n picks up the newline that we expect at the end of each line.
+  std::regex re("\\s*otbn\\." + reg_name + ".*0x([0-9a-f]{8})\\)\n");
+  std::smatch match;
+
+  uint32_t val = default_val;
+
+  for (const auto &line : lines) {
+    if (std::regex_match(line, match, re)) {
+      // Ahah! We have a match. We have captured exactly 8 hex digits, so know
+      // that we can safely parse them to a uint32_t without risking a parse
+      // failure or overflow.
+      assert(match.size() == 2);
+      val = (uint32_t)strtoul(match[1].str().c_str(), nullptr, 16);
+    }
+  }
+
+  return val;
+}
+
 ISSWrapper::ISSWrapper() : tmpdir(new TmpDir()) {
   std::string model_path(find_otbn_model());
 
@@ -293,10 +322,15 @@
   run_command(oss.str(), nullptr);
 }
 
-bool ISSWrapper::step() {
+std::pair<bool, uint32_t> ISSWrapper::step() {
   std::vector<std::string> lines;
   run_command("step\n", &lines);
-  return saw_busy_cleared(lines);
+
+  // The busy flag is bit 0 of the STATUS register, so is cleared on this cycle
+  // if we see a write that sets the value to an even number.
+  bool done = (read_ext_reg("STATUS", lines, 1) & 1) == 0;
+  uint32_t err_code = done ? read_ext_reg("ERR_CODE", lines, 0) : 0;
+  return std::make_pair(done, err_code);
 }
 
 void ISSWrapper::get_regs(std::array<uint32_t, 32> *gprs,
@@ -471,38 +505,3 @@
   fflush(child_write_file);
   return read_child_response(dst);
 }
-
-bool ISSWrapper::saw_busy_cleared(std::vector<std::string> &lines) const {
-  // We're interested in lines that show an update to otbn.STATUS. These look
-  // something like this:
-  //
-  //   otbn.STATUS &= ~ 0x000001 (from HW) (now 0x000000)
-  //
-  // The \n picks up the newline that we expect at the end of each line.
-  std::regex re("\\s*otbn\\.STATUS.*0x[0-9]+\\)\n");
-
-  bool is_cleared = false;
-
-  for (const auto &line : lines) {
-    if (std::regex_match(line, re)) {
-      // Ahah! We have a match. At this point, we can cheat because we happen
-      // to know that the the busy bit is bit zero, so we just need to check
-      // whether the last character of the hex constant is even. Since the
-      // regex has a fixed number (2) of characters after the hex constant, we
-      // can just count back from the end of the string.
-      char last_digit = (&line.back())[-2];
-
-      int as_num;
-      if ('0' <= last_digit && last_digit <= '9') {
-        as_num = last_digit - '0';
-      } else {
-        assert('a' <= last_digit && last_digit <= 'f');
-        as_num = 10 + (last_digit - 'a');
-      }
-
-      is_cleared = !(as_num & 1);
-    }
-  }
-
-  return is_cleared;
-}
diff --git a/hw/ip/otbn/dv/model/iss_wrapper.h b/hw/ip/otbn/dv/model/iss_wrapper.h
index 0e76258..475a599 100644
--- a/hw/ip/otbn/dv/model/iss_wrapper.h
+++ b/hw/ip/otbn/dv/model/iss_wrapper.h
@@ -38,7 +38,7 @@
 
   // Run simulation for a single cycle. Return true if it is now
   // finished (ECALL or error).
-  bool step();
+  std::pair<bool, uint32_t> step();
 
   // Read contents of the register file
   void get_regs(std::array<uint32_t, 32> *gprs, std::array<u256_t, 32> *wdrs);
@@ -61,10 +61,6 @@
   // value and dst argument behave as for read_child_response.
   bool run_command(const std::string &cmd, std::vector<std::string> *dst) const;
 
-  // Return true if lines contain a line that shows otbn.STATUS having
-  // its busy flag cleared.
-  bool saw_busy_cleared(std::vector<std::string> &lines) const;
-
   pid_t child_pid;
   FILE *child_write_file;
   FILE *child_read_file;
diff --git a/hw/ip/otbn/dv/model/otbn_core_model.sv b/hw/ip/otbn/dv/model/otbn_core_model.sv
index 8d3f324..fcfafdc 100644
--- a/hw/ip/otbn/dv/model/otbn_core_model.sv
+++ b/hw/ip/otbn/dv/model/otbn_core_model.sv
@@ -41,6 +41,8 @@
   input  logic  start_i, // start the operation
   output bit    done_o,  // operation done
 
+  output err_code_e err_code_o, // valid when done_o is asserted
+
   input  logic [ImemAddrWidth-1:0] start_addr_i, // start byte address in IMEM
 
   output bit err_o        // something went wrong
@@ -101,11 +103,17 @@
 
   // Extract particular bits of the status value.
   //
-  //   running: The ISS is currently running
-  //   failed_step: Something went wrong when trying to start or step the ISS.
-  //   failed_cmp:  The consistency check at the end of run failed.
+  //   [0]     running:      The ISS is currently running
+  //   [1]     failed_step:  Something went wrong when trying to start or step the ISS.
+  //   [2]     failed_cmp:   The consistency check at the end of run failed.
+  //   [31:16] raw_err_code: The code to expose as the ERR_CODE register
+  //
+
   bit failed_cmp, failed_step, running;
+  bit [15:0] raw_err_code;
   assign {failed_cmp, failed_step, running} = status[2:0];
+  assign raw_err_code = status[31:16];
+  assign err_code_o = err_code_e'(raw_err_code);
 
   always_ff @(posedge clk_i or negedge rst_ni) begin
     if (!rst_ni || !enable_i) begin
diff --git a/hw/ip/otbn/dv/model/otbn_model.cc b/hw/ip/otbn/dv/model/otbn_model.cc
index 5a8baab..77f62ed 100644
--- a/hw/ip/otbn/dv/model/otbn_model.cc
+++ b/hw/ip/otbn/dv/model/otbn_model.cc
@@ -31,6 +31,7 @@
 #define RUNNING_BIT (1U << 0)
 #define FAILED_STEP_BIT (1U << 1)
 #define FAILED_CMP_BIT (1U << 2)
+#define STATUS_MASK ((1U << 3) - 1)
 
 // The main entry point to the OTBN model, exported from here and used in
 // otbn_core_model.sv.
@@ -38,9 +39,10 @@
 // This communicates state with otbn_core_model.sv through the status
 // parameter, which has the following bits:
 //
-//    Bit 0:    running       True if the model is currently running
-//    Bit 1:    failed_step   Something failed when trying to start/step ISS
-//    Bit 2:    failed_cmp    Consistency check at end of run failed
+//    Bit 0:      running       True if the model is currently running
+//    Bit 1:      failed_step   Something failed when trying to start/step ISS
+//    Bit 2:      failed_cmp    Consistency check at end of run failed
+//    Bits 31:16: err_code      Error code to report on negedge of running
 //
 // The otbn_model_step function should only be called when either the model is
 // running (bit 0 of status) or when start_i is asserted. At other times, it
@@ -227,11 +229,19 @@
 }
 
 // Step once in the model. Returns 1 if the model has finished, 0 if not and -1
-// on failure.
-static int step_model(ISSWrapper *model) {
+// on failure. If the model has finished, writes otbn.ERR_CODE to *err_code.
+static int step_model(ISSWrapper *model, uint32_t *err_code) {
   assert(model);
+  assert(err_code);
+
   try {
-    return model->step() ? 1 : 0;
+    std::pair<bool, uint32_t> ret = model->step();
+    if (ret.first) {
+      *err_code = ret.second;
+      return 1;
+    } else {
+      return 0;
+    }
   } catch (const std::runtime_error &err) {
     std::cerr << "Error when stepping ISS: " << err.what() << "\n";
     return -1;
@@ -517,6 +527,9 @@
                                     unsigned start_addr, unsigned status) {
   assert(model && imem_scope && dmem_scope && design_scope);
 
+  // Clear out any ERR_CODE from a previous cycle
+  status &= STATUS_MASK;
+
   // Start the model if requested
   if (start_i) {
     switch (start_model(model, imem_scope, imem_words, dmem_scope, dmem_words,
@@ -537,14 +550,15 @@
     return status;
 
   // Step the model once
-  switch (step_model(model)) {
+  uint32_t err_code;
+  switch (step_model(model, &err_code)) {
     case 0:
       // Still running: no change
       break;
 
     case 1:
       // Finished
-      status = status & ~RUNNING_BIT;
+      status = (status & ~RUNNING_BIT) | (err_code << 16);
       break;
 
     default:
diff --git a/hw/ip/otbn/dv/otbnsim/sim/ext_regs.py b/hw/ip/otbn/dv/otbnsim/sim/ext_regs.py
index ca43c11..e585b2e 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/ext_regs.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/ext_regs.py
@@ -18,7 +18,7 @@
         self.new_value = new_value
 
     def trace(self) -> str:
-        return ("otbn.{} {} {:#08x}{} (now {:#08x})"
+        return ("otbn.{} {} {:#010x}{} (now {:#010x})"
                 .format(self.name,
                         self.op,
                         self.written,
diff --git a/hw/ip/otbn/dv/verilator/otbn_top_sim.sv b/hw/ip/otbn/dv/verilator/otbn_top_sim.sv
index fedbbb0..d4b3205 100644
--- a/hw/ip/otbn/dv/verilator/otbn_top_sim.sv
+++ b/hw/ip/otbn/dv/verilator/otbn_top_sim.sv
@@ -18,9 +18,10 @@
   localparam int ImemAddrWidth = prim_util_pkg::vbits(ImemSizeByte);
   localparam int DmemAddrWidth = prim_util_pkg::vbits(DmemSizeByte);
 
-  logic otbn_done_d, otbn_done_q;
-  logic otbn_start;
-  logic otbn_start_done;
+  logic      otbn_done_d, otbn_done_q;
+  err_code_e otbn_err_code_d, otbn_err_code_q;
+  logic      otbn_start;
+  logic      otbn_start_done;
 
   // Instruction memory (IMEM) signals
   logic                     imem_req;
@@ -45,31 +46,31 @@
     .ImemSizeByte ( ImemSizeByte ),
     .DmemSizeByte ( DmemSizeByte )
   ) u_otbn_core (
-    .clk_i         ( IO_CLK        ),
-    .rst_ni        ( IO_RST_N      ),
+    .clk_i         ( IO_CLK          ),
+    .rst_ni        ( IO_RST_N        ),
 
-    .start_i       ( otbn_start    ),
-    .done_o        ( otbn_done_d   ),
+    .start_i       ( otbn_start      ),
+    .done_o        ( otbn_done_d     ),
 
-    .err_code_o    (               ),
+    .err_code_o    ( otbn_err_code_d ),
 
-    .start_addr_i  ( ImemStartAddr ),
+    .start_addr_i  ( ImemStartAddr   ),
 
-    .imem_req_o    ( imem_req      ),
-    .imem_addr_o   ( imem_addr     ),
-    .imem_wdata_o  (               ),
-    .imem_rdata_i  ( imem_rdata    ),
-    .imem_rvalid_i ( imem_rvalid   ),
-    .imem_rerror_i ( imem_rerror   ),
+    .imem_req_o    ( imem_req        ),
+    .imem_addr_o   ( imem_addr       ),
+    .imem_wdata_o  (                 ),
+    .imem_rdata_i  ( imem_rdata      ),
+    .imem_rvalid_i ( imem_rvalid     ),
+    .imem_rerror_i ( imem_rerror     ),
 
-    .dmem_req_o    ( dmem_req      ),
-    .dmem_write_o  ( dmem_write    ),
-    .dmem_addr_o   ( dmem_addr     ),
-    .dmem_wdata_o  ( dmem_wdata    ),
-    .dmem_wmask_o  ( dmem_wmask    ),
-    .dmem_rdata_i  ( dmem_rdata    ),
-    .dmem_rvalid_i ( dmem_rvalid   ),
-    .dmem_rerror_i ( dmem_rerror   )
+    .dmem_req_o    ( dmem_req        ),
+    .dmem_write_o  ( dmem_write      ),
+    .dmem_addr_o   ( dmem_addr       ),
+    .dmem_wdata_o  ( dmem_wdata      ),
+    .dmem_wmask_o  ( dmem_wmask      ),
+    .dmem_rdata_i  ( dmem_rdata      ),
+    .dmem_rvalid_i ( dmem_rvalid     ),
+    .dmem_rerror_i ( dmem_rerror     )
   );
 
   bind otbn_core otbn_trace_intf #(.ImemAddrWidth, .DmemAddrWidth) i_otbn_trace_intf (.*);
@@ -82,6 +83,7 @@
       otbn_start      <= 1'b0;
       otbn_start_done <= 1'b0;
       otbn_done_q     <= 1'b0;
+      otbn_err_code_q <= ErrCodeNoError;
     end else begin
       if (!otbn_start_done) begin
         otbn_start      <= 1'b1;
@@ -91,6 +93,7 @@
       end
 
       otbn_done_q <= otbn_done_d;
+      otbn_err_code_q <= otbn_err_code_d;
     end
   end
 
@@ -186,8 +189,9 @@
   localparam string DmemScope = "..u_dmem.u_mem.gen_generic.u_impl_generic";
   localparam string DesignScope = "..u_otbn_core";
 
-  logic otbn_model_done;
-  bit   otbn_model_err;
+  logic      otbn_model_done;
+  err_code_e otbn_model_err_code;
+  bit        otbn_model_err;
 
   otbn_core_model #(
     .DmemSizeByte    ( DmemSizeByte ),
@@ -206,22 +210,32 @@
 
     .start_addr_i ( ImemStartAddr ),
 
+    .err_code_o   ( otbn_model_err_code ),
+
     .err_o        ( otbn_model_err )
   );
 
-  bit done_mismatch_latched;
+  bit done_mismatch_latched, err_code_mismatch_latched;
   bit model_err_latched;
 
   always_ff @(posedge IO_CLK or negedge IO_RST_N) begin
     if (!IO_RST_N) begin
-      done_mismatch_latched <= 1'b0;
-      model_err_latched     <= 1'b0;
+      done_mismatch_latched     <= 1'b0;
+      err_code_mismatch_latched <= 1'b0;
+      model_err_latched         <= 1'b0;
     end else begin
       if (otbn_done_q != otbn_model_done) begin
         $display("ERROR: At time %0t, otbn_done_q != otbn_model_done (%0d != %0d).",
                  $time, otbn_done_q, otbn_model_done);
         done_mismatch_latched <= 1'b1;
       end
+      if (otbn_done_q && otbn_model_done) begin
+        if (otbn_err_code_q != otbn_model_err_code) begin
+          $display("ERROR: At time %0t, otbn_err_code != otbn_model_err_code (%0d != %0d).",
+                   $time, otbn_err_code_q, otbn_model_err_code);
+          err_code_mismatch_latched <= 1'b1;
+        end
+      end
       model_err_latched <= model_err_latched | otbn_model_err;
     end
   end
@@ -253,7 +267,7 @@
   export "DPI-C" function otbn_err_get;
 
   function automatic bit otbn_err_get();
-    return model_err_latched | done_mismatch_latched;
+    return model_err_latched | done_mismatch_latched | err_code_mismatch_latched;
   endfunction
 
 endmodule
diff --git a/hw/ip/otbn/rtl/otbn.sv b/hw/ip/otbn/rtl/otbn.sv
index de59d21..b1b0a9d 100644
--- a/hw/ip/otbn/rtl/otbn.sv
+++ b/hw/ip/otbn/rtl/otbn.sv
@@ -471,6 +471,8 @@
       .start_i (start_model),
       .done_o (done_model),
 
+      .err_code_o (err_code_model),
+
       .start_addr_i (start_addr),
 
       .err_o ()