This adds a model for the RiscV PLIC (Platform level interrupt controller).
  - The PLIC is configurable with respect to number of interrupt sources
    and target contexts.
  - The PLIC supports both level and edge triggered interrupts.

This allows the PLIC to be modeled along side the RiscV core.

PiperOrigin-RevId: 709103612
Change-Id: I0178b84d6416cbfe0f7d18952cd36ad692ef06cb
diff --git a/riscv/BUILD b/riscv/BUILD
index fd00819..5167147 100644
--- a/riscv/BUILD
+++ b/riscv/BUILD
@@ -1208,6 +1208,27 @@
     ],
 )
 
+cc_library(
+    name = "riscv_plic",
+    srcs = [
+        "riscv_plic.cc",
+    ],
+    hdrs = [
+        "riscv_plic.h",
+    ],
+    deps = [
+        "@com_google_absl//absl/container:btree",
+        "@com_google_absl//absl/log",
+        "@com_google_absl//absl/numeric:bits",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_mpact-sim//mpact/sim/generic:core",
+        "@com_google_mpact-sim//mpact/sim/generic:instruction",
+        "@com_google_mpact-sim//mpact/sim/util/memory",
+        "@com_googlesource_code_re2//:re2",
+    ],
+)
+
 cc_binary(
     name = "rv32g_test_sim",
     srcs = [
diff --git a/riscv/riscv_clint.h b/riscv/riscv_clint.h
index 468b8e6..9226c0b 100644
--- a/riscv/riscv_clint.h
+++ b/riscv/riscv_clint.h
@@ -18,6 +18,7 @@
 #include <cstdint>
 
 #include "mpact/sim/generic/counters.h"
+#include "mpact/sim/generic/counters_base.h"
 #include "mpact/sim/generic/data_buffer.h"
 #include "mpact/sim/generic/instruction.h"
 #include "mpact/sim/generic/ref_count.h"
@@ -117,4 +118,4 @@
 }  // namespace sim
 }  // namespace mpact
 
