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
| Group | Instructions |
|---|---|
| LUI / AUIPC | LUI, AUIPC |
| OP-IMM | ADDI, 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 |
| LOAD | LB, LH, LW, LBU, LHU |
| STORE | SB, SH, SW |
| BRANCH | BEQ, BNE, BLT, BGE, BLTU, BGEU |
| JAL / JALR | JAL, JALR |
| SYSTEM | ECALL, EBREAK, MRET, CSRRW, CSRRS, CSRRC (+ I variants) |
| MISC-MEM | FENCE (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
| Test | Status |
|---|---|
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
| Test | Status |
|---|---|
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.10g++with C++17 supportftxui(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:
- Fetch β reads 4 bytes from memory at PC, advances PC by 4.
- Decode β extracts opcode, rd, rs1, rs2, funct3, funct7, immediates.
- 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β PASStohost == (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:
Spacestep,Rrun/pause,Qquit
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
- Official RISC-V Specification
- riscv-tests on GitHub
- RISC-V ISA Reference Card
- RV32I Unprivileged Spec (PDF)
- FTXUI β Terminal UI library
License
See LICENSE.