RISC-V Instruction Formats

A RISC-V instruction is a 32-bit word carved into fixed-position fields, and the carving is not arbitrary — it is the single most consequential piece of hardware-friendly design in the ISA. Six formats (R, I, S, B, U, J) cover the whole RV32I base, and across all six the register specifiers never move: rd is always inst[11:7], rs1 is always inst[19:15], rs2 is always inst[24:20]. The immediate bits, by contrast, appear scrambledimm[10:5] lives in inst[30:25] in four different formats, and the branch offset’s bit 11 is stashed in inst[7] where the store format keeps imm[0]. That scrambling is the point: it exists so a given immediate bit lands on the same physical wire in as many formats as possible, which is what lets a decoder build every immediate as a bundle of constant wires instead of a bank of multiplexers. The specification states the payoff outright — rotating the bits “reduce[s] instruction signal fanout and immediate mux costs by around a factor of 2” (RISC-V Unprivileged ISA v20250508, §2.3). This note owns the encoding — what the bits mean and why they sit where they do, including the nine 16-bit compressed (C) formats that RV32IMC requires. Its sibling Instruction Decode owns the hardware that turns those bits into control signals; where this note says “inst[14:12] is funct3”, that note says “funct3 drives the ALU operation mux.”

Which document this is

Everything cited here is from “The RISC-V Instruction Set Manual Volume I: Unprivileged Architecture”, revision 20250508, whose AsciiDoc source carries :revremark: This document is in ratified state. and which was published as GitHub release “RISC-V ISA Manual, version 20250508” on 2025-05-12 (release metadata). RISC-V ratifies incrementally, so “the RISC-V spec” is never a single thing; the base chapters cited below are RV32I Base Integer Instruction Set, Version 2.1 and “C” Extension for Compressed Instructions, Version 2.0, both inside that document. I read the AsciiDoc sources at git tag 20250508 rather than the rendered PDF, because the field-layout figures are WaveDrom source (src/images/wavedrom/*.edn) whose bit widths are machine-readable and cannot be misread off a rendering.

Every hexadecimal instruction word decoded in this note was produced on this machine by clang 22.1.8 (clang --target=riscv32 -march=rv32imc -mabi=ilp32) or GNU as from riscv64-linux-gnu binutils shipped with gcc 16.1.1, and disassembled with llvm-objdump 22.1.8 and riscv64-linux-gnu-objdump. Nothing below is transcribed from memory.

Mental Model — A Fixed Grid With One Moving Part

The productive way to think about a RISC-V instruction word is as a grid of slots at fixed bit offsets, most of which never move, plus one region — the immediate — that gets shuffled between formats. Draw the 32 bits once and you have drawn every RV32I instruction:

  • inst[6:0] — the opcode, always seven bits, always the same place. This is the top-level selector.
  • inst[11:7] — five bits that are rd in R/I/U/J, and imm bits in S/B.
  • inst[14:12]funct3, a three-bit sub-opcode present in every format except U and J.
  • inst[19:15]rs1, present in R/I/S/B.
  • inst[24:20]rs2 in R/S/B, immediate bits in I.
  • inst[31:25]funct7 in R, immediate bits everywhere else.
  • inst[31]always the sign bit of the immediate, in every format that has one.

Two of those bullets are load-bearing. The first is that the register-number fields are at constant offsets, which the spec justifies bluntly: “Decoding register specifiers is usually on the critical paths in implementations, and so the instruction format was chosen to keep all register specifiers at the same position in all formats at the expense of having to move immediate bits across formats.” The second is that inst[31] is the sign bit universally, “to allow sign extension to proceed in parallel with instruction decoding.” Both quotations are from §2.2–§2.3 of the ratified manual. Everything else about RISC-V encoding follows from those two commitments plus the desire to keep the opcode space large.

flowchart TB
    W["one 32-bit instruction word"]
    subgraph FIXED["Never moves — read it before you know what the instruction is"]
        OP["inst[6:0]<br/>opcode"]
        RD["inst[11:7]<br/>rd"]
        F3["inst[14:12]<br/>funct3"]
        R1["inst[19:15]<br/>rs1"]
        R2["inst[24:20]<br/>rs2"]
        SGN["inst[31]<br/>immediate sign"]
    end
    subgraph MOVES["Moves between formats — but always onto reused wires"]
        IMM["the remaining immediate bits<br/>inst[30:25] · inst[24:21] · inst[11:8] · inst[7] · inst[19:12]"]
    end
    W --> FIXED
    W --> MOVES
    RD -.->|"in S and B this<br/>field is immediate, not rd"| IMM
    R2 -.->|"in I this field<br/>is immediate, not rs2"| IMM
    OP -->|"selects which<br/>interpretation is live"| FMT{"format<br/>R I S B U J"}
    FMT --> IMM
    RF[("register file<br/>2 read ports")]
    R1 -->|"address port A"| RF
    R2 -->|"address port B"| RF
    SGN -->|"fans out to 12-21 bits<br/>of sign extension"| IMM

The instruction word split into the part that never moves and the part that does. What it shows: the opcode, the two source-register fields and the sign bit occupy constant offsets, so their consumers can be wired directly to instruction bits; only the immediate’s interior bits are format-dependent, and even those land on shared wires. The insight to take: because rs1 and rs2 are at constant offsets, the register file’s address inputs can be driven straight from inst[19:15] and inst[24:20] before the decoder has decided what the instruction is — the register read overlaps decode instead of following it. The dashed arrows are the honest caveat: in S and B format those same bits are immediate, so the register file will read a register nobody asked for, and the control signals simply discard the result.

Instruction-Length Encoding — Read the Bottom Two Bits First

Before any format question can be asked, one has to know how long the instruction is. RISC-V answers that in the lowest two bits of the lowest-addressed 16-bit parcel. Per §1.5 of the ratified manual: “All the 32-bit instructions in the base ISA have their lowest two bits set to 11. The optional compressed 16-bit instruction-set extensions have their lowest two bits equal to 00, 01, or 10.”

Two terms the spec defines and that a builder needs: IALIGN is the instruction-address alignment the implementation enforces — 32 bits in the base ISA, relaxed to 16 bits when the C extension is present, and never any other value. ILEN is the maximum instruction length supported, always a multiple of IALIGN; for a base-only implementation ILEN is 32. For RV32IMC, IALIGN = 16 and ILEN = 32.

Instructions are stored as a sequence of 16-bit little-endian parcels, at increasing halfword addresses, regardless of the memory system’s endianness. The spec explains why: fixing parcel order “ensure[s] that the length-encoding bits always appear first in halfword address order,” so a fetch unit can determine an instruction’s length by inspecting only the first few bits of the first parcel.

packet-beta
0-1: "op (len)"
2-15: "rest of parcel 0"
16-31: "parcel 1 - present only when op=11"

The length field’s position, drawn with bit 0 on the left. What it shows: the two bits that decide “16-bit or 32-bit” sit at the very bottom of the first parcel, and the second parcel exists only if those bits read 11. The insight to take: packet-beta numbers bits left-to-right from 0, which is the mirror image of the RISC-V manual’s own figures (they put bit 31 on the left). Every packet diagram in this note therefore reads low bit first; the field labels carry explicit inst[hi:lo] ranges so no reader has to infer orientation from the picture. This is a house-style consequence of the medium, noted in Drawing Wire Formats with Mermaid Packet Diagrams, not a claim about the ISA.

Two encodings are permanently illegal, and both are chosen for diagnostic value. An encoding with inst[15:0] all zeros is an illegal instruction — “considered to be of minimal length: 16 bits if any 16-bit instruction-set extension is present, otherwise 32 bits.” An encoding with inst[ILEN-1:0] all ones is likewise illegal. The rationale is stated in the manual: all-zero traps “erroneous jumps into zeroed memory regions,” and all-ones catches “the other common pattern observed with unprogrammed non-volatile memory devices, disconnected memory buses, or broken memory devices.” Both are worth wiring explicitly into a decoder — see Instruction Decode.

The Six Base Formats, Bit by Bit

The base RV32I ISA defines four core formats (R, I, S, U) plus two immediate variants (B, J) that differ from S and U only in how the immediate is assembled. All six are 32 bits. The layouts below are transcribed from the manual’s WaveDrom sources instruction-formats.edn and immediate-variants.edn at tag 20250508.

R-type — register–register

packet-beta
0-6: "opcode inst[6:0]"
7-11: "rd inst[11:7]"
12-14: "funct3"
15-19: "rs1 inst[19:15]"
20-24: "rs2 inst[24:20]"
25-31: "funct7 inst[31:25]"

R-type: three register operands and a 10-bit function code split into funct3 + funct7. What it shows: no immediate at all, and the widest function-code space of any format. The insight to take: the funct7/funct3 split is what lets ADD and SUB share opcode 0110011 and funct3=000, separated only by funct7 bit 5 (0000000 vs 0100000) — and it is why adding the whole M extension costs a decoder exactly one new funct7 value, 0000001, rather than a new opcode. MUL is funct7=0000001, funct3=000 on that same 0110011 opcode (RV32M listing, §RV32/64G).

I-type — register–immediate, loads, JALR

packet-beta
0-6: "opcode inst[6:0]"
7-11: "rd inst[11:7]"
12-14: "funct3"
15-19: "rs1 inst[19:15]"
20-31: "imm[11:0] = inst[31:20]"

I-type: one source register, one 12-bit signed immediate. What it shows: the immediate occupies the entire top 12 bits contiguously — the only format where reading the immediate is a single unbroken slice. The insight to take: twelve bits of signed immediate reaches ±2048, which is why addi, every load offset, jalr, and the CSR instructions all fit one format. The shift instructions are the exception that proves the rule: SLLI/SRLI/SRAI are I-type by opcode but reinterpret inst[24:20] as a 5-bit shift amount and inst[31:25] as a funct7-like selector (0000000 for logical, 0100000 for arithmetic), so a decoder must treat them as a special case inside OP-IMM.

S-type — stores

packet-beta
0-6: "opcode inst[6:0]"
7-11: "imm[4:0] = inst[11:7]"
12-14: "funct3"
15-19: "rs1 inst[19:15]"
20-24: "rs2 inst[24:20]"
25-31: "imm[11:5] = inst[31:25]"

S-type: two source registers, no destination, and a 12-bit immediate split in two. What it shows: the immediate’s low five bits sit exactly where rd sits in R/I/U/J, and its high seven bits sit exactly where funct7 sits in R. The insight to take: the split is not arbitrary — it is the only way to keep rs1 and rs2 at their constant offsets while still finding room for twelve immediate bits. Something has to give, and the ISA chose to fragment the immediate rather than move a register field.

B-type — conditional branches

packet-beta
0-6: "opcode inst[6:0]"
7-7: "imm[11] = inst[7]"
8-11: "imm[4:1] = inst[11:8]"
12-14: "funct3"
15-19: "rs1 inst[19:15]"
20-24: "rs2 inst[24:20]"
25-30: "imm[10:5] = inst[30:25]"
31-31: "imm[12] = inst[31]"

B-type: S-type with two bits rotated. What it shows: compared to S-type, inst[7] now carries imm[11] instead of imm[0], and inst[31] carries imm[12] instead of imm[11]; inst[11:8] and inst[30:25] keep the same immediate-bit roles they had in S. The insight to take: the branch offset is in multiples of 2, so imm[0] is always zero and need not be encoded. The manual’s own phrasing is the key sentence in the whole topic: “Instead of shifting all bits in the instruction-encoded immediate left by one in hardware as is conventionally done, the middle bits (imm[10:1]) and sign bit stay in fixed positions, while the lowest bit in S format (inst[7]) encodes a high-order bit in B format.” The ×2 scaling is done by the encoding, at zero hardware cost, instead of by a shifter.

U-type — LUI, AUIPC

packet-beta
0-6: "opcode inst[6:0]"
7-11: "rd inst[11:7]"
12-31: "imm[31:12] = inst[31:12]"

U-type: a 20-bit immediate that lands in the upper bits of the result. What it shows: the instruction’s top 20 bits map one-to-one onto result bits 31:12, with the low 12 bits of the value forced to zero. The insight to take: U-type exists because 12 bits is not enough to build a 32-bit constant, and the ISA refused to add a second immediate size to the ordinary formats. The manual’s rationale: “We chose an asymmetric immediate split (12 bits in regular instructions plus a special load-upper-immediate instruction with 20 bits) to increase the opcode space available for regular instructions.” LUI+ADDI therefore materialises any 32-bit constant in two instructions.

J-type — JAL

packet-beta
0-6: "opcode inst[6:0]"
7-11: "rd inst[11:7]"
12-19: "imm[19:12] = inst[19:12]"
20-20: "imm[11] = inst[20]"
21-30: "imm[10:1] = inst[30:21]"
31-31: "imm[20] = inst[31]"

J-type: U-type with the top 20 bits reinterpreted as a ×2-scaled signed offset. What it shows: inst[19:12] keeps exactly the role it has in U-type (imm[19:12]), while the remaining bits are rotated so that inst[30:21] supplies imm[10:1] and inst[20] supplies the single bit that would otherwise straddle. The insight to take: the manual says the U/J bit placement “is chosen to maximize overlap with the other formats and with each other.” Concretely: inst[30:25] gives imm[10:5] in I, S, B and J; inst[24:21] gives imm[4:1] in I and J; inst[19:12] gives imm[19:12] in U and J. Four formats, one wire bundle.

The opcode map

The seven-bit opcode is structured, not arbitrary. With inst[1:0] = 11 fixed (a 32-bit instruction), the remaining five bits decompose as inst[4:2] across and inst[6:5] down. This is the manual’s Table RISC-V base opcode map, transcribed verbatim:

inst[6:5] \ inst[4:2]000001010011100101110111 (>32b)
00LOADLOAD-FPcustom-0MISC-MEMOP-IMMAUIPCOP-IMM-32reserved
01STORESTORE-FPcustom-1AMOOPLUIOP-32reserved
10MADDMSUBNMSUBNMADDOP-FPOP-Vcustom-2reserved
11BRANCHJALRreservedJALSYSTEMOP-VEcustom-3reserved

The RV32/RV64 major-opcode map. What it shows: the eleven opcodes an RV32I core must recognise, and where the four custom slots that future standard extensions promise to avoid sit. The insight to take: a Stage-2 core needs exactly the eleven bold-named cells — LOAD (0000011), MISC-MEM (0001111), OP-IMM (0010011), AUIPC (0010111), STORE (0100011), OP (0110011), LUI (0110111), BRANCH (1100011), JALR (1100111), JAL (1101111), SYSTEM (1110011) — and everything else is an illegal-instruction trap. The map also explains a common confusion: AUIPC and LUI are one row apart because they share inst[4:2]=101 and differ only in inst[5].

Why the Immediate Bits Are Scrambled — the Central Insight

This is the part of RISC-V encoding that looks like a mistake until you draw the wires. Here are the five immediates, each labelled with the instruction bit that supplies each immediate bit, transcribed from the manual’s WaveDrom sources i-immediate.edn through j-immediate.edn:

immediate bitI-immS-immB-immU-immJ-imm
[31:20]inst[31] (sign)inst[31]inst[31]inst[31], inst[30:20]inst[31]
[19:12]inst[31]inst[31]inst[31]inst[19:12]inst[19:12]
[11]inst[31]inst[31]inst[7]0inst[20]
[10:5]inst[30:25]inst[30:25]inst[30:25]0inst[30:25]
[4:1]inst[24:21]inst[11:8]inst[11:8]0inst[24:21]
[0]inst[20]inst[7]000

Where every immediate bit comes from. What it shows: read across any row and count how many columns name the same instruction bits. inst[30:25] → imm[10:5] holds in four of the five immediates. inst[24:21] → imm[4:1] holds in I and J. inst[11:8] → imm[4:1] holds in S and B. inst[19:12] → imm[19:12] holds in U and J. inst[31] is the sign in all five. The insight to take: every one of those repeated cells is a wire that does not need a multiplexer. If the immediate had been laid out “sensibly” — bit 0 always adjacent to bit 1, contiguous in every format — then imm[10:5] would come from a different place in each format and the decoder would need a 5-input mux on each of those six bits. The scramble converts mux area into wire routing, and routing is free in a way that gates are not.

flowchart LR
    subgraph SRC["instruction bits"]
        B31["inst[31]"]
        B3025["inst[30:25]"]
        B2421["inst[24:21]"]
        B20["inst[20]"]
        B1912["inst[19:12]"]
        B118["inst[11:8]"]
        B7["inst[7]"]
    end
    subgraph DST["immediate bits"]
        SGNX["imm[31:12] sign fill"]
        I1912["imm[19:12]"]
        I11["imm[11]"]
        I105["imm[10:5]"]
        I41["imm[4:1]"]
        I0["imm[0]"]
    end
    B31 -->|"I S B U J - all five"| SGNX
    B3025 -->|"I S B J - four"| I105
    B2421 -->|"I and J"| I41
    B118 -->|"S and B"| I41
    B1912 -->|"U and J"| I1912
    B20 -->|"I - imm[0]"| I0
    B20 -->|"J - imm[11]"| I11
    B7 -->|"S - imm[0]"| I0
    B7 -->|"B - imm[11]"| I11

The immediate scramble drawn as wiring. What it shows: seven source bundles feeding six destination bundles, with the edge labels naming which formats reuse each wire. Only two source bundles fan out to more than one destination — inst[20] and inst[7], each of which is imm[0] in one format and imm[11] in another. The insight to take: the entire immediate-generation logic for all five types is this diagram, and it contains no gates at all — it is a permutation of wires plus a fan-out of the sign bit. Every 2-to-1 choice you can see (the inst[20] and inst[7] splits) is the only muxing the scramble failed to eliminate, and both of those are single-bit muxes. Compare the naïve alternative: contiguous immediates in each format would put imm[4:1] in three different places and imm[10:5] in four, forcing a wide mux on 10 of the 12 bits.

In practice a decoder does not even mux the immediates — it builds all five in parallel and muxes the finished results. This is the lowRISC Ibex core’s entire immediate generator, from rtl/ibex_decoder.sv (lines 161–165 at commit 34b0705):

assign imm_i_type_o = { {20{instr[31]}}, instr[31:20] };
assign imm_s_type_o = { {20{instr[31]}}, instr[31:25], instr[11:7] };
assign imm_b_type_o = { {19{instr[31]}}, instr[31], instr[7], instr[30:25], instr[11:8], 1'b0 };
assign imm_u_type_o = { instr[31:12], 12'b0 };
assign imm_j_type_o = { {12{instr[31]}}, instr[19:12], instr[20], instr[30:21], 1'b0 };

Read those five lines as hardware, not as code. Each right-hand side is a concatenation of constant slices of instr plus replicated instr[31] plus literal zeros — no operators, no arithmetic, no conditionals. Synthesis produces exactly zero logic cells for them; they are net renaming. The 1'b0 at the end of the B and J lines is the ×2 scaling, materialised as a tied-low wire rather than a shifter. That is the cash value of the scramble: five 32-bit immediates for the price of some routing, computed before the opcode has even been examined.

The PicoRV32 core writes the same idea inside out, scrambling the left-hand side instead:

{ decoded_imm_j[31:20], decoded_imm_j[10:1], decoded_imm_j[11],
  decoded_imm_j[19:12], decoded_imm_j[0] } <= $signed({mem_rdata_latched[31:12], 1'b0});

Here the source is a plain contiguous slice inst[31:12] and the destination bit-order is permuted. Same permutation, opposite notation — and a useful sanity check that you have understood the mapping, because the two forms must agree.

Sign Extension — One Bit, Everywhere

Except for the 5-bit uimm used by the CSR-immediate instructions (CSRRWI/CSRRSI/CSRRCI, which zero-extend), every RISC-V immediate is sign-extended, and the sign bit is always inst[31]. The manual gives both halves of the reasoning. On why sign extension rather than a mix: “Immediates are sign-extended because we did not observe a benefit to using zero extension for some immediates as in the MIPS ISA and wanted to keep the ISA as simple as possible.” On why bit 31 specifically: “Sign extension is one of the most critical operations on immediates (particularly for XLEN>32), and in RISC-V the sign bit for all immediates is always held in bit 31 of the instruction to allow sign extension to proceed in parallel with instruction decoding.”

Where does sign extension happen? In hardware it is not an operation at all — it is fan-out. {{20{instr[31]}}, ...} is one wire driving twenty inputs. The cost is capacitive load on inst[31], not a gate delay, which is why the placement matters: put the sign bit somewhere format-dependent and you would need a mux in front of a 20-way fan-out, on the critical path, for every instruction.

The compressed extension follows the same discipline with a different anchor. Per §16.2 of the C chapter: “Where immediates are sign-extended, the sign extension is always from bit 12.” So in a 16-bit parcel, c[12] plays the role inst[31] plays in a 32-bit word.

One consequence worth internalising before writing RTL: the decoder does not know or care how many of the immediate’s bits the consumer will use. SRAI a0, a0, 3 encodes as 0x40355513, whose I-immediate is 0x403 = 1027, not 3. The shift amount is imm[4:0] and inst[30] is the arithmetic/logical selector. The immediate generator emits 1027; the ALU takes the bottom five bits. Truncation is the consumer’s job.

The Register Fields Never Move — and What That Buys

rs1 = inst[19:15], rs2 = inst[24:20], rd = inst[11:7], in every format that has them. Ibex’s decoder makes the consequence explicit — these are assign statements at module scope, not inside the decode case:

assign instr_rs1 = instr[19:15];
assign instr_rs2 = instr[24:20];
assign instr_rd  = instr[11:7];

The payoff is a timing one. In a Classic Five-Stage Pipeline the ID stage must both decode the instruction and read two registers. If the register-file addresses depended on the decode result, those two would be serial: opcode → format → mux → address → SRAM read. Because the addresses are constant slices, the register read starts on the same clock edge the instruction word arrives, in parallel with the decoder’s opcode analysis. See The Register File for the read-port structure this feeds.

The price is that the register file sometimes reads registers the instruction does not use. LUI has no source registers, but inst[19:15] still names some register and port A still reads it. SRAI a0, a0, 3 = 0x40355513 has inst[24:20] = 00011, so port B dutifully reads x3. Nothing breaks — the control signals route those values nowhere — but two things follow for a real design. First, if you gate register-file reads for power, you need explicit rf_ren_a/rf_ren_b control signals (Ibex has exactly these). Second, a hazard-detection unit that watches rs1/rs2 unconditionally will report false dependencies on instructions that have no source registers, and will insert stalls that are not needed; the fix is to qualify hazard comparisons with the same rf_ren signals. This bites in Stage 3 of the build ladder, not Stage 2 — see Pipeline Hazards and Operand Forwarding.

Decoding Real Instructions by Hand

Everything above is testable in five minutes with the toolchain on this machine. Compile, disassemble, and take the hex apart. The source used here:

/* demo.c — one function per format */
int add_rr(int a, int b)    { return a + b; }            /* R: add       */
int addi42(void)            { return 42; }               /* I: addi      */
int load_w(int *p)          { return p[3]; }             /* I: lw        */
void store_w(int *p, int v) { p[3] = v; }                /* S: sw        */
int big(void)               { return 0x12345678; }       /* U: lui+addi  */
int shifts(int a)           { return (a << 5) ^ (a >> 3); }

Built with clang --target=riscv32 -march=rv32i -mabi=ilp32 -mno-relax -O1 -c demo.c and disassembled with llvm-objdump -d. (-march=rv32i suppresses compression so the base formats are visible; -mno-relax stops the assembler from deferring local branch offsets to the linker, which otherwise leaves them encoded as zero with a relocation.)

00a58533add a0, a1, a0 (R-type)

fieldbitsvaluemeaning
opcodeinst[6:0]0110011OP
rdinst[11:7]01010 = 10x10 = a0
funct3inst[14:12]000ADD/SUB family
rs1inst[19:15]01011 = 11x11 = a1
rs2inst[24:20]01010 = 10x10 = a0
funct7inst[31:25]0000000ADD (0100000 would be SUB)

02a00513li a0, 0x2a, i.e. addi a0, x0, 42 (I-type)

fieldbitsvaluemeaning
opcodeinst[6:0]0010011OP-IMM
rdinst[11:7]01010a0
funct3inst[14:12]000ADDI
rs1inst[19:15]00000x0
imm[11:0]inst[31:20]0000_0010_10100x02A = 42

li a0, 42 is a pseudo-instruction; the machine only has addi. Because x0 reads as zero, addi rd, x0, imm is a constant load. This single word is exactly the instruction Stage 2 of definitely-not-esp32 MOC asks a core to execute.

00c52503lw a0, 12(a0) and 00b52623sw a1, 12(a0)

These two are the clearest demonstration of the S-type split, because they carry the same offset:

lw (00c52503, I-type)sw (00b52623, S-type)
opcode0000011 LOAD0100011 STORE
funct3010 (word)010 (word)
rs1 (base)01010 = a001010 = a0
rd / rs2rd = 01010 = a0rs2 = 01011 = a1
immediateinst[31:20] = 0x00Cinst[31:25]=0000000, inst[11:7]=01100
value12000000001100 = 12

Same offset, two encodings. What it shows: the store has to give up the rd field to hold imm[4:0], because it needs rs2 for the data being written, and rs1/rs2 cannot move. The insight to take: concatenating inst[31:25] with inst[11:7] reproduces the identical 12-bit value the load reads contiguously — the split costs nothing at decode time because both halves are constant slices.

feb51de3bne a0, a1, -6 (B-type, and negative)

This one came out of a real compiled loop (for (i=0;i<n;i++) s += p[i]; at -O1), where the backward branch closes the loop. It is the best single instruction to hand-decode, because it exercises the sign bit, the rotated bits, and the implicit ×2 all at once.

immediate bitsourcevalue
imm[12]inst[31]1
imm[11]inst[7]1
imm[10:5]inst[30:25]111111
imm[4:1]inst[11:8]1101
imm[0]0

Assembled: 1_1_111111_1101_0 = 1111111111010, a 13-bit two’s-complement value = −6. The instruction sits at offset 0x10 in sum, and llvm-objdump prints the target as 0xa — which is 0x10 − 6. The remaining fields are funct3 = 001 (BNE), rs1 = 01010 (a0), rs2 = 01011 (a1).

Note what did not happen: nothing was shifted. The offset −6 has bit 0 clear by construction, and the encoding simply never allocates a bit for it.

12345537 + 67850513lui a0, 0x12345 ; addi a0, a0, 0x678

The U-type word 12345537 has opcode = 0110111 (LUI), rd = 01010 (a0), and imm[31:12] = inst[31:12] = 0x12345, giving the value 0x12345000. The following I-type adds 0x678. Sum: 0x12345678 — the constant in the C source. This is the standard two-instruction constant materialisation, and it is why 20 + 12 was chosen as the split.

00c000efjal ra, +12 (J-type)

Produced by assembling jal ra, target with GNU as where target is 12 bytes ahead.

immediate bitsourcevalue
imm[20]inst[31]0
imm[19:12]inst[19:12]00000000
imm[11]inst[20]0
imm[10:1]inst[30:21]0000000110
imm[0]0

imm[10:1] = 0000000110 sets imm[3] and imm[2], giving 8 + 4 = 12. rd = inst[11:7] = 00001 = x1 = ra. Opcode 1101111 = JAL.

The Compressed Formats — Nine Layouts in 16 Bits

RV32IMC’s C is not decoration. Per §16 of the ratified manual, RVC “reduces static and dynamic code size by adding short 16-bit instruction encodings for common operations,” and “typically, 50%–60% of the RISC-V instructions in a program can be replaced with RVC instructions, resulting in a 25%–30% code-size reduction.”

That claim is checkable. Compiling a small mixed workload (CRC-32 table build, strcmp, memcpy, recursive fib, quicksort) with clang 22.1.8 at -Os:

-march=.text size
rv32i542 bytes
rv32im542 bytes
rv32ic360 bytes
rv32imc360 bytes

A 33.6 % reduction, slightly better than the spec’s stated range. In the rv32imc build, counting disassembled instruction words by width gives 90 compressed (2-byte) and 45 uncompressed (4-byte) instructions — 66.7 % compressed, and 90 × 2 + 45 × 4 = 360, matching .text exactly, which validates the count. On an FPGA whose instruction memory is block RAM, one third off .text is the difference between fitting and not fitting; see Tang Nano 20K.

RVC uses four quadrants, selected by inst[1:0]. Quadrant 11 means “not compressed — this is a ≥32-bit instruction.” The other three each get a 3-bit funct3 at inst[15:13], giving 24 major compressed opcodes:

inst[1:0]000001010011100101110111
00 (C0)ADDI4SPNFLDLWFLWReservedFSDSWFSW
01 (C1)ADDI/NOPJALLILUI/ADDI16SPMISC-ALUJBEQZBNEZ
10 (C2)SLLIFLDSPLWSPFLWSPJ[AL]R/MV/ADDFSDSPSWSPFSWSP
11>16 bits — the base ISA lives here

The RVC opcode map for RV32 (floating-point entries greyed out for an integer-only core). What it shows: the twenty-four compressed major opcodes, of which an RV32IMC integer core must implement the fifteen shown in bold. The insight to take: the entire compressed decoder is a three-way case on inst[1:0] containing three eight-way cases on inst[15:13] — about twenty-four leaf cases, several of which subdivide further on inst[12] or inst[11:10]. That is a bounded, enumerable amount of work, which is why the standard advice is “get RV32I passing first, then add C” rather than “C is too hard.”

The nine formats, transcribed from §16.2’s format table:

format15:131211:109:76:54:21:0used by
CRfunct4 (15:12)rd/rs1 (11:7)rs2 (6:2)opc.jr, c.jalr, c.mv, c.add, c.ebreak
CIfunct3immrd/rs1 (11:7)imm (6:2)opc.addi, c.li, c.lui, c.slli, c.lwsp
CSSfunct3imm (12:7)rs2 (6:2)opc.swsp
CIWfunct3imm (12:5)rd′opc.addi4spn
CLfunct3imm (12:10)rs1′immrd′opc.lw
CSfunct3imm (12:10)rs1′immrs2′opc.sw
CAfunct6 (15:10)rd′/rs1′funct2rs2′opc.sub, c.xor, c.or, c.and
CBfunct3offset (12:10)rd′/rs1′offset (6:2)opc.beqz, c.bnez, c.srli, c.srai, c.andi
CJfunct3jump target (12:2)opc.j, c.jal

The nine RVC formats. What it shows: three of them (CR, CI, CSS) address all 32 registers; five (CIW, CL, CS, CA, CB) address only eight. The insight to take: the design rule is stated in the spec — “The formats were designed to keep bits for the two register source specifiers in the same place in all instructions, while the destination register field can move. When the full 5-bit destination register specifier is present, it is in the same place as in the 32-bit RISC-V encoding.” That is the same discipline as the base ISA, applied to a smaller budget: freeze what the register file needs, move what the immediate can afford to move.

The three-bit register fields rd′/rs1′/rs2′ do not encode x0x7. They encode x8x15:

rd′ value000001010011100101110111
registerx8x9x10x11x12x13x14x15
ABI names0s1a0a1a2a3a4a5

The compressed register mapping. What it shows: three bits select from a contiguous, naturally aligned window of eight registers starting at x8. The insight to take: the expansion is {2'b01, rdp} — a two-bit constant concatenated onto the field. Not an adder, not a table: two tied-high/low wires. The spec is explicit that the ABI was changed to make this true: “The RISC-V ABI was changed to make the frequently used registers map to registers x8-x15. This simplifies the decompression decoder by having a contiguous naturally aligned set of register numbers.” The ABI was bent to fit the encoder, not the other way round.

The expansion rule

The governing constraint, stated in §16.1: “RVC was designed under the constraint that each RVC instruction expands into a single 32-bit instruction in either the base ISA (RV32I/E or RV64I/E) or the F and D standard extensions where present.” One-to-one, no exceptions, no compressed instruction that does two things.

The spec lists the benefits in the same paragraph, and both matter for a from-scratch core: “Hardware designs can simply expand RVC instructions during decode, simplifying verification and minimizing modifications to existing microarchitectures,” and “Compilers can be unaware of the RVC extension and leave code compression to the assembler and linker.” The hardware consequence — expand first, then decode with the unmodified 32-bit decoder — is Instruction Decode’s subject.

Three details that catch people out:

  1. Immediates are scaled, not sign-extended, for data transfers. “Data-transfer instructions use zero-extended immediates that are scaled by the size of the data in bytes: ×4 for words.” So c.lw’s 5-bit immediate field reaches offset 124, not 31, and there is no way to encode a negative or misaligned offset.
  2. Zero immediates and x0 are frequently illegal, deliberately. “For many RVC instructions, zero-valued immediates are disallowed and x0 is not a valid 5-bit register specifier. These restrictions free up encoding space for other instructions requiring fewer operand bits.” c.lwsp with rd = x0 is reserved; c.addi4spn with uimm = 0 is reserved; c.lui with imm = 0 is reserved.
  3. The all-zero 16-bit parcel is permanently illegal. “A 16-bit instruction with all bits zero is permanently reserved as an illegal instruction,” and the spec adds that “the all-zero value should not be redefined in any non-standard extension.”

Hand-decoding compressed instructions from real output

The confirmed example from the project brief is int f(int a,int b){return a+b;} at -march=rv32imc, which disassembles to 952e add a0,a0,a1 and 8082 ret. Both decode cleanly.

0x952e = 1001 0101 0010 1110 — C.ADD, CR format

fieldbitsvaluemeaning
opc[1:0]10quadrant C2
funct4c[15:12]1001funct3=100, c[12]=1, rs2≠0 ⇒ C.ADD
rd/rs1c[11:7]01010 = 10a0
rs2c[6:2]01011 = 11a1

C.ADD “expands into add rd, rd, rs2”, so 0x952eadd a0, a0, a10x00b50533.

0x8082 = 1000 0000 1000 0010 — C.JR, CR format

fieldbitsvaluemeaning
opc[1:0]10C2
funct4c[15:12]1000funct3=100, c[12]=0, rs2=0 ⇒ C.JR
rs1c[11:7]00001 = 1ra
rs2c[6:2]00000must be zero for C.JR

C.JR expands to jalr x0, 0(rs1)jalr x0, 0(ra)0x00008067, which is what the uncompressed build of the same function actually contains.

0x852e vs 0x952e — the one-bit difference worth memorising. 0x852e disassembles to mv a0, a1. Its only difference from 0x952e is c[12]: 0 selects C.MV, 1 selects C.ADD. Same rd, same rs2, same everything else. When you write the expander, c[12] inside quadrant-2 funct3=100 is the single most load-bearing bit in the whole compressed decoder — it also distinguishes c.jr from c.jalr, and gates c.ebreak.

0x1141 — C.ADDI with a negative immediate. funct3 = 000, op = 01, rd/rs1 = c[11:7] = 00010 = sp. The immediate is {c[12], c[6:2]} = {1, 10000} = 110000, a 6-bit two’s-complement value, sign-extended from bit 5: −16. Expansion: addi sp, sp, -16 = 0xff010113. The disassembly of the uncompressed build of the same function contains exactly that word.

0x4548 — C.LW, and where the offset hides. funct3 = 010, op = 00. uimm[5:3] = c[12:10] = 001; rs1′ = c[9:7] = 010x10 = a0; uimm[2|6] = c[6:5] = 10uimm[2]=1, uimm[6]=0; rd′ = c[4:2] = 010a0. Assembling the offset: uimm = 0b0001100 = 12. Expansion lw a0, 12(a0) = 0x00c52503 — again matching the uncompressed build byte for byte.

Verified end to end, 2026-09-04

I wrote a 120-line combinational RVC expander in Verilog, built it with Verilator 5.046, and fed it 44 distinct compressed instruction words — 27 taken verbatim from clang-22.1.8 output for real C functions, and 17 assembled by GNU as to reach the formats the compiler did not emit (c.addi4spn, c.lui, c.addi16sp, c.andi, the four CA-format ALU ops, c.srli, c.beqz, c.bnez, c.j, c.jal, c.jr, c.jalr, c.slli, c.ebreak, c.nop). Each 32-bit output was written back into an object file and disassembled independently with riscv64-linux-gnu-objdump. All 44 expansions matched the compressed disassembly, including the PC-relative offsets of c.beqz, c.bnez, c.j and c.jal. The expander source and the control-signal walk-through live in Instruction Decode.

Failure Modes and Gotchas

Reading bit ranges off a rendered diagram. The manual’s figures put bit 31 on the left; packet-beta puts bit 0 on the left; WaveDrom’s {bits: N} lists fields from the least significant end. Three conventions, three chances to transpose a field. The defence is to work from the AsciiDoc/WaveDrom sources (which state widths, not positions) or from an explicit inst[hi:lo] table, and then to check against a real disassembly. A decoder with rd and rs1 swapped will still elaborate, still simulate, and fail riscv-tests in a way that looks like an ALU bug.

Assuming compressed instructions round-trip through the assembler. They do not, in one specific case. C.MV expands to add rd, x0, rs2 (0x00b00533 for c.mv a0, a1), but the assembler’s own mv a0, a1 pseudo-instruction emits addi a0, a1, 0 (0x00058513). Both are correct mvs; they are different instructions. The spec flags this explicitly: “C.MV expands to a different instruction than the canonical MV pseudoinstruction, which instead uses ADDI. Implementations that handle MV specially, e.g. using register-renaming hardware, may find it more convenient to expand C.MV to MV instead of ADD, at slight additional hardware cost.” If you write a self-check that assembles the expansion text and compares bytes, this will be your first false failure.

Forgetting that branch and jump immediates are pre-scaled. A common bug is to build imm_b correctly and then shift it left by one in the branch-target adder, doubling every offset. The 1'b0 in {..., instr[11:8], 1'b0} is the shift. Symptom: every branch lands twice as far as intended, and short forward branches land inside the following instruction.

Building the immediate from the wrong end in S-type. inst[31:25] is imm[11:5] and inst[11:7] is imm[4:0]; concatenating them in the other order produces a plausible-looking small number for small offsets (because both halves are often zero) and goes wrong the moment an offset exceeds 31. Compile a struct access with a large offset and check.

Treating funct7 as a 7-bit opcode for shifts. SLLI/SRLI/SRAI in RV32 use inst[31:25] as a selector but inst[24:20] as shamt. In RV64 the shift amount is six bits and the selector shrinks to inst[31:26]. RV32 code that decodes SRAI by testing inst[30] alone will accept illegal encodings where inst[31] or inst[29:25] are nonzero; the spec requires 0000000/0100000 exactly. My reference decoder flags anything else illegal.

Assuming a 32-bit instruction is 4-byte aligned. With C present, IALIGN is 16, so a 32-bit instruction may straddle a 4-byte boundary and require two fetches. This is a fetch-unit problem, not a decode problem, but it is the single largest structural change the C extension forces — Ibex’s documentation notes that the IF interface “performs word-aligned instruction fetches only. Misaligned instruction fetches are handled by performing two separate word-aligned instruction fetches.”

Expecting mtval to hold the expanded instruction. It holds “the shortest of: the actual faulting instruction / the first ILEN bits / the first MXLEN bits”, right-justified with upper bits zeroed. For a faulting compressed instruction, that is the 16-bit original, not the 32-bit expansion — which is why Ibex deliberately routes the compressed word forward alongside the expanded one. Detail in Instruction Decode and Control and Status Registers.

Uncertain

Verify: that no ratified revision of the Unprivileged ISA after 20250508 changes any base or C-extension field position cited here. Reason: the riscv/riscv-isa-manual repository publishes a riscv-isa-release-<sha>-<date> tag most days (the newest at time of writing is riscv-isa-release-59c2aec-2026-09-03), and I checked field layouts only against the 20250508 ratified tag, not against every subsequent snapshot. To resolve: diff src/images/wavedrom/*.edn and src/images/bytefield/rvc-instr-quad*.edn between 20250508 and the newest tag carrying :revremark: This document is in ratified state.. Base-ISA field positions are frozen by the ratification policy (“Ratified extensions are never revised”), so this is a low-probability check, not a live doubt. #uncertain

Alternatives and When to Choose Them

ARM Thumb-2 is the closest competitor and solves the same problem differently. Thumb-2 interleaves 16- and 32-bit instructions in one instruction set rather than defining a compressed alias of a 32-bit set, so a Thumb-2 16-bit instruction is not necessarily expressible as a 32-bit one. That buys slightly denser code but forfeits RVC’s one-to-one expansion property, which is precisely the property that lets a RISC-V core add C by bolting an expander onto the front of an unmodified decoder. If your goal is minimum decoder complexity for a given code density, RVC’s constraint is the better trade.

Fixed 32-bit only (RV32I, no C). Simplest possible fetch and decode: IALIGN 32, one parcel, no expander, no misaligned-fetch path. Choose this for the first working core, always. The measurement above says it costs about a third more instruction memory — a real cost on an FPGA, but a cost you should pay until RV32I passes riscv-tests. The definitely-not-esp32 MOC build ladder is explicit on this point: “The compressed extension is the last thing you add, not the first.”

RV32E halves the register file to 16 registers, which frees inst[19:16]-worth of encoding pressure and shrinks the register file itself. It does not change any instruction format — rs1 is still inst[19:15], with the top bit required to be zero. Choose it only if the register file is genuinely your area bottleneck; lowRISC’s published figures put a full RV32EC “micro” Ibex at 16.85 kGE against 26.60 kGE for RV32IMC “small”, but most of that delta is the M extension and the wider datapath, not the registers.

Zca / Zcb / Zcmp / Zcmt are the newer decomposition and extension of the C extension: Zca is essentially C-minus-floating-point, while Zcmp adds cm.push/cm.pop multi-register stack operations that expand into several 32-bit instructions and therefore break the one-to-one rule. Ibex implements them with an explicit state machine in the compressed decoder. For a first SoC this is firmly out of scope, but it is worth knowing that the “one C instruction, one base instruction” invariant is a property of the C extension specifically, not of RISC-V compression in general.

x86-style variable-length encoding is the anti-pattern here, and the RISC-V manual’s length-encoding rationale reads as a direct rebuttal: putting the length bits in fixed low positions of the first parcel means a fetch unit determines instruction length by “examining only the first few bits.” x86 requires partially decoding an instruction to learn where it ends, which is why x86 front-ends dedicate enormous area to length decoding and why RISC-V’s costs almost nothing.

Production Notes

The encoding is machine-readable, and you should use it. The riscv/riscv-opcodes repository holds the authoritative field/constant database in a terse line format — beq bimm12hi rs1 rs2 bimm12lo 14..12=0 6..2=0x18 1..0=3 — from which encoding.h, decoder tables, and test generators are produced. When your hand-written decoder and the disassembler disagree, this file is the tiebreaker, and it is small enough to read in full. Its C-extension counterpart, extensions/rv_c, encodes the quadrant/funct3 constants used in the map above.

Real cores build the immediates unconditionally. Both Ibex and PicoRV32 compute immediates before or independently of knowing the format, then select. Neither muxes instruction bits into an immediate assembler. If your design has a mux tree feeding a “generic immediate builder,” you have re-introduced exactly the cost the scramble was designed to eliminate.

Ibex replicates the instruction register to manage fan-out. Its decoder carries the comment: “To help timing the flops containing the current instruction are replicated to reduce fan-out. instr_alu is used to determine the ALU control logic and associated operand/imm select signals as the ALU is often on the more critical timing paths.” That is a production-grade admission that the instruction word drives a very large number of loads — the sign bit alone fans out to 20 immediate bits five times over — and that at some clock target you pay for it. Relevant when you get to Timing Closure and Fmax.

Density is workload-dependent; report the workload. The 33.6 % figure above is one small integer benchmark at -Os with one compiler version. The spec’s 25–30 % is the honest general range. Any claim about C-extension savings that does not name the workload, the optimisation level, and the compiler is an anecdote — the same discipline definitely-not-esp32 MOC demands of its Stage 10 benchmarks.

The -mno-relax flag matters when studying disassembly. By default the RISC-V assembler emits relocations for branches and calls so the linker can relax them, and an unlinked object file therefore shows branch offsets of zero with the real target hidden in a relocation entry. llvm-objdump -dr reveals the relocations; -mno-relax at compile time makes the assembler resolve local branches itself. Without this, hand-decoding a B-type immediate out of a .o file yields zero and looks like a bug in your arithmetic.

See Also