Add Renode memory interface

Add a memory interface class to access memory allocated by Renode. KelvinTop only accesses the memory via load/store, but it does not control the life cycle of the memory block.

PiperOrigin-RevId: 565185383
diff --git a/sim/BUILD b/sim/BUILD
index de6e67c..8ea30ec 100644
--- a/sim/BUILD
+++ b/sim/BUILD
@@ -131,6 +131,7 @@
         ":kelvin_decoder",
         ":kelvin_isa",
         ":kelvin_state",
+        "//sim/renode:kelvin_renode_memory",
         "@com_google_absl//absl/container:flat_hash_map",
         "@com_google_absl//absl/flags:flag",
         "@com_google_absl//absl/functional:bind_front",
diff --git a/sim/kelvin_top.cc b/sim/kelvin_top.cc
index 5ec7fa9..0862639 100644
--- a/sim/kelvin_top.cc
+++ b/sim/kelvin_top.cc
@@ -18,6 +18,7 @@
 #include "sim/decoder.h"
 #include "sim/kelvin_enums.h"
 #include "sim/kelvin_state.h"
+#include "sim/renode/kelvin_renode_memory.h"
 #include "absl/flags/flag.h"
 #include "absl/functional/bind_front.h"
 #include "absl/log/check.h"
@@ -81,6 +82,18 @@
   Initialize();
 }
 
