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
x5x7 t0t2 Temporaries Caller
x8 s0 / fp Saved / frame pointer Callee
x9 s1 Saved register Callee
x10x11 a0a1 Fn args / return values Caller
x12x17 a2a7 Fn args Caller
x18x27 s2s11 Saved registers Callee
x28x31 t3t6 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
Jumpjal jal ra, LBL No Yes (rd) Label
Jump-regjalr 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)