7 min read

RV32I-EMU

Table of Contents

RV32I-EMU

A RISC-V (RV32IM) emulator written in C++ from scratch, built for personal learning. The goal is to understand how a CPU works at the instruction cycle level: fetch, decode, and execute β€” and eventually run real programs and a small OS.


Current Status

The emulator loads ELF binaries, executes them in memory, and reports pass/fail using the riscv-tests convention via tohost. It includes an interactive TUI debugger (FTXUI) with register view, instruction trace, memory dump, and real-time test result panel.

Implemented Instructions

GroupInstructions
LUI / AUIPCLUI, AUIPC
OP-IMMADDI, SLTI, SLTIU, XORI, ORI, ANDI, SLLI, SRLI, SRAI
OP (RV32I)ADD, SUB, SLL, SLT, SLTU, XOR, SRL, SRA, OR, AND
OP (RV32M)MUL, MULH, MULHSU, MULHU, DIV, DIVU, REM, REMU
LOADLB, LH, LW, LBU, LHU
STORESB, SH, SW
BRANCHBEQ, BNE, BLT, BGE, BLTU, BGEU
JAL / JALRJAL, JALR
SYSTEMECALL, EBREAK, MRET, CSRRW, CSRRS, CSRRC (+ I variants)
MISC-MEMFENCE (no-op)

CSRs Implemented

mstatus, misa (read-only), medeleg, mideleg, mie, mtvec, mscratch, mepc, mcause, satp, pmpcfg0, pmpaddr0, mhartid (read-only)

Passing Tests

RV32UI β€” Base Integer

TestStatus
rv32ui-p-simpleβœ… PASS
rv32ui-p-addβœ… PASS
rv32ui-p-addiβœ… PASS
rv32ui-p-andβœ… PASS
rv32ui-p-andiβœ… PASS
rv32ui-p-auipcβœ… PASS
rv32ui-p-beqβœ… PASS
rv32ui-p-bgeβœ… PASS
rv32ui-p-bgeuβœ… PASS
rv32ui-p-bltβœ… PASS
rv32ui-p-bltuβœ… PASS
rv32ui-p-bneβœ… PASS
rv32ui-p-fence_iβœ… PASS
rv32ui-p-jalβœ… PASS
rv32ui-p-jalrβœ… PASS
rv32ui-p-lbβœ… PASS
rv32ui-p-lbuβœ… PASS
rv32ui-p-lhβœ… PASS
rv32ui-p-lhuβœ… PASS
rv32ui-p-luiβœ… PASS
rv32ui-p-lwβœ… PASS
rv32ui-p-orβœ… PASS
rv32ui-p-oriβœ… PASS
rv32ui-p-sbβœ… PASS
rv32ui-p-shβœ… PASS
rv32ui-p-sllβœ… PASS
rv32ui-p-slliβœ… PASS
rv32ui-p-sltβœ… PASS
rv32ui-p-sltiβœ… PASS
rv32ui-p-sltiuβœ… PASS
rv32ui-p-sltuβœ… PASS
rv32ui-p-sraβœ… PASS
rv32ui-p-sraiβœ… PASS
rv32ui-p-srlβœ… PASS
rv32ui-p-srliβœ… PASS
rv32ui-p-subβœ… PASS
rv32ui-p-swβœ… PASS
rv32ui-p-xorβœ… PASS
rv32ui-p-xoriβœ… PASS

RV32UM β€” Multiply/Divide Extension

TestStatus
rv32um-p-mulβœ… PASS
rv32um-p-mulhβœ… PASS
rv32um-p-mulhsuβœ… PASS
rv32um-p-mulhuβœ… PASS
rv32um-p-divβœ… PASS
rv32um-p-divuβœ… PASS
rv32um-p-remβœ… PASS
rv32um-p-remuβœ… PASS

47 / 47 tests passing.


Project Structure

