Easy RISC-V



Easy RISC-V



Easy RISC-V

(Last updated: 2025-10-27 14:51)

(Emulators disabled version)

This page is not designed to be used on a narrow screen or without
CSS. If you’re having issues using the emulator, try the emulators disabled version.

An interactive introduction to RISC-V assembly programming, by dramforever.

Interested in the code? Want to report an issue? Check out the GitHub
page: https://github.com/dramforever/easyriscv

Inspired by Easy
6502 by Nick Morgan
, this is a quick-ish introductory tutorial to
RISC-V assembly programming. This tutorial is intended for those with a
basic familiarity with low level computer science concepts, but
unfamiliar with RISC-V. If you’re curious about RISC-V, I hope this will
be a good start to your journey to learning about it.

RISC-V (pronounced “risk-five”), as its name suggests, is RISC
(Reduced instruction set computer)
architecture. Having started its
life at UC Berkerley, RISC-V has bred a lively community of students,
researchers, engineers and hobbyists working on software and hardware.
Some highlights of RISC-V include:

  • Clean design: Although loosely based on many previous designs,
    RISC-V is at its core a new and clean design. It does away with integer
    status flags like “carry” or “overflow”, and does not have MIPS’s branch
    delay slots. RISC-V is designed primarily as a target for compilers, but
    writing RISC-V assembly by hand is still quite pleasant.
  • Open standard: RISC-V specifications are developed publicly and
    anyone can use them without copyright or patent licensing issues. Many
    researchers and companies around the world have made their own RISC-V
    processor cores and chips based on these specificaions.
  • Community support: If you want to make your own processors, rather
    than paying a hefty license fee to Arm, or designing your own
    architecture, you can just use RISC-V. Using RISC-V instead of a custom
    architecture allows you to make use of the existing and growing software
    ecosystem instead of having to maintain your own.

RISC-V is less mature than more established architectures like x86 or
Arm, but it is quickly gaining steam and has found great success in many
areas of application, such as embedded systems, custom processors,
education, and research.

This article will cover the 32-bit bare bones RV32I_Zicsr instruction
set with a tiny subset of the privileged architecture. You’ll probably
never find a “real” chip with such bare bones instruction support. Most
of them will have more extensions for other features like
floating point or compressed instructions. However, I would still
consider what we have here a “complete” instruction set. For example,
Rust has Tier
2 support
for the target riscv32i-unknown-none-elf
which works completely fine with only the instructions we’ll cover
here.

Speaking of instructions we will cover, why don’t we meet the 45 of
them right here and now:

lui auipc
jal jalr
beq bne blt bge bltu bgeu
lb lh lw lbu lhu sb sh sw
addi slti sltiu xori ori andi slli srli srai
add sub slt sltu xor or and sll srl sra
ecall ebreak
csrrw csrrs csrrc csrrwi csrrsi csrrci

Some of these instruction names should ring a bell (add,
or, xor). Others will look like they have some
pattern to it. A few weird ones like auipc stand out. These
instructions form the foundation of RISC-V, performing the basic tasks a
processor would do.

You will also catch a glimpse of what creating an operating system on
RISC-V is like, namely handling exceptions and privilege levels.

Let’s get started.

Throughout this article you will see emulator panes like these:

(If you just see a code block, there’s a JavaScript problem. Make
sure you’ve enabled JavaScript, probably…)

start:
addi x10, x0, 0x123
ebreak

You can use the buttons to control each emulator. Go ahead and click
on ‘Start’. A register view should pop up showing the state of the
emulator. Now click on ‘Run’. You’ll notice that:

a0 (x10) 0x00000000

Changed into:

a0 (x10) 0x00000123

And the emulator stopped. Congratulations, you’ve run your first
RISC-V assembly program. First here, at least.

‘Start’ assembles your code and, well, starts the emulator. If
there’s a problem with your code, it will tell you about it and the
emulator will not start.

When the emulator is started, you can see the current state of the
registers in the side pane. More controls also becomes available. ‘Run’
runs until the end or until you hit ‘Pause’. ‘Step’ runs a single
step.

If you hit ‘Step’, you’ll notice that the above program takes two
steps to run. You may have guessed correctly that the first step
corresponds to addi, and the second corresponds to
ebreak. The top of the register panel shows
pc, the current instruction address, and in parentheses the
current instruction.

‘Dump’ opens a new window containing some text. There are two
sections: the first is the symbol table, which tells you about the
labels in your code:

# Symbols
# 0x40000000 start

The second section is an annotated version of your code:

start:
 0x40000000: 12300513  addi x10, x0, 0x123
 0x40000004: 00100073  ebreak

This tells you that the addi instruction encodes to hex
12300513, and starts at address hex 40000000.
Similarly, ebreak encodes as 00100073 at
address hex 40000004.

(Note: RISC-V instructions are little-endian, meaning that
the four bytes of addi are actually
13 05 30 12.)

We’ll talk in detail about all of pc, registers,
instructions, labels, and the two checkboxes later.

Now you may have also guessed that addi x10, x0, 0x123
means x10 = x0 + 0x123. As for ebreak, for
now, just remember that ebreak stops the emulator.

The program counter,
or pc is the address of
the current instruction. It points to the instruction to be
executed.

RV32I has 31 general
purpose registers
numbered x1 through
x31
. These can contain any 32-bit data.

(If you’re wondering, there are no flags for RV32I.)

The register x0 is a
special “zero register”. For computational instructions, you can use
x0 anywhere a register is expected. Reading it always gives
zero, and writing to it just gets ignored. The use of a special register
simplifies the design of the architecture, and this design is shared by
MIPS and Arm AArch64. We will make good use of x0 soon.

(Note: In the emulator, the instruction listed in parenthesis next to
pc in the register view is provided as a convenience and is
not part of the processor state.)

But before we can start talking about instructions themselves, we
need a way to talk about the instruction syntax so I
can, you know, write it down for you.

The syntax of an instruction is the instruction name and then several
comma-separated operands. For example, for this instruction we’ve seen
above:

addi x10, x0, 0x123

x10 is the destination register or
rd. The next operand is
the first (and only) source
register
or rs1. The last operand is an
immediate value or imm. Using these
abbreviations, we can summarize that the syntax for addi
is:

addi rd, rs1, imm

Some other instructions have a second source register or rs2. For example, the
non-immediate add instruction has this syntax:

add rd, rs1, rs2

Some other instructions have no operands, like ebreak.
Others have slightly more complex operands.

Using the registers as a playground of numbers, we can use
computational instructions to work with them.

Popek
and Goldberg conditions of virtualization
to work, specifically
because being able to read the current privilege level at a
lower-than-maximum privilege level would be a “sensitive” but
“unprivileged” instruction.

If you’re writing a program for a certain privilege level, you should
simply assume that it is correctly being run at that privilege
level.

https://github.com/dramforever/easyriscv if you have
suggestions, grievances, or just want to share some thoughts.

This tutorial is under the CC0
license. To the maximum extent permitted by law, this tutorial is
dedicated to the public domain.

Source link