RISC-V ISA Simulator

Project Description
For this project, the task was to write an instruction set architecture (ISA) level simulator for RISC-V in C. We were responsible for implementing the RV32I Base Integer Instruction Set as described in Chapter 2 of the RISC-V ISA Instruction Set Manual. Aside from the architecture itself, we specifically explored:

  • The impact of instruction set architecture changes on CPI
  • Instruction counts for various program mixes or benchmarks
  • Generation of trace files useful for analyzing and testing results of decisions in branch predictor algorithms and cache designs
  • Developing and debugging compilers and system software

Structure of Simulator
The simulator takes in three input parameters:
  • Input memory image file
  • Starting address
  • Stack address
The only registers the simulator initializes or loads are the sp, pc, and ra registers. The simulation also continues until a jr ra instruction is encountered with ra == 0.

Simulator output was split into two modes:
  • Verbose mode - Print the PC and hexadecimal value of each instruction as it's fetched along with the contents of each register after the instruction's execution
  • Silent mode - Print the PC of the final instruction and hexadecimal value of each register only at the end of the simulation.

The first phase of the simulator was responsible for accepting and parsing the command line arguments. This was a straightforward task, using the parameters entered to set the file to be read, start address of the PC, stack address and the mode the program would run in (i.e. verbose) as well as the option of setting breakpoints.

Next, the simulator reads in the memory image from the specified file. The total memory space was determined by dynamically allocating the memory large enough to house the value of the stack address entered. The contents of the memory image were then read into a 2-D array that housed the memory address as the first index, and the instructions as the second. After that, the contents of the array (our simulated memory) were ran through the IR_decoder function.

The IR_decoder function was defined in a header file, and made a reference to the Reg_decoder function. Directives were used to define the hex values of the bit masks as well as the maximum memory size. The Reg_decoder subroutine was responsible for extracting the appropriate fields of each instruction. This included the immediate fields, destination, source, opcode, function and shamt fields. Once the fields were extracted, the IR_decoder used the opcode field to pinpoint the type of each instruction. From there, the operations were performed on the registers specified. After an instruction had been performed (executed), the program counter was advanced by four to fetch the next instruction in the array (memory).

Subroutines were also implemented to traverse the code via breakpoints, print the contents of the register file and the PC. A function to print the contents of memory was also implemented.

The test strategy employed was based on unit testing. Every operation of the simulator was tested individually via test cases. A portion of those test cases were created by translating C code to RISC-V assembly with the use of rvgcc. Other test cases were created by directly writing RISC-V assembly code. Testing was implemented in two phases: Decode, which validated that the instruction was properly decoded by the simulator and Execute, which validated that the instruction performed the correct operation. Values were cycled through the test cases to check for correctness.