StudyRISC-V docs

RISC-V, from zero to pipeline

A guide that starts at "what is an instruction?" and ends with you reading branch-predictor state off a live pipeline. Every concept links to a simulator demo you can step through one clock cycle at a time — because the pipeline makes far more sense when you can watch it.

Why RISC-V?

Every processor speaks an instruction set architecture (ISA): the vocabulary of operations its hardware understands. For decades the dominant ISAs — x86 and Arm — have been proprietary. You can write software for them, but you cannot legally build a chip that speaks them without a license, and you cannot read a clean specification without wading through decades of accumulated history. x86 still carries instructions from 1978.

RISC-V, started at UC Berkeley in 2010, took the opposite path on both counts. It is open: the specification is free for anyone to implement, which is why it has spread from university labs into real silicon — storage controllers, microcontrollers, AI accelerators, and full application processors. And it is clean: the base integer ISA (RV32I) has just 47 instructions, designed in one coherent pass rather than accreted over four decades, with optional extensions (like M, the multiply/divide extension this simulator supports) layered on modularly.

That combination is what makes RISC-V the best ISA to learn on. When a textbook says "the processor decodes the instruction," RISC-V is simple enough that you can look at the 32 bits and decode them yourself. And the concepts — registers, loads and stores, pipelining, hazards, prediction — transfer directly to x86 and Arm, because those machines internally break their complex instructions into RISC-like operations anyway. Learning RISC-V is learning what processors actually do, minus the trivia.

What this simulator is for

A textbook can tell you that a pipelined processor overlaps five instructions, that a load followed immediately by a use of its result forces a one-cycle stall, and that a mispredicted branch throws away two cycles of work. Those are true sentences — and they slide right off the first ten times you read them.

The StudyRISC-V simulator supplies the missing feedback loop: you write real RV32IM assembly, press Step, and watch each instruction physically move through Fetch → Decode → Execute → Memory → Writeback, one clock cycle per keypress. When a hazard happens you don't read about it — you see the bubble appear, watch the forwarded value travel backwards along the green arrow, see mispredicted instructions flushed in red. You can even step backwards to replay the exact cycle that surprised you.

Concretely, after working through this page alongside the simulator, you will be able to:

  • read and write basic RV32IM assembly with confidence;
  • predict, before pressing Step, where a program will stall and why;
  • explain forwarding paths and count the cycles they save;
  • trace a 2-bit branch predictor's state across a loop's iterations;
  • compute and interpret CPI (cycles per instruction) for real code.

The honest boundary: this simulator deliberately models the classic in-order 5-stage pipeline — the one every architecture course starts with — not caches or out-of-order execution. It is the foundation those topics are built on, made visible.

What is an instruction?

By the time a program reaches the processor, it is a list of numbers in memory. On RV32, each 32-bit number encodes one instruction: a single small command like "add these two registers" or "load a word from this address." The processor's whole job is a loop: fetch the next number, decode what it means, do it, repeat — billions of times a second.

Here is one instruction, written in the assembly notation humans use:

addi x1, x0, 5   # x1 = x0 + 5

Read it as: "take the value in register x0, add the constant 5, put the result in register x1." Three parts recur everywhere: the mnemonic (addi, add-immediate), the register operands, and sometimes an immediate — a constant baked into the instruction itself. The assembler turns this line into the word 0x00500093; destination, source, and constant each occupy fixed bit positions, which is exactly what makes RISC-V fast to decode.

Your first program

addi x1, x0, 5    # x1 = 5
addi x2, x1, 3    # x2 = x1 + 3 = 8
add  x3, x2, x1   # x3 = x2 + x1 = 13

Notice that each instruction uses the result of the one before it. Hold that thought — in the pipeline sections it becomes the single most important pattern in this guide.

Try it in the simulator
this exact program is the "Add + forwarding" sample — press Step and watch each instruction enter the pipeline

Registers

Registers are the processor's hands: 32 small, blazingly fast storage slots, each holding one 32-bit value, wired directly into the arithmetic hardware. Main memory is vast but slow to reach; registers are tiny but right there. Nearly every instruction reads its inputs from registers and writes its result to one. Computation in the RISC model means: bring values into registers, work on them there, put results back.

RISC-V names them x0x31. Two deserve attention immediately:

  • x0 (zero) is hard-wired to 0: writes vanish, reads give 0. That sounds useless and is anything but — addi x1, x0, 5 is how you load a constant, and add x3, x2, x0 is a register copy.
  • x2 (sp) is by convention the stack pointer. The hardware doesn't care, but all software agrees — in this simulator it starts at 0x7FFFFFFC, the top of stack memory.