RV32I-EMU/
β”œβ”€β”€ core/
β”‚   β”œβ”€β”€ CPU/
β”‚   β”‚   β”œβ”€β”€ CPU.h              # CPU class definition
β”‚   β”‚   └── CPU.cpp            # Fetch / decode / execute loop
β”‚   β”œβ”€β”€ MMU/
β”‚   β”‚   β”œβ”€β”€ MMU.h              # MMU class definition
β”‚   β”‚   └── MMU.cpp            # Flat memory, tohost detection
β”‚   β”œβ”€β”€ InstructionSet/
β”‚   β”‚   β”œβ”€β”€ Instructionset.h   # Base class with opcode dispatch table
β”‚   β”‚   └── Instructionset.cpp # All instruction handlers (RV32I + RV32M)
β”‚   β”œβ”€β”€ Logger.h               # Thread-safe snapshot logger
β”‚   β”œβ”€β”€ DebugUI.h / .cpp       # FTXUI interactive debugger
β”‚   └── test/
β”‚       β”œβ”€β”€ test-ui/           # rv32ui-p-* binaries
β”‚       └── test-m/            # rv32um-p-* binaries
β”œβ”€β”€ main.cpp                   # Entry point: ELF loader, batch mode, TUI
└── CMakeLists.txt

How to Build

Requirements

  • cmake >= 3.10
  • g++ with C++17 support
  • ftxui (for the debug TUI)
  • riscv64-unknown-elf-gcc (only to regenerate test binaries)

Build

mkdir build && cd build
cmake ..
make

Run a single test (TUI mode)

./build/rv32i-emu core/test/test-ui/rv32ui-p-add

Run all tests (batch mode)

./build/rv32i-emu core/test/test-ui --batch
./build/rv32i-emu core/test/test-m  --batch

Generate test binaries

git clone --recurse-submodules https://github.com/riscv-software-src/riscv-tests
cd riscv-tests
autoconf && ./configure --prefix=$PWD/build
make isa XLEN=32

How It Works

Instruction Cycle

Each cpu.step() runs a full fetch–decode–execute cycle:

  1. Fetch β€” reads 4 bytes from memory at PC, advances PC by 4.
  2. Decode β€” extracts opcode, rd, rs1, rs2, funct3, funct7, immediates.
  3. Execute β€” dispatches via a 128-entry function pointer table indexed by opcode.

RV32M Dispatch

The M extension shares opcode 0x33 with base R-type instructions. Dispatch is done inside OP() by checking funct7 == 0x01 before the RV32I switch, with correct handling of all edge cases: division by zero, signed overflow (INT_MIN / -1).

Memory (MMU)

Flat byte vector with base address 0x80000000. Supports 1, 2, and 4-byte little-endian accesses. Monitors a tohost address to detect test completion.

Test Result Detection

Tests write to the tohost symbol at the end of execution:

  • tohost == 1 β†’ PASS
  • tohost == (N << 1) | 1 β†’ FAIL, test case N

Debug TUI

Interactive terminal UI (FTXUI):

  • Left panel β€” all 32 registers with ABI names, highlighted on change
  • Center panel β€” instruction trace (last 20 instructions)
  • Right panel β€” memory hex dump around PC + test result
  • Controls: Space step, R run/pause, Q quit

Roadmap

Next β€” RV32C (Compressed Instructions)

The C extension adds 16-bit encodings for the most common instructions. Required to run binaries compiled without -mno-rvc.

  • Implement 16-bit instruction fetch and decode
  • Map all C.* instructions to their 32-bit equivalents
  • Pass rv32uc-p-* tests

Medium Term

  • Run real C programs compiled with riscv32-unknown-elf-gcc + newlib
  • Implement basic syscalls: write, exit, brk
  • GDB stub (RSP protocol) for connecting a real debugger
  • Exception/trap dispatch through mtvec

Long Term β€” Goal: Boot a Small OS

The final goal is to boot xv6-riscv or FreeRTOS on the emulator. This requires:

  • Supervisor mode + Sv32 paging (MMU with page table walker)
  • UART, CLINT, PLIC as MMIO peripherals
  • RV32A atomic instructions
  • Timer interrupts

References


License

See LICENSE.