-
-
Notifications
You must be signed in to change notification settings - Fork 1
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.
| 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-format —
opcode(6) | rs(5) | rt(5) | rd(5) | shamt(5) | funct(6) -
I-format —
opcode(6) | rs(5) | rt(5) | imm(16) -
J-format —
opcode(6) | target(26)
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.
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.
| 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 |
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:
- Freezes the
IF/IDregister (re-fetches the same instruction next cycle) - Inserts a bubble (NOP) into the
EXstage - Holds the PC constant
This inserts one stall cycle. After the stall, forwarding from MEM/WB covers the dependency.
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.
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).
After each step() call, PipelinedCpu::pipeline_state() returns a PipelineState struct containing:
- A
StageSnapshotfor 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.
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.
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.
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.
The CPU detects halt using the idiom:
loop: j loop # 0x08000000 — jump to selfWhen the PC jumps to its own address and stays there, the UI reports StepResult::Halt and stops auto-run.