Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RISC-V

RISC-V (pronounced “risk five”) is an open, free ISA (Instruction Set Architecture) developed at UC Berkeley starting in 2010. Unlike ARM or x86, nobody owns RISC-V - it is governed by RISC-V International, a non-profit with hundreds of member companies. Anyone can implement it without paying royalties or signing NDAs.

This openness has made RISC-V attractive for research, custom silicon, embedded systems, and increasingly for general-purpose computing.

ISA structure

RISC-V is modular: a small mandatory base plus optional standard extensions.

Base ISAs

NameWidthDescription
RV32I32-bitInteger base - minimum viable ISA
RV64I64-bit64-bit integers, 32-bit compatibility mode
RV128I128-bitFuture use

Standard extensions

LetterExtensionNotes
MInteger Multiply/Dividemul, div, rem
AAtomic instructionslr, sc, amo*
FSingle-precision float32-bit IEEE 754
DDouble-precision float64-bit IEEE 754, requires F
CCompressed instructions16-bit encodings, reduces code size ~25%
VVectorSIMD-style operations
BBit manipulation
HHypervisor
ZicsrControl & Status RegistersRequired for OS
ZifenceiInstruction-fetch fenceRequired for self-modifying code

G = IMAFD + Zicsr + Zifencei - the general-purpose combination used for Linux systems.

A target is described by combining letters: rv64gc means 64-bit + G + Compressed.

Registers

RISC-V has 32 integer registers (+ 32 float registers with F/D extension):

RegisterABI nameConvention
x0zeroHardwired zero - writes ignored
x1raReturn address
x2spStack pointer
x3gpGlobal pointer
x4tpThread pointer
x5–x7t0t2Temporaries (caller-saved)
x8s0 / fpSaved / frame pointer (callee-saved)
x9s1Saved (callee-saved)
x10–x11a0a1Args / return values
x12–x17a2a7Arguments
x18–x27s2s11Saved (callee-saved)
x28–x31t3t6Temporaries (caller-saved)

Key instructions (RV64I)

# Data movement
li   a0, 42          # load immediate (pseudo, expands to lui+addi)
mv   a0, a1          # move register (pseudo: addi a0, a1, 0)
la   a0, label       # load address (pseudo)
ld   a0, 0(sp)       # load doubleword from memory
sd   a0, 0(sp)       # store doubleword to memory
lw   a0, 0(sp)       # load word (32-bit, sign-extended)
sw   a0, 0(sp)       # store word

# Arithmetic
add  a0, a1, a2      # a0 = a1 + a2
addi a0, a1, 10      # a0 = a1 + 10 (immediate)
sub  a0, a1, a2      # a0 = a1 - a2
mul  a0, a1, a2      # a0 = a1 * a2 (M extension)
div  a0, a1, a2      # a0 = a1 / a2 (M extension)

# Logical
and  a0, a1, a2
or   a0, a1, a2
xor  a0, a1, a2
sll  a0, a1, a2      # shift left logical
srl  a0, a1, a2      # shift right logical
sra  a0, a1, a2      # shift right arithmetic

# Branches (compare-and-branch, no flags register)
beq  a0, a1, label   # branch if a0 == a1
bne  a0, a1, label   # branch if a0 != a1
blt  a0, a1, label   # branch if a0 < a1 (signed)
bltu a0, a1, label   # branch if a0 < a1 (unsigned)
bge  a0, a1, label   # branch if a0 >= a1 (signed)
bgeu a0, a1, label   # branch if a0 >= a1 (unsigned)

# Jumps
jal  ra, label       # jump and link (call)
jalr ra, 0(ra)       # jump and link register (used for ret)
ret                  # return (pseudo: jalr zero, 0(ra))

Hello world (RV64 Linux)

.section .data
msg:    .string "Hello, RISC-V!\n"
len = . - msg

.section .text
.global _start
_start:
    li   a7, 64         # syscall: write
    li   a0, 1          # fd: stdout
    la   a1, msg        # buffer address
    li   a2, len        # length
    ecall

    li   a7, 93         # syscall: exit
    li   a0, 0          # status
    ecall
riscv64-linux-gnu-as -o hello.o hello.s
riscv64-linux-gnu-ld -o hello hello.o
./hello   # on RISC-V hardware or QEMU

Key differences from ARM/x86