"By convention" is doing real work there: registers also carry ABI names describing their role in function calls — a0–a7 for arguments and return values, t0–t6 for temporaries, s0–s11 for values a function must preserve, ra for the return address. The simulator's Registers tab shows both namings; the full table is in the reference.

Try it in the simulator
open Registers & Memory, run the sample, and watch x1, x2, x3 flash green as each write lands

Memory, loads, and stores

Thirty-two registers can't hold your data. Everything else — arrays, strings, the stack — lives in memory, one huge byte-addressed array. RISC-V is a load/store architecture: arithmetic instructions never touch memory. Data moves between memory and registers through exactly two instruction families:

lw  t3, 0(t0)    # load word:  t3 = memory[t0 + 0]
sw  t3, 8(t0)    # store word: memory[t0 + 8] = t3

The address is always "a register plus a small constant offset." That's how arrays work: keep the base address in a register and either vary the offset (0(t0), 4(t0), 8(t0)…) or advance the base with addi t0, t0, 4 as you walk. Words are 4 bytes, so consecutive elements sit 4 addresses apart.

This simulator lays out memory the way its programs expect:

RegionStarts atUsed for
Text0x00000000your instructions, 4 bytes each
Data0x10000000.data — anything declared with .word etc.
Stack0x7FFFFFFC (top)grows downward; sp starts here
Try it in the simulator
the "Array sum" sample walks four words in .data — watch t0 stride through addresses in the Memory panel

Branches and loops

So far every program runs top to bottom. Real programs decide and repeat, and in assembly both come from one mechanism: the conditional branch — compare two registers and, if the comparison holds, continue at a labeled instruction instead of the next one.

addi x1, x0, 3     # x1 = 3 (loop counter)
addi x2, x0, 0     # x2 = 0 (accumulator)
loop:
addi x2, x2, 1     # x2 += 1
addi x1, x1, -1    # x1 -= 1
bne  x1, x0, loop  # if x1 != 0, go back to loop

bne is "branch if not equal." Together with beq, blt/bge, a label, and a counter, this builds every if, while, and for you have ever written — not as a metaphor; it is literally what your compiler emits. The simulator's Translate tab will show you this loop reconstructed as do { … } while (x1 != 0); pseudocode.

Branches also introduce the pipeline's hardest problem: the processor must guess which way a branch goes before it knows. That story gets its own section.

Try it in the simulator
the "Branch prediction" sample is this exact loop — step it and watch the branch resolve three times

The pipeline: five stages

Executing one instruction involves several distinct jobs: fetch it, decode it and read its registers, do the arithmetic, touch memory if it's a load or store, write the result back. Five steps. A naive processor does all five, then starts the next instruction: one instruction finished every five cycles.

The pipelined insight is that those five jobs use different hardware. While instruction A does arithmetic, instruction B can be decoding and C can be getting fetched. It's a laundromat: you don't wait for the first load to finish drying before starting the second wash. With the pipeline full, one instruction finishes every cycle, even though each still takes five cycles start to end.

The five stages, as the simulator draws them left to right:

StageNameWhat happens
IFFetchread the instruction word at pc
IDDecodedecode fields, read the two source registers
EXExecuteALU work: arithmetic, address math, branch decisions
MEMMemoryloads read, stores write
WBWritebackthe result lands in the destination register

The scoreboard for all of this is CPI — cycles per instruction. A perfect pipeline scores 1.0; real code scores worse, and the reasons are precisely the next two sections. The simulator computes CPI live in its stats row — watching it move as you add hazards to a program is the fastest intuition-builder there is.

Try it in the simulator
this link pre-steps 3 cycles — three instructions in flight at once, each in a different stage

Hazards and forwarding

Overlap five instructions and a problem appears immediately. The first sample again:

addi x1, x0, 5
addi x2, x1, 3   # needs x1 — which hasn't reached Writeback yet!

When the second instruction reaches EX and needs x1, the first is only in MEM — its result won't be written to the register file for two more cycles. Reading the register file would return the old value. This is a data hazard (read-after-write), and it is not an edge case: real code is exactly this, each line building on the last.

Forwarding: the fix that usually works

The result of addi x1, x0, 5 exists the moment EX computes it — it just hasn't been filed. So the hardware adds shortcuts: wires from the EX/MEM and MEM/WB pipeline registers straight back into EX's inputs, skipping the register file. That is forwarding (also called bypassing). No time is lost; the value takes a shorter road. In the simulator it's the green arrow beneath the pipeline, with a chip showing the actual value in transit.

The load-use stall: the case forwarding can't fix