-#endif  // THIRD_PARTY_MPACT_RISCV_RISCV_CLINT_H_
\ No newline at end of file
+#endif  // THIRD_PARTY_MPACT_RISCV_RISCV_CLINT_H_
diff --git a/riscv/riscv_plic.cc b/riscv/riscv_plic.cc
new file mode 100644
index 0000000..10fb89c
--- /dev/null
+++ b/riscv/riscv_plic.cc
@@ -0,0 +1,587 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "riscv/riscv_plic.h"
+
+#include <cstdint>
+#include <cstring>
+
+#include "absl/log/log.h"
+#include "absl/numeric/bits.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "re2/re2.h"
+
+namespace mpact {
+namespace sim {
+namespace riscv {
+
+RiscVPlic::RiscVPlic(int num_sources, int num_contexts)
+    : num_sources_(num_sources), num_contexts_(num_contexts) {
+  // Initialize the gateway info.
+  gateway_info_ = new GatewayInfo[num_sources_];
+  // Initialize the context interface.
+  context_if_ = new RiscVPlicIrqInterface *[num_contexts_];
+  context_irq_ = new bool[num_contexts_];
+  for (int i = 0; i < num_contexts_; ++i) {
+    context_if_[i] = nullptr;
+    context_irq_[i] = false;
+  }
+  // Initialize the interrupt priority.
+  interrupt_priority_ = new uint32_t[num_sources_];
+  std::memset(interrupt_priority_, 0, sizeof(uint32_t) * num_sources_);
+  // Initialize the interrupt pending bits.
+  interrupt_pending_ = new uint32_t[num_sources_ / 32 + 1];
+  std::memset(interrupt_pending_, 0,
+              sizeof(uint32_t) * (num_sources_ / 32 + 1));
+  // Initialize the interrupt enabled bits.
+  interrupt_enabled_ = new uint32_t *[num_contexts_];
+  for (int i = 0; i < num_contexts_; ++i) {
+    interrupt_enabled_[i] = new uint32_t[num_sources_ / 32 + 1];
+    std::memset(interrupt_enabled_[i], 0,
+                sizeof(uint32_t) * (num_sources_ / 32 + 1));
+  }
+  // Initialize the priority threshold.
+  priority_threshold_ = new uint32_t[num_contexts_];
+  std::memset(priority_threshold_, 0, sizeof(uint32_t) * num_contexts_);
+  // Initialize the interrupt claim/complete bits.
+  interrupt_claim_complete_ = new uint32_t[num_contexts_];
+  std::memset(interrupt_claim_complete_, 0, sizeof(uint32_t) * num_contexts_);
+}
+
+RiscVPlic::~RiscVPlic() {
+  // Clean up all the allocated memory.
+  delete[] gateway_info_;
+  gateway_info_ = nullptr;
+  delete[] context_if_;
+  context_if_ = nullptr;
+  delete[] context_irq_;
+  context_irq_ = nullptr;
+  delete[] interrupt_priority_;
+  interrupt_priority_ = nullptr;
+  delete[] interrupt_pending_;
+  interrupt_pending_ = nullptr;
+  for (int i = 0; i < num_contexts_; ++i) {
+    delete[] interrupt_enabled_[i];
+  }
+  delete[] interrupt_enabled_;
+  interrupt_enabled_ = nullptr;
+  delete[] priority_threshold_;
+  priority_threshold_ = nullptr;
+  delete[] interrupt_claim_complete_;
+  interrupt_claim_complete_ = nullptr;
+}
+
+absl::Status RiscVPlic::Configure(absl::string_view source_cfg,
+                                  absl::string_view context_cfg) {
+  // List of "<source>=<priority>;" items.
+  RE2 re_source("^(\\d+)\\s*=\\s*(\\d+)\\s*(;|$)");
+  int source;
+  int priority;
+  absl::string_view cfg = source_cfg;
+  while (RE2::Consume(&cfg, re_source, &source, &priority)) {
+    if (source >= num_sources_) {
+      return absl::InvalidArgumentError(
+          absl::StrCat("Invalid source number: ", source));
+    }
+    interrupt_priority_[source] = priority;
+  }
+  if (!cfg.empty()) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid source configuration: ", cfg));
+  }
+  // List of "<context>=<source>,<threshold>,<enable>;" items.
+  RE2 re_context("^(\\d+)\\s*=\\s*(\\d+)\\s*,\\s*");
+  RE2 re_context_source("(\\d+)\\s*,\\s*(\\d)\\s*(,|;)\\s*");
+  int context;
+  int threshold;
+  int enable;
+  char terminator;
+  cfg = context_cfg;
+  while (RE2::Consume(&cfg, re_context, &context, &threshold)) {
+    if (context >= num_contexts_) {
+      return absl::InvalidArgumentError(
+          absl::StrCat("Invalid context number: ", context));
+    }
+    priority_threshold_[context] = threshold;
+    while (
+        RE2::Consume(&cfg, re_context_source, &source, &enable, &terminator)) {
+      if (source >= num_sources_) {
+        return absl::InvalidArgumentError(
+            absl::StrCat("Invalid source number: ", source));
+      }
+      source_to_context_.insert({source, context});
+      context_to_source_.insert({context, source});
+      int bit = source & 0x1f;
+      int word = source / 32;
+      uint32_t mask = ~(1 << bit);
+      uint32_t value = interrupt_enabled_[context][word];
+      value = (value & mask) | (enable << bit);
+      interrupt_enabled_[context][word] = value;
+      if (terminator == ';') break;
+    }
+  }
+  if (!cfg.empty()) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Invalid context configuration: ", cfg));
+  }
+  return absl::OkStatus();
+}
+
+void RiscVPlic::SetInterrupt(int source, bool value, bool is_level) {
+  // Make sure the source is in range.
+  if ((source < 0) || (source >= num_sources_)) {
+    LOG(WARNING) << "Invalid interrupt source: " << source;
+    return;
+  }
+  // No action for clearing a non-level based interrupt.
+  if (!value && !is_level) return;
+
+  auto &info = gateway_info_[source];
+  if (!info.ready || !value) {
+    if (is_level) info.pending = value;
+    return;
+  }
+
+  // If it is level based, latch the pending bit in the gateway.
+  if (is_level) info.pending = true;
+
+  // If the priority is 0, the interrupt is disabled.
+  if (interrupt_priority_[source] == 0) return;
+
+  // Accept the request, set ready to false to prevent any other requests
+  // until this has been processed.
+  info.ready = false;
+  // Set the plic pending bit.
+  SetPlicPendingInterrupt(source);
+}
+
+void RiscVPlic::SetPlicPendingInterrupt(int source) {
+  if ((source <= 0) || (source >= num_sources_)) {
+    LOG(ERROR) << "Invalid interrupt source: " << source;
+    return;
+  }
+  // Return if source is already pending.
+  if (IsPending(source)) return;
+  // Get interrupt priority.
+  auto priority = interrupt_priority_[source];
+  // Return if source has priority 0.
+  if (priority == 0) return;
+  // Set source to pending.
+  SetPending(source, true);
+  // Iterate over all contexts that have this source enabled.
+  auto [begin, end] = source_to_context_.equal_range(source);
+  for (auto it = begin; it != end; ++it) {
+    auto context = it->second;
+    // If the priority is less or equal to the threshold, do not trigger the
+    // interrupt.
+    if (priority <= priority_threshold_[context]) continue;
+    if (context_if_[context] == nullptr) continue;
+    // Trigger the interrupt.
+    context_if_[context]->SetIrq(true);
+    context_irq_[context] = true;
+  }
+}
+
+// Implementation of the memory load interface for reading memory mapped
+// registers.
+void RiscVPlic::Load(uint64_t address, DataBuffer *db, Instruction *inst,
+                     ReferenceCount *context) {
+  uint32_t offset = address & 0xff'ffff;
+  switch (db->size<uint8_t>()) {
+    case 1:
+      db->Set<uint8_t>(0, static_cast<uint8_t>(Read(offset)));
+      break;
+    case 2:
+      db->Set<uint16_t>(0, static_cast<uint16_t>(Read(offset)));
+      break;
+    case 4:
+      db->Set<uint32_t>(0, static_cast<uint32_t>(Read(offset)));
+      break;
+    case 8:
+      db->Set<uint32_t>(0, static_cast<uint32_t>(Read(offset)));
+      db->Set<uint32_t>(1, static_cast<uint32_t>(Read(offset + 4)));
+      break;
+    default:
+      ::memset(db->raw_ptr(), 0, sizeof(db->size<uint8_t>()));
+      break;
+  }
+  // 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);
+    }
+  }
+}
+
+// No support for vector loads.
+void RiscVPlic::Load(DataBuffer *address_db, DataBuffer *mask_db, int el_size,
+                     DataBuffer *db, Instruction *inst,
+                     ReferenceCount *context) {
+  LOG(FATAL) << "RiscVPlic does not support vector loads";
+}
+
+// Implementation of memory store interface to support writes to memory mapped
+// registers.
+void RiscVPlic::Store(uint64_t address, DataBuffer *db) {
+  uint32_t offset = address & 0xff'ffff;
+  switch (db->size<uint8_t>()) {
+    case 1:
+      return Write(offset, static_cast<uint32_t>(db->Get<uint8_t>(0)));
+    case 2:
+      return Write(offset, static_cast<uint32_t>(db->Get<uint16_t>(0)));
+    case 4:
+      return Write(offset, static_cast<uint32_t>(db->Get<uint32_t>(0)));
+    case 8:
+      return Write(offset, static_cast<uint32_t>(db->Get<uint32_t>(0)));
+      return Write(offset + 4, static_cast<uint32_t>(db->Get<uint32_t>(1)));
+    default:
+      return;
+  }
+}
+
+void RiscVPlic::SetContext(int context_no, RiscVPlicIrqInterface *context_if) {
+  context_if_[context_no] = context_if;
+}
+
+// No support for vector stores.
+void RiscVPlic::Store(DataBuffer *address, DataBuffer *mask, int el_size,
+                      DataBuffer *db) {
+  LOG(FATAL) << "RiscVPlic does not support vector stores";
+}
+
+uint32_t RiscVPlic::Read(uint32_t offset) {
+  uint32_t value = 0;
+  // Interrupt priority bit by source.
+  if (offset < 0x00'1000) {
+    value = interrupt_priority_[offset >> 2];
+    return value;
+  }
+
+  // Interrupt pending bits by source/
+  if (offset < 0x00'2000) {
+    offset -= 0x00'1000;
+    if (offset > (num_sources_ / 32)) {
+      LOG(WARNING) << "Invalid offset: " << offset;
+      return 0;
+    }
+    value = interrupt_pending_[offset];
+    return value;
+  }
+
+  // Interrupt enable bits for sources by context.
+  if (offset < 0x20'0000) {
+    offset -= 0x00'2000;
+    int context = offset >> 7;
+    // Get the context id.
+    if (context >= num_contexts_) {
+      LOG(ERROR) << "Invalid context number: " << context;
+      return 0;
+    }
+    // Get the word index in the source dimension.
+    int word = offset & 0x7f;
+    if (word * 32 >= num_sources_ + 32) {
+      LOG(ERROR) << "Invalid source number";
+      return 0;
+    }
+    value = interrupt_enabled_[context][word];
+    return value;
+  }
+
+  // Interrupt priority threshold and claim/complete.
+  // Get context id.
+  offset -= 0x20'0000;
+  int context = offset >> 12;
+  if (context >= num_contexts_) {
+    LOG(ERROR) << "Invalid context number: " << context;
+    return 0;
+  }
+  auto reg_id = offset & 0xfff;
+  switch (reg_id) {
+    case 0x0:
+      // Priority threshold.
+      value = priority_threshold_[context];
+      return value;
+    case 0x4:
+      // Claim/complete interrupt.
+      value = ClaimInterrupt(context);
+      return value;
+    default:
+      LOG(ERROR) << "Invalid offset: " << offset;
+      break;
+  }
+  return value;
+}
+
+void RiscVPlic::Write(uint32_t offset, uint32_t value) {
+  // Interrupt priority bit by source.
+  if (offset < 0x00'1000) {
+    int source = offset >> 3;
+    if (source >= num_sources_) {
+      LOG(ERROR) << "Invalid source number: " << source;
+      return;
+    }
+    uint32_t prev = interrupt_priority_[source];
+    interrupt_priority_[source] = value;
+    // If the priority is being changed from 0 to non-zero, see if there is a
+    // pending level based interrupt, and if so, set the plic pending bit.
+    if (prev == 0 && value != 0) {
+      auto &info = gateway_info_[source];
+      if (info.ready && info.pending) {
+        SetPlicPendingInterrupt(source);
+      }
+    }
+    return;
+  }
+
+  // Interrupt pending bits.
+  if (offset < 0x00'2000) {
+    offset -= 0x00'1000;
+    if (offset > (num_sources_ / 32)) {
+      LOG(WARNING) << "Invalid offset: " << offset;
+      return;
+    }
+    uint32_t prev = interrupt_pending_[offset];
+    interrupt_pending_[offset] = value;
+    // Determine which bits are being set.
+    uint32_t bits_set = (value ^ prev) & value;
+    // Trigger interrupts for any of the newly set pending bits.
+    while (bits_set != 0) {
+      int bit = absl::countr_zero(bits_set);
+      int source = offset * 32 + bit;
+      bits_set &= ~(1 << bit);
+      auto [begin, end] = source_to_context_.equal_range(source);
+      for (auto it = begin; it != end; ++it) {
+        auto context = it->second;
+        // If the context IRQ line is not already set, see if the priority is
+        // above the threshold, and if so, set the IRQ line.
+        if (!context_irq_[context]) {
+          // If the priority is less or equal to the threshold, do not trigger
+          // the interrupt.
+          if (interrupt_priority_[source] <= priority_threshold_[context])
+            continue;
+          if (context_if_[context] == nullptr) {
+            LOG(ERROR) << "No context interface for context " << context;
+            continue;
+          }
+          // Trigger the interrupt.
+          context_if_[context]->SetIrq(true);
+          context_irq_[context] = true;
+        }
+      }
+    }
+    return;
+  }
+
+  // Interrupt enable bits for sources by context.
+  if (offset < 0x20'0000) {
+    offset -= 0x00'2000;
+    // Convert from word address to index.
+    offset >>= 2;
+    // Get the word index in the source dimension.
+    int word = (offset >> 2) & 0x1f;
+    // Get the context id.
+    int context = offset >> 7;
+    uint32_t prev = interrupt_enabled_[context][word];
+    // Determine which bits are being set.
+    uint32_t bits_set = (value ^ prev) & value;
+    uint32_t bits_cleared = (value ^ prev) & ~value;
+    // Update the enable bits.
+    interrupt_enabled_[context][word] = value;
+    // Iterate over the set bits and add them to the source/context maps.
+    while (bits_set != 0) {
+      int bit = absl::countr_zero(bits_set);
+      int source = word * 32 + bit;
+      source_to_context_.insert({source, context});
+      context_to_source_.insert({context, source});
+      bits_set &= ~(1 << bit);
+      if (context_if_[context] == nullptr) {
+        LOG(ERROR) << "No context interface for context " << context;
+        continue;
+      }
+      // If there is no pending IRQ for this context, and the priority is
+      // above the threshold, set the IRQ.
+      if (!context_irq_[context]) {
+        if (interrupt_priority_[source] > priority_threshold_[context]) {
+          context_irq_[context] = true;
+          context_if_[context]->SetIrq(true);
+        }
+      }
+    }
+    // Iterate over the cleared bits and erase them from the source/context
+    // maps.
+    while (bits_cleared != 0) {
+      int bit = absl::countr_zero(bits_cleared);
+      int source = word * 32 + bit;
+      auto [s2c_begin, s2c_end] = source_to_context_.equal_range(source);
+      for (auto s2c_it = s2c_begin; s2c_it != s2c_end; ++s2c_it) {
+        if (s2c_it->second == context) {
+          source_to_context_.erase(s2c_it);
+          break;
+        }
+      }
+      auto [c2s_begin, c2s_end] = context_to_source_.equal_range(context);
+      for (auto c2s_it = c2s_begin; c2s_it != c2s_end; ++c2s_it) {
+        if (c2s_it->second == source) {
+          context_to_source_.erase(c2s_it);
+          break;
+        }
+      }
+      bits_cleared &= ~(1 << bit);
+    }
+    return;
+  }
+
+  offset -= 0x20'0000;
+  // Interrupt priority threshold and claim/complete.
+  // Get context id.
+  int context = offset >> 12;
+  if (context >= num_contexts_) {
+    LOG(ERROR) << "Invalid context number: " << context;
+    return;
+  }
+  auto reg_id = offset & 0xfff;
+  switch (reg_id) {
+    case 0x0: {
+      // Priority threshold.
+      uint32_t prev = priority_threshold_[context];
+      priority_threshold_[context] = value;
+      if (value < prev) {
+        // If the priority threshold is being lowered and there is no active
+        // interrupt, see if there is a pending interrupt for this context,
+        // and if so, set the IRQ.
+        if (!context_irq_[context]) {
+          if (context_if_[context] == nullptr) {
+            LOG(ERROR) << "No context interface for context " << context;
+            return;
+          }
+          auto [begin, end] = context_to_source_.equal_range(context);
+          for (auto it = begin; it != end; ++it) {
+            if (interrupt_priority_[it->second] > value) {
+              context_irq_[context] = true;
+              context_if_[context]->SetIrq(true);
+              break;
+            }
+          }
+        }
+      }
+      return;
+    }
+    case 0x4:
+      // Claim/complete interrupt.
+      CompleteInterrupt(context, value);
+      return;
+    default:
+      LOG(ERROR) << "Invalid offset: " << offset;
+      break;
+  }
+}
+
+uint32_t RiscVPlic::ClaimInterrupt(int context) {
+  if (context < 0 || context >= num_contexts_) return 0;
+  uint32_t id = 0;
+  int priority = 0;
+  // Find the id of the highest-priority pending interrupt for this context.
+  auto [begin, end] = context_to_source_.equal_range(context);
+  int count = 0;
+  int source = 0;
+  for (auto it = begin; it != end; ++it) {
+    source = it->second;
+    // If the source is not pending, skip it.
+    if (!IsPending(source)) continue;
+    // If the priority is less or equal to the current priority, skip it.
+    if (priority > interrupt_priority_[source]) continue;
+    // If the priority is the same, the lower id is chosen, so if the new
+    // source is greater than the current, go to the next.
+    if ((priority == interrupt_priority_[source]) && (id < source)) continue;
+    id = source;
+    priority = interrupt_priority_[source];
+    count++;
+  }
+  if (id != 0) {
+    SetPending(id, false);
+    interrupt_claim_complete_[context] = id;
+    count--;
+  }
+  if (count == 0) {
+    // If there are zero remaining pending interrupts, clear the IRQ line.
+    context_if_[context]->SetIrq(false);
+    context_irq_[context] = false;
+  }
+  return id;
+}
+
+void RiscVPlic::CompleteInterrupt(int context, uint32_t id) {
+  if (context < 0 || context >= num_contexts_) return;
+  // Check if id is in the set of enabled sources for context.
+  auto [begin, end] = context_to_source_.equal_range(context);
+  bool found = false;
+  for (auto it = begin; it != end; ++it) {
+    if (it->second == id) {
+      found = true;
+      break;
+    }
+  }
+  if (!found) return;
+  // The PLIC spec only requires that the id be valid for the set of
+  // interrupts enabled for the context, not that it matches the
+  // interrupt_claim_complete_ value.
+  auto source = interrupt_claim_complete_[context];
+  interrupt_claim_complete_[context] = 0;
+  auto &info = gateway_info_[source];
+  // Check to see if there's a pending level based interrupt w priority > 0.
+  if (info.pending && (interrupt_priority_[source] > 0)) {
+    // Set the plic pending bit but no need to set the ready bit as this will
+    // be forwarded to the plic core right away.
+    SetPlicPendingInterrupt(source);
+    return;
+  }
+  // Set the gateway ready bit to true.
+  info.ready = true;
+}
+
+void RiscVPlic::SetPending(int source, bool value) {
+  int word = source >> 5;
+  int bit = source & 0x1f;
+  if (value) {
+    interrupt_pending_[word] |= 1 << bit;
+  } else {
+    interrupt_pending_[word] &= ~(1 << bit);
+  }
+}
+
+bool RiscVPlic::IsPending(int source) {
+  int word = source >> 5;
+  int bit = source & 0x1f;
+  return (interrupt_pending_[word] & (1 << bit)) != 0;
+}
+
+RiscVPlicSourceInterface::RiscVPlicSourceInterface(RiscVPlic *plic, int source,
+                                                   bool is_level)
+    : plic_(plic), source_(source), is_level_(is_level) {}
+
+}  // namespace riscv
+}  // namespace sim
+}  // namespace mpact
diff --git a/riscv/riscv_plic.h b/riscv/riscv_plic.h
new file mode 100644
index 0000000..8d8745f
--- /dev/null
+++ b/riscv/riscv_plic.h
@@ -0,0 +1,173 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef THIRD_PARTY_MPACT_RISCV_RISCV_PLIC_H_
+#define THIRD_PARTY_MPACT_RISCV_RISCV_PLIC_H_
+
+#include <cstdint>
+
+#include "absl/container/btree_map.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#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"
+
+// This file implements the RiscV PLIC (Platform Level Interrupt Controller).
+// It has a memory mapped register interface that is used to control the
+// interrupts to one or more contexts across one or more RiscV cores.
+//
+// The interrupt targets (contexts) are notified using the
+// RiscVPlicIrqInterface interface. Therefore each target much register its
+// own instance of this interface with the plic using the SetContext method.
+//
+// Interrupt sources communicate with the PLIC using the SetInterrupt method.
+//
+// The PLIC supports both level and edge triggered interrupts.
+
+namespace mpact {
+namespace sim {
+namespace riscv {
+
+using ::mpact::sim::generic::DataBuffer;
+using ::mpact::sim::generic::Instruction;
+using ::mpact::sim::generic::ReferenceCount;
+
+using ::mpact::sim::util::MemoryInterface;
+
+class RiscVPlicIrqInterface {
+ public:
+  virtual ~RiscVPlicIrqInterface() = default;
+  virtual void SetIrq(bool irq_value) = 0;
+};
+
+class RiscVPlic : public MemoryInterface {
+ public:
+  // The constructor takes the number of interrupt sources and the number of
+  // contexts that the PLIC can send interrupts to. A core usually has multiple
+  // contexts, one for each privilege level (machine, supervisor, etc.) that is
+  // capable of receiving and handling interrupts.
+  RiscVPlic(int num_sources, int num_contexts);
+  RiscVPlic() = delete;
+  RiscVPlic(const RiscVPlic &) = delete;
+  RiscVPlic &operator=(const RiscVPlic &) = delete;
+  ~RiscVPlic() override;
+
+  // Configure the PLIC state according to the source and context configuration
+  // strings. The source configuration string is a semicolon separated list of
+  // <source>=<priority> items, where <source> is the interrupt source number
+  // and <priority> is an non-negative integer priority value. Higher values
+  // have higher priorities. A value of zero disables the source. Any source
+  // that is not configured is disabled by default. However, MMR writes can
+  // change the configured values.
+  //
+  // The context configuration string is a semicolon separated list of
+  // <context>=<threshold>,(<source>,<enable>)+ items, where <context> is the
+  // context number, <source> is the source number, <threshold> is a non
+  // negative integer that is the priority threshold for that context, and
+  // <enable> is a 0 or 1 value that indicates whether the interrupt source is
+  // enabled for that context. Multiple lines for the same context with
+  // different sources is allowed. Any context not configured is assumed to
+  // disable all sources with a zero priority threshold.
+  absl::Status Configure(absl::string_view source_cfg,
+                         absl::string_view context_cfg);
+  // Interrupt request from the given interrupt source.
+  void SetInterrupt(int source, bool value, bool is_level);
+
+  // MemoryInterface overrides.
+  // Non-vector load method.
+  void Load(uint64_t address, DataBuffer *db, Instruction *inst,
+            ReferenceCount *context) override;
+  // Vector load method - this is stubbed out.
+  void Load(DataBuffer *address_db, DataBuffer *mask_db, int el_size,
+            DataBuffer *db, Instruction *inst,
+            ReferenceCount *context) override;
+  // Non-vector store method.
+  void Store(uint64_t address, DataBuffer *db) override;
+  // Vector store method - this is stubbed out.
+  void Store(DataBuffer *address, DataBuffer *mask, int el_size,
+             DataBuffer *db) override;
+
+  void SetContext(int context_no, RiscVPlicIrqInterface *context_if);
+
+ private:
+  struct GatewayInfo {
+    // The gateway is able to accept the interrupt and send it to the plic core.
+    bool ready;
+    // Pending bit in gateway. Only gets set if the interrupt is level based.
+    bool pending;
+    GatewayInfo() : ready(true), pending(false) {}
+  };
+
+  // MMR read/write methods.
+  uint32_t Read(uint32_t offset);
+  void Write(uint32_t offset, uint32_t value);
+  // Interrupt claim.
+  uint32_t ClaimInterrupt(int context);
+  // Signal interrupt completion for the given context and interrupt id.
+  void CompleteInterrupt(int context, uint32_t id);
+  // Set plic core pending interrupt bit and trigger interrupt to context as
+  // needed.
+  void SetPlicPendingInterrupt(int source);
+  // Handling pending bits.
+  void SetPending(int source, bool value);
+  bool IsPending(int source);
+
+  int num_sources_;
+  int num_contexts_;
+  // Interface to call to write the IRQ line for a context.
+  RiscVPlicIrqInterface **context_if_;
+  // Last value written to the IRQ line for a context.
+  bool *context_irq_ = nullptr;
+  // Source gateway info.
+  GatewayInfo *gateway_info_ = nullptr;
+  // Interrupt priorities by source.
+  uint32_t *interrupt_priority_ = nullptr;
+  // Pending interrupts by source - 32 bits per word.
+  uint32_t *interrupt_pending_ = nullptr;
+  // Enable bits per context per source - 32 bits per word.
+  // Array is organized as interrupt_enabled_[context][source / 32].
+  uint32_t **interrupt_enabled_ = nullptr;
+  // Priority threshold by context.
+  uint32_t *priority_threshold_ = nullptr;
+  // Interrupt claim/complete register by context.
+  uint32_t *interrupt_claim_complete_ = nullptr;
+  // Map from source to context that has the source enabled. This must be
+  // updated whenever an an enable bit is changed for a context.
+  absl::btree_multimap<int, int> source_to_context_;
+  // Map from context to source for which the context has the source enabled.
+  absl::btree_multimap<int, int> context_to_source_;
+};
+
+class RiscVPlicSourceInterface : public RiscVPlicIrqInterface {
+ public:
+  RiscVPlicSourceInterface(RiscVPlic *plic, int source, bool is_level);
+  RiscVPlicSourceInterface() = delete;
+  ~RiscVPlicSourceInterface() override = default;
+  void SetIrq(bool irq_value) override {
+    if (plic_ != nullptr) plic_->SetInterrupt(source_, irq_value, is_level_);
+  };
+
+ private:
+  RiscVPlic *plic_ = nullptr;
+  int source_ = 0;
+  bool is_level_ = false;
+};
+
+}  // namespace riscv
+}  // namespace sim
+}  // namespace mpact
+
+#endif  // THIRD_PARTY_MPACT_RISCV_RISCV_PLIC_H_
diff --git a/riscv/test/BUILD b/riscv/test/BUILD
index 82910b6..ea7a94d 100644
--- a/riscv/test/BUILD
+++ b/riscv/test/BUILD
@@ -727,6 +727,21 @@
 )
 
 cc_test(
+    name = "riscv_plic_test",
+    size = "small",
+    srcs = [
+        "riscv_plic_test.cc",
+    ],
+    deps = [
+        "//riscv:riscv_plic",
+        "@com_google_absl//absl/log:check",
+        "@com_google_googletest//:gtest_main",
+        "@com_google_mpact-sim//mpact/sim/generic:core",
+        "@com_googlesource_code_re2//:re2",
+    ],
+)
+
+cc_test(
     name = "librenode_mpact_riscv32.so_test",
     size = "small",
     srcs = ["librenode_mpact_riscv32_so_test.cc"],
diff --git a/riscv/test/riscv_plic_test.cc b/riscv/test/riscv_plic_test.cc
new file mode 100644
index 0000000..62c150c
--- /dev/null
+++ b/riscv/test/riscv_plic_test.cc
@@ -0,0 +1,315 @@
+
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "riscv/riscv_plic.h"
+
+#include <cstdint>
+
+#include "absl/log/check.h"
+#include "googlemock/include/gmock/gmock.h"
+#include "mpact/sim/generic/data_buffer.h"
+
+// This file contains unit tests for the RiscV PLIC model.
+
+namespace {
+
+using ::mpact::sim::generic::DataBuffer;
+using ::mpact::sim::generic::DataBufferFactory;
+using ::mpact::sim::riscv::RiscVPlic;
+using ::mpact::sim::riscv::RiscVPlicIrqInterface;
+
+constexpr int kNumSources = 32;
+constexpr int kNumContexts = 3;
+
+// Mock interrupt target for a context.
+class MockRiscVInterruptTarget : public RiscVPlicIrqInterface {
+ public:
+  void SetIrq(bool irq_value) override { irq_value_ = irq_value; }
+
+  bool irq_value() const { return irq_value_; }
+  void set_irq_value(bool value) { irq_value_ = value; }
+
+ private:
+  bool irq_value_ = false;
+};
+
+// Test fixture.
+class RiscVPlicTest : public ::testing::Test {
+ protected:
+  RiscVPlicTest() {
+    plic_ = new RiscVPlic(kNumSources, kNumContexts);
+    for (int i = 0; i < kNumContexts; ++i) {
+      target_[i] = new MockRiscVInterruptTarget();
+      plic_->SetContext(i, target_[i]);
+    }
+    db_ = db_factory_.Allocate<uint32_t>(1);
+    db_->set_latency(0);
+  }
+
+  ~RiscVPlicTest() override {
+    delete plic_;
+    for (int i = 0; i < kNumContexts; ++i) {
+      delete target_[i];
+    }
+    db_->DecRef();
+  }
+
+  // Convenience methods to read/write the PLIC state using its memory
+  // interface.
+  bool GetEnable(int source, int context) {
+    uint32_t word = 0x2000 + (context * 0x80) + (source >> 5);
+    int bit = source & 0x1f;
+    plic()->Load(word, db_, nullptr, nullptr);
+    return (db_->Get<uint32_t>(0) & (1 << bit)) != 0;
+  }
+
+  void SetEnable(int source, int context, bool value) {
+    uint32_t word = 0x2000 + (context * 0x80) + (source >> 5);
+    int bit = source & 0x1f;
+    plic()->Load(word, db_, nullptr, nullptr);
+    auto span = db_->Get<uint32_t>();
+    uint32_t mask = ~(1 << bit);
+    uint32_t u_val = static_cast<uint32_t>(value);
+    span[0] = (span[0] & mask) | (u_val << bit);
+    plic()->Store(word, db_);
+  }
+
+  bool GetPending(int source) {
+    uint32_t word = 0x1000 + ((source >> 5) << 2);
+    int bit = source & 0x1f;
+    plic()->Load(word, db_, nullptr, nullptr);
+    return (db_->Get<uint32_t>(0) & (1 << bit)) != 0;
+  }
+
+  void SetPending(int source, bool value) {
+    int word = 0x1000 + ((source >> 5) << 2);
+    int bit = source & 0x1f;
+    plic()->Load(word, db_, nullptr, nullptr);
+    auto span = db_->Get<uint32_t>();
+    uint32_t mask = ~(1 << bit);
+    span[0] = (span[0] & mask) | (static_cast<uint32_t>(value) << bit);
+    plic()->Store(word, db_);
+  }
+
+  uint32_t GetPriority(int source) {
+    plic()->Load(source << 2, db_, nullptr, nullptr);
+    return db_->Get<uint32_t>(0);
+  }
+
+  void SetPriority(int source, uint32_t priority) {
+    db_->Set<uint32_t>(0, priority);
+    plic()->Store(source << 2, db_);
+  }
+
+  int GetPriorityThreshold(int context) {
+    plic()->Load(0x20'0000 + context * 0x1000, db_, nullptr, nullptr);
+    return db_->Get<uint32_t>(0);
+  }
+
+  void SetPriorityThreshold(int context, uint32_t threshold) {
+    db_->Set<uint32_t>(0, threshold);
+    plic()->Store(0x20'0000 + context * 0x1000, db_);
+  }
+
+  uint32_t GetInterruptClaim(int context) {
+    plic()->Load(0x20'0000 + context * 0x1000 + 4, db_, nullptr, nullptr);
+    return db_->Get<uint32_t>(0);
+  }
+
+  void SetInterruptClaim(int context, uint32_t claim) {
+    db_->Set<uint32_t>(0, claim);
+    plic()->Store(0x20'0000 + context * 0x1000 + 4, db_);
+  }
+
+  void SetDefaultConfig() {
+    // Set priorities to increasing values for all sources.
+    // Thresholds are set 8, 16 and 24 for the three contexts respectively.
+    // Source 30 is enabled for all contexts.
+    auto status = plic()->Configure(
+        "0=0;1=1;2=2;3=3;4=4;5=5;6=6;7=7;8=8;9=9;10=10;11=11;"
+        "12=12;13=13;14=14;15=15;16=16;17=17;18=18;19=19;20=20;"
+        "21=21;22=22;23=23;24=24;25=25;26=26;27=27;28=28;29=29;"
+        "30=30;31=31;",
+        "0=8,1,1,2,1,3,1,4,1,5,1,6,1,7,1,8,1,9,1,10,1,30,1;"
+        "1=16,11,1,12,1,13,1,14,1,15,1,16,1,17,1,18,1,19,1,20,1,30,1;"
+        "2=24,19,1,20,1,21,1,22,1,23,1,24,1,25,1,26,1,27,1,28,1,29,1,30,1,31,"
+        "1;");
+    CHECK_OK(status);
+  }
+
+  // Accessors.
+  RiscVPlic *plic() { return plic_; }
+  DataBufferFactory &db_factory() { return db_factory_; }
+  MockRiscVInterruptTarget *target(int i) { return target_[i]; }
+
+ private:
+  DataBuffer *db_;
+  DataBufferFactory db_factory_;
+  RiscVPlic *plic_ = nullptr;
+  MockRiscVInterruptTarget *target_[kNumContexts];
+};
+
+// Test that the initial state of the PLIC is as expected. Nothing enabled,
+// priorities at zero, priority thresholds at zero, no pending interrupts,
+// no claimed interrupts, no IRQ lines set.
+TEST_F(RiscVPlicTest, InitialState) {
+  for (int s = 0; s < kNumSources; ++s) {
+    for (int c = 0; c < kNumContexts; ++c) {
+      EXPECT_FALSE(GetEnable(s, c));
+      EXPECT_FALSE(GetPending(s));
+      EXPECT_EQ(GetPriority(s), 0) << "Source " << s;
+      EXPECT_EQ(GetPriorityThreshold(c), 0);
+      EXPECT_EQ(GetInterruptClaim(c), 0);
+      EXPECT_FALSE(target(c)->irq_value());
+    }
+  }
+}
+
+// Verify that the PLIC is configured as expected with the default configuration
+// strings.
+TEST_F(RiscVPlicTest, DefaultConfiguration) {
+  SetDefaultConfig();
+  for (int s = 0; s < kNumSources; ++s) {
+    for (int c = 0; c < kNumContexts; ++c) {
+      switch (c) {
+        case 0:
+          EXPECT_EQ(GetEnable(s, c), ((s >= 1) && (s <= 10)) || (s == 30))
+              << "Source " << s << " context " << c;
+          break;
+        case 1:
+          EXPECT_EQ(GetEnable(s, c), ((s >= 11) && (s <= 20)) || (s == 30))
+              << "Source " << s << " context " << c;
+          break;
+        case 2:
+          EXPECT_EQ(GetEnable(s, c), (s >= 19) && (s <= 31))
+              << "Source " << s << " context " << c;
+          break;
+      }
+      EXPECT_FALSE(GetPending(s));
+      EXPECT_EQ(GetPriority(s), s) << "Source " << s;
+      EXPECT_EQ(GetPriorityThreshold(c), (c + 1) * 8) << "Context " << c;
+      EXPECT_EQ(GetInterruptClaim(c), 0) << "Context " << c;
+      EXPECT_FALSE(target(c)->irq_value()) << "Context " << c;
+    }
+  }
+}
+
+// Test operation of level triggered interrupts.
+TEST_F(RiscVPlicTest, LevelTriggeredInterrupt) {
+  SetDefaultConfig();
+  // Set irq for source 10 level sensitive.
+  plic()->SetInterrupt(10, /*value=*/true, /*is_level=*/true);
+  // Expect pending to be set.
+  EXPECT_TRUE(GetPending(10));
+  // Clear the irq.
+  plic()->SetInterrupt(10, /*value=*/false, /*is_level=*/true);
+  // Expect pending to still be set.
+  EXPECT_TRUE(GetPending(10));
+  // Expect irq to be set for context 0;
+  EXPECT_TRUE(target(0)->irq_value());
+  // Clear the irq.
+  target(0)->set_irq_value(false);
+  // Now raise the irq again.
+  plic()->SetInterrupt(10, /*value=*/true, /*is_level=*/true);
+  // Expect pending to be set.
+  EXPECT_TRUE(GetPending(10));
+  // Claim the interrupt.
+  uint32_t id = GetInterruptClaim(0);
+  EXPECT_EQ(id, 10);
+  // A second claim should return 0.
+  EXPECT_EQ(GetInterruptClaim(0), 0);
+  // Interrupt is no longer pending.
+  EXPECT_FALSE(GetPending(10));
+  // Complete the interrupt.
+  SetInterruptClaim(0, id);
+  // Expect pending to be set, as the level is still high.
+  EXPECT_TRUE(GetPending(10));
+  // Lower the irq.
+  plic()->SetInterrupt(10, /*value=*/false, /*is_level=*/true);
+  // Expect pending to still be set.
+  EXPECT_TRUE(GetPending(10));
+}
+
+// Test operation of edge triggered interrupts.
+TEST_F(RiscVPlicTest, EdgeTriggeredInterrupt) {
+  SetDefaultConfig();
+  // Set irq for source 10 level sensitive.
+  plic()->SetInterrupt(10, /*value=*/true, /*is_level=*/false);
+  // Expect pending to be set.
+  EXPECT_TRUE(GetPending(10));
+  // Expect irq to be set for context 0;
+  EXPECT_TRUE(target(0)->irq_value());
+  // Claim the interrupt.
+  uint32_t id = GetInterruptClaim(0);
+  EXPECT_EQ(id, 10);
+  // A second claim should return 0.
+  EXPECT_EQ(GetInterruptClaim(0), 0);
+  // Interrupt is no longer pending.
+  EXPECT_FALSE(GetPending(10));
+  // Complete the interrupt.
+  SetInterruptClaim(0, id);
+  // Expect pending to be cleared.
+  EXPECT_FALSE(GetPending(10));
+}
+
+TEST_F(RiscVPlicTest, PriorityThreshold) {
+  SetDefaultConfig();
+  // Signal interrupts for sources 5-10.
+  for (int i = 5; i <= 10; ++i) {
+    plic()->SetInterrupt(i, /*value=*/true, /*is_level=*/false);
+    // Only sources 9 and 10 should trigger interrupts.
+    EXPECT_EQ(target(0)->irq_value(), i > 8);
+  }
+  // Now claim the interrupts, all should be claimed in order of priority,'
+  // even those below the threshold. The IRQ line should remain high until
+  // there are no more pending interrupts.
+  for (int i = 10; i >= 5; --i) {
+    EXPECT_TRUE(target(0)->irq_value());
+    EXPECT_TRUE(GetPending(i));
+    uint32_t id = GetInterruptClaim(0);
+    EXPECT_EQ(id, i);
+    SetInterruptClaim(0, id);
+    EXPECT_FALSE(GetPending(i));
+    // IRQ remains high until the last interrupt is completed.
+    EXPECT_EQ(target(0)->irq_value(), i != 5);
+  }
+}
+
+TEST_F(RiscVPlicTest, MultipleTargets) {
+  SetDefaultConfig();
+  // Signal interrupt for source 30.
+  plic()->SetInterrupt(30, /*value=*/true, /*is_level=*/false);
+  EXPECT_TRUE(GetPending(30));
+  // Expect irq to be set for all contexts.
+  for (int c = 0; c < kNumContexts; ++c) {
+    EXPECT_TRUE(target(c)->irq_value());
+  }
+  // Try claiming the interrupt for each context, only the first will succeed.
+  for (int c = 0; c < kNumContexts; ++c) {
+    uint32_t id = GetInterruptClaim(c);
+    // Expect pending to be cleared.
+    EXPECT_FALSE(GetPending(30));
+    if (c == 0) {
+      EXPECT_EQ(id, 30);
+    } else {
+      EXPECT_EQ(id, 0);
+    }
+    // The target should be cleared.
+    EXPECT_EQ(target(c)->irq_value(), 0);
+  }
+  // Complete the interrupt for context 0.
+  SetInterruptClaim(0, 30);
+}
+}  // namespace