Skip to content

MIPS CPU Emulator

Kevin Henderson edited this page Jul 1, 2026 · 4 revisions

MIPS CPU Emulator

clearCore simulates a subset of the 32-bit MIPS ISA. Two CPU implementations coexist behind the IProcessor interface: a simple single-cycle datapath and a full 5-stage pipeline.


Supported ISA subset

Category Instructions
Arithmetic ADD, ADDU, SUB, SUBU, ADDI, ADDIU
Logic AND, OR, NOR, XOR, ANDI, ORI, XORI
Shifts SLL, SRL, SRA, SLLV, SRLV, SRAV
Comparison SLT, SLTU, SLTI, SLTIU
Memory LW, SW, LH, LHU, SH, LB, LBU, SB
Branches BEQ, BNE, BGTZ, BLEZ, BGEZ, BLTZ
Jumps J, JAL, JR, JALR
Upper immediate LUI

The decoder handles all three MIPS instruction formats:

  • R-formatopcode(6) | rs(5) | rt(5) | rd(5) | shamt(5) | funct(6)
  • I-formatopcode(6) | rs(5) | rt(5) | imm(16)
  • J-formatopcode(6) | target(26)

Single-cycle CPU

SingleCycleCpu simulates the full datapath in a single step() call:

Fetch → Decode → Control → Execute → Memory → Writeback

Every instruction takes exactly one cycle. This makes the control flow easy to follow and is the recommended starting point when learning the emulator.

The control unit generates all signals (RegWrite, MemRead, MemWrite, Branch, Jump, ALUSrc, ALUOp, MemToReg, RegDst) from the decoded opcode/funct pair via derive_control(). The ALU result and memory data are computed in the same call and written back to the register file before step() returns.


Pipelined CPU

PipelinedCpu runs five instructions concurrently, one per stage:

Cycle N:    IF      ID      EX      MEM     WB
Cycle N+1:  IF      ID      EX      MEM     WB
              ↑       ↑       ↑       ↑       ↑
          instr5  instr4  instr3  instr2  instr1

Each stage reads from a pipeline register (a struct holding the outputs of the previous stage) and writes into the next one. Between cycles, the pipeline registers shift forward.

Pipeline registers

Register Contents
IF/ID Fetched instruction word, incremented PC
ID/EX Decoded register values, sign-extended immediate, control signals
EX/MEM ALU result, zero flag, forwarded operands, control signals
MEM/WB Memory read data or ALU result, destination register, control signals

Hazard detection

Load-use stall

When a LW instruction is in EX and the immediately following instruction reads the register being loaded, the pipeline cannot forward in time (the memory value won't exist until the end of the MEM stage). The hazard unit:

  1. Freezes the IF/ID register (re-fetches the same instruction next cycle)
  2. Inserts a bubble (NOP) into the EX stage
  3. Holds the PC constant

This inserts one stall cycle. After the stall, forwarding from MEM/WB covers the dependency.

Branch and jump flush

Branches are resolved in the EX stage. When a branch is taken (or an unconditional jump is decoded), instructions already in IF and ID are invalid. The hazard unit flushes those two stages by zeroing the pipeline registers, discarding two in-flight instructions.

Data forwarding

Forwarding eliminates most stalls by routing a result directly to the ALU input of a dependent instruction before it has been written back to the register file.

Path When it fires
EX/MEM → EX The instruction in EX reads a register that the instruction in EX/MEM just computed
MEM/WB → EX The instruction in EX reads a register that the instruction in MEM/WB produced

The forwarding unit checks both paths every cycle and selects the correct operand source for each ALU input (register file, EX/MEM forward, or MEM/WB forward).


PipelineState snapshot

After each step() call, PipelinedCpu::pipeline_state() returns a PipelineState struct containing:

  • A StageSnapshot for each of the five stages (PC, instruction word, decoded fields, control signals)
  • stall — whether a stall bubble was inserted this cycle
  • forward_ex_mem / forward_mem_wb — which forwarding paths fired
  • flush — whether a branch/jump flush occurred

Both UIs read this struct to drive their visualizations. No additional state extraction is needed.


Execution trace

The CPU maintains an 8-entry ring buffer of the last committed instructions (sourced at the WB stage). The TUI renders this as a scrolling execution history panel; the Qt6 GUI shows it in the Pipeline Trace tab.


Telemetry

Each step() increments internal counters:

Counter Meaning
tel_cycles Total clock cycles elapsed
tel_stalls Stall bubbles inserted
tel_forwards Forwarding operations performed
tel_flushes Pipeline stages flushed (branch/jump)

CPI is derived as tel_cycles / instructions_committed. Counters reset on reset() or when the CPU mode switches.


Loading programs

Programs are flat arrays of 32-bit instruction words. Load them via:

  • TUI Tab 3 — paste hex words one per line
  • Qt6 Code Editor — write MIPS assembly; the in-app assembler (label + branch support) emits the word array
  • IProcessor::load(std::vector<uint32_t>) — call directly from C++ for testing

Memory is byte-addressable. The program is placed starting at address 0x00000000. The stack conventionally grows down from 0xFFFFFFFF.


Halt detection

The CPU detects halt using the idiom:

loop: j loop   # 0x08000000 — jump to self

When the PC jumps to its own address and stays there, the UI reports StepResult::Halt and stops auto-run.

Clone this wiki locally