FeatureRISC-VARM (AArch64)x86-64
Registers32 int31 int + SP16
Flags registerNoYes (NZCV)Yes
BranchesCompare-and-branchFlags-basedFlags-based
Instruction size32-bit (+ 16-bit with C)32-bit (+ 16-bit Thumb)Variable 1–15 bytes
ISA ownershipOpenArm HoldingsIntel/AMD
RoyaltiesNoneYesYes

Notable: RISC-V has no flags register. All branches explicitly compare two registers, making pipelines simpler.

Toolchain

As with ARM, there are two toolchain families:

Toolchain prefixTargetUse case
riscv32-unknown-elf-Bare-metal 32-bitMicrocontrollers, custom emulators, no OS
riscv64-linux-gnu-Linux userspace 64-bitSBCs, servers, standard OS development

Linux cross-compilation (RV64)

# Debian/Ubuntu
sudo apt install gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

# Cross-compile C
riscv64-linux-gnu-gcc -march=rv64gc -mabi=lp64d -o hello hello.c

# Rust
rustup target add riscv64gc-unknown-linux-gnu
cargo build --target riscv64gc-unknown-linux-gnu

ABI naming

ABIFloatInteger
lp64None (software float)64-bit
lp64fF (single) in FP regs64-bit
lp64dD (double) in FP regs64-bit

lp64d is standard for Linux on capable hardware.

Bare-metal (riscv32-unknown-elf)

For bare-metal RV32 targets (no OS, no libc) use the ELF toolchain:

sudo apt install gcc-riscv64-linux-gnu   # includes riscv32 multilib on some distros
# Or build from source / use a pre-built release:
# https://github.com/riscv-collab/riscv-gnu-toolchain

# Compile bare-metal RV32IM
riscv32-unknown-elf-gcc \
  -march=rv32im \
  -mabi=ilp32 \
  -nostdlib \
  -T linker.ld \
  -o program.elf startup.s main.c

# Extract raw binary (no ELF headers)
riscv32-unknown-elf-objcopy -O binary program.elf program.bin

Key flags:

  • -march=rv32im - RV32 base + M extension (no float, no C)
  • -mabi=ilp32 - 32-bit integers, software float, 32-bit pointers
  • -nostdlib - no libc, no startup files; drop this and add --specs=nano.specs to use [[development/newlib|newlib]]
  • -T linker.ld - provide your own memory layout

QEMU emulation

sudo apt install qemu-system-riscv64 qemu-user-static

# User-mode (run a single binary)
qemu-riscv64-static ./hello

# System emulation with OpenSBI + U-Boot
qemu-system-riscv64 \
  -M virt \
  -m 2G \
  -smp 4 \
  -bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.bin \
  -kernel u-boot.bin \
  -drive file=rootfs.img,format=raw,id=hd0 \
  -device virtio-blk-device,drive=hd0 \
  -nographic

Linux distributions

DistributionStatusNotes
Fedora✓ Tier 2Official since Fedora 38
Ubuntu22.04+ supports riscv64
Debianports.debian.org/debian-ports
openSUSETumbleweed
AlpineGood container base
Arch Linux RISC-VCommunity port

Hardware

Development boards

BoardSoCCPU coresNotes
SiFive HiFive UnmatchedFU7404× U74 RV64GCFirst desktop-class RISC-V
StarFive VisionFive 2JH71104× U74 RV64GCPopular, affordable
Milk-V PioneerSG204264× C920 RV64GCBHigh-core-count server
Milk-V DuoCV1800BC906 RV64GCVUltra-low-cost
Sipeed LicheePi 4ATH15204× C910 RV64GCDXVGood performance/price
PINE64 Star64JH71104× U74 RV64GC

Microcontrollers

DeviceSoCNotes
Espressif ESP32-C3/C6RISC-VWi-Fi/BT, popular for IoT
WCH CH32VRISC-VCheap, widely available
GigaDevice GD32VF103Bumblebee N200First RISC-V MCU on mass market

Privileged architecture

RISC-V defines three privilege levels:

LevelNameUse
MMachineFirmware (OpenSBI), full hardware access
SSupervisorOS kernel
UUserApplication code

OpenSBI (Open Source Supervisor Binary Interface) is the standard M-mode firmware, equivalent to UEFI/BIOS on x86 or ATF on ARM.

Boot sequence: OpenSBI → U-Boot → Linux kernel

emu - RV32IM bare-metal workbench

