[otbn,dv] Refactor code in otbn_model.cc

No functional change, but more stuff is now encapsulated in the
OtbnModel class and things should feel generally tidier.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/model/otbn_model.cc b/hw/ip/otbn/dv/model/otbn_model.cc
index 11b267a..01f26e7 100644
--- a/hw/ip/otbn/dv/model/otbn_model.cc
+++ b/hw/ip/otbn/dv/model/otbn_model.cc
@@ -33,47 +33,90 @@
  public:
   OtbnModel(const std::string &mem_scope, const std::string &design_scope,
             unsigned imem_size_words, unsigned dmem_size_words)
-      : mem_util(mem_scope),
+      : mem_util_(mem_scope),
         design_scope_(design_scope),
         imem_size_words_(imem_size_words),
         dmem_size_words_(dmem_size_words) {}
 
-  // This class is a thin wrapper around ISSWrapper. The point is that we want
-  // to create the model in an initial block in the SystemVerilog simulation,
-  // but might not actually want to spawn the ISS. To handle that in a non-racy
-  // way, the most convenient thing is to spawn the ISS the first time it's
-  // actually needed.
-  //
-  // If ensure is true, this constructs an ISS wrapper if necessary. If
-  // something goes wrong, this function prints a message and then returns
-  // null. If ensure is true, it will never return null without printing a
-  // message, so error handling at the callsite can silently return a failure
-  // code.
-  ISSWrapper *get_wrapper(bool ensure) {
-    if (!iss) {
+  // True if this model is running in a simulation that has an RTL
+  // implementation too (which needs checking).
+  bool has_rtl() const { return !design_scope_.empty(); }
+
+  // Start a new run with the model, writing IMEM/DMEM and jumping to the given
+  // start address. Returns 0 on success; -1 on failure.
+  int start(unsigned start_addr);
+
+  // Step once in the model. Returns 1 if the model has finished, 0 if not and
+  // -1 on failure. If gen_trace is true, pass trace entries to the trace
+  // checker. If the model has finished, writes otbn.ERR_BITS to *err_bits.
+  int step(svLogic edn_rnd_data_valid,
+           svLogicVecVal *edn_rnd_data, /* logic [255:0] */
+           svLogic edn_urnd_data_valid, svBitVecVal *insn_cnt /* bit [31:0] */,
+           svBitVecVal *err_bits /* bit [31:0] */,
+           svBitVecVal *stop_pc /* bit [31:0] */);
+
+  // Check model against RTL (if there is any) when a run has finished. Prints
+  // messages to stderr on failure or mismatch. Returns 1 for a match, 0 for a
+  // mismatch, -1 for some other failure.
+  int check() const;
+
+  // Grab contents of dmem from the model and load it back into the RTL
+  // simulation. This is used when there's no RTL model of the design. Returns
+  // 0 on success; -1 on failure.
+  int load_dmem() const;
+
+  // Flush any information in the model
+  void reset();
+
+ private:
+  // Constructs an ISS wrapper if necessary. If something goes wrong, this
+  // function prints a message and then returns null. If ensure is true, it
+  // will never return null without printing a message, so error handling at
+  // the callsite can silently return a failure code.
+  ISSWrapper *ensure_wrapper() {
+    if (!iss_) {
       try {
-        iss.reset(new ISSWrapper());
+        iss_.reset(new ISSWrapper());
       } catch (const std::runtime_error &err) {
         std::cerr << "Error when constructing ISS wrapper: " << err.what()
                   << "\n";
         return nullptr;
       }
     }
-    assert(iss);
-    return iss.get();
+    assert(iss_);
+    return iss_.get();
   }
 
   std::vector<uint8_t> get_sim_memory(bool is_imem) const {
-    const MemArea &mem_area = mem_util.GetMemArea(is_imem);
+    const MemArea &mem_area = mem_util_.GetMemArea(is_imem);
     return mem_area.Read(0, mem_area.GetSizeWords());
   }
 
   void set_sim_memory(bool is_imem, const std::vector<uint8_t> &data) const {
-    mem_util.GetMemArea(is_imem).Write(0, data);
+    mem_util_.GetMemArea(is_imem).Write(0, data);
   }
 
-  std::unique_ptr<ISSWrapper> iss;
-  OtbnMemUtil mem_util;
+  // Grab contents of dmem from the model and compare them with the RTL. Prints
+  // messages to stderr on failure or mismatch. Returns true on success; false
+  // on mismatch. Throws a std::runtime_error on failure.
+  bool check_dmem(ISSWrapper &iss) const;
+
+  // Compare contents of ISS registers with those from the design. Prints
+  // messages to stderr on failure or mismatch. Returns true on success; false
+  // on mismatch. Throws a std::runtime_error on failure.
+  bool check_regs(ISSWrapper &iss) const;
+
+  // Compare contents of ISS call stack with those from the design. Prints
+  // messages to stderr on failure or mismatch. Returns true on success; false
+  // on mismatch. Throws a std::runtime_error on failure.
+  bool check_call_stack(ISSWrapper &iss) const;
+
+  // We want to create the model in an initial block in the SystemVerilog
+  // simulation, but might not actually want to spawn the ISS. To handle that
+  // in a non-racy way, the most convenient thing is to spawn the ISS the first
+  // time it's actually needed. Use ensure_iss() to create as needed.
+  std::unique_ptr<ISSWrapper> iss_;
+  OtbnMemUtil mem_util_;
   std::string design_scope_;
   unsigned imem_size_words_, dmem_size_words_;
 };
@@ -93,45 +136,6 @@
 #define FAILED_STEP_BIT (1U << 2)
 #define FAILED_CMP_BIT (1U << 3)
 
-// The main entry point to the OTBN model, exported from here and used in
-// otbn_core_model.sv.
-//
-// 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:      check_due     True if the model finished running last cycle
-//    Bit 2:      failed_step   Something failed when trying to start/step ISS
-//    Bit 3:      failed_cmp    Consistency check at end of run failed
-//
-// The otbn_model_step function should only be called when either the model is
-// running (bit 0 of status), has a check due (bit 1 of status), or when start
-// is asserted. At other times, it will return immediately (but wastes a DPI
-// call).
-//
-// If the model is running and start is false, otbn_model_step steps the ISS by
-// a single cycle. If something goes wrong, it will set failed_step to true and
-// running to false. Otherwise, it writes the new value of otbn.INSN_CNT to
-// *insn_cnt.
-//
-// If nothing goes wrong and the ISS finishes its run, we set running to false,
-// write out err_bits and stop_pc and do the post-run task. If the model's
-// design_scope is non-empty, it should be the scope of an RTL implementation.
-// In that case, we compare register and memory contents with that
-// implementation, printing to stderr and setting the failed_cmp bit if there
-// are any mismatches. If the model's design_scope is the empty string, we grab
-// the contents of DMEM from the ISS and inject them into the simulation
-// memory.
-//
-// If start is true, we start the model at start_addr and then step once (as
-// described above).
-extern "C" unsigned otbn_model_step(
-    OtbnModel *model, svLogic start, unsigned start_addr, unsigned status,
-    svLogic edn_rnd_data_valid, svLogicVecVal *edn_rnd_data, /* logic [255:0] */
-    svLogic edn_urnd_data_valid, svBitVecVal *insn_cnt /* bit [31:0] */,
-    svBitVecVal *err_bits /* bit [31:0] */,
-    svBitVecVal *stop_pc /* bit [31:0] */);
-
 static std::vector<uint8_t> read_vector_from_file(const std::string &path,
                                                   size_t num_bytes) {
   std::filebuf fb;
@@ -177,50 +181,6 @@
   }
 }
 
