This project demonstrates how to use RISC-V ISA Simulator as a library, and integrate it with an external simulator. In this demonstration, external simulator is a simple memory simulator. In our use case, all memory operations (loads, stores, and instruction fetches) are handled by the external simulator, without any of riscv-isa-sim's internal memories.
Several parameters are hardcoded in src/main.cc
:
- Start PC:
0x20000000
- Memory size: 1GB (1024 * 1024 * 1024 bytes)
- ISA string: "rv64imafdcv"
- Privilege levels: "MSU" (Machine, Supervisor, and User)
- CPU runs sw
src/sw/main.elf
. Checksrc/sw/README.md
for more details.
The program will terminate with either:
- "PASS" message: When the test program writes
0x5555
to the finisher address - "FAIL with status {x}" message: When any other non-zero value is written to the finisher address
In both cases, when detected write to finisher address, memory_simulator
will print the message, call assert(0)
and terminate.
Clone the RISC-V ISA Simulator repository and checkout the required version:
git clone https://github.com/riscv-software-src/riscv-isa-sim.git
Set the environment variable to point to your RISC-V ISA Simulator repository:
export SPIKE_SOURCE_DIR=/path/to/riscv-isa-sim
There are several build targets in the Makefile.
First you need to compile the source files. You can do it with compile_only
target:
make compile_only
Then you can link the object files with the Spike libraries dynamically. You can do it with link_demo_dynamic
target:
make link_demo_dynamic
Those two steps can be combined into one with demo
target:
make demo
The project uses dynamic linking to connect with the Spike libraries:
link_demo_dynamic: $(OBJS)
$(CXX) -std=c++17 $(OBJS) -o demo \
-L../lib \
-Wl,-rpath,../lib/ \
-Wl,--no-as-needed \
-lriscv \
-lsoftfloat \
-latomic
Key components:
-L../lib
: Specifies the library search path-Wl,-rpath,../lib/
: Embeds the runtime library path in the executable so it can find libraries at runtime-Wl,--no-as-needed
: Ensures all specified libraries are linked, even if they don't resolve any symbols-lriscv
,-lsoftfloat
,-latomic
: Required libraries for the RISC-V simulator
If you want to compile riscv-isa-sim
from scratch, you can use the build_spike
target:
make build_spike
Note: We are forcing C++17 standard in the Makefile and therefore some Warnings are expected.
Output of the build process is demo
executable in src
folder.
To enable memory operation observation/debugging, you can build with observability hooks enabled:
make USE_HOOKS=1
This will:
- Include the
hooks.h
header automatically in the build - Enable printing of memory operations:
- Load operations: address, data, and length
- Fetch operations: address, instruction, and length
Our small example software is in src/sw
folder. Elf filename is hardcoded in main.cc
cd src/sw
# follow instructions in sw/README.md
make
This step is optional because we have added a precompiled elf file in src/sw/main.elf
Run the compiled demo:
./src/demo
class cfg_t has a new field: external_simulator
. That field is a pointer to an object that implements abstract_sim_if_t
interface.
class cfg_t {
public:
// ...
std::optional<abstract_sim_if_t*> external_simulator;
// ...
};
Usage is in main.cc
cfg.external_simulator = &ext_sim;
A custom processor implementation that inherits from riscv-isa-sim's simif_t
.
Since recently, bus_t
has a fallback parameter, in case no device is found on the bus, request is forwarded to the fallback device. In our case it is the external simulator.
Example is in constructor of demo_core
if (cfg->external_simulator.has_value()) {
auto* ext_sim = cfg->external_simulator.value();
bus_fallback = new external_sim_device_t(ext_sim);
}
if (bus_fallback != nullptr) {
bus = std::make_unique<bus_t>(bus_fallback);
} else {
bus = std::make_unique<bus_t>();
}
This is the interface that external simulator must implement. It has two methods: load
and store
.
class abstract_sim_if_t {
public:
virtual bool load(reg_t addr, size_t len, uint8_t* bytes) = 0;
virtual bool store(reg_t addr, size_t len, const uint8_t* bytes) = 0;
};
We use this class as API to integrate with riscv-isa-sim
.
We have a base class and two implementation approaches:
memory_simulator
: Base class implementing core memory operations:- Read/write operations
- ELF file loading
- ROM content initialization
- Sparse array-based memory management
Two different ways to integrate with riscv-isa-sim:
memory_simulator_wrapper
: Inherits from bothmemory_simulator
andabstract_sim_if_t
memory_sim_bridge
: Bridge pattern implementation that wraps amemory_simulator
instance
Direct inheritance approach that combines memory_simulator
and abstract_sim_if_t
:
class memory_simulator_wrapper : public memory_simulator, public abstract_sim_if_t {
public:
memory_simulator_wrapper(uint64_t size);
~memory_simulator_wrapper() override;
bool load(reg_t addr, size_t len, uint8_t* bytes) override;
bool store(reg_t addr, size_t len, const uint8_t* bytes) override;
};
It is typically used technique to make a wrapper class when integration with some other library is needed.
Wrapper class that uses composition to integrate the memory simulator with riscv-isa-sim:
class memory_sim_bridge {
public:
memory_sim_bridge(memory_simulator* sim);
bool load(reg_t addr, size_t len, uint8_t* bytes);
bool store(reg_t addr, size_t len, const uint8_t* bytes);
private:
memory_simulator* sim;
};
This approach uses composition, where the memory_sim_bridge
class holds a pointer to a memory_simulator
instance and forwards memory operations to it.
Note: In both approaches we need to define load
and store
methods from abstract_sim_if_t
interface.
The main program (src/main.cc
) demonstrates how to set up and run the RISC-V simulator with an external memory simulator. Here's a detailed breakdown:
-
Configuration Setup
cfg_t cfg;
Creates a configuration object that defines:
- ISA string with supported extensions
- Privilege levels (MSU)
- Starting PC address (0x20000000)
- setting external simulator field
-
Memory Simulator Initialization
memory_simulator_wrapper ext_sim(1024 * 1024 * 1024, START_PC);
In case you want to use memory_sim_bridge
instead of memory_simulator_wrapper
, you need to define #define USE_BRIDGE 1
before main()
.
-
External Simulator Integration
cfg.external_simulator = &ext_sim;
-
Processor Creation
demo_core demo_riscv_core(&cfg);
-
Runtime Execution
demo_riscv_core.reset(); while (1) demo_riscv_core.step(5000);
The program terminates when the test software writes to the finisher address:
- 0x5555: Successful completion ("PASS")
- Any other non-zero value: Error condition ("FAIL with status {x}")