Add Renode interface

Add Renode interface to build a shared library of kelvin_sim that can run with Renode framework as an external CPU

PiperOrigin-RevId: 559857693
diff --git a/sim/renode/BUILD b/sim/renode/BUILD
new file mode 100644
index 0000000..f87d6be
--- /dev/null
+++ b/sim/renode/BUILD
@@ -0,0 +1,70 @@
+# Build Renode interface for kelvin
+
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+cc_library(
+    name = "renode_mpact",
+    srcs = [
+        "renode_mpact.cc",
+    ],
+    hdrs = [
+        "renode_mpact.h",
+    ],
+    deps = [
+        ":renode_debug_interface",
+        "@com_google_absl//absl/container:flat_hash_map",
+        "@com_google_absl//absl/log",
+        "@com_google_absl//absl/strings",
+        "@com_google_mpact-sim//mpact/sim/generic:core_debug_interface",
+        "@com_google_mpact-sim//mpact/sim/util/program_loader:elf_loader",
+    ],
+    alwayslink = True,
+)
+
+cc_library(
+    name = "renode_debug_interface",
+    hdrs = ["renode_debug_interface.h"],
+    deps = [
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
+        "@com_google_mpact-sim//mpact/sim/generic:core_debug_interface",
+    ],
+)
+
+cc_library(
+    name = "kelvin_renode",
+    srcs = [
+        "kelvin_renode.cc",
+        "kelvin_renode_reigster_info.cc",
+    ],
+    hdrs = [
+        "kelvin_renode.h",
+        "kelvin_renode_register_info.h",
+    ],
+    deps = [
+        ":renode_debug_interface",
+        "//sim:kelvin_top",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
+        "@com_google_absl//absl/strings",
+        "@com_google_mpact-riscv//riscv:riscv_debug_info",
+        "@com_google_mpact-sim//mpact/sim/generic:core",
+        "@com_google_mpact-sim//mpact/sim/generic:core_debug_interface",
+        "@com_google_mpact-sim//mpact/sim/generic:instruction",
+        "@com_google_mpact-sim//mpact/sim/generic:type_helpers",
+    ],
+    alwayslink = True,
+)
+
+# This rule generates the shared library that is loaded and used by Renode to instantiate and
+# control kelvin sim
+cc_binary(
+    name = "renode_kelvin",
+    linkshared = True,
+    deps = [
+        ":kelvin_renode",
+        ":renode_mpact",
+    ],
+)
diff --git a/sim/renode/kelvin_renode.cc b/sim/renode/kelvin_renode.cc
new file mode 100644
index 0000000..a1df4e7
--- /dev/null
+++ b/sim/renode/kelvin_renode.cc
@@ -0,0 +1,144 @@
+#include "sim/renode/kelvin_renode.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <string>
+
+#include "sim/kelvin_top.h"
+#include "sim/renode/kelvin_renode_register_info.h"
+#include "sim/renode/renode_debug_interface.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "riscv/riscv_debug_info.h"
+#include "mpact/sim/generic/core_debug_interface.h"
+#include "mpact/sim/generic/data_buffer.h"
+#include "mpact/sim/generic/instruction.h"
+
+kelvin::sim::renode::RenodeDebugInterface *CreateKelvinSim(std::string name) {
+  auto *top = new kelvin::sim::KelvinRenode(name);
+  return top;
+}
+
+namespace kelvin::sim {
+
+using HaltReason = mpact::sim::generic::CoreDebugInterface::HaltReason;
+using RunStatus = mpact::sim::generic::CoreDebugInterface::RunStatus;
+using Instruction = mpact::sim::generic::Instruction;
+using RiscVDebugInfo = mpact::sim::riscv::RiscVDebugInfo;
+
+KelvinRenode::KelvinRenode(std::string name) {
+  kelvin_top_ = new KelvinTop(name);
+}
+
+KelvinRenode::~KelvinRenode() { delete kelvin_top_; }
+
+absl::Status KelvinRenode::Halt() { return kelvin_top_->Halt(); }
+absl::StatusOr<int> KelvinRenode::Step(int num_steps) {
+  return kelvin_top_->Step(num_steps);
+}
+absl::Status KelvinRenode::Run() { return kelvin_top_->Run(); }
+absl::Status KelvinRenode::Wait() { return kelvin_top_->Wait(); }
+absl::StatusOr<RunStatus> KelvinRenode::GetRunStatus() {
+  return kelvin_top_->GetRunStatus();
+}
+absl::StatusOr<HaltReason> KelvinRenode::GetLastHaltReason() {
+  return kelvin_top_->GetLastHaltReason();
+}
+
+absl::StatusOr<uint64_t> KelvinRenode::ReadRegister(const std::string &name) {
+  return kelvin_top_->ReadRegister(name);
+}
+
+absl::Status KelvinRenode::WriteRegister(const std::string &name,
+                                         uint64_t value) {
+  return kelvin_top_->WriteRegister(name, value);
+}
+
+absl::StatusOr<size_t> KelvinRenode::ReadMemory(uint64_t address, void *buf,
+                                                size_t length) {
+  return kelvin_top_->ReadMemory(address, buf, length);
+}
+
+absl::StatusOr<size_t> KelvinRenode::WriteMemory(uint64_t address,
+                                                 const void *buf,
+                                                 size_t length) {
+  return kelvin_top_->WriteMemory(address, buf, length);
+}
+
+absl::StatusOr<mpact::sim::generic::DataBuffer *>
+KelvinRenode::GetRegisterDataBuffer(const std::string &name) {
+  return kelvin_top_->GetRegisterDataBuffer(name);
+}
+
+bool KelvinRenode::HasBreakpoint(uint64_t address) {
+  return kelvin_top_->HasBreakpoint(address);
+}
+
+absl::Status KelvinRenode::SetSwBreakpoint(uint64_t address) {
+  return kelvin_top_->SetSwBreakpoint(address);
+}
+
+absl::Status KelvinRenode::ClearSwBreakpoint(uint64_t address) {
+  return kelvin_top_->ClearSwBreakpoint(address);
+}
+
+absl::Status KelvinRenode::ClearAllSwBreakpoints() {
+  return kelvin_top_->ClearAllSwBreakpoints();
+}
+
+absl::StatusOr<mpact::sim::generic::Instruction *> KelvinRenode::GetInstruction(
+    uint64_t address) {
+  return kelvin_top_->GetInstruction(address);
+}
+
+absl::StatusOr<std::string> KelvinRenode::GetDisassembly(uint64_t address) {
+  return kelvin_top_->GetDisassembly(address);
+}
+
+absl::StatusOr<uint64_t> KelvinRenode::ReadRegister(uint32_t reg_id) {
+  auto ptr = RiscVDebugInfo::Instance()->debug_register_map().find(reg_id);
+  if (ptr == RiscVDebugInfo::Instance()->debug_register_map().end()) {
+    return absl::NotFoundError(
+        absl::StrCat("Not found reg id: ", absl::Hex(reg_id)));
+  }
+
+  return ReadRegister(ptr->second);
+}
+
+absl::Status KelvinRenode::WriteRegister(uint32_t reg_id, uint64_t value) {
+  auto ptr = RiscVDebugInfo::Instance()->debug_register_map().find(reg_id);
+  if (ptr == RiscVDebugInfo::Instance()->debug_register_map().end()) {
+    return absl::NotFoundError(
+        absl::StrCat("Not found reg id: ", absl::Hex(reg_id)));
+  }
+
+  return WriteRegister(ptr->second, value);
+}
+
+int32_t KelvinRenode::GetRenodeRegisterInfoSize() const {
+  return KelvinRenodeRegisterInfo::GetRenodeRegisterInfo().size();
+}
+
+absl::Status KelvinRenode::GetRenodeRegisterInfo(int32_t index, int32_t max_len,
+                                                 char *name,
+                                                 RenodeCpuRegister &info) {
+  auto const &register_info = KelvinRenodeRegisterInfo::GetRenodeRegisterInfo();
+  if ((index < 0 || index >= register_info.size())) {
+    return absl::OutOfRangeError(
+        absl::StrCat("Register info index (", index, ") out of range"));
+  }
+  info = register_info[index];
+  auto const &reg_map = RiscVDebugInfo::Instance()->debug_register_map();
+  auto ptr = reg_map.find(info.index);
+  if (ptr == reg_map.end()) {
+    name[0] = '\0';
+  } else {
+    strncpy(name, ptr->second.c_str(), max_len);
+  }
+
+  return absl::OkStatus();
+}
+
+}  // namespace kelvin::sim
diff --git a/sim/renode/kelvin_renode.h b/sim/renode/kelvin_renode.h
new file mode 100644
index 0000000..e2a5591
--- /dev/null
+++ b/sim/renode/kelvin_renode.h
@@ -0,0 +1,84 @@
+#ifndef LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_H_
+#define LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <string>
+
+#include "sim/kelvin_top.h"
+#include "sim/renode/renode_debug_interface.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "mpact/sim/generic/core_debug_interface.h"
+#include "mpact/sim/generic/data_buffer.h"
+#include "mpact/sim/generic/instruction.h"
+
+// This file defines a wrapper class for the KelvinTop. In addition, the .cc
+// class defines a global namespace function that is used by the renode wrapper
+// to create a top simulator instance.
+
+extern kelvin::sim::renode::RenodeDebugInterface *CreateKelvinSim(
+    std::string name);
+
+namespace kelvin::sim {
+
+class KelvinRenode : public renode::RenodeDebugInterface {
+ public:
+  using mpact::sim::generic::CoreDebugInterface::HaltReason;
+  using mpact::sim::generic::CoreDebugInterface::RunStatus;
+  using RenodeCpuRegister = kelvin::sim::renode::RenodeCpuRegister;
+
+  explicit KelvinRenode(std::string name);
+  ~KelvinRenode() override;
+
+  // Request that core stop running override;
+  absl::Status Halt() override;
+  // Step the core by num instructions.
+  absl::StatusOr<int> Step(int num) override;
+  // Allow the core to free-run. The loop to run the instructions should be
+  // in a separate thread so that this method can return. This allows a user
+  // interface built on top of this interface to handle multiple cores running
+  // at the same time.
+  absl::Status Run() override;
+  // Wait until the current core halts execution.
+  absl::Status Wait() override;
+  // Returns the current run status.
+  absl::StatusOr<RunStatus> GetRunStatus() override;
+  // Returns the reason for the most recent halt.
+  absl::StatusOr<HaltReason> GetLastHaltReason() override;
+  // Read/write the named registers.
+  absl::StatusOr<uint64_t> ReadRegister(const std::string &name) override;
+  absl::Status WriteRegister(const std::string &name, uint64_t value) override;
+
+  // Read/write the numeric id registers.
+  absl::StatusOr<uint64_t> ReadRegister(uint32_t reg_id) override;
+  absl::Status WriteRegister(uint32_t reg_id, uint64_t value) override;
+  absl::StatusOr<mpact::sim::generic::DataBuffer *> GetRegisterDataBuffer(
+      const std::string &name) override;
+  // Read/write the buffers to memory.
+  absl::StatusOr<size_t> ReadMemory(uint64_t address, void *buf,
+                                    size_t length) override;
+  absl::StatusOr<size_t> WriteMemory(uint64_t address, const void *buf,
+                                     size_t length) override;
+  bool HasBreakpoint(uint64_t address) override;
+  // Set/Clear software breakpoints at the given addresses.
+  absl::Status SetSwBreakpoint(uint64_t address) override;
+  absl::Status ClearSwBreakpoint(uint64_t address) override;
+  // Remove all software breakpoints.
+  absl::Status ClearAllSwBreakpoints() override;
+  absl::StatusOr<mpact::sim::generic::Instruction *> GetInstruction(
+      uint64_t address) override;
+  // Return the string representation for the instruction at the given address.
+  absl::StatusOr<std::string> GetDisassembly(uint64_t address) override;
+  // Return register information.
+  int32_t GetRenodeRegisterInfoSize() const override;
+  absl::Status GetRenodeRegisterInfo(int32_t index, int32_t max_len, char *name,
+                                     RenodeCpuRegister &info) override;
+
+ private:
+  KelvinTop *kelvin_top_ = nullptr;
+};
+
+}  // namespace kelvin::sim
+
+#endif  // LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_H_
diff --git a/sim/renode/kelvin_renode_register_info.h b/sim/renode/kelvin_renode_register_info.h
new file mode 100644
index 0000000..b54ff85
--- /dev/null
+++ b/sim/renode/kelvin_renode_register_info.h
@@ -0,0 +1,30 @@
+#ifndef LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_REGISTER_INFO_H_
+#define LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_REGISTER_INFO_H_
+
+#include <vector>
+
+#include "sim/renode/renode_debug_interface.h"
+
+namespace kelvin::sim {
+
+// This file defines a class that is used to store the register information
+// that needs to be provided to renode for the Kelvin registers.
+
+class KelvinRenodeRegisterInfo {
+ public:
+  using RenodeRegisterInfo = std::vector<renode::RenodeCpuRegister>;
+  static const RenodeRegisterInfo &GetRenodeRegisterInfo();
+
+ private:
+  KelvinRenodeRegisterInfo();
+  static KelvinRenodeRegisterInfo *Instance();
+  void InitializeRenodeRegisterInfo();
+  const RenodeRegisterInfo &GetRenodeRegisterInfoPrivate();
+
+  static KelvinRenodeRegisterInfo *instance_;
+  RenodeRegisterInfo renode_register_info_;
+};
+
+}  // namespace kelvin::sim
+
+#endif  // LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_REGISTER_INFO_H_
diff --git a/sim/renode/kelvin_renode_reigster_info.cc b/sim/renode/kelvin_renode_reigster_info.cc
new file mode 100644
index 0000000..8a3be56
--- /dev/null
+++ b/sim/renode/kelvin_renode_reigster_info.cc
@@ -0,0 +1,54 @@
+#include "sim/renode/kelvin_renode_register_info.h"
+#include "riscv/riscv_debug_info.h"
+#include "mpact/sim/generic/type_helpers.h"
+
+namespace kelvin::sim {
+
+KelvinRenodeRegisterInfo *KelvinRenodeRegisterInfo::instance_ = nullptr;
+
+void KelvinRenodeRegisterInfo::InitializeRenodeRegisterInfo() {
+  using DbgReg = mpact::sim::riscv::DebugRegisterEnum;
+
+  renode_register_info_ = {
+      {*DbgReg::kPc, 32, true, false},  {*DbgReg::kX0, 32, true, true},
+      {*DbgReg::kX1, 32, true, false},  {*DbgReg::kX2, 32, true, false},
+      {*DbgReg::kX3, 32, true, false},  {*DbgReg::kX4, 32, true, false},
+      {*DbgReg::kX5, 32, true, false},  {*DbgReg::kX6, 32, true, false},
+      {*DbgReg::kX7, 32, true, false},  {*DbgReg::kX8, 32, true, false},
+      {*DbgReg::kX9, 32, true, false},  {*DbgReg::kX10, 32, true, false},
+      {*DbgReg::kX11, 32, true, false}, {*DbgReg::kX12, 32, true, false},
+      {*DbgReg::kX13, 32, true, false}, {*DbgReg::kX14, 32, true, false},
+      {*DbgReg::kX15, 32, true, false}, {*DbgReg::kX16, 32, true, false},
+      {*DbgReg::kX17, 32, true, false}, {*DbgReg::kX18, 32, true, false},
+      {*DbgReg::kX19, 32, true, false}, {*DbgReg::kX20, 32, true, false},
+      {*DbgReg::kX21, 32, true, false}, {*DbgReg::kX22, 32, true, false},
+      {*DbgReg::kX23, 32, true, false}, {*DbgReg::kX24, 32, true, false},
+      {*DbgReg::kX25, 32, true, false}, {*DbgReg::kX26, 32, true, false},
+      {*DbgReg::kX27, 32, true, false}, {*DbgReg::kX28, 32, true, false},
+      {*DbgReg::kX29, 32, true, false}, {*DbgReg::kX30, 32, true, false},
+      {*DbgReg::kX31, 32, true, false},
+  };
+}
+
+KelvinRenodeRegisterInfo::KelvinRenodeRegisterInfo() {
+  InitializeRenodeRegisterInfo();
+}
+
+const KelvinRenodeRegisterInfo::RenodeRegisterInfo &
+KelvinRenodeRegisterInfo::GetRenodeRegisterInfo() {
+  return Instance()->GetRenodeRegisterInfoPrivate();
+}
+
+KelvinRenodeRegisterInfo *KelvinRenodeRegisterInfo::Instance() {
+  if (instance_ == nullptr) {
+    instance_ = new KelvinRenodeRegisterInfo();
+  }
+  return instance_;
+}
+
+const KelvinRenodeRegisterInfo::RenodeRegisterInfo &
+KelvinRenodeRegisterInfo::GetRenodeRegisterInfoPrivate() {
+  return renode_register_info_;
+}
+
+}  // namespace kelvin::sim
diff --git a/sim/renode/renode_debug_interface.h b/sim/renode/renode_debug_interface.h
new file mode 100644
index 0000000..4368763
--- /dev/null
+++ b/sim/renode/renode_debug_interface.h
@@ -0,0 +1,41 @@
+#ifndef LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_RENODE_DEBUG_INTERFACE_H_
+#define LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_RENODE_DEBUG_INTERFACE_H_
+
+#include <cstdint>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "mpact/sim/generic/core_debug_interface.h"
+
+namespace kelvin::sim::renode {
+
+// This structure mirrors the one defined in renode to provide information
+// about the target registers. Do not change, as it maps to the marshaling
+// structure used by renode.
+struct RenodeCpuRegister {
+  int index;
+  int width;
+  bool is_general;
+  bool is_read_only;
+};
+
+class RenodeDebugInterface : public mpact::sim::generic::CoreDebugInterface {
+ public:
+  // These using declarations are required to tell the compiler that these
+  // methods are not overridden nor hidden by the two virtual methods declared
+  // in this class.
+  using mpact::sim::generic::CoreDebugInterface::ReadRegister;
+  using mpact::sim::generic::CoreDebugInterface::WriteRegister;
+  // Read/write the numeric id registers.
+  virtual absl::StatusOr<uint64_t> ReadRegister(uint32_t reg_id) = 0;
+  virtual absl::Status WriteRegister(uint32_t reg_id, uint64_t value) = 0;
+  // Get register information.
+  virtual int32_t GetRenodeRegisterInfoSize() const = 0;
+  virtual absl::Status GetRenodeRegisterInfo(int32_t index, int32_t max_len,
+                                             char *name,
+                                             RenodeCpuRegister &info) = 0;
+};
+
+}  // namespace kelvin::sim::renode
+
+#endif  // LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_RENODE_DEBUG_INTERFACE_H_
diff --git a/sim/renode/renode_mpact.cc b/sim/renode/renode_mpact.cc
new file mode 100644
index 0000000..51e111e
--- /dev/null
+++ b/sim/renode/renode_mpact.cc
@@ -0,0 +1,383 @@
+#include "sim/renode/renode_mpact.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <fstream>
+#include <ios>
+#include <limits>
+#include <string>
+
+#include "sim/renode/renode_debug_interface.h"
+#include "absl/log/log.h"
+#include "absl/strings/str_cat.h"
+#include "mpact/sim/generic/core_debug_interface.h"
+#include "mpact/sim/util/program_loader/elf_program_loader.h"
+
+// This function must be defined in the library.
+extern kelvin::sim::renode::RenodeDebugInterface *CreateKelvinSim(std::string);
+
+// External "C" functions visible to Renode.
+using kelvin::sim::renode::RenodeAgent;
+using kelvin::sim::renode::RenodeCpuRegister;
+
+// Implementation of the C interface functions. They each forward the call to
+// the corresponding method in RenodeAgent.
+int32_t construct(int32_t max_name_length) {
+  return RenodeAgent::Instance()->Construct(max_name_length);
+}
+int32_t destruct(int32_t id) { return RenodeAgent::Instance()->Destroy(id); }
+int32_t reset(int32_t id) { return RenodeAgent::Instance()->Reset(id); }
+int32_t get_reg_info_size(int32_t id) {
+  return RenodeAgent::Instance()->GetRegisterInfoSize(id);
+}
+int32_t get_reg_info(int32_t id, int32_t index, char *name, void *info) {
+  if (info == nullptr) return -1;
+  return RenodeAgent::Instance()->GetRegisterInfo(
+      id, index, name, static_cast<RenodeCpuRegister *>(info));
+}
+uint64_t load_executable(int32_t id, const char *elf_file_name,
+                         int32_t *status) {
+  return RenodeAgent::Instance()->LoadExecutable(id, elf_file_name, status);
+}
+int32_t load_image(int32_t id, const char *file_name, uint64_t address) {
+  return RenodeAgent::Instance()->LoadImage(id, file_name, address);
+}
+int32_t read_register(int32_t id, uint32_t reg_id, uint64_t *value) {
+  return RenodeAgent::Instance()->ReadRegister(id, reg_id, value);
+}
+int32_t write_register(int32_t id, uint32_t reg_id, uint64_t value) {
+  return RenodeAgent::Instance()->WriteRegister(id, reg_id, value);
+}
+uint64_t read_memory(int32_t id, uint64_t address, char *buffer,
+                     uint64_t length) {
+  return RenodeAgent::Instance()->ReadMemory(id, address, buffer, length);
+}
+uint64_t write_memory(int32_t id, uint64_t address, const char *buffer,
+                      uint64_t length) {
+  return RenodeAgent::Instance()->WriteMemory(id, address, buffer, length);
+}
+uint64_t step(int32_t id, uint64_t num_to_step, int32_t *status) {
+  return RenodeAgent::Instance()->Step(id, num_to_step, status);
+}
+int32_t halt(int32_t id, int32_t *status) {
+  return RenodeAgent::Instance()->Halt(id, status);
+}
+
+namespace kelvin::sim::renode {
+
+RenodeAgent *RenodeAgent::instance_ = nullptr;
+uint32_t RenodeAgent::count_ = 0;
+
+// Create the debug instance by calling the factory function.
+int32_t RenodeAgent::Construct(int32_t max_name_length) {
+  std::string name = absl::StrCat("renode", count_);
+  auto *dbg = CreateKelvinSim(name);
+  if (dbg == nullptr) {
+    return -1;
+  }
+  core_dbg_instances_.emplace(RenodeAgent::count_, dbg);
+  name_length_map_.emplace(RenodeAgent::count_, max_name_length);
+  return RenodeAgent::count_++;
+}
+
+// Destroy the debug instance.
+int32_t RenodeAgent::Destroy(int32_t id) {
+  // Check for valid instance.
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) return -1;
+  delete dbg_iter->second;
+  core_dbg_instances_.erase(dbg_iter);
+  return 0;
+}
+
+int32_t RenodeAgent::Reset(int32_t id) {
+  // Check for valid instance.
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) return -1;
+  // For now, do nothing.
+  return 0;
+}
+
+int32_t RenodeAgent::GetRegisterInfoSize(int32_t id) const {
+  // Check for valid instance.
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) return -1;
+  auto *dbg = dbg_iter->second;
+  return dbg->GetRenodeRegisterInfoSize();
+}
+
+int32_t RenodeAgent::GetRegisterInfo(int32_t id, int32_t index, char *name,
+                                     RenodeCpuRegister *info) {
+  // Check for valid instance.
+  if (info == nullptr) return -1;
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) return -1;
+  auto *dbg = dbg_iter->second;
+  int32_t max_len = name_length_map_.at(id);
+  auto result = dbg->GetRenodeRegisterInfo(index, max_len, name, *info);
+  if (!result.ok()) return -1;
+  return 0;
+}
+
+// Read the register given by the id.
+int32_t RenodeAgent::ReadRegister(int32_t id, uint32_t reg_id,
+                                  uint64_t *value) {
+  // Check for valid instance.
+  if (value == nullptr) return -1;
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) return -1;
+  // Read register.
+  auto *dbg = dbg_iter->second;
+  auto result = dbg->ReadRegister(reg_id);
+  if (!result.ok()) return -1;
+  *value = result.value();
+  return 0;
+}
+
+int32_t RenodeAgent::WriteRegister(int32_t id, uint32_t reg_id,
+                                   uint64_t value) {
+  // Check for valid instance.
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) return -1;
+  // Write register.
+  auto *dbg = dbg_iter->second;
+  auto result = dbg->WriteRegister(reg_id, value);
+  if (!result.ok()) return -1;
+  return 0;
+}
+
+uint64_t RenodeAgent::ReadMemory(int32_t id, uint64_t address, char *buffer,
+                                 uint64_t length) {
+  // Check for valid desktop.
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) {
+    LOG(ERROR) << "No such core dbg instance: " << id;
+    return 0;
+  }
+  auto *dbg = dbg_iter->second;
+  auto res = dbg->ReadMemory(address, buffer, length);
+  if (!res.ok()) return 0;
+  return res.value();
+}
+
+uint64_t RenodeAgent::WriteMemory(int32_t id, uint64_t address,
+                                  const char *buffer, uint64_t length) {
+  // Check for valid desktop.
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) {
+    LOG(ERROR) << "No such core dbg instance: " << id;
+    return 0;
+  }
+  auto *dbg = dbg_iter->second;
+  auto res = dbg->WriteMemory(address, buffer, length);
+  if (!res.ok()) return 0;
+  return res.value();
+}
+
+uint64_t RenodeAgent::LoadExecutable(int32_t id, const char *file_name,
+                                     int32_t *status) {
+  // Check for valid desktop.
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) {
+    LOG(ERROR) << "No such core dbg instance: " << id;
+    *status = -1;
+    return 0;
+  }
+  // Instantiate loader and try to load the file.
+  auto *dbg = dbg_iter->second;
+  mpact::sim::util::ElfProgramLoader loader(dbg);
+  auto load_res = loader.LoadProgram(file_name);
+  if (!load_res.ok()) {
+    LOG(ERROR) << "Failed to load program: " << load_res.status().message();
+    *status = -1;
+    return 0;
+  }
+  uint64_t entry = load_res.value();
+  if (!dbg->WriteRegister("pc", entry).ok()) {
+    LOG(ERROR) << "Failed to write to the pc: " << load_res.status().message();
+    *status = -1;
+    return 0;
+  }
+  *status = 0;
+  return entry;
+}
+
+int32_t RenodeAgent::LoadImage(int32_t id, const char *file_name,
+                               uint64_t address) {
+  // Get the debug interface.
+  auto dbg_iter = core_dbg_instances_.find(id);
+  if (dbg_iter == core_dbg_instances_.end()) {
+    LOG(ERROR) << "No such core dbg instance: " << id;
+    return -1;
+  }
+  auto *dbg = dbg_iter->second;
+  // Open up the image file.
+  std::ifstream image_file;
+  image_file.open(file_name, std::ios::in | std::ios::binary);
+  if (!image_file.good()) {
+    LOG(ERROR) << "LoadImage: Input file not in good state";
+    return -1;
+  }
+  char buffer[kBufferSize];
+  size_t gcount = 0;
+  uint64_t load_address = address;
+  do {
+    // Fill buffer.
+    image_file.read(buffer, kBufferSize);
+    // Get the number of bytes that was read.
+    gcount = image_file.gcount();
+    // Write to the simulator memory.
+    auto res = dbg->WriteMemory(load_address, buffer, gcount);
+    // Check that the write succeeded, increment address if it did.
+    if (!res.ok()) {
+      LOG(ERROR) << "LoadImage: Memory write failed";
+      return -1;
+    }
+    if (res.value() != gcount) {
+      LOG(ERROR) << "LoadImage: Memory write failed to write all the bytes";
+      return -1;
+    }
+    load_address += gcount;
+  } while (image_file.good() && (gcount > 0));
+  image_file.close();
+  return 0;
+}
+
+uint64_t RenodeAgent::Step(int32_t id, uint64_t num_to_step, int32_t *status) {
+  // Set the default execution status
+  if (status != nullptr) {
+    *status = static_cast<int32_t>(ExecutionResult::kAborted);
+  }
+
+  // Get the core debug if object.
+  auto *dbg = RenodeAgent::Instance()->core_dbg(id);
+  // Is the debug interface valid?
+  if (dbg == nullptr) {
+    return 0;
+  }
+  if (num_to_step == 0) {
+    if (status != nullptr) {
+      *status = static_cast<int32_t>(ExecutionResult::kOk);
+    }
+    return 0;
+  }
+  // Check the previous halt reason.
+  auto halt_res = dbg->GetLastHaltReason();
+  if (!halt_res.ok()) {
+    return 0;
+  }
+  // If the previous halt reason was a semihost halt request, then we shouldn't
+  // step any further. Just return with "waiting for interrupt" code.
+  using HaltReason = RenodeDebugInterface::HaltReason;
+  if (halt_res.value() == HaltReason::kSemihostHaltRequest) {
+    if (status != nullptr) {
+      *status = static_cast<int32_t>(ExecutionResult::kAborted);
+    }
+    return 0;
+  }
+  // Perform the stepping.
+  uint32_t total_executed = 0;
+  while (num_to_step > 0) {
+    // Check how far to step, and make multiple calls if the number
+    // is greater than <int>::max();
+    int step_count = (num_to_step > std::numeric_limits<int>::max())
+                         ? std::numeric_limits<int>::max()
+                         : static_cast<int>(num_to_step);
+    auto res = dbg->Step(step_count);
+    // An error occurred.
+    if (!res.ok()) {
+      return total_executed;
+    }
+    int num_executed = res.value();
+    total_executed += num_executed;
+    // Check if the execution was halted due to a semihosting halt request,
+    // i.e., program exit.
+    halt_res = dbg->GetLastHaltReason();
+    if (!halt_res.ok()) {
+      return total_executed;
+    }
+    switch (halt_res.value()) {
+      case HaltReason::kSemihostHaltRequest:
+        return total_executed;
+        break;
+      case HaltReason::kSoftwareBreakpoint:
+      case HaltReason::kHardwareBreakpoint:
+        if (status != nullptr) {
+          *status = static_cast<int32_t>(ExecutionResult::kStoppedAtBreakpoint);
+        }
+        return total_executed;
+        break;
+      case HaltReason::kUserRequest:
+        if (status != nullptr) {
+          *status = static_cast<int32_t>(ExecutionResult::kOk);
+        }
+        return total_executed;
+        break;
+      default:
+        break;
+    }
+
+    // If we stepped fewer instructions than anticipated, stop stepping and
+    // return with no error.
+    if (num_executed < step_count) {
+      if (status != nullptr) {
+        *status = static_cast<int32_t>(ExecutionResult::kOk);
+      }
+      return total_executed;
+    }
+    num_to_step -= num_executed;
+  }
+  if (status != nullptr) {
+    *status = static_cast<int32_t>(ExecutionResult::kOk);
+  }
+  return total_executed;
+}
+
+// Signal the simulator to halt.
+int32_t RenodeAgent::Halt(int32_t id, int32_t *status) {
+  // Get the core debug if object.
+  auto *dbg = RenodeAgent::Instance()->core_dbg(id);
+  // Is the debug interface valid?
+  if (dbg == nullptr) {
+    return -1;
+  }
+  // Request halt.
+  auto halt_status = dbg->Halt();
+  if (!halt_status.ok()) {
+    if (status != nullptr) {
+      *status = static_cast<int32_t>(ExecutionResult::kAborted);
+    }
+    return -1;
+  }
+  // Get the halt status.
+  auto halt_res = dbg->GetLastHaltReason();
+  if (!halt_res.ok()) {
+    if (status != nullptr) {
+      *status = static_cast<int32_t>(ExecutionResult::kAborted);
+    }
+    return -1;
+  }
+  // Map the halt status appropriately.
+  using HaltReason = RenodeDebugInterface::HaltReason;
+  if (status != nullptr) {
+    switch (halt_res.value()) {
+      case HaltReason::kSemihostHaltRequest:
+        *status = static_cast<int32_t>(ExecutionResult::kAborted);
+        break;
+      case HaltReason::kSoftwareBreakpoint:
+        *status = static_cast<int32_t>(ExecutionResult::kStoppedAtBreakpoint);
+        break;
+      case HaltReason::kUserRequest:
+        *status = static_cast<int32_t>(ExecutionResult::kInterrupted);
+        break;
+      case HaltReason::kNone:
+        *status = static_cast<int32_t>(ExecutionResult::kOk);
+        break;
+      default:
+        break;
+    }
+  }
+  return 0;
+}
+
+}  // namespace kelvin::sim::renode
diff --git a/sim/renode/renode_mpact.h b/sim/renode/renode_mpact.h
new file mode 100644
index 0000000..a295478
--- /dev/null
+++ b/sim/renode/renode_mpact.h
@@ -0,0 +1,111 @@
+#ifndef LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_RENODE_MPACT_H_
+#define LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_RENODE_MPACT_H_
+
+#include <cstddef>
+#include <cstdint>
+
+#include "sim/renode/renode_debug_interface.h"
+#include "absl/container/flat_hash_map.h"
+
+// This file defines the interface that Renode uses to communicate with the
+// simulator as well as support classes to implement the interface.
+// C interface used by Renode.
+extern "C" {
+// Construct a debug instance connected to a simulator. Returns the non-zero
+// id of the created instance. A return value of zero indicates an error.
+int32_t construct(int32_t max_name_length);
+// Destruct the given debug instance. A negative return value indicates an
+// error.
+int32_t destruct(int32_t id);
+// Return the number of register entries.
+int32_t get_reg_info_size(int32_t id);
+// Return the register entry with the given index. The info pointer should
+// store an object of type RenodeCpuRegister.
+int32_t get_reg_info(int32_t id, int32_t index, char *name, void *info);
+// Load the given executable into the instance with the given id. Return the
+// entry point.
+uint64_t load_executable(int32_t id, const char *elf_file_name,
+                         int32_t *status);
+// Load the content of the given file into memory, starting at the given
+// address.
+int32_t load_image(int32_t id, const char *file_name, uint64_t address);
+// Read register reg_id in the instance id, store the value in the pointer
+// given. A return value < 0 is an error.
+int32_t read_register(int32_t id, uint32_t reg_id, uint64_t *value);
+// Write register reg_id in the instance id. A return value < 0 is an error.
+int32_t write_register(int32_t id, uint32_t reg_id, uint64_t value);
+
+uint64_t read_memory(int32_t id, uint64_t address, char *buffer,
+                     uint64_t length);
+uint64_t write_memory(int32_t id, uint64_t address, const char *buffer,
+                      uint64_t length);
+// Reset the instance. A return value < 0 is an error.
+int32_t reset(int32_t id);
+// Step the instance id by num_to_step instructions. Return the number of
+// instructions stepped. The status is written to the pointer *status.
+uint64_t step(int32_t id, uint64_t num_to_step, int32_t *status);
+// Halt a free running simulator.
+int32_t halt(int32_t id, int32_t *status);
+}
+
+namespace kelvin::sim::renode {
+
+// Execution results.
+enum class ExecutionResult : int32_t {
+  kOk = 0,
+  kInterrupted = 1,
+  kWaitingForInterrupt = 2,
+  kStoppedAtBreakpoint = 3,
+  kStoppedAtWatchpoint = 4,
+  kExternalMmuFault = 5,
+  kAborted = -1,
+};
+// Intermediary between the C interface above and the actual debug interface
+// of the simulator.
+class RenodeAgent {
+ public:
+  using RenodeCpuRegister = kelvin::sim::renode::RenodeCpuRegister;
+  constexpr static size_t kBufferSize = 64 * 1024;
+  // This is a singleton class, so need a static Instance method.
+  static RenodeAgent *Instance() {
+    if (instance_ != nullptr) return instance_;
+    instance_ = new RenodeAgent();
+    return instance_;
+  }
+  // These methods correspond to the C methods defined above.
+  int32_t Construct(int32_t max_name_length);
+  int32_t Destroy(int32_t id);
+  int32_t Reset(int32_t id);
+  int32_t GetRegisterInfoSize(int32_t id) const;
+  int32_t GetRegisterInfo(int32_t id, int32_t index, char *name,
+                          RenodeCpuRegister *info);
+  int32_t ReadRegister(int32_t id, uint32_t reg_id, uint64_t *value);
+  int32_t WriteRegister(int32_t id, uint32_t reg_id, uint64_t value);
+  uint64_t ReadMemory(int32_t id, uint64_t address, char *buffer,
+                      uint64_t length);
+  uint64_t WriteMemory(int32_t id, uint64_t address, const char *buffer,
+                       uint64_t length);
+  uint64_t LoadExecutable(int32_t id, const char *elf_file_name,
+                          int32_t *status);
+  int32_t LoadImage(int32_t id, const char *file_name, uint64_t address);
+  uint64_t Step(int32_t id, uint64_t num_to_step, int32_t *status);
+  int32_t Halt(int32_t id, int32_t *status);
+  // Accessor.
+  kelvin::sim::renode::RenodeDebugInterface *core_dbg(int32_t id) const {
+    auto ptr = core_dbg_instances_.find(id);
+    if (ptr != core_dbg_instances_.end()) return ptr->second;
+    return nullptr;
+  }
+
+ private:
+  static RenodeAgent *instance_;
+  static uint32_t count_;
+  RenodeAgent() = default;
+  absl::flat_hash_map<uint32_t, kelvin::sim::renode::RenodeDebugInterface *>
+      core_dbg_instances_;
+  absl::flat_hash_map<uint32_t, int32_t> name_length_map_;
+};
+
+}  // namespace kelvin::sim::renode
+
+#endif  // LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_RENODE_MPACT_H_
diff --git a/sim/renode/test/BUILD b/sim/renode/test/BUILD
new file mode 100644
index 0000000..50538d3
--- /dev/null
+++ b/sim/renode/test/BUILD
@@ -0,0 +1,43 @@
+# This project contains a small set of tests to test the functionality of
+# the MPACT-Sim/Renode interface.
+
+cc_test(
+    name = "renode_mpact_test",
+    size = "small",
+    srcs = ["renode_mpact_test.cc"],
+    data = [
+        "//sim/test:testfiles/hello_world_mpause.bin",
+        "//sim/test:testfiles/hello_world_mpause.elf",
+    ],
+    deps = [
+        "//sim/renode:kelvin_renode",
+        "//sim/renode:renode_mpact",
+        "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+        "@com_google_mpact-riscv//riscv:riscv_debug_info",
+    ],
+)
+
+cc_test(
+    name = "kelvin_renode_test",
+    size = "small",
+    srcs = ["kelvin_renode_test.cc"],
+    data = [
+        "//sim/test:testfiles/hello_world_mpause.bin",
+        "//sim/test:testfiles/hello_world_mpause.elf",
+    ],
+    deps = [
+        "//sim:kelvin_top",
+        "//sim/renode:kelvin_renode",
+        "//sim/renode:renode_debug_interface",
+        "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_googletest//:gtest_main",
+        "@com_google_mpact-riscv//riscv:riscv_debug_info",
+        "@com_google_mpact-sim//mpact/sim/generic:core",
+        "@com_google_mpact-sim//mpact/sim/generic:core_debug_interface",
+        "@com_google_mpact-sim//mpact/sim/util/program_loader:elf_loader",
+    ],
+)
diff --git a/sim/renode/test/kelvin_renode_test.cc b/sim/renode/test/kelvin_renode_test.cc
new file mode 100644
index 0000000..82c01e7
--- /dev/null
+++ b/sim/renode/test/kelvin_renode_test.cc
@@ -0,0 +1,119 @@
+#include "sim/renode/kelvin_renode.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <fstream>
+#include <ios>
+#include <string>
+
+#include "sim/kelvin_top.h"
+#include "sim/renode/renode_debug_interface.h"
+#include "googletest/include/gtest/gtest.h"
+#include "absl/log/check.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+#include "riscv/riscv_debug_info.h"
+#include "mpact/sim/generic/core_debug_interface.h"
+#include "mpact/sim/util/program_loader/elf_program_loader.h"
+
+namespace {
+
+using kelvin::sim::KelvinTop;
+using kelvin::sim::renode::RenodeDebugInterface;
+
+constexpr char kFileName[] = "hello_world_mpause.elf";
+constexpr char kBinFileName[] = "hello_world_mpause.bin";
+// The depot path to the test directory.
+constexpr char kDepotPath[] = "sim/test/";
+constexpr char kTopName[] = "test";
+
+class KelvinRenodeTest : public testing::Test {
+ protected:
+  KelvinRenodeTest() { top_ = new kelvin::sim::KelvinRenode(kTopName); }
+
+  ~KelvinRenodeTest() override { delete top_; }
+
+  RenodeDebugInterface *top_ = nullptr;
+};
+
+// Test the implementation of the added methods in the RenodeDebugInterface.
+TEST_F(KelvinRenodeTest, RegisterIds) {
+  uint32_t word_value;
+  for (int i = 0; i < 32; i++) {
+    uint32_t reg_id =
+        i + static_cast<uint32_t>(mpact::sim::riscv::DebugRegisterEnum::kX0);
+    auto result = top_->ReadRegister(reg_id);
+    EXPECT_TRUE(result.status().ok());
+    word_value = result.value();
+    EXPECT_TRUE(top_->WriteRegister(reg_id, word_value + 1).ok());
+    result = top_->ReadRegister(reg_id);
+    EXPECT_TRUE(result.status().ok());
+    EXPECT_EQ(result.value(), word_value + 1);
+  }
+  // Not found.
+  EXPECT_EQ(top_->ReadRegister(0xfff).status().code(),
+            absl::StatusCode::kNotFound);
+  EXPECT_EQ(top_->WriteRegister(0xfff, word_value).code(),
+            absl::StatusCode::kNotFound);
+}
+
+TEST_F(KelvinRenodeTest, RunElfProgram) {
+  std::string file_name = absl::StrCat(kDepotPath, "testfiles/", kFileName);
+  // Load the program.
+  auto *loader = new mpact::sim::util::ElfProgramLoader(top_);
+  auto result = loader->LoadProgram(file_name);
+  CHECK_OK(result);
+  auto entry_point = result.value();
+  // Run the program.
+  testing::internal::CaptureStdout();
+  EXPECT_TRUE(top_->WriteRegister("pc", entry_point).ok());
+  EXPECT_TRUE(top_->Run().ok());
+  EXPECT_TRUE(top_->Wait().ok());
+  // Check the results.
+  auto halt_result = top_->GetLastHaltReason();
+  CHECK_OK(halt_result);
+  EXPECT_EQ(static_cast<int>(halt_result.value()),
+            static_cast<int>(KelvinTop::HaltReason::kUserRequest));
+  const std::string stdout_str = testing::internal::GetCapturedStdout();
+  EXPECT_EQ("Program exits properly\n", stdout_str);
+  // Clean up.
+  delete loader;
+}
+
+TEST_F(KelvinRenodeTest, RunBinProgram) {
+  std::string file_name = absl::StrCat(kDepotPath, "testfiles/", kBinFileName);
+  constexpr uint32_t kBufferSize = 1024;
+  constexpr uint64_t kBinFileAddress = 0x0;
+  constexpr uint64_t kBinFileEntryPoint = 0x0;
+
+  char buffer[kBufferSize];
+  size_t gcount = 0;
+  uint64_t load_address = kBinFileAddress;
+  std::ifstream image_file;
+  image_file.open(file_name, std::ios::in | std::ios::binary);
+  EXPECT_TRUE(image_file.good());
+  do {
+    image_file.read(buffer, kBufferSize);
+    gcount = image_file.gcount();
+    auto result = top_->WriteMemory(load_address, buffer, gcount);
+    EXPECT_TRUE(result.ok());
+    EXPECT_EQ(result.value(), gcount);
+    load_address += gcount;
+  } while (image_file.good() && (gcount > 0));
+  image_file.close();
+
+  // Run the program.
+  testing::internal::CaptureStdout();
+  EXPECT_TRUE(top_->WriteRegister("pc", kBinFileEntryPoint).ok());
+  EXPECT_TRUE(top_->Run().ok());
+  EXPECT_TRUE(top_->Wait().ok());
+  // Check the results.
+  auto halt_result = top_->GetLastHaltReason();
+  CHECK_OK(halt_result);
+  EXPECT_EQ(static_cast<int>(halt_result.value()),
+            static_cast<int>(KelvinTop::HaltReason::kUserRequest));
+  const std::string stdout_str = testing::internal::GetCapturedStdout();
+  EXPECT_EQ("Program exits properly\n", stdout_str);
+}
+
+}  // namespace
diff --git a/sim/renode/test/renode_mpact_test.cc b/sim/renode/test/renode_mpact_test.cc
new file mode 100644
index 0000000..c4fa7d1
--- /dev/null
+++ b/sim/renode/test/renode_mpact_test.cc
@@ -0,0 +1,214 @@
+#include "sim/renode/renode_mpact.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <fstream>
+#include <ios>
+#include <string>
+
+#include "googletest/include/gtest/gtest.h"
+#include "absl/log/check.h"
+#include "absl/strings/str_cat.h"
+#include "riscv/riscv_debug_info.h"
+
+// This file contains a test of the RenodeMpact interface using kelvin.
+
+namespace {
+
+using kelvin::sim::renode::ExecutionResult;
+using mpact::sim::riscv::DebugRegisterEnum;
+
+constexpr char kExecutableFileName[] = "hello_world_mpause.elf";
+constexpr char kBinFileName[] = "hello_world_mpause.bin";
+constexpr uint64_t kBinFileAddress = 0x0;
+constexpr uint64_t kBinFileEntryPoint = 0x0;
+constexpr size_t kBufferSize = 1024;
+
+// The depot path to the test directory.
+constexpr char kDepotPath[] = "sim/test/";
+
+class RenodeMpactTest : public testing::Test {
+ protected:
+  RenodeMpactTest() {
+    sim_id_ = construct(1);
+    CHECK_GE(sim_id_, 0) << sim_id_;
+  }
+
+  ~RenodeMpactTest() override {
+    int32_t result = destruct(sim_id_);
+    CHECK_EQ(result, 0);
+  }
+  int32_t sim_id_;
+};
+
+// Test the reset call.
+TEST_F(RenodeMpactTest, Reset) {
+  CHECK_EQ(reset(sim_id_), 0);
+  CHECK_EQ(reset(10), -1);
+}
+
+// Test that registers can be read and written using the id numbers.
+TEST_F(RenodeMpactTest, RegisterId) {
+  uint32_t xreg = static_cast<uint32_t>(DebugRegisterEnum::kX0);
+  uint64_t value = 0;
+  for (int i = 0; i < 8; i++) {
+    auto error = write_register(sim_id_, xreg + i, value++);
+    EXPECT_EQ(error, 0);
+  }
+  value = 0;
+  uint64_t reg_value;
+  for (int i = 0; i < 8; i++) {
+    auto error = read_register(sim_id_, xreg + i, &reg_value);
+    EXPECT_EQ(error, 0);
+    EXPECT_EQ(reg_value, value);
+    value++;
+  }
+  // Wrong debug ids.
+  auto error = read_register(10, xreg, &reg_value);
+  EXPECT_EQ(error, -1);
+  error = write_register(10, xreg, reg_value);
+  EXPECT_EQ(error, -1);
+  // Wrong register numbers.
+  error = read_register(sim_id_, xreg - 1, &reg_value);
+  EXPECT_EQ(error, -1);
+  error = write_register(sim_id_, xreg - 1, reg_value);
+  EXPECT_EQ(error, -1);
+  // Nullptr.
+  error = read_register(sim_id_, xreg, nullptr);
+  EXPECT_EQ(error, -1);
+}
+
+TEST_F(RenodeMpactTest, LoadExecutable) {
+  const std::string input_file_name =
+      absl::StrCat(kDepotPath, "testfiles/", kExecutableFileName);
+  // Fails - wrong sim id.
+  int32_t status;
+  uint64_t entry_pt;
+  entry_pt = load_executable(100, input_file_name.c_str(), &status);
+  EXPECT_EQ(status, -1);
+  // Fails - wrong file name.
+  entry_pt = load_executable(sim_id_, "wrong_file_name", &status);
+  EXPECT_EQ(status, -1);
+  // Succeeds - correct sim id and file name.
+  entry_pt = load_executable(sim_id_, input_file_name.c_str(), &status);
+  EXPECT_EQ(status, 0);
+  // Read the pc, verify that the entry point matches.
+  uint64_t pc_value;
+  status = read_register(sim_id_, static_cast<uint32_t>(DebugRegisterEnum::kPc),
+                         &pc_value);
+  EXPECT_EQ(pc_value, entry_pt);
+  EXPECT_EQ(status, 0);
+}
+
+TEST_F(RenodeMpactTest, ReadWriteMem) {
+  constexpr uint8_t kBytes[] = {0x01, 0x02, 0x03, 0x04, 0xff, 0xfe, 0xfd, 0xfc};
+  int res =
+      write_memory(sim_id_, 0x100, reinterpret_cast<const char *>(kBytes), 8);
+  EXPECT_EQ(res, 8);
+  uint8_t mem_bytes[8] = {0xde, 0xad, 0xbe, 0xef, 0x5a, 0xa5, 0xff, 0x00};
+  res = read_memory(sim_id_, 0x104, reinterpret_cast<char *>(mem_bytes), 1);
+  EXPECT_EQ(res, 1);
+  EXPECT_EQ(mem_bytes[0], kBytes[4]);
+  res = read_memory(sim_id_, 0x100, reinterpret_cast<char *>(mem_bytes), 8);
+  for (int i = 0; i < 8; i++) EXPECT_EQ(kBytes[i], mem_bytes[i]);
+}
+
+TEST_F(RenodeMpactTest, LoadImage) {
+  const std::string input_file_name =
+      absl::StrCat(kDepotPath, "testfiles/", kBinFileName);
+  int32_t ret =
+      load_image(sim_id_, (input_file_name + "xyz").c_str(), kBinFileAddress);
+  EXPECT_LT(ret, 0);
+  ret = load_image(sim_id_, input_file_name.c_str(), kBinFileAddress);
+  EXPECT_EQ(ret, 0);
+  std::ifstream bin_file;
+  bin_file.open(input_file_name, std::ios::in | std::ios::binary);
+  CHECK_EQ(bin_file.good(), true);
+  char file_data[kBufferSize];
+  char memory_data[kBufferSize];
+  uint64_t data_address = kBinFileAddress;
+  size_t gcount;
+  size_t total = 0;
+  do {
+    bin_file.read(file_data, kBufferSize);
+    gcount = bin_file.gcount();
+    auto res = read_memory(sim_id_, data_address, memory_data, gcount);
+    EXPECT_GT(res, 0);
+    EXPECT_EQ(res, gcount);
+    for (int i = 0; i < kBufferSize; ++i)
+      EXPECT_EQ(file_data[i], memory_data[i]) << "Byte: " << total + i;
+    data_address += gcount;
+    total += gcount;
+  } while (gcount == kBufferSize);
+  EXPECT_TRUE(bin_file.eof());
+  bin_file.close();
+}
+
+TEST_F(RenodeMpactTest, StepExecutableProgram) {
+  testing::internal::CaptureStdout();
+  const std::string input_file_name =
+      absl::StrCat(kDepotPath, "testfiles/", kExecutableFileName);
+  int32_t status;
+  (void)load_executable(sim_id_, input_file_name.c_str(), &status);
+  CHECK_EQ(status, 0);
+  // Fail if you use the wrong sim_id_.
+  auto count = step(10, 1, &status);
+  EXPECT_EQ(status, -1);
+  EXPECT_EQ(count, 0);
+  // Zero steps, but no failure if step count is == 0.
+  count = step(sim_id_, 0, &status);
+  EXPECT_EQ(status, static_cast<int32_t>(ExecutionResult::kOk));
+  EXPECT_EQ(count, 0);
+  // Null status should work.
+  count = step(10, 1, nullptr);
+  EXPECT_EQ(count, 0);
+  count = step(10, 0, nullptr);
+  EXPECT_EQ(count, 0);
+  count = step(sim_id_, 1, nullptr);
+  EXPECT_EQ(count, 1);
+  // Counts from 1 to 10.
+  for (int i = 0; i < 10; i++) {
+    count = step(sim_id_, i, &status);
+    EXPECT_EQ(count, i);
+    EXPECT_EQ(status, static_cast<int32_t>(ExecutionResult::kOk));
+  }
+  constexpr uint64_t kStepCount = 500;
+  while (true) {
+    count = step(sim_id_, kStepCount, &status);
+    if (count != kStepCount) break;
+    EXPECT_EQ(status, static_cast<int32_t>(ExecutionResult::kOk));
+  }
+  // Execution should now have completed and the program has printed the proper
+  // exit message.
+  EXPECT_GT(kStepCount, count);
+  EXPECT_EQ(status, static_cast<int32_t>(ExecutionResult::kOk));
+  const std::string stdout_str = testing::internal::GetCapturedStdout();
+  EXPECT_EQ("Program exits properly\n", stdout_str);
+}
+
+TEST_F(RenodeMpactTest, StepImageProgram) {
+  const std::string input_file_name =
+      absl::StrCat(kDepotPath, "testfiles/", kBinFileName);
+  testing::internal::CaptureStdout();
+  auto ret = load_image(sim_id_, input_file_name.c_str(), kBinFileAddress);
+  EXPECT_EQ(ret, 0);
+  auto xreg = static_cast<uint32_t>(DebugRegisterEnum::kPc);
+  auto error = write_register(sim_id_, xreg, kBinFileEntryPoint);
+  EXPECT_EQ(error, 0);
+  constexpr uint64_t kStepCount = 1000;
+  int32_t status;
+  int32_t count;
+  while (true) {
+    count = step(sim_id_, kStepCount, &status);
+    if (count != kStepCount) break;
+    EXPECT_EQ(status, static_cast<int32_t>(ExecutionResult::kOk));
+  }
+  // Execution should now have completed and the program has printed the proper
+  // exit message.
+  EXPECT_GT(kStepCount, count);
+  EXPECT_EQ(status, static_cast<int32_t>(ExecutionResult::kOk));
+  const std::string stdout_str = testing::internal::GetCapturedStdout();
+  EXPECT_EQ("Program exits properly\n", stdout_str);
+}
+
+}  // namespace
diff --git a/sim/test/BUILD b/sim/test/BUILD
index 76ad1e3..5ea7564 100644
--- a/sim/test/BUILD
+++ b/sim/test/BUILD
@@ -1,13 +1,18 @@
 # Unit tests for kelvin simulator.
 
-exports_files([
-    "testfiles/hello_world_rv32imf.elf",
-    "testfiles/kelvin_vldvst.elf",
-    "testfiles/rv32i.elf",
-    "testfiles/rv32m.elf",
-    "testfiles/rv32soft_fp.elf",
-    "testfiles/rv32uf_fadd.elf",
-])
+exports_files(
+    srcs = [
+        "testfiles/hello_world_mpause.bin",
+        "testfiles/hello_world_mpause.elf",
+        "testfiles/hello_world_rv32imf.elf",
+        "testfiles/kelvin_vldvst.elf",
+        "testfiles/rv32i.elf",
+        "testfiles/rv32m.elf",
+        "testfiles/rv32soft_fp.elf",
+        "testfiles/rv32uf_fadd.elf",
+    ],
+    visibility = ["//visibility:public"],
+)
 
 cc_test(
     name = "kelvin_encoding_test",
diff --git a/sim/test/testfiles/hello_world_mpause.bin b/sim/test/testfiles/hello_world_mpause.bin
new file mode 100644
index 0000000..d30b157
--- /dev/null
+++ b/sim/test/testfiles/hello_world_mpause.bin
Binary files differ