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