Another important source of information is of course the RISC-V reference manual. There you learn that there are several flavors of the RISC-V standard. Let us start from the simplest one (RV32I, that is, 32 bits base integer instruction set). Then we will see how to add things, one thing at a time. This is a very nice feature of RISC-V, since the instruction set is modular, you can start with a very small self-contained kernel, and this kernel will be compliant with the norm. This means standard tools (compiler, assembler, linker) will be able to generate code for this kernel. Then I started reading Chapter 2 (page 13 to page 30). Seeing also the table page 130, there are in fact only 11 different instrutions ! (I say for instance that an AND, an OR, an ADD ... are the same instruction, the operation is just an additional parameter). Now we just try to have an idea of the overall picture, no need to dive into the details for now. Let's take a global look at these 11 instructions:
instruction | description | algo |
---|---|---|
branch | conditional jump, 6 variants | if(reg OP reg) PC<-PC+imm |
ALU reg | Three-registers ALU ops, 10 variants | reg <- reg OP reg |
ALU imm | Two-registers ALU ops, 9 variants | reg <- reg OP imm |
load | Memory-to-register, 5 variants | reg <- mem[reg + imm] |
store | Register-to-memory, 3 variants | mem[reg+imm] <- reg |
LUI |
load upper immediate | reg <- (im << 12) |
AUIPC |
add upper immediate to PC | reg <- PC+(im << 12) |
JAL |
jump and link | reg <- PC+4 ; PC <- PC+imm |
JALR |
jump and link register | reg <- PC+4 ; PC <- reg+imm |
FENCE |
memory-ordering for multicores | (not detailed here, skipped for now) |
SYSTEM |
system calls, breakpoints | (not detailed here, skipped for now) |
-
The 6 branch variants are conditional jumps, that depend on a test on two registers.
-
ALU operations can be of the form
register <- register OP register
orregister <- register OP immediate
-
Then we have load and store, that can operate on bytes, on 16 bit values (called half-words) or 32 bit values (called words). In addition byte and half-word loads can do sign expansion. The source/target address is obtained by adding an immediate offset to the content of a register.
-
The remaining instructions are more special (one may skip their description in a first read, you just need to know that they are used to implement unconditional jumps, function calls, memory ordering for multicores, system calls and breaks):
-
LUI
(load upper immediate) is used to load the upper 20 bits of a constant. The lower bits can then be set usingADDI
orORI
. At first sight it may seem weird that we need two instructions to load a 32 bit constant in a register, but in fact it is a smart choice, because all instructions are 32-bit long. -
AUIPC
(add upper immediate to PC) adds a constant to the current program counter and places the result in a register. It is meant to be used in combination withJALR
to reach a 32-bit PC-relative address. -
JAL
(jump and link) adds an offset to the PC and stores the address of the instruction following the jump in a register. It can be used to implement function calls.JALR
does the same thing, but adds the offset to a register. -
FENCE
andSYSTEMS
are used to implement memory ordering in multicore systems, and system calls/breaks respectively.
-
To summarize, we got branches (conditional jumps), ALU operations, load and store, and a couple of special instructions used to implement unconditional jumps and function calls. There are also two functions for memory ordering and system calls (but we will ignore these two ones for now). OK, in fact only 9 instructions then, it seems doable... At this point, I had not understood everything, so I'll start from what I think to be the simplest parts (register file and ALU), then we will see the instruction decoder and how things are interconnected.