+KelvinTop::KelvinTop(std::string name, uint64_t memory_block_size_bytes,
+                     uint64_t memory_size_bytes,
+                     uint8_t **memory_block_ptr_list)
+    : Component(std::move(name)),
+      counter_num_instructions_{"num_instructions", 0},
+      counter_num_cycles_{"num_cycles", 0} {
+  // Use Kelvin renode memory for this core.
+  memory_ = new renode::KelvinRenodeMemory(
+      memory_block_size_bytes, memory_size_bytes, memory_block_ptr_list);
+  Initialize();
+}
+
 KelvinTop::~KelvinTop() {
   // If the simulator is still running, request a halt (set halted_ to true),
   // and wait until the simulator finishes before continuing the destructor.
diff --git a/sim/kelvin_top.h b/sim/kelvin_top.h
index 3f8aace..0ab6792 100644
--- a/sim/kelvin_top.h
+++ b/sim/kelvin_top.h
@@ -46,6 +46,9 @@
   using RunStatus = mpact::sim::generic::CoreDebugInterface::RunStatus;
 
   explicit KelvinTop(std::string name);
+  KelvinTop(std::string name, uint64_t memory_block_size_bytes,
+            uint64_t memory_size_bytes, uint8_t **memory_block_ptr_list);
+
   ~KelvinTop() override;
 
   // Methods inherited from CoreDebugInterface.
diff --git a/sim/renode/BUILD b/sim/renode/BUILD
index a215a01..e19ac2e 100644
--- a/sim/renode/BUILD
+++ b/sim/renode/BUILD
@@ -36,6 +36,24 @@
 )
 
 cc_library(
+    name = "kelvin_renode_memory",
+    srcs = [
+        "kelvin_renode_memory.cc",
+    ],
+    hdrs = [
+        "kelvin_renode_memory.h",
+    ],
+    deps = [
+        "//sim:kelvin_state",
+        "@com_google_absl//absl/base:core_headers",
+        "@com_google_absl//absl/numeric:bits",
+        "@com_google_mpact-sim//mpact/sim/generic:core",
+        "@com_google_mpact-sim//mpact/sim/generic:instruction",
+        "@com_google_mpact-sim//mpact/sim/util/memory",
+    ],
+)
+
+cc_library(
     name = "kelvin_renode",
     srcs = [
         "kelvin_renode.cc",
diff --git a/sim/renode/kelvin_renode.cc b/sim/renode/kelvin_renode.cc
index 618a08f..700feaa 100644
--- a/sim/renode/kelvin_renode.cc
+++ b/sim/renode/kelvin_renode.cc
@@ -21,6 +21,14 @@
   return top;
 }
 
+kelvin::sim::renode::RenodeDebugInterface *CreateKelvinSim(
+    std::string name, uint64_t memory_block_size_bytes,
+    uint64_t memory_size_bytes, uint8_t **block_ptr_list) {
+  auto *top = new kelvin::sim::KelvinRenode(name, memory_block_size_bytes,
+                                            memory_size_bytes, block_ptr_list);
+  return top;
+}
+
 namespace kelvin::sim {
 
 using HaltReasonValueType =
@@ -33,6 +41,13 @@
   kelvin_top_ = new KelvinTop(name);
 }
 
+KelvinRenode::KelvinRenode(std::string name, uint64_t memory_block_size_bytes,
+                           uint64_t memory_size_bytes,
+                           uint8_t **block_ptr_list) {
+  kelvin_top_ = new KelvinTop(name, memory_block_size_bytes, memory_size_bytes,
+                              block_ptr_list);
+}
+
 KelvinRenode::~KelvinRenode() { delete kelvin_top_; }
 
 absl::Status KelvinRenode::Halt() { return kelvin_top_->Halt(); }
diff --git a/sim/renode/kelvin_renode.h b/sim/renode/kelvin_renode.h
index 331d912..0763f34 100644
--- a/sim/renode/kelvin_renode.h
+++ b/sim/renode/kelvin_renode.h
@@ -20,6 +20,10 @@
 extern kelvin::sim::renode::RenodeDebugInterface *CreateKelvinSim(
     std::string name);
 
+extern kelvin::sim::renode::RenodeDebugInterface *CreateKelvinSim(
+    std::string name, uint64_t memory_block_size_bytes,
+    uint64_t memory_size_bytes, uint8_t **block_ptr_list);
+
 namespace kelvin::sim {
 
 class KelvinRenode : public renode::RenodeDebugInterface {
@@ -29,6 +33,9 @@
   using RenodeCpuRegister = kelvin::sim::renode::RenodeCpuRegister;
 
   explicit KelvinRenode(std::string name);
+  explicit KelvinRenode(std::string name, uint64_t memory_block_size_bytes,
+                        uint64_t memory_size_bytes, uint8_t **block_ptr_list);
+
   ~KelvinRenode() override;
 
   // Request that core stop running override;
diff --git a/sim/renode/kelvin_renode_memory.cc b/sim/renode/kelvin_renode_memory.cc
new file mode 100644
index 0000000..15f5d37
--- /dev/null
+++ b/sim/renode/kelvin_renode_memory.cc
@@ -0,0 +1,165 @@
+#include "sim/renode/kelvin_renode_memory.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+
+#include "sim/kelvin_state.h"
+#include "absl/base/macros.h"
+#include "absl/numeric/bits.h"
+
+namespace kelvin::sim::renode {
+
+KelvinRenodeMemory::KelvinRenodeMemory(uint64_t block_size_bytes,
+                                       uint64_t memory_size_bytes,
+                                       uint8_t **block_ptr_list,
+                                       uint64_t base_address,
+                                       unsigned addressable_unit_size)
+    : addressable_unit_size_(addressable_unit_size),
+      allocation_byte_size_(block_size_bytes * addressable_unit_size),
+      memory_block_size_bytes_(block_size_bytes),
+      base_address_(base_address),
+      max_address_(base_address + memory_size_bytes) {
+  // Available memory should be greater than Kelvin's default 4MB address space.
+  ABSL_HARDENING_ASSERT(max_address_ > kelvin::sim::kKelvinMaxMemoryAddress);
+  uint64_t num_block = (max_address_ + block_size_bytes - 1) / block_size_bytes;
+  // Build the block map.
+  for (int i = 0; i < num_block; ++i) {
+    block_map_.push_back(block_ptr_list[i]);
+  }
+  addressable_unit_shift_ = absl::bit_width(addressable_unit_size) - 1;
+}
+
+bool KelvinRenodeMemory::IsValidAddress(uint64_t address,
+                                        uint64_t high_address) {
+  return (address >= base_address_) && (high_address <= max_address_);
+}
+
+void KelvinRenodeMemory::Load(uint64_t address, DataBuffer *db,
+                              Instruction *inst, ReferenceCount *context) {
+  int size_in_units = db->size<uint8_t>() / addressable_unit_size_;
+  uint64_t high = address + size_in_units;
+  ABSL_HARDENING_ASSERT(IsValidAddress(address, high));
+  ABSL_HARDENING_ASSERT(size_in_units > 0);
+  uint8_t *byte_ptr = static_cast<uint8_t *>(db->raw_ptr());
+  // Load the data into the data buffer.
+  LoadStoreHelper(address, byte_ptr, size_in_units, true);
+  // Execute the instruction to process and write back the load data.
+  if (nullptr != inst) {
+    if (db->latency() > 0) {
+      inst->IncRef();
+      if (context != nullptr) context->IncRef();
+      inst->state()->function_delay_line()->Add(db->latency(),
+                                                [inst, context]() {
+                                                  inst->Execute(context);
+                                                  if (context != nullptr)
+                                                    context->DecRef();
+                                                  inst->DecRef();
+                                                });
+    } else {
+      inst->Execute(context);
+    }
+  }
+}
+
+void KelvinRenodeMemory::Load(DataBuffer *address_db, DataBuffer *mask_db,
+                              int el_size, DataBuffer *db, Instruction *inst,
+                              ReferenceCount *context) {
+  auto mask_span = mask_db->Get<bool>();
+  auto address_span = address_db->Get<uint64_t>();
+  uint8_t *byte_ptr = static_cast<uint8_t *>(db->raw_ptr());
+  int size_in_units = el_size / addressable_unit_size_;
+  ABSL_HARDENING_ASSERT(size_in_units > 0);
+  // This is either a gather load, or a unit stride load depending on size of
+  // the address span.
+  bool gather = address_span.size() > 1;
+  for (unsigned i = 0; i < mask_span.size(); i++) {
+    if (!mask_span[i]) continue;
+    uint64_t address = gather ? address_span[i] : address_span[0] + i * el_size;
+    uint64_t high = address + size_in_units;
+    ABSL_HARDENING_ASSERT(IsValidAddress(address, high));
+    LoadStoreHelper(address, &byte_ptr[el_size * i], size_in_units, true);
+  }
+  // Execute the instruction to process and write back the load data.
+  if (nullptr != inst) {
+    if (db->latency() > 0) {
+      inst->IncRef();
+      if (context != nullptr) context->IncRef();
+      inst->state()->function_delay_line()->Add(db->latency(),
+                                                [inst, context]() {
+                                                  inst->Execute(context);
+                                                  if (context != nullptr)
+                                                    context->DecRef();
+                                                  inst->DecRef();
+                                                });
+    } else {
+      inst->Execute(context);
+    }
+  }
+}
+
+void KelvinRenodeMemory::Store(uint64_t address, DataBuffer *db) {
+  int size_in_units = db->size<uint8_t>() / addressable_unit_size_;
+  uint64_t high = address + size_in_units;
+  ABSL_HARDENING_ASSERT(IsValidAddress(address, high));
+  ABSL_HARDENING_ASSERT(size_in_units > 0);
+  uint8_t *byte_ptr = static_cast<uint8_t *>(db->raw_ptr());
+  LoadStoreHelper(address, byte_ptr, size_in_units, /*is_load*/ false);
+}
+
+void KelvinRenodeMemory::Store(DataBuffer *address_db, DataBuffer *mask_db,
+                               int el_size, DataBuffer *db) {
+  auto mask_span = mask_db->Get<bool>();
+  auto address_span = address_db->Get<uint64_t>();
+  uint8_t *byte_ptr = static_cast<uint8_t *>(db->raw_ptr());
+  int size_in_units = el_size / addressable_unit_size_;
+  ABSL_HARDENING_ASSERT(size_in_units > 0);
+  // If the address_span.size() > 1, then this is a scatter store, otherwise
+  // it's a unit stride store.
+  bool scatter = address_span.size() > 1;
+  for (unsigned i = 0; i < mask_span.size(); i++) {
+    if (!mask_span[i]) continue;
+    uint64_t address =
+        scatter ? address_span[i] : address_span[0] + i * el_size;
+    uint64_t high = address + size_in_units;
+    ABSL_HARDENING_ASSERT(IsValidAddress(address, high));
+    LoadStoreHelper(address, &byte_ptr[el_size * i], size_in_units,
+                    /*is_load*/ false);
+  }
+}
+
+void KelvinRenodeMemory::LoadStoreHelper(uint64_t address, uint8_t *byte_ptr,
+                                         int size_in_units, bool is_load) {
+  ABSL_HARDENING_ASSERT(address < max_address_);
+  do {
+    // Find the block in the map.
+    uint64_t block_idx = address / memory_block_size_bytes_;
+
+    uint8_t *block = block_map_[block_idx];
+
+    int block_unit_offset = (address - block_idx * memory_block_size_bytes_);
+
+    // Compute how many addressable units to load/store from/to the current
+    // block.
+    int store_size_in_units =
+        std::min(size_in_units, allocation_byte_size_ - block_unit_offset);
+
+    // Translate from unit size to byte size.
+    int store_size_in_bytes = store_size_in_units << addressable_unit_shift_;
+    int block_byte_offset = block_unit_offset << addressable_unit_shift_;
+
+    if (is_load) {
+      std::memcpy(byte_ptr, &block[block_byte_offset], store_size_in_bytes);
+    } else {
+      std::memcpy(&block[block_byte_offset], byte_ptr, store_size_in_bytes);
+    }
+
+    // Adjust address, data pointer and the remaining data left to be
+    // loaded/stored.
+    size_in_units -= store_size_in_units;
+    byte_ptr += store_size_in_bytes;
+    address += store_size_in_units;
+  } while (size_in_units > 0);
+}
+
+}  // namespace kelvin::sim::renode
diff --git a/sim/renode/kelvin_renode_memory.h b/sim/renode/kelvin_renode_memory.h
new file mode 100644
index 0000000..83e0354
--- /dev/null
+++ b/sim/renode/kelvin_renode_memory.h
@@ -0,0 +1,82 @@
+#ifndef LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_MEMORY_H_
+#define LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_MEMORY_H_
+
+#include <cstdint>
+#include <vector>
+
+#include "mpact/sim/generic/data_buffer.h"
+#include "mpact/sim/generic/instruction.h"
+#include "mpact/sim/generic/ref_count.h"
+#include "mpact/sim/util/memory/memory_interface.h"
+
+namespace kelvin::sim::renode {
+
+using ::mpact::sim::generic::DataBuffer;
+using ::mpact::sim::generic::Instruction;
+using ::mpact::sim::generic::ReferenceCount;
+
+// A memory interface class with memory blocks created, Initialize, and shared
+// by Renode's MappedMemory module as an array of memory block pointers, the
+// size of each block, and the total size of the memory (uint64_t
+// memory_block_size_bytes, uint64_t memory_size_byte, uint8_t **
+// memory_block_pointer_list). The class is tied to the Kelvin configuration
+// with memory size check.
+class KelvinRenodeMemory : public mpact::sim::util::MemoryInterface {
+ public:
+  KelvinRenodeMemory(uint64_t block_size_bytes, uint64_t memory_size_bytes,
+                     uint8_t **block_ptr_list, uint64_t base_address,
+                     unsigned addressable_unit_size);
+  KelvinRenodeMemory(uint64_t block_size_bytes, uint64_t memory_size_bytes,
+                     uint8_t **block_ptr_list)
+      : KelvinRenodeMemory(block_size_bytes, memory_size_bytes, block_ptr_list,
+                           0, 1) {}
+
+  // The memory is not allocated by this class, so there is nothing to release
+  // in the destructor.
+  ~KelvinRenodeMemory() override = default;
+
+  // Implementation of the MemoryInterface methods.
+  void Load(uint64_t address, DataBuffer *db, Instruction *inst,
+            ReferenceCount *context) override;
+
+  void Load(DataBuffer *address_db, DataBuffer *mask_db, int el_size,
+            DataBuffer *db, Instruction *inst,
+            ReferenceCount *context) override;
+
+  // Convenience template function that calls the above function with the
+  // element size as the sizeof() the template parameter type.
+  template <typename T>
+  void Load(DataBuffer *address_db, DataBuffer *mask_db, DataBuffer *db,
+            Instruction *inst, ReferenceCount *context) {
+    Load(address_db, mask_db, sizeof(T), db, inst, context);
+  }
+
+  void Store(uint64_t address, DataBuffer *db) override;
+  void Store(DataBuffer *address_db, DataBuffer *mask_db, int el_size,
+             DataBuffer *db) override;
+
+  // Convenience template function that calls the above function with the
+  // element size as the sizeof() the template parameter type.
+  template <typename T>
+  void Store(DataBuffer *address_db, DataBuffer *mask_db, DataBuffer *db) {
+    Store(address_db, mask_db, sizeof(T), db);
+  }
+
+ private:
+  void LoadStoreHelper(uint64_t address, uint8_t *byte_ptr, int size_in_units,
+                       bool is_load);
+  bool IsValidAddress(uint64_t address, uint64_t high_address);
+
+  int addressable_unit_shift_;
+  int addressable_unit_size_;
+  int allocation_byte_size_;
+  uint64_t memory_block_size_bytes_;
+  uint64_t base_address_;
+  uint64_t max_address_;
+
+  std::vector<uint8_t *> block_map_;
+};
+
+}  // namespace kelvin::sim::renode
+
+#endif  // LEARNING_BRAIN_RESEARCH_KELVIN_SIM_RENODE_KELVIN_RENODE_MEMORY_H_
diff --git a/sim/renode/renode_mpact.cc b/sim/renode/renode_mpact.cc
index 3c25082..3d11cb7 100644
--- a/sim/renode/renode_mpact.cc
+++ b/sim/renode/renode_mpact.cc
@@ -8,13 +8,17 @@
 #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/generic/type_helpers.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);
 
+extern kelvin::sim::renode::RenodeDebugInterface *CreateKelvinSim(std::string,
+                                                                  uint64_t,
+                                                                  uint64_t,
+                                                                  uint8_t **);
+
 // External "C" functions visible to Renode.
 using kelvin::sim::renode::RenodeAgent;
 using kelvin::sim::renode::RenodeCpuRegister;
@@ -24,6 +28,16 @@
 int32_t construct(int32_t max_name_length) {
   return RenodeAgent::Instance()->Construct(max_name_length);
 }
+
+int32_t construct_with_memory(int32_t max_name_length,
+                              uint64_t memory_block_size_bytes,
+                              uint64_t memory_size_bytes,
+                              uint8_t **mem_block_ptr_list) {
+  return RenodeAgent::Instance()->Construct(
+      max_name_length, memory_block_size_bytes, memory_size_bytes,
+      mem_block_ptr_list);
+}
+
 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) {
@@ -79,6 +93,21 @@
   return RenodeAgent::count_++;
 }
 
+int32_t RenodeAgent::Construct(int32_t max_name_length,
+                               uint64_t memory_block_size_bytes,
+                               uint64_t memory_size_bytes,
+                               uint8_t **mem_block_ptr_list) {
+  std::string name = absl::StrCat("renode", count_);
+  auto *dbg = CreateKelvinSim(name, memory_block_size_bytes, memory_size_bytes,
+                              mem_block_ptr_list);
+  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.
diff --git a/sim/renode/renode_mpact.h b/sim/renode/renode_mpact.h
index a295478..edb583b 100644
--- a/sim/renode/renode_mpact.h
+++ b/sim/renode/renode_mpact.h
@@ -14,6 +14,14 @@
 // 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);
+// Construct a debug instance connected to a simulator with memory passed from
+// renode. Returns the non-zero id of the created instance. A return value of
+// zero indicates an error. Note the pointer array needs to be the last argument
+// to comply with renode's import binding signature.
+int32_t construct_with_memory(int32_t max_name_length,
+                              uint64_t memory_block_size_bytes,
+                              uint64_t memory_size_bytes,
+                              uint8_t **mem_block_ptr_list);
 // Destruct the given debug instance. A negative return value indicates an
 // error.
 int32_t destruct(int32_t id);
@@ -74,6 +82,8 @@
   }
   // These methods correspond to the C methods defined above.
   int32_t Construct(int32_t max_name_length);
+  int32_t Construct(int32_t max_name_length, uint64_t memory_block_size_bytes,
+                    uint64_t memory_size_bytes, uint8_t **mem_block_ptr_list);
   int32_t Destroy(int32_t id);
   int32_t Reset(int32_t id);
   int32_t GetRegisterInfoSize(int32_t id) const;
diff --git a/sim/renode/test/BUILD b/sim/renode/test/BUILD
index b1d3ac2..0d6c7b0 100644
--- a/sim/renode/test/BUILD
+++ b/sim/renode/test/BUILD
@@ -39,6 +39,7 @@
         "@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
index a64c689..d5f157a 100644
--- a/sim/renode/test/kelvin_renode_test.cc
+++ b/sim/renode/test/kelvin_renode_test.cc
@@ -1,6 +1,7 @@
 #include "sim/renode/kelvin_renode.h"
 
 #include <cstdint>
+#include <cstring>
 #include <string>
 
 #include "sim/kelvin_top.h"
@@ -10,12 +11,14 @@
 #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;
+using RunStatus = mpact::sim::generic::CoreDebugInterface::RunStatus;
 
 constexpr char kFileName[] = "hello_world_mpause.elf";
 constexpr char kBinFileName[] = "hello_world_mpause.bin";
@@ -114,12 +117,59 @@
   EXPECT_TRUE(top_->Run().ok());
   EXPECT_TRUE(top_->Wait().ok());
   // Check the results.
+  auto run_status = top_->GetRunStatus();
+  EXPECT_TRUE(run_status.ok());
+  EXPECT_EQ(run_status.value(), RunStatus::kHalted);
   auto halt_result = top_->GetLastHaltReason();
-  CHECK_OK(halt_result);
+  EXPECT_TRUE(halt_result.ok());
   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);
 }
 