One hazard beats forwarding. A load's value doesn't exist until MEM — but the next instruction wants it in EX at that same moment. No wire moves data backwards in time, so the pipeline does the only honest thing: it stalls. The dependent instruction holds in ID for one cycle while a bubble drifts through EX; then MEM→EX forwarding finishes the job. One cycle lost per load-use pair — which is why compilers try to schedule an unrelated instruction between a load and its first use.

lw  x1, 0(x2)    # x1 arrives from memory in MEM
add x3, x1, x4   # needs x1 in EX one cycle too early → stall
Try it in the simulator
the "Load-use stall" sample — step until the yellow bubble appears in EX, then watch forwarding deliver x1 the next cycle

Branch prediction

Data hazards are about values arriving late; control hazards are about not knowing which instruction comes next. A branch decides in EX — but by then two younger instructions have already been fetched and decoded. Fetched from where? The pipeline had to pick a direction before the answer existed.

So it guesses — systematically. This simulator implements the classic scheme: a 2-bit saturating counter per branch, tracked in a branch target buffer. Four states: strongly not-taken, weakly not-taken, weakly taken, strongly taken. Every actual taken outcome nudges the counter up; every not-taken nudges it down; the prediction is whichever half the counter sits in. The two-bit design means one surprise doesn't flip the prediction — a loop that branches back 99 times and exits once mispredicts twice total, not three times.

A correct guess costs nothing — the pipeline never breaks stride. A wrong guess means the two wrong-path instructions already in flight are flushed — discarded before they can change any state — and fetch restarts on the correct path: a two-cycle penalty, drawn in red in the simulator.

Watch the three-iteration loop: the predictor starts cold (mispredicts the first taken branch), learns (predicts taken correctly as the counter saturates), then mispredicts once more on the final not-taken exit. Cold miss, learned hits, exit miss — that rhythm is the entire soul of branch prediction, visible in about fifteen clock cycles.

Try it in the simulator
run the loop at 5 cyc/s and watch the predictor meter fill as the counter learns — then the red flush at loop exit

Seeing C become assembly

Once the pipeline makes sense instruction by instruction, the next connection is where instructions come from. The simulator's language selector accepts a real subset of C — variables, arrays, loops, functions, recursion — compiles it to RV32IM with a built-in translator, and runs it on the same pipeline as everything else. The Translate tab shows the generated assembly; the reverse view shows RISC-V in the editor reconstructed as annotated C-style pseudocode, loops and if/else recovered from the branches.

This closes a loop most courses leave open: for (i = 0; i < 4; i++) sum += a[i]; stops being abstract when you watch it become a label, a lw, an add, and a bne — and then watch those stall and forward their way through the pipeline.

Try it in the simulator
open the Translate tab — or pick C in the language menu and run gcd(48, 18) end to end

The riscvsim CLI

Everything above also runs in your terminal. The open-source riscvsim CLI embeds the exact same execution engine as this site — same assembler, same pipeline, same hazard logic, one shared Rust crate — so results always agree:

$ riscvsim run add.s        # assemble, run, print registers + stats
$ riscvsim tui add.s        # full interactive pipeline TUI in the terminal
$ riscvsim serve add.s      # step through a local session in the browser

Install from the homepage (curl script or Homebrew). The TUI gives you the same five stages, forwarding animation, registers, and memory — clickable, in a terminal.

Quick reference

Registers and ABI names

RegisterABI nameRole
x0zeroalways 0
x1rareturn address
x2spstack pointer (starts at 0x7FFFFFFC)
x3, x4gp, tpglobal / thread pointer
x5x7, x28x31t0t6temporaries (caller-saved)
x8, x9, x18x27s0s11saved (callee-saved)
x10x17a0a7arguments and return values

Instruction families (RV32IM)

FamilyInstructionsNotes
Arithmeticadd sub addiaddi rd, x0, n loads a constant
Logic & shiftsand or xor sll srl sra (+ i forms)sra preserves the sign bit
Compareslt sltu slti sltiuresult is 0 or 1
Multiply / dividemul mulh div divu rem remuthe M extension
Memorylw lh lb lhu lbu sw sh sbaddress = register + offset
Branchesbeq bne blt bge bltu bgeucompare two registers, jump to a label
Jumpsjal jalr (pseudo: call ret j)calls save pc+4 into ra
Constantslui auipc (pseudo: li la mv)build 32-bit constants and addresses
Systemecall ebreakends the program here; result convention: a0

Memory map

AddressRegion
0x00000000text — instructions
0x10000000data — .data section
0x7FFFFFFCinitial sp; stack grows down

Pipeline costs at a glance

EventCostShown as
Forwarded dependency0 cyclesgreen arrow + value chip
Load-use dependency1 cycle stallyellow bubble in EX
Correctly predicted branch0 cyclespredictor chip, ✓
Mispredicted branch2 cycles flushedred flushed boxes, ✗