RISC-V ISA Tutorial
A quick run-through to get you writing RISC-V assembly fast if you already know ARMv7M. Full reference pages following CG3207 slides can be found on the left menu.
The RISC-V mental model
- Load/store architecture — Only
lwandswtouch memory. Every other instruction operates purely register-to-register/ immediate; there's no mixing of memory access with computation or branching. - No flags register — Compare and branch are fused into one instruction:
beq,bne,blt,bge, … There is no separateCMP— the comparison happens inside the branch itself. - ≤ 2 source registers, ≤ 1 destination — Every instruction reads at
most two registers and writes at most one. This is why there's no
LDM/STM, noPUSH/POP, no pre/post-index, single-instruction long multiplies, and no multiply-accumulate — and why the ISA stays simple and regular.
Registers
| Register | ABI Name | Description | Saver |
|---|---|---|---|
x0 |
zero |
Zero constant | — |
x1 |
ra |
Return address | Caller |
x2 |
sp |
Stack pointer | Callee |
x3 |
gp |
Global pointer | — |
x4 |
tp |
Thread pointer | — |
x5–x7 |
t0–t2 |
Temporaries | Caller |
x8 |
s0 / fp |
Saved / frame pointer | Callee |
x9 |
s1 |
Saved register | Callee |
x10–x11 |
a0–a1 |
Fn args / return values | Caller |
x12–x17 |
a2–a7 |
Fn args | Caller |
x18–x27 |
s2–s11 |
Saved registers | Callee |
x28–x31 |
t3–t6 |
Temporaries | Caller |
x0is hardwired to 0 — reads always return 0; writes are ignored. It's howmvand other conveniences are synthesized fromadd.- There is no visible PC — unlike ARM's
R15, the PC can't be read or written directly; only touched viaauipc,jal,jalr, and branches. x1-x31are general-purpose at ISA / hardware level. They acquire special purposes only at ABI level.
Full ABI table and details: Registers.
Instruction syntax
add t0, t1, t2 # t0 = t1 + t2 (data processing register type)
addi t0, t0, 1 # t0 = t0 + 1 (data processing register immediate type)
lw t0, 0(s0) # t0 = Mem[s0+0] (memory read)
sw t0, 4(s0) # Mem[s0+4] = t0 (memory write)
addi/lw/sw take a plain 12-bit immediate; no leading #, unlike ARM.
| . | Example | Cond.? | Links? | Target |
|---|---|---|---|---|
| Branch | blt t0,t1,LBL |
Yes | No | Label |
Jump — jal |
jal ra, LBL |
No | Yes (rd) | Label |
Jump-reg — jalr |
jalr ra,0(t0) |
No | Yes (rd) | Reg + offset |
Rule of thumb
Branches ask a question and can go either way - taken or not taken; jal/jalr
is always taken, and can remember where they came from (link).
Full instruction tables: Data Processing · Memory · Control.
Constants and addresses
lui — load upper immediate
lui rd, imm20 # rd = imm20 << 12
Sets bits [31:12] of rd directly; the lower 12 bits are cleared. The
only base instruction that writes a register's upper bits.
auipc — add upper immediate to PC
auipc rd, imm20 # rd = PC + (imm20 << 12)
Builds a PC-relative base address, 4 KiB-page at a time — RISC-V's mechanism for position-independent addressing.
No ARM equivalent
ARM builds large constants via a PC-relative load
(LDR Rd, =const), where the const can be a number or an address (label). In RISC-V, every 32-bit
number or address is built from lui/auipc + addi.
li rd, imm → lui rd, imm[31:12]
addi rd, rd, imm[11:0]
(li emits just addi if the constant fits in 12 bits)
la rd, LBL → auipc rd, Δ[31:12]
addi rd, rd, Δ[11:0]
(Δ is PC-relative, so the code stays position-independent)
Putting it together
# if (x > 0)
blez t0, else # branch to else if x <= 0
addi t1, t1, 1 # y++
j endif # skip else block
else:
addi t1, t1, -1 # y--
endif:
li t0, 10 # count = 10
loop:
beqz t0, done # continue if count > 0; exit if count == 0
addi t0, t0, -1 # count--
j loop
done:
# caller
li a0, 4 # arg0
li a1, 7 # arg1
call add2 # result = add2(arg0, arg1);
# result in a0
# rest of the caller statements, with a jump at the end
add2: # callee
add a0, a0, a1
ret
Notice
All three patterns use only branches, jumps, and plain register moves — no stack, no flags, no hidden state.
Full worked examples, including a stack-based function call: RISC-V Assembly.
Quick reference
op rd, rs1, rs2 # DP, register
op rd, rs1, imm # DP, immediate
op rd, imm(rs1) # load
op rs2, imm(rs1) # store
op rs1, rs2, LABEL # branch
jal rd, LABEL # jump + link
jalr rd, imm(rs1) # jump + link, register target
li rd, imm # lui rd, imm[31:12] ; addi rd, rd, imm[11:0]
la rd, LABEL # auipc rd, Δ[31:12] ; addi rd, rd, Δ[11:0]
lw rd, LABEL # auipc rd, Δ[31:12] ; lw rd, Δ[11:0](rd)
sw rs, LABEL, rt # auipc rt, Δ[31:12] ; sw rs, Δ[11:0](rt)
mv rd, rs # addi rd, rs, 0 - can be assembler-dependent
j LABEL # jal x0, LABEL
ret # jalr x0, 0(x1)
call LABEL # auipc x1, Δ[31:12] ; jalr x1, Δ[11:0](x1)
nop # addi x0, x0, 0 - can be assembler-dependent
beqz rs, LABEL # beq rs, x0, LABEL
# Δ = the PC-relative delta (offset) to the label: LABEL - pc
# (where pc is the address of the auipc)
Links and references
- Some tables are based on the work by (c) James Zhu. Modifications/corrections have been made to the original tables. The modified LaTeX files are available on request.
- RISC-V reference card — a fantastic reference card, which is almost all we need.
- RISC-V official specifications
- Wikipedia article on RISC-V — has lots of info.
- riscv.org — the RISC-V website.