+// Setup external memory to run a binary program
+TEST_F(KelvinRenodeTest, RunBinProgramWithExternalMemory) {
+  std::string file_name = absl::StrCat(kDepotPath, "testfiles/", kBinFileName);
+  constexpr uint64_t kBinFileAddress = 0x0;
+  constexpr uint64_t kBinFileEntryPoint = 0x0;
+
+  // Setup the external memory.
+  constexpr uint64_t kMemoryBlockSize = 0x40000;  // 256KB
+  constexpr uint64_t kNumBlock = 16;              // 4MB / 256KB
+  uint8_t *memory_block[kNumBlock] = {nullptr};
+  // Allocate memory blocks.
+  for (int i = 0; i < kNumBlock; ++i) {
+    memory_block[i] = new uint8_t[kMemoryBlockSize];
+    memset(memory_block[i], 0, kMemoryBlockSize);
+  }
+
+  // Reset top with external memory.
+  delete top_;
+  top_ = new kelvin::sim::KelvinRenode(
+      kTopName, kMemoryBlockSize, kNumBlock * kMemoryBlockSize, memory_block);
+
+  auto res = top_->LoadImage(file_name, kBinFileAddress);
+  EXPECT_TRUE(res.ok());
+  // 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 run_status = top_->GetRunStatus();
+  EXPECT_TRUE(run_status.ok());
+  EXPECT_EQ(run_status.value(), RunStatus::kHalted);
+  auto halt_result = top_->GetLastHaltReason();
+  EXPECT_TRUE(halt_result.ok());
+  EXPECT_EQ(halt_result.value(), *KelvinTop::HaltReason::kUserRequest);
+  const std::string stdout_str = testing::internal::GetCapturedStdout();
+  EXPECT_EQ("Program exits properly\n", stdout_str);
+
+  // Release the memory blocks.
+  for (int i = 0; i < kNumBlock; ++i) {
+    delete[] memory_block[i];
+  }
+}
+
 }  // namespace
