Chapter 13: RISC-V Pseudo-Instructions

September 17, 2026 · View on GitHub

Introduction

Pseudo-instructions are convenience mnemonics that the assembler translates into one or more real machine instructions. They make assembly code more readable without adding new hardware features. RISC-V relies heavily on pseudo-instructions because the base ISA is deliberately minimal — many common patterns require specific register choices or instruction combinations that pseudo-instructions abstract away. This chapter catalogs every pseudo-instruction used in our blink driver.

li — Load Immediate

  li    t0, XOSC_STARTUP                         # load XOSC_STARTUP address

li loads an arbitrary 32-bit constant into a register. The assembler chooses the shortest expansion:

Value RangeExpansionInstructions
-2048 to 2047addi rd, x0, imm1
Upper 20 bits only (lower 12 = 0)lui rd, imm201
Arbitrary 32-bitlui rd, upper20 + addi rd, rd, lower122

Examples from our firmware:

  li    a2, 16                                   # addi a2, x0, 16
  li    t0, 3600                                 # lui t0, 1 + addi t0, t0, -496
  li    t0, XOSC_BASE                            # lui t0, 0x40048 + addi t0, t0, 0
  li    t1, 0x00FABAA0                           # lui t1, upper + addi t1, t1, lower

li is the most frequently used pseudo-instruction in our firmware — it appears in every source file.

la — Load Address

  la    t0, Default_Trap_Handler                 # trap target

la loads the address of a symbol using PC-relative addressing. It expands to:

auipc  t0, %pcrel_hi(Default_Trap_Handler)
addi   t0, t0, %pcrel_lo(Default_Trap_Handler)

Unlike li, which encodes an absolute value, la computes the address relative to the current PC. Our firmware uses la in Init_Trap_Vector to load the trap handler address.

call — Function Call

  call  GPIO_Config                              # call GPIO_Config

call performs a function call by saving the return address in ra and jumping to the target. It expands to:

auipc  ra, %pcrel_hi(GPIO_Config)
jalr   ra, %pcrel_lo(GPIO_Config)(ra)

For nearby targets (within ±1 MB), the assembler may optimize to a single jal ra, offset.

Every function invocation in our firmware uses call:

CallerTargetFile
Reset_HandlerInit_Stackreset_handler.s
Reset_HandlerInit_Trap_Vectorreset_handler.s
Reset_HandlerInit_XOSCreset_handler.s
Reset_HandlerEnable_XOSC_Peri_Clockreset_handler.s
Reset_HandlerInit_Subsystemreset_handler.s
Reset_HandlerEnable_Coprocessorreset_handler.s
mainGPIO_Configmain.s
mainGPIO_Setmain.s
mainGPIO_Clearmain.s
mainDelay_MSmain.s

ret — Return from Function

  ret                                            # return to caller

ret returns to the caller by jumping to the address in ra. It expands to:

jalr  x0, 0(ra)

Writing to x0 discards the link — this is a pure jump, not a call. Every function in our firmware ends with ret.

j — Unconditional Jump

  j     .Loop                                    # loop forever

j performs an unconditional jump without saving a return address. It expands to:

jal  x0, offset

Our firmware uses j in three places:

  1. j .Loop in main.s — the infinite blink loop
  2. j main in reset_handler.s — entering main (no return needed)
  3. j Default_Trap_Handler — infinite loop in the trap handler

not — Bitwise NOT

  not   t2, t2                                   # t2 = ~t2

not inverts all bits of a register. It expands to:

xori  t2, t2, -1

Since -1 in two's complement is 0xFFFFFFFF, XOR with -1 flips every bit.

In reset.s, not creates a clear mask:

  li    t2, (1<<6)                               # IO_BANK0 reset mask
  not   t2, t2                                   # invert: 0xFFFFFFBF
  and   t1, t1, t2                               # clear IO_BANK0 bit

Branch Pseudo-Instructions

beqz — Branch if Zero

  beqz  t1, .GPIO_Subsystem_Reset_Wait           # loop if bit not set

Expands to beq t1, x0, label.

bnez — Branch if Not Zero

  bnez  t1, .Delay_MS_Loop                       # loop until counter reaches 0

Expands to bne t1, x0, label.

bgez — Branch if Greater or Equal to Zero

  bgez  t1, .Init_XOSC_Wait                      # loop if bit 31 is clear

Expands to bge t1, x0, label.

blez — Branch if Less or Equal to Zero

  blez  a0, .Delay_MS_Done                       # if ms <= 0, skip

Expands to bge x0, a0, label. Note the operand swap: the base instruction tests x0 >= a0, which is equivalent to a0 <= 0.

CSR Pseudo-Instructions

csrw — Write CSR

  csrw  mtvec, t0                                # mtvec = t0

Expands to csrrw x0, mtvec, t0. The csrrw instruction atomically swaps the CSR value with the register, but by writing to x0 the old value is discarded — making this a pure write.

Our firmware uses csrw once, in Init_Trap_Vector, to set the machine trap vector.

Complete Pseudo-Instruction Reference

Pseudo-instructionExpansionUsed In
li rd, immlui+addi or addiAll files
la rd, symbolauipc+addireset_handler.s
call labelauipc ra+jalr rareset_handler.s, main.s
retjalr x0, 0(ra)All functions
j labeljal x0, offsetmain.s, reset_handler.s
not rd, rsxori rd, rs, -1reset.s
beqz rs, labelbeq rs, x0, labelreset.s
bnez rs, labelbne rs, x0, labeldelay.s
bgez rs, labelbge rs, x0, labelxosc.s
blez rs, labelbge x0, rs, labeldelay.s
csrw csr, rscsrrw x0, csr, rsreset_handler.s

Why Pseudo-Instructions Matter

Without pseudo-instructions, the programmer would need to write:

lui   t0, %hi(0x40048000)
addi  t0, t0, %lo(0x40048000)

instead of:

  li    t0, XOSC_BASE                            # load XOSC base address

And:

auipc ra, %pcrel_hi(GPIO_Set)
jalr  ra, %pcrel_lo(GPIO_Set)(ra)

instead of:

  call  GPIO_Set                                 # call GPIO_Set

Pseudo-instructions keep the source readable while the assembler generates optimal machine code.

Summary

  • li and la load constants and addresses — the most common pseudo-instructions.
  • call and ret implement function call and return using ra.
  • j provides unconditional jumps without saving a return address.
  • not inverts all bits via XOR with -1.
  • Branch pseudo-instructions (beqz, bnez, bgez, blez) compare against x0.
  • csrw writes control/status registers.
  • Pseudo-instructions make RISC-V assembly readable while expanding to optimal base instructions.