~/Projects/riscv/emu/ is a self-contained RV32IM emulator and workbench. It is not a QEMU wrapper - it is a full software implementation of a RISC-V CPU plus a set of memory-mapped peripherals, with both a native CLI and a WebAssembly build for in-browser use.

What it implements

  • ISA: RV32IM - 32-bit integer base + M extension (multiply/divide)
  • Privilege: Machine mode (M-mode) only - no OS, no MMU
  • Memory: Configurable 64 KB – 16 MB, little-endian
  • Peripherals (memory-mapped):
PeripheralBase addressNotes
GPIO0x10000000Input/output, edge/level interrupts
UART0x10001000TX/RX ring buffers
I2C0x10003000PCF8574 expander → 16×2 LCD display
Interrupt controller0x10005000Vectored or direct trap dispatch

Memory layout

0x00000000  RAM (default 256 KB, configurable)
0x10000000  GPIO
0x10001000  UART
0x10003000  I2C / LCD
0x10005000  Interrupt controller

Build

cd ~/Projects/riscv/emu

# Native CLI
mkdir build && cd build
cmake .. && cmake --build .
# Produces: build/riscv-emu  build/riscv-as

# WebAssembly (requires Emscripten)
./build-wasm.sh
# Produces: web/emu.js  web/emu.wasm  web/assembler.js

Running programs

# Assemble with the included Python assembler
./tools/riscv-asm.py examples/fibonacci.s examples/fibonacci.bin

# Run the binary (max 100 instructions)
./build/riscv-emu examples/fibonacci.bin 100

# Run with more memory (1 MB)
./build/riscv-emu examples/fibonacci.bin 0 1024

Output shows final register state and the LCD display buffer if used.

Bare-metal program structure

Every program needs a startup file and a linker script:

start.s - sets up the stack and clears BSS:

.section .text._start
.global _start

_start:
    la   sp, _stack_top     # load stack pointer from linker symbol

    # clear BSS
    la   t0, _bss_start
    la   t1, _bss_end
1:  beq  t0, t1, 2f
    sw   zero, 0(t0)
    addi t0, t0, 4
    j    1b
2:
    call main
    ecall                   # signal emulator to stop

linker.ld - memory layout for the emulator:

MEMORY {
    RAM (rwx) : ORIGIN = 0x00000000, LENGTH = 256K
}

SECTIONS {
    . = 0x00000000;
    .text   : { KEEP(*(.text._start)) *(.text*) } > RAM
    .rodata : { *(.rodata*) }                     > RAM
    .data   : { *(.data*) }                       > RAM
    .bss    : {
        _bss_start = .;
        *(.bss*) *(COMMON)
        _bss_end = .;
    } > RAM
    .stack (NOLOAD) : {
        . = ALIGN(16);
        _stack_top = . + 16K;
    } > RAM
}

Build and run a C program:

riscv32-unknown-elf-gcc \
  -march=rv32im -mabi=ilp32 -nostdlib \
  -T linker.ld \
  -o program.elf start.s main.c

riscv32-unknown-elf-objcopy -O binary program.elf program.bin
./build/riscv-emu program.bin

UART output

#define UART_BASE  0x10001000
#define UART_CTRL  (*(volatile uint32_t*)(UART_BASE + 0x08))
#define UART_DATA  (*(volatile uint32_t*)(UART_BASE + 0x00))

void putchar(char c) {
    UART_CTRL = 1;   // enable
    UART_DATA = c;
}

Interrupt handling

# Install trap vector
la   t0, trap_handler
csrw mtvec, t0          # set machine trap vector

# Enable machine external interrupt + global interrupts
li   t0, 0x800          # MIE.MEIE
csrw mie, t0
li   t0, 0x8            # MSTATUS.MIE
csrw mstatus, t0

trap_handler:
    csrr t0, mcause     # read cause
    # handle ...
    mret                # return from trap (restores MSTATUS.MIE)

WebAssembly / web workbench

The emulator compiles to WASM (~18 KB) and is callable from JavaScript:

createRISCVEmu().then(Module => {
    const emu = Module._emu_create(65536);       // 64 KB memory
    Module._emu_load_program(emu, ptr, size);    // load .bin
    Module._emu_run(emu, 1000);                  // run ≤1000 instructions
    const a0 = Module._emu_get_register(emu, 10);
    const lcd = Module.UTF8ToString(Module._emu_get_lcd_line1(emu));
    Module._emu_destroy(emu);
});

Open web/index.html locally to use the full workbench (editor + assembler + emulator + LCD display).

Resources