From 1d823bd911768c0552ffdbbdb4106d155e8809d6 Mon Sep 17 00:00:00 2001 From: Chris Hurley Date: Thu, 22 Oct 2026 10:28:59 -0700 Subject: [PATCH 1/5] feat(sysc): add an rv32imac core with PMP and AIA Registers rv32imac_mp_aia, the rv32imac_mp core with the Advanced Interrupt Architecture enabled. Co-authored-by: Mark Karlstad --- src/sysc/register_cores.cpp | 9 ++++++++- tests/sysc/core_complex_test.cpp | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/sysc/register_cores.cpp b/src/sysc/register_cores.cpp index 721c7d5..39cacc5 100644 --- a/src/sysc/register_cores.cpp +++ b/src/sysc/register_cores.cpp @@ -50,7 +50,7 @@ namespace iss { namespace interp { using namespace sysc; -__attribute__((used)) volatile std::array riscv_init = { +__attribute__((used)) volatile std::array riscv_init = { iss_factory::instance().register_creator("rv32i_m:interp", [](unsigned gdb_port, sysc::riscv::core_complex_if* cc) -> iss_factory::base_t { auto* cpu = new core2sc_adapter>(cc); @@ -73,6 +73,13 @@ __attribute__((used)) volatile std::array riscv_init = { std::make_unique>(cpu->get_priv_if())); return {sysc::core_ptr{cpu}, vm_ptr{create(static_cast(cpu), gdb_port)}}; }), + iss_factory::instance().register_creator("rv32imac_mp_aia:interp", // rv32imac_mp with AIA + [](unsigned gdb_port, sysc::riscv::core_complex_if* cc) -> iss_factory::base_t { + auto* cpu = new core2sc_adapter>(cc); + cpu->memories.insert_before_last( + std::make_unique>(cpu->get_priv_if())); + return {sysc::core_ptr{cpu}, vm_ptr{create(static_cast(cpu), gdb_port)}}; + }), iss_factory::instance().register_creator("rv32imac_mu:interp", [](unsigned gdb_port, sysc::riscv::core_complex_if* cc) -> iss_factory::base_t { auto* cpu = new core2sc_adapter>(cc); diff --git a/tests/sysc/core_complex_test.cpp b/tests/sysc/core_complex_test.cpp index 61b4524..f05d63e 100644 --- a/tests/sysc/core_complex_test.cpp +++ b/tests/sysc/core_complex_test.cpp @@ -56,6 +56,14 @@ TEST_F(CoreComplexTest, RunsAProgramFromTheResetAddress) { EXPECT_GT(loop_count(), 0u); } +TEST_F(CoreComplexTest, RunsAProgramOnAnRv32imacCoreWithPmpAndAia) { + tb->cpu.core_type.set_value("rv32imac_mp_aia"); + sc_start(sc_time(100, SC_US)); + + EXPECT_EQ(start_count(), 1u); + EXPECT_GT(loop_count(), 0u); +} + TEST_F(CoreComplexTest, AdvancesSimulationTimeWhileRunning) { sc_start(sc_time(100, SC_US)); auto loops_at_100us = loop_count(); From 402d96f7ceec7f32666cf44ee4b25de7496e1915 Mon Sep 17 00:00:00 2001 From: Chris Hurley Date: Thu, 22 Oct 2026 09:38:08 -0700 Subject: [PATCH 2/5] test(sysc): drive local interrupts and CSRs from the testbench The testbench takes a CLINT input count and binds one signal initiator per input, and the encoder gains the CSR instructions a trap test needs. --- tests/sysc/rv32_program.h | 6 ++++++ tests/sysc/testbench.h | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/sysc/rv32_program.h b/tests/sysc/rv32_program.h index ee9ab43..c1ebf96 100644 --- a/tests/sysc/rv32_program.h +++ b/tests/sysc/rv32_program.h @@ -14,6 +14,12 @@ constexpr uint32_t lw(unsigned rd, int32_t offset, unsigned rs1) { return i_type constexpr uint32_t addi(unsigned rd, unsigned rs1, int32_t imm) { return i_type(imm, rs1, 0, rd, 0x13); } +constexpr uint32_t csrrw(unsigned rd, unsigned csr, unsigned rs1) { return i_type(csr, rs1, 1, rd, 0x73); } + +constexpr uint32_t csrrs(unsigned rd, unsigned csr, unsigned rs1) { return i_type(csr, rs1, 2, rd, 0x73); } + +constexpr uint32_t csrrsi(unsigned rd, unsigned csr, unsigned uimm) { return i_type(csr, uimm, 6, rd, 0x73); } + constexpr uint32_t sw(unsigned rs2, int32_t offset, unsigned rs1) { auto imm = static_cast(offset & 0xfff); return ((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (2 << 12) | ((imm & 0x1f) << 7) | 0x23; diff --git a/tests/sysc/testbench.h b/tests/sysc/testbench.h index ba1121e..0b8b4df 100644 --- a/tests/sysc/testbench.h +++ b/tests/sysc/testbench.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -13,14 +14,19 @@ struct testbench : sc_core::sc_module { static constexpr uint64_t ram_size = 1ULL << 20; - sysc::riscv::core_complex<> cpu{"cpu"}; + sysc::riscv::core_complex<> cpu; scc::router<> bus{"bus", 1, 2}; scc::memory ram{"ram"}; sc_core::sc_signal clk{"clk", sc_core::sc_time(10, sc_core::SC_NS)}; sc_core::sc_signal rst{"rst", false}; - - explicit testbench(sc_core::sc_module_name const& nm) - : sc_core::sc_module(nm) { + sc_core::sc_vector clint_irq{"clint_irq"}; + + explicit testbench(sc_core::sc_module_name const& nm, size_t clint_irq_count = 32) + : sc_core::sc_module(nm) + , cpu("cpu", clint_irq_count) { + clint_irq.init(clint_irq_count); + for(auto i = 0U; i < clint_irq_count; ++i) + clint_irq[i].bind(cpu.clint_irq_i[i]); cpu.ibus(bus.target[0]); cpu.dbus(bus.target[1]); bus.bind_target(ram.target, 0, 0, ram_size); From 93b3cf7d1c8d95e6ae9432739f7ea3fee4e8888a Mon Sep 17 00:00:00 2001 From: Chris Hurley Date: Thu, 22 Oct 2026 10:28:59 -0700 Subject: [PATCH 3/5] fix: deliver AIA local interrupts above 31 on RV32 AIA adds mieh/miph so an RV32 hart can take 64 local interrupts, but core_complex capped the CLINT inputs at XLEN, built the irq bit with an int shift and masked mcause to 32 causes. A FEAT_AIA RV32 m_p hart now accepts 64 inputs and reports their causes in full. Co-authored-by: Mark Karlstad --- src/iss/arch/riscv_hart_common.h | 9 ++++-- src/iss/arch/riscv_hart_m_p.h | 1 + src/sysc/core2sc_adapter.h | 9 ++---- tests/sysc/core_complex_test.cpp | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/iss/arch/riscv_hart_common.h b/src/iss/arch/riscv_hart_common.h index 56f6780..143ad4c 100644 --- a/src/iss/arch/riscv_hart_common.h +++ b/src/iss/arch/riscv_hart_common.h @@ -1019,8 +1019,11 @@ template struct riscv_hart_common : public BAS void set_max_irq_num(unsigned i) { mcause_max_irq = std::max(1u << util::ilog2(i), 16u); } void set_clint_irq_num(unsigned num) { - assert(num <= traits::XLEN); - clint_custom_irq_mask = std::numeric_limits::max() >> (traits::XLEN - num); + assert(num <= max_clint_irq_num && "CLINT irq count exceeds what the hart supports"); + clint_custom_irq_mask = num >= 64 ? std::numeric_limits::max() : (1ULL << num) - 1; + // mcause needs to be able to hold the highest irq number + while(mcause_max_irq < num) + mcause_max_irq <<= 1; } protected: @@ -1091,6 +1094,8 @@ template struct riscv_hart_common : public BAS int64_t instret_offset{0}; semihosting_cb_t semihosting_cb; unsigned mcause_max_irq{traits::XLEN}; + // AIA adds mieh/miph on RV32, so a hart with FEAT_AIA may raise this to 64 + unsigned max_clint_irq_num{traits::XLEN}; uint64_t clint_custom_irq_mask{0xffff}; }; diff --git a/src/iss/arch/riscv_hart_m_p.h b/src/iss/arch/riscv_hart_m_p.h index 6b9e540..74d57d1 100644 --- a/src/iss/arch/riscv_hart_m_p.h +++ b/src/iss/arch/riscv_hart_m_p.h @@ -116,6 +116,7 @@ riscv_hart_m_p::riscv_hart_m_p() this->add_debug_csrs(); if(FEAT & FEAT_AIA && traits::XLEN == 32) { + this->max_clint_irq_num = 64; this->csr_rd_cb[miph] = MK_CSR_RD_CB(read_ip); this->csr_wr_cb[miph] = MK_CSR_WR_CB(write_ip); this->csr_rd_cb[mieh] = MK_CSR_RD_CB(read_ie); diff --git a/src/sysc/core2sc_adapter.h b/src/sysc/core2sc_adapter.h index 5b93f22..a67adae 100644 --- a/src/sysc/core2sc_adapter.h +++ b/src/sysc/core2sc_adapter.h @@ -324,13 +324,8 @@ template class core2sc_adapter : public PLAT, public sc2core_if } void _local_irq(short id, bool value) { - uint64_t mask = 0; - if(sizeof(reg_t) == 4) { - assert(id < 32 && "CLINT cannot handle more than 32 irq"); - } else { - assert(id < 64 && "CLINT cannot handle more than 64 irq"); - } - mask = 1 << id; + assert(id < this->max_clint_irq_num && "CLINT irq id exceeds what the hart supports"); + uint64_t const mask = 1ULL << id; if(value) { this->mip_csr |= mask; wfi_evt.notify(); diff --git a/tests/sysc/core_complex_test.cpp b/tests/sysc/core_complex_test.cpp index f05d63e..f055c00 100644 --- a/tests/sysc/core_complex_test.cpp +++ b/tests/sysc/core_complex_test.cpp @@ -97,4 +97,57 @@ TEST_F(CoreComplexTest, RestartsAtTheResetAddressAfterAResetWhileRunning) { EXPECT_EQ(start_count(), 2u); } +constexpr uint64_t trap_handler_addr = 0x100; +constexpr int32_t mcause_addr = 0x408; +constexpr unsigned upper_local_irq = 40; + +// Enables only upper_local_irq through mieh, then spins. The trap handler +// stores mcause and spins. +std::vector upper_local_irq_program() { + using namespace rv32; + constexpr unsigned mstatus = 0x300, mtvec = 0x305, mieh = 0x314, mcause = 0x342; + constexpr unsigned mstatus_mie = 0x8; + std::vector program{ + addi(5, 0, trap_handler_addr), // t0 = handler + csrrw(0, mtvec, 5), // mtvec = t0 + addi(5, 0, 1 << (upper_local_irq - 32)), // t0 = enable bit + csrrw(0, mieh, 5), // mieh = t0 + csrrsi(0, mstatus, mstatus_mie), // mstatus.MIE = 1 + jal(0, 0), // spin + }; + program.resize(trap_handler_addr / 4, 0); + std::vector trap_handler{ + csrrs(6, mcause, 0), // t1 = mcause + sw(6, mcause_addr, 0), // store t1 + jal(0, 0), // spin + }; + program.insert(program.end(), trap_handler.begin(), trap_handler.end()); + return program; +} + +class AiaCoreComplexTest : public SystemCTestFixture { +protected: + void SetUp() override { + tlm::tlm_global_quantum::instance().set(sc_time(1, SC_US)); + tb = std::make_unique("tb", 64); + tb->cpu.core_type.set_value("rv32imac_mp_aia"); + tb->load(reset_address, upper_local_irq_program()); + } + + void TearDown() override { + tb.reset(); + SystemCTestFixture::TearDown(); + } + + std::unique_ptr tb; +}; + +TEST_F(AiaCoreComplexTest, TakesALocalInterruptAbove31OnRv32) { + sc_spawn([this] { tb->clint_irq[upper_local_irq].write_now(true); }); + sc_start(sc_time(100, SC_US)); + + constexpr uint32_t interrupt_bit = 1U << 31; + EXPECT_EQ(tb->read_word(mcause_addr), interrupt_bit | upper_local_irq); +} + } // namespace From 970b2db7eca9146c1c1cf9bb92c16934ff128175 Mon Sep 17 00:00:00 2001 From: Chris Hurley Date: Thu, 22 Oct 2026 10:28:59 -0700 Subject: [PATCH 4/5] fix(arch): write the upper half of mip through miph in mu_p write_ip tested for mieh, which never reaches it, so a miph write landed in the lower half. An RV32 mu_p hart with AIA also accepts 64 local interrupts now, as m_p does. Co-authored-by: Mark Karlstad --- src/iss/arch/riscv_hart_mu_p.h | 3 ++- tests/sysc/CMakeLists.txt | 1 + tests/sysc/aia_csr_test.cpp | 46 ++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/sysc/aia_csr_test.cpp diff --git a/src/iss/arch/riscv_hart_mu_p.h b/src/iss/arch/riscv_hart_mu_p.h index 2340b78..5153d02 100644 --- a/src/iss/arch/riscv_hart_mu_p.h +++ b/src/iss/arch/riscv_hart_mu_p.h @@ -153,6 +153,7 @@ riscv_hart_mu_p::riscv_hart_mu_p() this->add_debug_csrs(); if(FEAT & FEAT_AIA && traits::XLEN == 32) { + this->max_clint_irq_num = 64; this->csr_rd_cb[miph] = MK_CSR_RD_CB(read_ip); this->csr_wr_cb[miph] = MK_CSR_WR_CB(write_ip); this->csr_rd_cb[mieh] = MK_CSR_RD_CB(read_ie); @@ -400,7 +401,7 @@ template iss::status riscv_hart_mu_p iss::status riscv_hart_mu_p::write_ip(unsigned addr, reg_t val) { uint64_t lval = val; - if(addr == mieh) { + if(addr == miph) { lval <<= 32; lval |= static_cast(this->mip_csr); } else if(sizeof(reg_t) == 4) diff --git a/tests/sysc/CMakeLists.txt b/tests/sysc/CMakeLists.txt index 8bd96e3..fd1bb5d 100644 --- a/tests/sysc/CMakeLists.txt +++ b/tests/sysc/CMakeLists.txt @@ -4,6 +4,7 @@ include(GoogleTest) add_executable(sysc_tests test_main.cpp core_complex_test.cpp + aia_csr_test.cpp ) target_link_libraries(sysc_tests PRIVATE scc::components GTest::gtest) # whole-archive so the static core registrations in register_cores.cpp are kept diff --git a/tests/sysc/aia_csr_test.cpp b/tests/sysc/aia_csr_test.cpp new file mode 100644 index 0000000..d4cfbf5 --- /dev/null +++ b/tests/sysc/aia_csr_test.cpp @@ -0,0 +1,46 @@ +#include +#include +#include + +namespace { + +using namespace iss::arch; + +template class AiaCsrTest : public ::testing::Test { +protected: + using reg_t = typename HART::reg_t; + + void SetUp() override { + hart.reset(0); + hart.set_clint_irq_num(64); + } + + reg_t read_csr(unsigned csr) { + reg_t val = 0; + EXPECT_EQ(hart.read(csr_addr(iss::access_type::READ, csr), sizeof(val), reinterpret_cast(&val)), iss::Ok); + return val; + } + + void write_csr(unsigned csr, reg_t val) { + EXPECT_EQ(hart.write(csr_addr(iss::access_type::WRITE, csr), sizeof(val), reinterpret_cast(&val)), iss::Ok); + } + + HART hart; + +private: + static iss::addr_t csr_addr(iss::access_type access, unsigned csr) { + return {iss::address_type::LOGICAL, access, traits::CSR, csr}; + } +}; + +using MuAiaCsrTest = AiaCsrTest>; + +TEST_F(MuAiaCsrTest, WritingMiphSetsTheUpperHalfOfMipAndLeavesTheLowerHalfAlone) { + constexpr reg_t irq40 = 1U << (40 - 32); + write_csr(miph, irq40); + + EXPECT_EQ(read_csr(miph), irq40); + EXPECT_EQ(read_csr(mip), 0u); +} + +} // namespace From 3d1f07f98f6eb73eb0a4d3801f466ccacef2785a Mon Sep 17 00:00:00 2001 From: Chris Hurley Date: Thu, 22 Oct 2026 10:28:59 -0700 Subject: [PATCH 5/5] fix(arch): read the upper half of mip through miph in msu_vp read_ip tested for mieh, which never reaches it, so a miph read returned the lower half. An RV32 msu_vp hart with AIA also accepts 64 local interrupts now, as m_p and mu_p do. this_class dropped FEAT, so msu_vp did not compile with any feature set and no AIA build of it existed. Co-authored-by: Mark Karlstad --- src/iss/arch/riscv_hart_msu_vp.h | 5 +++-- tests/sysc/aia_csr_test.cpp | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/iss/arch/riscv_hart_msu_vp.h b/src/iss/arch/riscv_hart_msu_vp.h index 0765a7f..d5e58fd 100644 --- a/src/iss/arch/riscv_hart_msu_vp.h +++ b/src/iss/arch/riscv_hart_msu_vp.h @@ -63,7 +63,7 @@ template class riscv_hart_msu_vp : public: using core = BASE; using base = riscv_hart_common; - using this_class = riscv_hart_msu_vp; + using this_class = riscv_hart_msu_vp; using reg_t = typename core::reg_t; using phys_addr_t = typename core::phys_addr_t; @@ -187,6 +187,7 @@ riscv_hart_msu_vp::riscv_hart_msu_vp() this->add_debug_csrs(); if(FEAT & FEAT_AIA && traits::XLEN == 32) { + this->max_clint_irq_num = 64; this->csr_rd_cb[miph] = MK_CSR_RD_CB(read_ip); this->csr_wr_cb[miph] = MK_CSR_WR_CB(write_ip); this->csr_rd_cb[mieh] = MK_CSR_RD_CB(read_ie); @@ -484,7 +485,7 @@ template iss::status riscv_hart_msu_vpmip_csr & mask) >> 32; else val = this->mip_csr & mask; diff --git a/tests/sysc/aia_csr_test.cpp b/tests/sysc/aia_csr_test.cpp index d4cfbf5..afba4e8 100644 --- a/tests/sysc/aia_csr_test.cpp +++ b/tests/sysc/aia_csr_test.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include namespace { @@ -43,4 +45,14 @@ TEST_F(MuAiaCsrTest, WritingMiphSetsTheUpperHalfOfMipAndLeavesTheLowerHalfAlone) EXPECT_EQ(read_csr(mip), 0u); } +using MsuAiaCsrTest = AiaCsrTest>; + +TEST_F(MsuAiaCsrTest, WritingMiphSetsTheUpperHalfOfMipAndLeavesTheLowerHalfAlone) { + constexpr reg_t irq40 = 1U << (40 - 32); + write_csr(miph, irq40); + + EXPECT_EQ(read_csr(miph), irq40); + EXPECT_EQ(read_csr(mip), 0u); +} + } // namespace