diff --git a/sim/renode/test/renode_mpact_test.cc b/sim/renode/test/renode_mpact_test.cc
index 5eda348..6c88bef 100644
--- a/sim/renode/test/renode_mpact_test.cc
+++ b/sim/renode/test/renode_mpact_test.cc
@@ -2,6 +2,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <cstring>
 #include <fstream>
 #include <ios>
 #include <string>
@@ -243,4 +244,52 @@
   EXPECT_EQ("Program exits properly\n", stdout_str);
 }
 
+// Test stepping over a binary image program with external memory.
+TEST_F(RenodeMpactTest, StepImageProgramWithExternalMemory) {
+  const std::string input_file_name =
+      absl::StrCat(kDepotPath, "testfiles/", kBinFileName);
+
+  // Setup the external memory.
+  constexpr uint64_t kMemoryBlockSize = 0x40000;  // 256KB
+  constexpr uint64_t kNumBlock = 16;              // 4MB / 256KB
+  uint8_t *memory_block[kNumBlock] = {nullptr};
+  // Allocate memory blocks.
+  for (int i = 0; i < kNumBlock; ++i) {
+    memory_block[i] = new uint8_t[kMemoryBlockSize];
+    memset(memory_block[i], 0, kMemoryBlockSize);
+  }
+
+  // Reset the simulator with the external memory.
+  destruct(sim_id_);
+  sim_id_ = construct_with_memory(1, kMemoryBlockSize,
+                                  kNumBlock * kMemoryBlockSize, memory_block);
+  EXPECT_GE(sim_id_, 1) << sim_id_;  // the agent count keeps incrementing.
+
+  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);
+
+  // Release the memory blocks.
+  for (int i = 0; i < kNumBlock; ++i) {
+    delete[] memory_block[i];
+  }
+}
+
 }  // namespace
