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 simulatorthis 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 x0–x31. 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, 5is how you load a constant, andadd x3, x2, x0is 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 at0x7FFFFFFC, 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.
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:
| Region | Starts at | Used for |
|---|---|---|
| Text | 0x00000000 | your instructions, 4 bytes each |
| Data | 0x10000000 | .data — anything declared with .word etc. |
| Stack | 0x7FFFFFFC (top) | grows downward; sp starts here |
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 simulatorthe "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:
| Stage | Name | What happens |
|---|---|---|
IF | Fetch | read the instruction word at pc |
ID | Decode | decode fields, read the two source registers |
EX | Execute | ALU work: arithmetic, address math, branch decisions |
MEM | Memory | loads read, stores write |
WB | Writeback | the 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 simulatorthis 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 → stallTry 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 simulatorrun 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.
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
| Register | ABI name | Role |
|---|---|---|
x0 | zero | always 0 |
x1 | ra | return address |
x2 | sp | stack pointer (starts at 0x7FFFFFFC) |
x3, x4 | gp, tp | global / thread pointer |
x5–x7, x28–x31 | t0–t6 | temporaries (caller-saved) |
x8, x9, x18–x27 | s0–s11 | saved (callee-saved) |
x10–x17 | a0–a7 | arguments and return values |
Instruction families (RV32IM)
| Family | Instructions | Notes |
|---|---|---|
| Arithmetic | add sub addi | addi rd, x0, n loads a constant |
| Logic & shifts | and or xor sll srl sra (+ i forms) | sra preserves the sign bit |
| Compare | slt sltu slti sltiu | result is 0 or 1 |
| Multiply / divide | mul mulh div divu rem remu | the M extension |
| Memory | lw lh lb lhu lbu sw sh sb | address = register + offset |
| Branches | beq bne blt bge bltu bgeu | compare two registers, jump to a label |
| Jumps | jal jalr (pseudo: call ret j) | calls save pc+4 into ra |
| Constants | lui auipc (pseudo: li la mv) | build 32-bit constants and addresses |
| System | ecall ebreak | ends the program here; result convention: a0 |
Memory map
| Address | Region |
|---|---|
0x00000000 | text — instructions |
0x10000000 | data — .data section |
0x7FFFFFFC | initial sp; stack grows down |
Pipeline costs at a glance
| Event | Cost | Shown as |
|---|---|---|
| Forwarded dependency | 0 cycles | green arrow + value chip |
| Load-use dependency | 1 cycle stall | yellow bubble in EX |
| Correctly predicted branch | 0 cycles | predictor chip, ✓ |
| Mispredicted branch | 2 cycles flushed | red flushed boxes, ✗ |