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/src/sysc/core_complex.cpp b/src/sysc/core_complex.cpp index 488fdbe..8a840ff 100644 --- a/src/sysc/core_complex.cpp +++ b/src/sysc/core_complex.cpp @@ -125,7 +125,17 @@ 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) { + 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(); +} template inline std::pair core_complex::load_file(std::string const& name) { iss::arch_if* cc = vm->get_arch(); @@ -385,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 f76b44a..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 @@ -309,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/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..61b4524 --- /dev/null +++ b/tests/sysc/core_complex_test.cpp @@ -0,0 +1,92 @@ +#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. +std::vector counting_program() { + using namespace rv32; + return { + 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); +} + +// 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); +} + +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 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); + } +};