Skip to content

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 lw and sw touch 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 separate CMP — 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, no PUSH/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
  • x0 is hardwired to 0 — reads always return 0; writes are ignored. It's how mv and other conveniences are synthesized from add.
  • There is no visible PC — unlike ARM's R15, the PC can't be read or written directly; only touched via auipc, jal, jalr, and branches.
  • x1-x31 are 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)