-extern "C" OtbnModel *otbn_model_init(const char *mem_scope,
-                                      const char *design_scope,
-                                      unsigned imem_words,
-                                      unsigned dmem_words) {
-  assert(mem_scope && design_scope);
-  return new OtbnModel(mem_scope, design_scope, imem_words, dmem_words);
-}
-
-extern "C" void otbn_model_destroy(OtbnModel *model) { delete model; }
-
-// Start a new run with the model, writing IMEM/DMEM and jumping to the given
-// start address. Returns 0 on success; -1 on failure.
-static int start_model(OtbnModel &model, unsigned start_addr) {
-  const MemArea &imem = model.mem_util.GetMemArea(true);
-  assert(start_addr % 4 == 0);
-  assert(start_addr / 4 < imem.GetSizeWords());
-
-  ISSWrapper *iss = model.get_wrapper(true);
-  if (!iss)
-    return -1;
-
-  std::string dfname(iss->make_tmp_path("dmem"));
-  std::string ifname(iss->make_tmp_path("imem"));
-
-  try {
-    write_vector_to_file(dfname, model.get_sim_memory(false));
-    write_vector_to_file(ifname, model.get_sim_memory(true));
-  } catch (const std::exception &err) {
-    std::cerr << "Error when dumping memory contents: " << err.what() << "\n";
-    return -1;
-  }
-
-  try {
-    iss->load_d(dfname);
-    iss->load_i(ifname);
-    iss->start(start_addr);
-  } catch (const std::runtime_error &err) {
-    std::cerr << "Error when starting ISS: " << err.what() << "\n";
-    return -1;
-  }
-
-  return 0;
-}
-
 // Extract 256-bit RND EDN data from a 4 state logic value. RND data is placed
 // into 8 element uint32_t array dst.
 static void set_rnd_data(uint32_t dst[8], const svLogicVecVal src[8]) {
@@ -233,132 +193,6 @@
 
 static bool is_xz(svLogic l) { return l == sv_x || l == sv_z; }
 
-// Step once in the model. Returns 1 if the model has finished, 0 if not and -1
-// on failure. If gen_trace is true, pass trace entries to the trace checker.
-// Writes the new value of otbn.INSN_CNT to *insn_cnt.
-//
-// If the model has finished, writes otbn.ERR_BITS to *err_bits and the final
-// PC to *stop_pc.
-static int step_model(OtbnModel &model, svLogic edn_rnd_data_valid,
-                      svLogicVecVal *edn_rnd_data, /* logic [255:0] */
-                      svLogic edn_urnd_data_valid, bool gen_trace,
-                      svBitVecVal *insn_cnt /* bit [31:0] */,
-                      svBitVecVal *err_bits /* bit [31:0] */,
-                      svBitVecVal *stop_pc /* bit [31:0] */) {
-  assert(err_bits);
-
-  ISSWrapper *iss = model.get_wrapper(true);
-  if (!iss)
-    return -1;
-
-  assert(!is_xz(edn_rnd_data_valid));
-  assert(!is_xz(edn_urnd_data_valid));
-
-  try {
-    if (edn_rnd_data_valid) {
-      uint32_t int_edn_rnd_data[8];
-      set_rnd_data(int_edn_rnd_data, edn_rnd_data);
-      iss->edn_rnd_data(int_edn_rnd_data);
-    }
-
-    if (edn_urnd_data_valid) {
-      iss->edn_urnd_reseed_complete();
-    }
-
-    switch (iss->step(gen_trace)) {
-      case -1:
-        // Something went wrong, such as a trace mismatch. We've already printed
-        // a message to stderr so can just return -1.
-        return -1;
-
-      case 1:
-        // The simulation has stopped. Fill in insn_cnt, err_bits and stop_pc.
-        set_sv_u32(insn_cnt, iss->get_insn_cnt());
-        set_sv_u32(err_bits, iss->get_err_bits());
-        set_sv_u32(stop_pc, iss->get_stop_pc());
-        return 1;
-
-      case 0:
-        // The simulation is still running. Update insn_cnt.
-        set_sv_u32(insn_cnt, iss->get_insn_cnt());
-        return 0;
-
-      default:
-        // This shouldn't happen
-        assert(0);
-    }
-  } catch (const std::runtime_error &err) {
-    std::cerr << "Error when stepping ISS: " << err.what() << "\n";
-    return -1;
-  }
-}
-
-// Grab contents of dmem from the model and load it back into the RTL. Returns
-// 0 on success; -1 on failure.
-static int load_dmem(OtbnModel &model) {
-  ISSWrapper *iss = model.get_wrapper(false);
-  if (!iss) {
-    std::cerr << "Cannot load dmem from OTBN model: ISS has not started.\n";
-    return -1;
-  }
-
-  const MemArea &dmem = model.mem_util.GetMemArea(false);
-
-  std::string dfname(iss->make_tmp_path("dmem_out"));
-  try {
-    iss->dump_d(dfname);
-    model.set_sim_memory(false,
-                         read_vector_from_file(dfname, dmem.GetSizeBytes()));
-  } catch (const std::exception &err) {
-    std::cerr << "Error when loading dmem from ISS: " << err.what() << "\n";
-    return -1;
-  }
-  return 0;
-}
-
-// Grab contents of dmem from the model and compare it with the RTL.
-// Prints messages to stderr on failure or mismatch. Returns true on
-// success; false on mismatch. Throws a std::runtime_error on failure.
-static bool check_dmem(OtbnModel &model, ISSWrapper &iss) {
-  const MemArea &dmem = model.mem_util.GetMemArea(false);
-  uint32_t dmem_bytes = dmem.GetSizeBytes();
-
-  std::string dfname(iss.make_tmp_path("dmem_out"));
-
-  iss.dump_d(dfname);
-  std::vector<uint8_t> iss_data = read_vector_from_file(dfname, dmem_bytes);
-  assert(iss_data.size() == dmem_bytes);
-
-  std::vector<uint8_t> rtl_data = model.get_sim_memory(false);
-  assert(rtl_data.size() == dmem_bytes);
-
-  // If the arrays match, we're done.
-  if (0 == memcmp(&iss_data[0], &rtl_data[0], dmem_bytes))
-    return true;
-
-  // If not, print out the first 10 mismatches
-  std::ios old_state(nullptr);
-  old_state.copyfmt(std::cerr);
-  std::cerr << "ERROR: Mismatches in dmem data:\n"
-            << std::hex << std::setfill('0');
-  int bad_count = 0;
-  for (size_t i = 0; i < dmem_bytes; ++i) {
-    if (iss_data[i] != rtl_data[i]) {
-      std::cerr << " @offset 0x" << std::setw(3) << i << ": rtl has 0x"
-                << std::setw(2) << (int)rtl_data[i] << "; iss has 0x"
-                << std::setw(2) << (int)iss_data[i] << "\n";
-      ++bad_count;
-
-      if (bad_count == 10) {
-        std::cerr << " (skipping further errors...)\n";
-        break;
-      }
-    }
-  }
-  std::cerr.copyfmt(old_state);
-  return false;
-}
-
 template <typename T>
 static std::array<T, 32> get_rtl_regs(const std::string &reg_scope) {
   std::array<T, 32> ret;
@@ -412,12 +246,20 @@
   while (1) {
     int peek_result = otbn_stack_element_peek(i, buf);
 
+    // otbn_stack_element_peek is defined in otbn_stack_snooper_if.sv. Possible
+    // return values are: 0 on success, if we've returned an element. 1 if the
+    // stack doesn't have an element at index i. 2 if something terrible has
+    // gone wrong (such as a completely bogus index).
+    assert(peek_result <= 2);
+
     if (peek_result == 2) {
       std::ostringstream oss;
       oss << "Failed to peek into RTL to get value of stack element " << i
           << " at scope `" << stack_scope << "'.";
       throw std::runtime_error(oss.str());
-    } else if (peek_result == 1) {
+    }
+
+    if (peek_result == 1) {
       // No more elements on stack
       break;
     }
@@ -432,15 +274,204 @@
   return ret;
 }
 
-// Compare contents of ISS registers with those from the design. Prints
-// messages to stderr on failure or mismatch. Returns true on success; false on
-// mismatch. Throws a std::runtime_error on failure.
-static bool check_regs(OtbnModel &model, ISSWrapper &iss) {
+int OtbnModel::start(unsigned start_addr) {
+  const MemArea &imem = mem_util_.GetMemArea(true);
+  assert(start_addr % 4 == 0);
+  assert(start_addr / 4 < imem.GetSizeWords());
+
+  ISSWrapper *iss = ensure_wrapper();
+  if (!iss)
+    return -1;
+
+  std::string dfname(iss->make_tmp_path("dmem"));
+  std::string ifname(iss->make_tmp_path("imem"));
+
+  try {
+    write_vector_to_file(dfname, get_sim_memory(false));
+    write_vector_to_file(ifname, get_sim_memory(true));
+  } catch (const std::exception &err) {
+    std::cerr << "Error when dumping memory contents: " << err.what() << "\n";
+    return -1;
+  }
+
+  try {
+    iss->load_d(dfname);
+    iss->load_i(ifname);
+    iss->start(start_addr);
+  } catch (const std::runtime_error &err) {
+    std::cerr << "Error when starting ISS: " << err.what() << "\n";
+    return -1;
+  }
+
+  return 0;
+}
+
+int OtbnModel::step(svLogic edn_rnd_data_valid,
+                    svLogicVecVal *edn_rnd_data, /* logic [255:0] */
+                    svLogic edn_urnd_data_valid,
+                    svBitVecVal *insn_cnt /* bit [31:0] */,
+                    svBitVecVal *err_bits /* bit [31:0] */,
+                    svBitVecVal *stop_pc /* bit [31:0] */) {
+  assert(err_bits);
+
+  ISSWrapper *iss = ensure_wrapper();
+  if (!iss)
+    return -1;
+
+  assert(!is_xz(edn_rnd_data_valid));
+  assert(!is_xz(edn_urnd_data_valid));
+
+  try {
+    if (edn_rnd_data_valid) {
+      uint32_t int_edn_rnd_data[8];
+      set_rnd_data(int_edn_rnd_data, edn_rnd_data);
+      iss->edn_rnd_data(int_edn_rnd_data);
+    }
+
+    if (edn_urnd_data_valid) {
+      iss->edn_urnd_reseed_complete();
+    }
+
+    switch (iss->step(has_rtl())) {
+      case -1:
+        // Something went wrong, such as a trace mismatch. We've already printed
+        // a message to stderr so can just return -1.
+        return -1;
+
+      case 1:
+        // The simulation has stopped. Fill in insn_cnt, err_bits and stop_pc.
+        set_sv_u32(insn_cnt, iss->get_insn_cnt());
+        set_sv_u32(err_bits, iss->get_err_bits());
+        set_sv_u32(stop_pc, iss->get_stop_pc());
+        return 1;
+
+      case 0:
+        // The simulation is still running. Update insn_cnt.
+        set_sv_u32(insn_cnt, iss->get_insn_cnt());
+        return 0;
+
+      default:
+        // This shouldn't happen
+        assert(0);
+    }
+  } catch (const std::runtime_error &err) {
+    std::cerr << "Error when stepping ISS: " << err.what() << "\n";
+    return -1;
+  }
+}
+
+int OtbnModel::check() const {
+  if (!has_rtl())
+    return 1;
+
+  ISSWrapper *iss = iss_.get();
+  if (!iss) {
+    std::cerr << "Cannot check OTBN model: ISS has not started.\n";
+    return -1;
+  }
+
+  bool good = true;
+
+  good &= OtbnTraceChecker::get().Finish();
+
+  try {
+    good &= check_dmem(*iss);
+  } catch (const std::exception &err) {
+    std::cerr << "Failed to check DMEM: " << err.what() << "\n";
+    return -1;
+  }
+
+  try {
+    good &= check_regs(*iss);
+  } catch (const std::exception &err) {
+    std::cerr << "Failed to check registers: " << err.what() << "\n";
+    return -1;
+  }
+
+  try {
+    good &= check_call_stack(*iss);
+  } catch (const std::exception &err) {
+    std::cerr << "Failed to check call stack: " << err.what() << "\n";
+    return -1;
+  }
+
+  return good ? 1 : 0;
+}
+
+int OtbnModel::load_dmem() const {
+  ISSWrapper *iss = iss_.get();
+  if (!iss) {
+    std::cerr << "Cannot load dmem from OTBN model: ISS has not started.\n";
+    return -1;
+  }
+
+  const MemArea &dmem = mem_util_.GetMemArea(false);
+
+  std::string dfname(iss->make_tmp_path("dmem_out"));
+  try {
+    iss->dump_d(dfname);
+    set_sim_memory(false, read_vector_from_file(dfname, dmem.GetSizeBytes()));
+  } catch (const std::exception &err) {
+    std::cerr << "Error when loading dmem from ISS: " << err.what() << "\n";
+    return -1;
+  }
+  return 0;
+}
+
+void OtbnModel::reset() {
+  ISSWrapper *iss = iss_.get();
+  if (iss)
+    iss->reset(has_rtl());
+}
+
+bool OtbnModel::check_dmem(ISSWrapper &iss) const {
+  const MemArea &dmem = mem_util_.GetMemArea(false);
+  uint32_t dmem_bytes = dmem.GetSizeBytes();
+
+  std::string dfname(iss.make_tmp_path("dmem_out"));
+
+  iss.dump_d(dfname);
+  std::vector<uint8_t> iss_data = read_vector_from_file(dfname, dmem_bytes);
+  assert(iss_data.size() == dmem_bytes);
+
+  std::vector<uint8_t> rtl_data = get_sim_memory(false);
+  assert(rtl_data.size() == dmem_bytes);
+
+  // If the arrays match, we're done.
+  if (0 == memcmp(&iss_data[0], &rtl_data[0], dmem_bytes))
+    return true;
+
+  // If not, print out the first 10 mismatches
+  std::ios old_state(nullptr);
+  old_state.copyfmt(std::cerr);
+  std::cerr << "ERROR: Mismatches in dmem data:\n"
+            << std::hex << std::setfill('0');
+  int bad_count = 0;
+  for (size_t i = 0; i < dmem_bytes; ++i) {
+    if (iss_data[i] != rtl_data[i]) {
+      std::cerr << " @offset 0x" << std::setw(3) << i << ": rtl has 0x"
+                << std::setw(2) << (int)rtl_data[i] << "; iss has 0x"
+                << std::setw(2) << (int)iss_data[i] << "\n";
+      ++bad_count;
+
+      if (bad_count == 10) {
+        std::cerr << " (skipping further errors...)\n";
+        break;
+      }
+    }
+  }
+  std::cerr.copyfmt(old_state);
+  return false;
+}
+
+bool OtbnModel::check_regs(ISSWrapper &iss) const {
+  assert(design_scope_.size());
+
   std::string base_scope =
-      model.design_scope_ +
+      design_scope_ +
       ".u_otbn_rf_base.gen_rf_base_ff.u_otbn_rf_base_inner.u_snooper";
   std::string wide_scope =
-      model.design_scope_ +
+      design_scope_ +
       ".u_otbn_rf_bignum.gen_rf_bignum_ff.u_otbn_rf_bignum_inner.u_snooper";
 
   auto rtl_gprs = get_rtl_regs<uint32_t>(base_scope);
@@ -494,12 +525,11 @@
   return good;
 }
 
-// Compare contents of ISS call stack with those from the design. Prints
-// messages to stderr on failure or mismatch. Returns true on success; false on
-// mismatch.  Throws a std::runtime_error on failure.
-static bool check_call_stack(OtbnModel &model, ISSWrapper &iss) {
+bool OtbnModel::check_call_stack(ISSWrapper &iss) const {
+  assert(design_scope_.size());
+
   std::string call_stack_snooper_scope =
-      model.design_scope_ + ".u_otbn_rf_base.u_call_stack_snooper";
+      design_scope_ + ".u_otbn_rf_base.u_call_stack_snooper";
 
   auto rtl_call_stack = get_stack<uint32_t>(call_stack_snooper_scope);
 
@@ -533,46 +563,48 @@
   return good;
 }
 
-// Check model against RTL when a run has finished. Prints messages to stderr
-// on failure or mismatch. Returns 1 for a match, 0 for a mismatch, -1 for some
-// other failure.
-int check_model(OtbnModel *model) {
-  assert(model);
-
-  ISSWrapper *iss = model->get_wrapper(false);
-  if (!iss) {
-    std::cerr << "Cannot check OTBN model: ISS has not started.\n";
-    return -1;
-  }
-
-  bool good = true;
-
-  good &= OtbnTraceChecker::get().Finish();
-
-  try {
-    good &= check_dmem(*model, *iss);
-  } catch (const std::exception &err) {
-    std::cerr << "Failed to check DMEM: " << err.what() << "\n";
-    return -1;
-  }
-
-  try {
-    good &= check_regs(*model, *iss);
-  } catch (const std::exception &err) {
-    std::cerr << "Failed to check registers: " << err.what() << "\n";
-    return -1;
-  }
-
-  try {
-    good &= check_call_stack(*model, *iss);
-  } catch (const std::exception &err) {
-    std::cerr << "Failed to check call stack: " << err.what() << "\n";
-    return -1;
-  }
-
-  return good ? 1 : 0;
+extern "C" OtbnModel *otbn_model_init(const char *mem_scope,
+                                      const char *design_scope,
+                                      unsigned imem_words,
+                                      unsigned dmem_words) {
+  assert(mem_scope && design_scope);
+  return new OtbnModel(mem_scope, design_scope, imem_words, dmem_words);
 }
 
+extern "C" void otbn_model_destroy(OtbnModel *model) { delete model; }
+
+// The main entry point to the OTBN model, exported from here and used in
+// otbn_core_model.sv.
+//
+// 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:      check_due     True if the model finished running last cycle
+//    Bit 2:      failed_step   Something failed when trying to start/step ISS
+//    Bit 3:      failed_cmp    Consistency check at end of run failed
+//
+// The otbn_model_step function should only be called when either the model is
+// running (bit 0 of status), has a check due (bit 1 of status), or when start
+// is asserted. At other times, it will return immediately (but wastes a DPI
+// call).
+//
+// If the model is running and start is false, otbn_model_step steps the ISS by
+// a single cycle. If something goes wrong, it will set failed_step to true and
+// running to false. Otherwise, it writes the new value of otbn.INSN_CNT to
+// *insn_cnt.
+//
+// If nothing goes wrong and the ISS finishes its run, we set running to false,
+// write out err_bits and stop_pc and do the post-run task. If the model's
+// design_scope is non-empty, it should be the scope of an RTL implementation.
+// In that case, we compare register and memory contents with that
+// implementation, printing to stderr and setting the failed_cmp bit if there
+// are any mismatches. If the model's design_scope is the empty string, we grab
+// the contents of DMEM from the ISS and inject them into the simulation
+// memory.
+//
+// If start is true, we start the model at start_addr and then step once (as
+// described above).
 extern "C" unsigned otbn_model_step(
     OtbnModel *model, svLogic start, unsigned start_addr, unsigned status,
     svLogic edn_rnd_data_valid, svLogicVecVal *edn_rnd_data, /* logic [255:0] */
@@ -583,9 +615,8 @@
 
   // Run model checks if needed. This usually happens just after an operation
   // has finished.
-  bool check_rtl = (model->design_scope_.size() > 0);
-  if (check_rtl && (status & CHECK_DUE_BIT)) {
-    switch (check_model(model)) {
+  if (model->has_rtl() && (status & CHECK_DUE_BIT)) {
+    switch (model->check()) {
       case 1:
         // Match (success)
         break;
@@ -606,7 +637,7 @@
 
   // Start the model if requested
   if (start) {
-    switch (start_model(*model, start_addr)) {
+    switch (model->start(start_addr)) {
       case 0:
         // All good
         status |= RUNNING_BIT;
@@ -623,9 +654,8 @@
     return status;
 
   // Step the model once
-  switch (step_model(*model, edn_rnd_data_valid, edn_rnd_data,
-                     edn_urnd_data_valid, check_rtl, insn_cnt, err_bits,
-                     stop_pc)) {
+  switch (model->step(edn_rnd_data_valid, edn_rnd_data, edn_urnd_data_valid,
+                      insn_cnt, err_bits, stop_pc)) {
     case 0:
       // Still running: no change
       break;
@@ -646,8 +676,8 @@
 
   // If we've just stopped running and there's no RTL, load the contents of
   // DMEM back from the ISS
-  if (!check_rtl) {
-    switch (load_dmem(*model)) {
+  if (!model->has_rtl()) {
+    switch (model->load_dmem()) {
       case 0:
         // Success
         break;
@@ -664,10 +694,5 @@
 // Flush any information in the model
 extern "C" void otbn_model_reset(OtbnModel *model) {
   assert(model);
-
-  ISSWrapper *iss = model->get_wrapper(false);
-  if (iss) {
-    bool check_rtl = (model->design_scope_.size() > 0);
-    iss->reset(check_rtl);
-  }
+  model->reset();
 }