Five modules ago you wrote your first assign; last module you followed a design all
the way to
A real RV32I core implements 47 instructions; ten are enough to compute anything and to expose every interesting design problem — arithmetic, memory, and control flow:
| Instruction | Format | Meaning | Example |
|---|---|---|---|
ADD | R | rd = rs1 + rs2 | add x3, x1, x2 |
SUB | R | rd = rs1 − rs2 | sub x4, x1, x2 |
AND | R | rd = rs1 & rs2 | and x5, x1, x2 |
OR | R | rd = rs1 | rs2 | or x6, x1, x2 |
ADDI | I | rd = rs1 + imm | addi x1, x0, 5 |
LW | I | rd = mem[rs1 + imm] | lw x5, 8(x2) |
SW | S | mem[rs1 + imm] = rs2 | sw x5, 12(x2) |
BEQ | B | if rs1 == rs2, pc += imm | beq x1, x3, loop |
JAL | J | rd = pc + 4; pc += imm | jal x1, func |
LUI | U | rd = imm << 12 | lui x7, 0x12345 |
Why RISC-V and not something homemade? Three reasons. It is open — no licence, no permission, which is why it has become the lingua franca of computer architecture courses and a thousand startups. Its encodings are ruthlessly regular, which will make our decoder almost embarrassingly small. And it has real toolchains: the moment our CPU works, an off-the-shelf compiler can generate programs for it. A homemade ISA gets you none of that — and teaches you less, because its warts are yours alone.
RV32I gives software 32 registers, x0 through x31, each 32 bits wide.
Thirty-one of them are ordinary storage. The first is not: x0 is hardwired to
zero. Reads always return 0; writes disappear without a trace. This one odd register is a
quiet masterstroke — it gives the ISA a free constant zero, and it turns missing instructions into
pseudo-instructions: addi x5, x0, 7 is "load constant 7", add x5, x1, x0
is "copy x1", jal x0, target is "jump and discard the return address", and
addi x0, x0, 0 is the official NOP. We will meet x0 again in
every single lesson of this module, usually as a bug it either prevents or causes.
Every RV32I instruction is exactly 32 bits, built from the same few fields. Reading the columns right to left (bit 0 is on the right):
| Format | Bits 31 … 25 | 24 … 20 | 19 … 15 | 14 … 12 | 11 … 7 | 6 … 0 | Used by |
|---|---|---|---|---|---|---|---|
| R | funct7 | rs2 | rs1 | funct3 | rd | opcode | ADD SUB AND OR |
| I | imm[11:0] | rs1 | funct3 | rd | opcode | ADDI LW | |
| S | imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | SW |
| B | imm[12|10:5] | rs2 | rs1 | funct3 | imm[4:1|11] | opcode | BEQ |
| U | imm[31:12] | rd | opcode | LUI | |||
| J | imm[20|10:1|11|19:12] | rd | opcode | JAL | |||
Stare at the columns, not the rows: rs1 is always bits 19:15,
rs2 always 24:20, rd always 11:7, opcode always 6:0. No
format ever moves them. That is the regularity our decoder will cash in: the register-file read
addresses can be wired straight out of the instruction word before we even know which instruction
it is. The price is paid by the immediates, which get scrambled (S splits its immediate around
rs2; B and J interleave bits) precisely so the register fields never move. B and J
immediates are also in multiples of 2 — the low bit is implicit — buying double the branch reach
for free.
Because hardware pays for muxes, not for weird-looking documentation. The RISC-V
architects chose the scrambling so that each immediate bit comes from as few different
instruction-word positions as possible across all formats — the sign bit, for instance, is
always bit 31, so sign extension never waits for format decoding. A human assembler
finds imm[4:1|11] ugly; a decoder finds it nearly free. It is a beautiful example of
an ISA being co-designed with the datapaths that will implement it — the spec looks strange on
paper and elegant in gates. The scrambling costs software nothing: assemblers do the shuffling,
and no human ever encodes a branch by hand. Well — except us, in the code block below.
Here is the whole project on one napkin. Every instruction, whatever its format, will take the same
walk: the pc fetches a word from instruction memory, decode splits it into fields, the
register file supplies rs1/rs2, the ALU computes, data memory is touched
if needed, and the result loops back — into rd and into the next pc:
The module plan follows that drawing. Next lesson builds the
The commonest beginner mistake in this project is to start drawing hardware while the instruction
list is still wobbling. Resist it. An ISA says what must happen — "after
ADD, rd holds the sum" — and says nothing about
how. The same ten-instruction contract we just signed will be implemented
three different ways in this very module: as a functional TypeScript model, as a
single-cycle datapath, and as a five-stage pipeline. A phone core, a laptop core and a
supercomputer core implementing RV32I share not one wire of design — yet run the same binaries.
That separation is the whole economic miracle of the ISA: software outlives any particular
silicon. Nail the contract; then design. (It is also why the contract will be our
referee in the final lesson: any implementation can be checked against a simpler one.)
Our first deliverable is not hardware but tooling — a tiny assembler and disassembler we will reuse in every lesson of this module. Encoding is just fields shifted into place and OR'd together; decoding is the reverse. Run it, then try encoding your own instruction — change a register or an immediate and check the hex word moves the way the format table predicts:
Two details worth savouring in the output. The word for addi x1, x0, 5,
0x00500093, is the exact bit pattern a real RISC-V compiler emits — our toy speaks the
genuine language. And look at the raw rd/rs2 columns for the store and
branch rows: the decoder pulls fields from fixed positions first and only then asks the
opcode what they mean — for SW the "rd" bits are really immediate bits. Fixed
positions, format-dependent meaning: that is the RISC-V decode idiom, and it is exactly how our
hardware decoder will work in two lessons' time.