From bfadc59417c3f5a87eac4519de1fd31bacfce623 Mon Sep 17 00:00:00 2001 From: Chris Hurley Date: Tue, 13 Oct 2026 15:07:28 -0700 Subject: [PATCH 1/3] test(sysc): add a GoogleTest suite for core_complex The SystemC wrapper is not built in CI because conanfile.py pulls in no SystemC, so nothing exercises core_complex. Setting WITH_SYSTEMC adds SystemC 3, CCI and GoogleTest, and DBT_RISE_RISCV_BUILD_SC_TESTS builds tests/sysc against them. systemc is an override because systemc-cci on conancenter still pins 2.3. Each test gets a fresh simulation context, so one binary runs the whole suite. The testbench is a core and a RAM behind a router, running a hand-encoded program so no RISC-V toolchain is needed. --- .github/workflows/ci.yml | 32 ++++++++++++++++ CMakeLists.txt | 5 +++ conanfile.py | 4 ++ tests/sysc/CMakeLists.txt | 11 ++++++ tests/sysc/core_complex_test.cpp | 65 ++++++++++++++++++++++++++++++++ tests/sysc/rv32_program.h | 28 ++++++++++++++ tests/sysc/test_main.cpp | 29 ++++++++++++++ tests/sysc/test_main.h | 19 ++++++++++ tests/sysc/testbench.h | 56 +++++++++++++++++++++++++++ 9 files changed, 249 insertions(+) create mode 100644 tests/sysc/CMakeLists.txt create mode 100644 tests/sysc/core_complex_test.cpp create mode 100644 tests/sysc/rv32_program.h create mode 100644 tests/sysc/test_main.cpp create mode 100644 tests/sysc/test_main.h create mode 100644 tests/sysc/testbench.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a68f23d..21f3143 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,38 @@ jobs: build/riscv-sim build/libdbt-rise-riscv.so.2.1.0 + systemc-tests: + name: SystemC Wrapper Tests + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + WITH_SYSTEMC: 1 + steps: + - uses: actions/checkout@v4 + + - name: Update submodules + run: git submodule update --init --recursive + + - name: Cache Conan + uses: actions/cache@v4 + with: + path: ~/.conan2 + key: conan-${{ runner.os }}-systemc-${{ hashFiles('conanfile.py') }} + + - name: Install dependencies + run: | + sudo apt-get install -y g++ python3-pip cmake + pip3 install conan + + - name: Configure + run: cmake --preset Release -B build -DDBT_RISE_RISCV_BUILD_SC_TESTS=ON -DDBT_RISE_RISCV_ENABLE_VECTOR=OFF + + - name: Build + run: cmake --build build -j --target sysc_tests + + - name: Test + run: ctest --test-dir build --output-on-failure + pmp-tests: name: PMP Functional Tests runs-on: ubuntu-24.04 diff --git a/CMakeLists.txt b/CMakeLists.txt index a994a13..5818f4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ option(UPDATE_EXTERNAL_PROJECT "Whether to pull changes in external projects" ON option(OPTIMIZE_FOR_NATIVE "Build with -march=native" OFF) option(NO_GENERIC_CORES "Leave out the generic cores to improve build times" OFF) option(DBT_RISE_RISCV_ENABLE_VECTOR "Build RISC-V vector cores and VMs" ON) +option(DBT_RISE_RISCV_BUILD_SC_TESTS "Build the GoogleTest suite for the SystemC wrapper (needs SystemC, CCI and GTest)" OFF) include(GNUInstallDirs) include(flink) @@ -357,6 +358,10 @@ if(TARGET scc-sysc) else() add_library(${PROJECT_NAME} ALIAS ${PROJECT_NAME}_tlm) endif() + if(DBT_RISE_RISCV_BUILD_SC_TESTS) + enable_testing() + add_subdirectory(tests/sysc) + endif() endif() project(elfio-test) diff --git a/conanfile.py b/conanfile.py index d6fd649..7a6969a 100644 --- a/conanfile.py +++ b/conanfile.py @@ -47,6 +47,10 @@ def requirements(self): self.requires("asmjit/cci.20240531") if "WITH_LLVM" in os.environ: self.requires("llvm-core/19.1.7") + if "WITH_SYSTEMC" in os.environ: + self.requires("systemc/3.0.1", override=True) + self.requires("systemc-cci/1.0.0") + self.requires("gtest/1.15.0") if os.path.isdir("dbt-rise-plugins"): self.requires("lua/5.4.3") diff --git a/tests/sysc/CMakeLists.txt b/tests/sysc/CMakeLists.txt new file mode 100644 index 0000000..8bd96e3 --- /dev/null +++ b/tests/sysc/CMakeLists.txt @@ -0,0 +1,11 @@ +find_package(GTest REQUIRED) +include(GoogleTest) + +add_executable(sysc_tests + test_main.cpp + core_complex_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 +target_force_link_libraries(sysc_tests PRIVATE dbt-rise-riscv_sc_tlm) +gtest_discover_tests(sysc_tests PROPERTIES TIMEOUT 60) diff --git a/tests/sysc/core_complex_test.cpp b/tests/sysc/core_complex_test.cpp new file mode 100644 index 0000000..940535a --- /dev/null +++ b/tests/sysc/core_complex_test.cpp @@ -0,0 +1,65 @@ +#include "rv32_program.h" +#include "test_main.h" +#include "testbench.h" + +#include +#include +#include + +namespace { + +using namespace sc_core; + +constexpr uint64_t reset_address = 0x0; +constexpr int32_t start_count_addr = 0x400; +constexpr int32_t loop_count_addr = 0x404; + +// Counts its own starts in one RAM word, then spins incrementing another. +// RAM survives a core reset, so the start count tells how often the core +// came out of reset. It starts with a nop because a reset taken while the core +// waits in its quantum keeper resumes at the instruction after the reset +// address. +std::vector counting_program() { + using namespace rv32; + return { + addi(0, 0, 0), lw(5, start_count_addr, 0), addi(5, 5, 1), sw(5, start_count_addr, 0), lw(6, loop_count_addr, 0), // loop: + addi(6, 6, 1), sw(6, loop_count_addr, 0), jal(0, -12), // j loop + }; +} + +class CoreComplexTest : public SystemCTestFixture { +protected: + void SetUp() override { + tlm::tlm_global_quantum::instance().set(sc_time(1, SC_US)); + tb = std::make_unique("tb"); + tb->load(reset_address, counting_program()); + } + + void TearDown() override { + tb.reset(); + SystemCTestFixture::TearDown(); + } + + uint32_t start_count() { return tb->read_word(start_count_addr); } + uint32_t loop_count() { return tb->read_word(loop_count_addr); } + + std::unique_ptr tb; +}; + +TEST_F(CoreComplexTest, RunsAProgramFromTheResetAddress) { + 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(); + sc_start(sc_time(100, SC_US)); + + EXPECT_EQ(sc_time_stamp(), sc_time(200, SC_US)); + EXPECT_GT(loop_count(), loops_at_100us); +} + +} // namespace diff --git a/tests/sysc/rv32_program.h b/tests/sysc/rv32_program.h new file mode 100644 index 0000000..ee9ab43 --- /dev/null +++ b/tests/sysc/rv32_program.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +// Minimal RV32I encoder, enough to hand-assemble test programs without a +// RISC-V toolchain. +namespace rv32 { + +constexpr uint32_t i_type(int32_t imm, unsigned rs1, unsigned funct3, unsigned rd, unsigned opcode) { + return (static_cast(imm & 0xfff) << 20) | (rs1 << 15) | (funct3 << 12) | (rd << 7) | opcode; +} + +constexpr uint32_t lw(unsigned rd, int32_t offset, unsigned rs1) { return i_type(offset, rs1, 2, rd, 0x03); } + +constexpr uint32_t addi(unsigned rd, unsigned rs1, int32_t imm) { return i_type(imm, rs1, 0, rd, 0x13); } + +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; +} + +constexpr uint32_t jal(unsigned rd, int32_t offset) { + auto imm = static_cast(offset); + return (((imm >> 20) & 0x1) << 31) | (((imm >> 1) & 0x3ff) << 21) | (((imm >> 11) & 0x1) << 20) | (((imm >> 12) & 0xff) << 12) | + (rd << 7) | 0x6f; +} + +} // namespace rv32 diff --git a/tests/sysc/test_main.cpp b/tests/sysc/test_main.cpp new file mode 100644 index 0000000..6a261fc --- /dev/null +++ b/tests/sysc/test_main.cpp @@ -0,0 +1,29 @@ +#include "test_main.h" + +#include +#include +#include +#include +#include + +int sc_main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + // Also registers the global CCI broker, which must exist exactly once and + // outlive every simulation context the fixture creates. + auto* verbose = std::getenv("SCC_TEST_VERBOSE"); + auto level = verbose ? static_cast(std::min(std::strtoul(verbose, nullptr, 10) + 4, 7UL)) : scc::log::WARNING; + scc::init_logging(scc::LogConfig().logLevel(level).logAsync(false)); + return RUN_ALL_TESTS(); +} + +SystemCTestFixture::SystemCTestFixture() { + sc_core::sc_report_handler::set_actions(sc_core::SC_ID_NO_SC_START_ACTIVITY_, sc_core::SC_DO_NOTHING); + ResetSystemCContext(); +} + +void SystemCTestFixture::ResetSystemCContext() { + sc_core::sc_curr_simcontext = new sc_core::sc_simcontext(); + sc_core::sc_default_global_context = sc_core::sc_curr_simcontext; +} + +void SystemCTestFixture::TearDown() { ResetSystemCContext(); } diff --git a/tests/sysc/test_main.h b/tests/sysc/test_main.h new file mode 100644 index 0000000..661fab5 --- /dev/null +++ b/tests/sysc/test_main.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +// Gives every test its own SystemC simulation context, so one process can +// elaborate and simulate a fresh design per test. +class SystemCTestFixture : public ::testing::Test { +protected: + SystemCTestFixture(); + + // SystemC cannot destroy a simulation context, so each reset leaks the + // previous one on purpose. + void ResetSystemCContext(); + + // Derived fixtures must destroy the modules they own before calling this: + // a module has to die while the context that registered it is current. + void TearDown() override; +}; diff --git a/tests/sysc/testbench.h b/tests/sysc/testbench.h new file mode 100644 index 0000000..ba1121e --- /dev/null +++ b/tests/sysc/testbench.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// One rv32 core whose instruction and data buses reach a single RAM through a +// router, since a TLM target socket accepts only one initiator. +struct testbench : sc_core::sc_module { + static constexpr uint64_t ram_size = 1ULL << 20; + + sysc::riscv::core_complex<> cpu{"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) { + cpu.ibus(bus.target[0]); + cpu.dbus(bus.target[1]); + bus.bind_target(ram.target, 0, 0, ram_size); + cpu.clk_i(clk); + cpu.rst_i(rst); + } + + void load(uint64_t addr, std::vector const& words) { + for(auto w : words) { + write_word(addr, w); + addr += 4; + } + } + + void write_word(uint64_t addr, uint32_t value) { debug_access(tlm::TLM_WRITE_COMMAND, addr, value); } + + uint32_t read_word(uint64_t addr) { + uint32_t value = 0; + debug_access(tlm::TLM_READ_COMMAND, addr, value); + return value; + } + +private: + void debug_access(tlm::tlm_command cmd, uint64_t addr, uint32_t& value) { + tlm::tlm_generic_payload gp; + gp.set_command(cmd); + gp.set_address(addr); + gp.set_data_ptr(reinterpret_cast(&value)); + gp.set_data_length(sizeof(value)); + gp.set_streaming_width(sizeof(value)); + ram.target.get_base_interface().transport_dbg(gp); + } +}; From 5dd45851730d39b4ed3e5d8ada0d6a31aa4dfeae Mon Sep 17 00:00:00 2001 From: Chris Hurley Date: Tue, 13 Oct 2026 15:22:28 -0700 Subject: [PATCH 2/3] fix(sysc): resync the quantum keeper when a core is reset Since reset zeroes all registers, the cycle counter restarts at 0 while last_sync_cycle keeps its old value. The next qk_sync then computes a wrapped, huge cycle delta and the core waits in its quantum keeper for good. This happens whenever another process calls core_complex::reset() while the core runs, for example a peripheral releasing it from a stall. reset() now resyncs last_sync_cycle. qk_sync records the cycle before it syncs, since the reset can arrive while the core is suspended there and the old order overwrote the resynced value on resume. --- src/sysc/core_complex.cpp | 6 +++++- src/sysc/core_complex.h | 3 ++- tests/sysc/core_complex_test.cpp | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/sysc/core_complex.cpp b/src/sysc/core_complex.cpp index 488fdbe..6b94df3 100644 --- a/src/sysc/core_complex.cpp +++ b/src/sysc/core_complex.cpp @@ -125,7 +125,11 @@ int core_complex::cmd_sysc(int argc, char* argv[], debugger::out_f return Err; } -template void core_complex::reset(uint64_t addr) { vm->reset(addr); } +template void core_complex::reset(uint64_t addr) { + vm->reset(addr); + // the reset clears the cycle counter, qk_sync would otherwise see a wrapped delta and stall the core + last_sync_cycle = vm->get_arch()->get_instrumentation_if()->get_total_cycles(); +} template inline std::pair core_complex::load_file(std::string const& name) { iss::arch_if* cc = vm->get_arch(); diff --git a/src/sysc/core_complex.h b/src/sysc/core_complex.h index f76b44a..4103045 100644 --- a/src/sysc/core_complex.h +++ b/src/sysc/core_complex.h @@ -191,8 +191,9 @@ class core_complex : public sc_core::sc_module, public scc::traceable, public co qk_preaccounted_cycles -= preaccounted; assert(qk_preaccounted_cycles <= cycle_delta); auto core_inc = curr_clk * (cycle_delta - preaccounted); - quantum_keeper.check_and_sync(core_inc); + // record before syncing: the core may be reset while suspended here and reset() resyncs last_sync_cycle last_sync_cycle = cycle; + quantum_keeper.check_and_sync(core_inc); } void qk_reset(uint64_t cycle) override { quantum_keeper.reset(quantum_keeper.get_local_absolute_time()); diff --git a/tests/sysc/core_complex_test.cpp b/tests/sysc/core_complex_test.cpp index 940535a..1e693f1 100644 --- a/tests/sysc/core_complex_test.cpp +++ b/tests/sysc/core_complex_test.cpp @@ -62,4 +62,18 @@ TEST_F(CoreComplexTest, AdvancesSimulationTimeWhileRunning) { EXPECT_GT(loop_count(), loops_at_100us); } +// A peripheral may reset the core from its own process while the core thread +// is suspended in its quantum keeper. +TEST_F(CoreComplexTest, KeepsExecutingAfterAResetWhileRunning) { + sc_spawn([this] { + wait(sc_time(100, SC_US)); + tb->cpu.reset(reset_address); + }); + sc_start(sc_time(100, SC_US) + sc_time(1, SC_NS)); + auto loops_at_reset = loop_count(); + sc_start(sc_time(100, SC_US)); + + EXPECT_GT(loop_count(), loops_at_reset); +} + } // namespace From 3b61bd23ee89a09b4500a9024e340df24dc64ec0 Mon Sep 17 00:00:00 2001 From: Chris Hurley Date: Tue, 13 Oct 2026 16:01:56 -0700 Subject: [PATCH 3/3] fix(sysc): defer a reset taken while the core is running A reset() from another process lands while the core thread is suspended in the middle of an instruction. Resetting the registers there lets that instruction finish and overwrite PC with its successor, so the core skips the instruction at the reset address. While the ISS runs, reset() now only records the address and stops the core at the next instruction boundary; run() applies the reset and restarts the core. The qk_sync reordering from the previous fix is no longer needed, because last_sync_cycle can no longer be resynced while qk_sync waits. --- src/sysc/core_complex.cpp | 13 +++++++++++++ src/sysc/core_complex.h | 6 ++++-- tests/sysc/core_complex_test.cpp | 23 ++++++++++++++++++----- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/sysc/core_complex.cpp b/src/sysc/core_complex.cpp index 6b94df3..8a840ff 100644 --- a/src/sysc/core_complex.cpp +++ b/src/sysc/core_complex.cpp @@ -126,6 +126,12 @@ int core_complex::cmd_sysc(int argc, char* argv[], debugger::out_f } template void core_complex::reset(uint64_t addr) { + if(iss_running) { + // the core may be suspended mid-instruction, so stop it at the next instruction boundary and let run() apply the reset + pending_reset_addr = addr; + core->set_interrupt_execution(true); + return; + } vm->reset(addr); // the reset clears the cycle counter, qk_sync would otherwise see a wrapped delta and stall the core last_sync_cycle = vm->get_arch()->get_instrumentation_if()->get_total_cycles(); @@ -389,7 +395,14 @@ template void core_complex::r } quantum_keeper.reset(sc_core::sc_time_stamp()); core->set_interrupt_execution(false); + iss_running = true; run_iss(); + iss_running = false; + if(pending_reset_addr) { + reset(*pending_reset_addr); + pending_reset_addr.reset(); + core->set_interrupt_execution(false); + } } while(!core->get_interrupt_execution()); if(finish_evt_inuse) finish_evt.notify(); diff --git a/src/sysc/core_complex.h b/src/sysc/core_complex.h index 4103045..969ab31 100644 --- a/src/sysc/core_complex.h +++ b/src/sysc/core_complex.h @@ -59,6 +59,7 @@ #endif #endif #include +#include #include #include #include @@ -191,9 +192,8 @@ class core_complex : public sc_core::sc_module, public scc::traceable, public co qk_preaccounted_cycles -= preaccounted; assert(qk_preaccounted_cycles <= cycle_delta); auto core_inc = curr_clk * (cycle_delta - preaccounted); - // record before syncing: the core may be reset while suspended here and reset() resyncs last_sync_cycle - last_sync_cycle = cycle; quantum_keeper.check_and_sync(core_inc); + last_sync_cycle = cycle; } void qk_reset(uint64_t cycle) override { quantum_keeper.reset(quantum_keeper.get_local_absolute_time()); @@ -310,6 +310,8 @@ class core_complex : public sc_core::sc_module, public scc::traceable, public co // /////////////////////////////////////////////////////////////////////////////// uint64_t last_sync_cycle = 0; + bool iss_running{false}; + std::optional pending_reset_addr; uint64_t qk_preaccounted_cycles{0}; util::range_lut fetch_lut{tlm_dmi_ext()}; util::range_lut& get_read_lut(unsigned space) { diff --git a/tests/sysc/core_complex_test.cpp b/tests/sysc/core_complex_test.cpp index 1e693f1..61b4524 100644 --- a/tests/sysc/core_complex_test.cpp +++ b/tests/sysc/core_complex_test.cpp @@ -16,14 +16,17 @@ constexpr int32_t loop_count_addr = 0x404; // Counts its own starts in one RAM word, then spins incrementing another. // RAM survives a core reset, so the start count tells how often the core -// came out of reset. It starts with a nop because a reset taken while the core -// waits in its quantum keeper resumes at the instruction after the reset -// address. +// came out of reset. std::vector counting_program() { using namespace rv32; return { - addi(0, 0, 0), lw(5, start_count_addr, 0), addi(5, 5, 1), sw(5, start_count_addr, 0), lw(6, loop_count_addr, 0), // loop: - addi(6, 6, 1), sw(6, loop_count_addr, 0), jal(0, -12), // j loop + lw(5, start_count_addr, 0), + addi(5, 5, 1), + sw(5, start_count_addr, 0), + lw(6, loop_count_addr, 0), // loop: + addi(6, 6, 1), + sw(6, loop_count_addr, 0), + jal(0, -12), // j loop }; } @@ -76,4 +79,14 @@ TEST_F(CoreComplexTest, KeepsExecutingAfterAResetWhileRunning) { EXPECT_GT(loop_count(), loops_at_reset); } +TEST_F(CoreComplexTest, RestartsAtTheResetAddressAfterAResetWhileRunning) { + sc_spawn([this] { + wait(sc_time(100, SC_US)); + tb->cpu.reset(reset_address); + }); + sc_start(sc_time(200, SC_US)); + + EXPECT_EQ(start_count(), 2u); +} + } // namespace