diff --git a/sim/test/kelvin_top_test.cc b/sim/test/kelvin_top_test.cc
index 85d2491..66686ff 100644
--- a/sim/test/kelvin_top_test.cc
+++ b/sim/test/kelvin_top_test.cc
@@ -1,6 +1,8 @@
 #include "sim/kelvin_top.h"
 
+#include <algorithm>
 #include <cstdint>
+#include <cstring>
 #include <string>
 #include <tuple>
 
@@ -445,4 +447,113 @@
   EXPECT_THAT(stdout_str, testing::HasSubstr("vld_vst test passed!"));
 }
 
+constexpr int kMemoryBlockSize = 256 * 1024;  // 256KB
+// Default max memory address is 4MB - 1. Round up to find the number of memory
+// blocks.
+constexpr int kNumMemoryBlocks =
+    (kelvin::sim::kKelvinMaxMemoryAddress + kMemoryBlockSize) /
+    kMemoryBlockSize;
+
+class KelvinTopExternalMemoryTest : public testing::Test {
+ protected:
+  KelvinTopExternalMemoryTest()
+      : memory_size_(kMemoryBlockSize * kNumMemoryBlocks) {
+    // Set the memory blocks outside of KelvinTop.
+    for (int i = 0; i < kNumMemoryBlocks; ++i) {
+      memory_blocks_[i] = new uint8_t[kMemoryBlockSize];
+      memset(memory_blocks_[i], 0, kMemoryBlockSize);
+    }
+    kelvin_top_ =
+        new KelvinTop("Kelvin", kMemoryBlockSize, memory_size_, memory_blocks_);
+    // Set up the elf loader.
+    loader_ = new ElfProgramLoader(kelvin_top_->memory());
+  }
+
+  ~KelvinTopExternalMemoryTest() override {
+    delete loader_;
+    delete kelvin_top_;
+    for (int i = 0; i < kNumMemoryBlocks; ++i) {
+      delete[] memory_blocks_[i];
+    }
+  }
+
+  void LoadFile(const std::string file_name) {
+    const std::string input_file_name =
+        absl::StrCat(kDepotPath, "testfiles/", file_name);
+    auto result = loader_->LoadProgram(input_file_name);
+    CHECK_OK(result);
+    entry_point_ = result.value();
+  }
+
+  uint32_t entry_point_;
+  KelvinTop *kelvin_top_ = nullptr;
+  ElfProgramLoader *loader_ = nullptr;
+  uint8_t *memory_blocks_[kNumMemoryBlocks] = {nullptr};
+  uint64_t memory_size_;
+};
+
+// Run a vector program from beginning to end.
+TEST_F(KelvinTopExternalMemoryTest, RunKelvinVectorProgram) {
+  LoadFile(kKelvinVldVstFileName);
+  testing::internal::CaptureStdout();
+  EXPECT_OK(kelvin_top_->WriteRegister("pc", entry_point_));
+
+  EXPECT_OK(kelvin_top_->Run());
+  EXPECT_OK(kelvin_top_->Wait());
+  auto halt_result = kelvin_top_->GetLastHaltReason();
+  CHECK_OK(halt_result);
+  EXPECT_EQ(halt_result.value(), *HaltReason::kUserRequest);
+  const std::string stdout_str = testing::internal::GetCapturedStdout();
+  EXPECT_THAT(stdout_str, testing::HasSubstr("vld_vst test passed!"));
+}
+
+// Step a regular program from beginning to end.
+TEST_F(KelvinTopExternalMemoryTest, StepMPauseProgram) {
+  LoadFile(kMpauseElfFileName);
+  testing::internal::CaptureStdout();
+  EXPECT_OK(kelvin_top_->WriteRegister("pc", entry_point_));
+
+  constexpr int kStep = 2000;
+  absl::StatusOr<kelvin::sim::HaltReasonValueType> halt_result;
+  do {
+    auto res = kelvin_top_->Step(kStep);
+    EXPECT_OK(res.status());
+    halt_result = kelvin_top_->GetLastHaltReason();
+    EXPECT_OK(halt_result);
+  } while (halt_result.value() == *HaltReason::kNone);
+
+  EXPECT_EQ(halt_result.value(), *HaltReason::kUserRequest);
+  const std::string stdout_str = testing::internal::GetCapturedStdout();
+  EXPECT_EQ("Program exits properly\n", stdout_str);
+}
+
+// Read/Write memory
+TEST_F(KelvinTopExternalMemoryTest, AccessMemory) {
+  constexpr uint64_t kTestMemerySize = 8;
+  const uint64_t test_access_address[] = {
+      0x1000, kMemoryBlockSize - 4, kMemoryBlockSize + 4,
+      kelvin::sim::kKelvinMaxMemoryAddress - 4};
+  uint8_t mem_bytes[kTestMemerySize] = {1, 2, 3, 4, 5, 6, 7, 8};
+  uint8_t mem_bytes_return[kTestMemerySize] = {0};
+
+  // Write new values to the memory.
+  for (int i = 0; i < sizeof(test_access_address) / sizeof(uint64_t); ++i) {
+    auto result = kelvin_top_->WriteMemory(test_access_address[i], mem_bytes,
+                                           sizeof(mem_bytes));
+    uint64_t expected_length =
+        std::min(kTestMemerySize, memory_size_ - test_access_address[i]);
+    EXPECT_OK(result);
+    EXPECT_EQ(result.value(), expected_length);
+    // Read back the content from the external memory.
+    result = kelvin_top_->ReadMemory(test_access_address[i], mem_bytes_return,
+                                     sizeof(mem_bytes_return));
+    EXPECT_OK(result);
+    EXPECT_EQ(result.value(), expected_length);
+    for (int i = 0; i < result.value(); ++i) {
+      EXPECT_EQ(mem_bytes[i], mem_bytes_return[i]);
+    }
+    memset(mem_bytes_return, 0, sizeof(mem_bytes_return));
+  }
+}
+
 }  // namespace