The Single-Cycle Processor
A single-cycle processor executes one complete instruction per clock cycle: fetch, decode, read registers, compute, access memory and write back all happen inside one clock period, with the result committed at the rising edge that ends it. Its cycles-per-instruction is exactly 1.000, by construction and regardless of what the program does — measured below on three different programs on a real core. And it is, for that same reason, the slowest useful way to build a CPU, because the clock period must be long enough for the worst instruction, and every cheap instruction is billed at that rate.
That combination — trivially correct, structurally slow — is what makes it worth building. A single-cycle core has no pipeline registers, no hazards, no forwarding paths, no branch prediction and no stalls. There is nothing in it that can be subtly wrong in a timing-dependent way. If it computes the wrong answer, the bug is in your decode table or your immediate generator, and it is visible in a single cycle of waveform. Berkeley’s teaching core collection describes its own one-stage RV32I implementation as “essentially an ISA simulator” (riscv-sodor README, read 2026-09-04) — which is precisely the property you want from a reference model.
The definitely-not-esp32 MOC states the consequence directly, and it is the argument this note is built around: the single-cycle core “is not a stepping stone you throw away — it is the reference model you will compare the pipelined version against for the rest of the project.” The section Why Build It Anyway — The Golden Reference Model, below, makes that case in mechanical detail rather than as an assertion.
Scope. Datapath and Control owns the organizing split — what each element does, what each control signal means, and how control logic relocates as the microarchitecture changes. This note owns the assembled machine: how a real instruction moves through it, why the clock period is what it is, and what to do with the thing once it works. The two notes share a vocabulary deliberately; read that one first if
ALUSrcBandWBSelare not already familiar.
Everything measured in this note was measured on a core built for it
A working RV32I single-cycle core —
regfile.v,alu.v,imm_gen.v,control.v,core.v, about 350 lines of Verilog — was written, linted clean under Verilator 5.046, and simulated under both Verilator 5.046 and Icarus Verilog 13.0, producing byte-identical output. Test programs were assembled with clang 22.1.8 (--target=riscv32 -march=rv32i -mabi=ilp32), linked withriscv64-linux-gnu-ld -m elf32lriscvfrom binutils for riscv64-linux-gnu (GCC 16.1.1 toolchain), and converted to$readmemhimages withobjcopy -O binary. Every number and every simulator transcript below is real output, not an illustration.
Mental Model — One Instruction, One Clock Edge
The single-cycle machine has one rule: between two rising clock edges, exactly one instruction happens, completely.
Everything follows from that. At the start of the cycle the PC holds an address and all the combinational logic downstream of it is settling — the instruction memory is producing bits, the decoder is producing control signals, the register file is producing operands, the ALU is producing a result, the data memory is producing a load value. None of this is “happening in order” in any temporal sense that the design cares about; it is one enormous cloud of gates whose outputs ripple to their final values somewhere inside the cycle. At the rising edge, three things sample simultaneously: the PC takes its next value, the register file takes its write, and the data memory takes its write. Then the next cycle begins.
sequenceDiagram participant CLK as clock edge participant PC as PC register participant COMB as combinational cloud participant ST as state (regfile, dmem) CLK->>PC: edge n — PC := next_pc PC->>COMB: address propagates Note over COMB: imem read → decode → regfile read<br/>→ ALU → dmem read → wb mux<br/>ALL settling, no order enforced COMB->>ST: values present at inputs, not yet committed CLK->>ST: edge n+1 — regfile and dmem sample CLK->>PC: edge n+1 — PC := next_pc (new) Note over CLK,ST: one instruction retired per edge, always
One cycle of a single-cycle core. What it shows: state changes only at clock edges, and everything between edges is combinational settling with no enforced ordering. The insight to take: the “stages” of a single-cycle core are a description of signal propagation, not of time slots. There is no fetch phase followed by a decode phase — there is one gate delay chain, and the stage names are just convenient labels for points along it. This is exactly why the same five names (IF, ID, EX, MEM, WB) can later be turned into real time slots by inserting registers between them, which is what Classic Five-Stage Pipeline does.
Three consequences are worth stating explicitly, because each is something the next microarchitecture will lose.
There are no hazards. A data hazard is one instruction reading a register another has not yet written. In a single-cycle core the previous instruction’s write completed at the previous clock edge, before this instruction’s read even began, so the register file always holds the architecturally correct value. A control hazard is fetching past a branch before knowing whether it is taken; here the branch resolves inside the same cycle that computes next_pc, so there is nothing to mispredict. Pipeline Hazards is entirely a description of problems this design does not have.
CPI is 1.000 exactly and cannot vary. Not “approximately 1”, not “1 for common instructions”. Every instruction, including loads, takes precisely one cycle. This is measurable and was measured — see the transcripts below.
Every fast instruction is billed at the speed of the slowest one. This is the price, and it is the whole content of the next-but-one section. addi x1, x0, 42 needs a register read and an add; lw needs a register read, an add, and a memory access. Both take the same clock period, because there is only one clock period.
The Complete Machine
This is the diagram. It is the most reproduced figure in computer architecture, and every version of it — Patterson and Hennessy’s, Berkeley’s, Harris and Harris’s — is the same picture with different labels. Solid arrows carry data; dashed arrows carry control.
flowchart LR PC["PC<br/>32-bit register"] IM["instruction memory<br/>ROM · read-only"] ADD4["+4"] CTRL["CONTROL UNIT<br/>combinational<br/>no clock, no state"] IG["immediate<br/>generator"] RF["REGISTER FILE<br/>32 x 32<br/>2 read · 1 write"] MUXA{"mux A<br/>ALUSrcA"} MUXB{"mux B<br/>ALUSrcB"} ALU["ALU"] CMP["branch<br/>comparator"] DM["data memory<br/>RAM · read + write"] MUXW{"write-back mux<br/>WBSel"} BADD["branch adder<br/>PC + imm"] MUXPC{"next-PC mux<br/>Branch · Jump"} PC --> IM PC --> ADD4 PC --> BADD PC --> MUXA IM -->|"instr[31:0]"| CTRL IM -->|"instr[31:7]"| IG IM -->|"rs1 = instr[19:15]"| RF IM -->|"rs2 = instr[24:20]"| RF IM -->|"rd = instr[11:7]"| RF IG --> MUXB IG --> BADD RF -->|"rs1 value"| MUXA RF -->|"rs1 value"| CMP RF -->|"rs2 value"| MUXB RF -->|"rs2 value"| CMP RF -->|"rs2 value = store data"| DM MUXA --> ALU MUXB --> ALU ALU -->|"result / address"| DM ALU --> MUXW ALU -->|"jalr target"| MUXPC DM -->|"load data"| MUXW ADD4 -->|"PC+4 for jal/jalr"| MUXW MUXW -->|"rd write data"| RF ADD4 --> MUXPC BADD --> MUXPC MUXPC --> PC CTRL -.->|"ALUSrcA"| MUXA CTRL -.->|"ALUSrcB"| MUXB CTRL -.->|"ALUOp"| ALU CTRL -.->|"RegWrite"| RF CTRL -.->|"MemWrite / MemRead"| DM CTRL -.->|"WBSel"| MUXW CTRL -.->|"Branch · Jump"| MUXPC CMP -.->|"condition true?"| MUXPC
The classic single-cycle RV32I datapath with control overlaid. What it shows: the complete machine — every element, every mux, and every control signal that steers one. Solid = data, dashed = control. The insight to take: trace the longest solid path with your finger. PC → instruction memory → register file → mux B → ALU → data memory → write-back mux → register file touches six elements in series, and that path belongs to exactly one instruction class: the load. That finger-trace is the entire reason single-cycle designs are slow, and it is drawn explicitly two sections below.
A few structural points that the diagram encodes and that are easy to miss.
The PC feeds four places. Instruction memory (obviously), the +4 adder, the branch adder, and mux A. The last is only for auipc, and if you drop auipc from your instruction subset you can drop mux A entirely — a genuinely useful simplification for a first core.
There are two adders besides the ALU. +4 and PC + imm are separate hardware, not ALU operations. They could be ALU operations in a multi-cycle design, where the ALU is reused across cycles; in a single-cycle design they cannot be, because the ALU is already busy computing this instruction’s result in the same cycle. Every case where a single-cycle core needs two of something is a case where a multi-cycle core needs one. That is the trade in one sentence.
Instruction memory and data memory are separate boxes. This is forced, not stylistic — see The Memory Question — Why Single-Cycle Forces a Harvard Split, below.
The branch comparator is drawn separately from the ALU. Many textbook versions route rs1 - rs2 through the ALU and use its zero flag. That works for beq/bne and is a genuine gate saving, but it puts the ALU on the path to the next-PC mux as well as on the path to the write-back mux, and it cannot correctly implement blt without care about overflow. The core built for this note uses a dedicated comparator; the trade-off is discussed under Failure Modes and Gotchas, below.
Instruction by Instruction Through the Same Hardware
The single datapath above executes every RV32I instruction. Nothing is added or removed per instruction — only the muxes move. This section walks five instructions through it, using the real control-signal values dumped from the working core (the full measured table is in Datapath and Control).
addi x1, x0, 42 — the first instruction the core ever ran
Encoding 0x02a00093. Control: RegWrite=1, ALUSrcA=0, ALUSrcB=1, MemWrite=0, WBSel=0, Branch=0, Jump=0, ALUOp=ADD.
The PC (0) addresses instruction memory, which returns the word. The decoder sees opcode 0010011 and asserts RegWrite and ALUSrcB. The immediate generator sign-extends instr[31:20] to 0x0000002A. The register file reads rs1 = x0, which is hardwired to zero. Mux A passes rs1 (=0); mux B passes the immediate (=42). The ALU adds: 42. The data memory is addressed with 42 and produces something, which nobody uses because WBSel = 0 selects the ALU result. At the clock edge, x1 takes the value 42 and the PC takes PC+4. Two elements did real work; four were on but ignored.
add x2, x2, x3 — the same path with one mux moved
Encoding 0x00310133. Control: identical to addi except ALUSrcB = 0.
That single bit is the entire difference between register-immediate and register-register arithmetic. Mux B now passes the register file’s rs2 output instead of the immediate. The immediate generator still runs — it produces a nonsense value from an R-type instruction word — and is simply not selected. Nothing in the datapath is disabled; the mux just points elsewhere.
lw x6, 0(x0) — the long path
Encoding 0x00002303. Control: RegWrite=1, ALUSrcB=1, MemRead=1, MemWrite=0, WBSel=1, ALUOp=ADD.
flowchart LR PC["PC"] --> IM["instruction<br/>memory"] IM --> RF["register file<br/>read rs1"] IM --> IG["immediate<br/>generator<br/>I-type"] RF -->|"rs1 = base"| MB{"mux B<br/>ALUSrcB=1"} IG -->|"offset"| MB MB --> ALU["ALU<br/>ADD → address"] ALU -->|"effective address"| DM["data memory<br/>read"] DM -->|"load data"| MW{"write-back mux<br/>WBSel=1"} MW -->|"rd"| RFW["register file<br/>write rd"] ADD4["+4"] -.->|"not selected"| MW ALUX["ALU result"] -.->|"not selected"| MW PC --> A4["+4"] --> MPC{"next-PC mux<br/>Branch=0 Jump=0"} --> PCN["PC ← PC+4"] classDef hot fill:#ffe0b2,stroke:#e65100,stroke-width:3px classDef cold fill:#eeeeee,stroke:#bdbdbd,color:#9e9e9e class PC,IM,RF,IG,MB,ALU,DM,MW,RFW hot class ADD4,ALUX cold
The lw path, active elements highlighted. What it shows: nine elements in series between the clock edge that launches the instruction and the clock edge that commits it — PC, instruction memory, register file read, immediate generator, mux B, ALU, data memory, write-back mux, register file write. The insight to take: this is the longest chain any RV32I instruction takes, and because there is only one clock period, this chain defines it. lw is not slow relative to other instructions in a single-cycle core — nothing is relatively slow, because everything takes one cycle. lw is slow in the sense that it makes everything else slow.
sw x2, 0(x0) — the load path minus the return trip
Encoding 0x00202023. Control: RegWrite=0, ALUSrcB=1, MemWrite=1, Branch=0, Jump=0, ALUOp=ADD.
Address computation is identical to lw: rs1 + S-immediate. The difference is where the data comes from. rs2’s value goes directly from the register file to the data memory’s write-data port, bypassing the ALU entirely, and RegWrite is 0 so nothing is written back. Notice that sw is genuinely shorter than lw: it ends at the memory rather than going through the memory, the write-back mux and the register file’s write port. In a design where the clock could vary per instruction, a store would be cheaper than a load. It cannot, so it is not.
bne x3, x4, loop — the path that changes the PC
Encoding 0xfe419ce3. Control: RegWrite=0, ALUSrcB=0, MemWrite=0, Branch=1, Jump=0.
flowchart LR PC["PC"] --> IM["instruction<br/>memory"] IM --> RF["register file<br/>read rs1, rs2"] IM --> IG["immediate generator<br/>B-type · scrambled bits<br/>implicit LSB = 0"] RF -->|"rs1"| CMP["branch comparator<br/>funct3 = 001 → rs1 != rs2"] RF -->|"rs2"| CMP PC --> BADD["branch adder<br/>PC + imm"] IG --> BADD PC --> A4["+4"] CMP -->|"taken?"| MPC{"next-PC mux<br/>Branch=1"} A4 --> MPC BADD --> MPC MPC --> PCN["PC ← target or PC+4"] ALU["ALU"] -.->|"result discarded"| X["no write-back<br/>RegWrite=0"] DM["data memory"] -.->|"read but unused"| X classDef hot fill:#ffe0b2,stroke:#e65100,stroke-width:3px classDef cold fill:#eeeeee,stroke:#bdbdbd,color:#9e9e9e class PC,IM,RF,IG,CMP,BADD,A4,MPC,PCN hot class ALU,DM,X cold
The bne path, active elements highlighted. What it shows: a branch never touches the write-back path at all. Its work is entirely in the comparator, the branch adder, and the next-PC mux. The insight to take: compare this diagram to the lw one. They share only the first three elements and then diverge completely, and the branch’s chain is visibly shorter. In a pipelined core this asymmetry becomes an opportunity — resolve the branch in EX and you save a stage of misprediction penalty. In a single-cycle core it is pure waste, because the branch still waits for the load’s clock period.
The same story as a table
| Instruction | ALUSrcA | ALUSrcB | ALUOp | RegWrite | MemWrite | WBSel | next PC |
|---|---|---|---|---|---|---|---|
addi rd, rs1, imm | rs1 | imm | ADD | 1 | 0 | ALU | PC+4 |
add rd, rs1, rs2 | rs1 | rs2 | ADD | 1 | 0 | ALU | PC+4 |
sub rd, rs1, rs2 | rs1 | rs2 | SUB | 1 | 0 | ALU | PC+4 |
lw rd, imm(rs1) | rs1 | imm | ADD | 1 | 0 | memory | PC+4 |
sw rs2, imm(rs1) | rs1 | imm | ADD | 0 | 1 | — | PC+4 |
beq/bne rs1, rs2, off | rs1 | rs2 | SUB | 0 | 0 | — | PC+imm if taken |
jal rd, off | — | — | — | 1 | 0 | PC+4 | PC+imm |
jalr rd, imm(rs1) | rs1 | imm | ADD | 1 | 0 | PC+4 | (rs1+imm) & ~1 |
lui rd, imm | — | imm | PASS B | 1 | 0 | ALU | PC+4 |
auipc rd, imm | PC | imm | ADD | 1 | 0 | ALU | PC+4 |
Every RV32I class through one datapath. What it shows: the bold cells are the only ones that differ from the addi baseline. The insight to take: the whole instruction set is nine columns of a truth table over one piece of hardware. Ten instruction classes, and the datapath is byte-for-byte the same for all of them — which is why “adding an instruction” to a working core usually means adding one row here and, at most, one input to one mux.
The Fundamental Problem — The Slowest Instruction Sets the Clock
A synchronous digital circuit works only if every combinational path between two registers settles before the next clock edge. The clock period must therefore satisfy
T_clk ≥ t_cq + t_logic(worst path) + t_setup + t_skewwhere t_cq is the clock-to-output delay of the source flip-flop (how long after the edge its output is valid), t_logic(worst path) is the propagation delay through the longest chain of combinational logic between any two registers, t_setup is how long the destination flip-flop needs its input stable before the edge, and t_skew is the worst-case difference in clock arrival time between the two flip-flops. The maximum frequency is F_max = 1 / T_clk. This is the subject of Timing Closure and Fmax; what matters here is the single term t_logic(worst path).
In a single-cycle processor there are only three destination registers — the PC, the register file, and the data memory — and the path to each starts at the PC. So t_logic(worst path) is the longest chain from the PC output to any of those three inputs. That chain belongs to lw.
flowchart LR E0(["clock edge n"]) --> PC["PC output valid<br/>t_cq"] PC --> IM["instruction memory<br/>address → data"] IM --> DEC["decode + immediate gen<br/>(parallel with regfile read)"] IM --> RFR["register file read<br/>address → data"] DEC --> MUXB["mux B select"] RFR --> MUXB MUXB --> ALU["ALU<br/>32-bit add<br/>carry propagation"] ALU --> DM["data memory<br/>address → data"] DM --> MUXW["write-back mux"] MUXW --> RFW["register file<br/>write-data setup<br/>t_setup"] RFW --> E1(["clock edge n+1"]) style ALU fill:#ffe0b2,stroke:#e65100,stroke-width:3px style DM fill:#ffcdd2,stroke:#b71c1c,stroke-width:3px style IM fill:#ffcdd2,stroke:#b71c1c,stroke-width:3px
The critical path of a single-cycle RV32I core: the lw chain. What it shows: eight serial delay contributors between two clock edges, with the two memory accesses (red) and the 32-bit adder (orange) dominating. The insight to take: the two memory accesses are in series in the same cycle — the instruction memory read must finish before the register file read can start, which must finish before the ALU can compute the address, which must finish before the data memory read can start. Every other instruction’s path is a prefix or a branch off this one, and every other instruction pays this period anyway.
Walk the chain and it becomes obvious why the memory terms dominate. An instruction fetch is a full memory access. A register-file read is a small memory access. A 32-bit add is a carry chain — on an FPGA that maps to dedicated carry logic and is fast, but it is still 32 stages of carry. Then a second full memory access for the load. Then a mux and a setup time. Two large memory accesses plus a 32-bit adder, all in series, all inside one clock period.
Contrast the paths:
| Instruction | Serial elements from PC to commit | Relative path length |
|---|---|---|
beq (not taken) | PC → imem → regfile read → comparator → next-PC mux | shortest |
add | PC → imem → regfile read → mux → ALU → wb mux → regfile write | medium |
sw | PC → imem → regfile read → mux → ALU → dmem write setup | medium |
lw | PC → imem → regfile read → mux → ALU → dmem read → wb mux → regfile write | longest |
Path lengths by instruction class. What it shows: lw is the only class that puts two memory accesses and an adder in series. The insight to take: in a design that could vary its clock per instruction, a beq might run at three or four times the frequency of a lw. Because it cannot, the branch runs at the load’s frequency. This is the precise sense in which “correct but slow” is a structural property, not an implementation weakness — no amount of careful RTL fixes it, because the problem is the absence of a register in the middle of that chain.
Uncertain
Verify: absolute nanosecond delays and an
F_maxfigure for the specific core built for this note. Reason: no synthesis tool (yosys, Vivado, Gowin EDA) is installed on the machine where this note was written — only Verilator 5.046 and Icarus Verilog 13.0, which are functional simulators and report no timing information whatsoever. Every timing statement here is therefore structural (which elements are in series) and not measured (how many nanoseconds). To resolve: synthesizecore.vwithyosys+nextpnr-gowinfor the Tang Nano 20K, or with Gowin EDA, and read the critical path out of the timing report. That report will also settle whether the instruction memory, the ALU, or the data memory actually dominates on that specific fabric — it is frequently not the one you expect. Do not quote a number for this core until that is done. uncertain
What can be quoted are published F_max figures for real cores that made the opposite trade, which bound the problem from the other side. PicoRV32, a multi-cycle RV32I core explicitly “optimized for size and fmax, not performance”, is place-and-routed by its authors at 2.4 ns (416 MHz) on a Xilinx Kintex-7T -2 part and down to 1.3 ns (769 MHz) on Kintex UltraScale+ -3, in 761–2019 slice LUTs (README, evaluations run with Vivado 2017.3, read 2026-09-04). It buys those periods by never putting two memory accesses in one cycle — and pays for it with an average CPI of about 4, measured at 4.100 on Dhrystone for 0.516 DMIPS/MHz.
The Memory Question — Why Single-Cycle Forces a Harvard Split
There is a second, harder consequence of “one instruction per cycle” and it is architectural rather than merely slow.
In the same clock cycle, a lw must read the instruction (at the PC) and read the data (at rs1 + imm). Those are two reads at two unrelated addresses at the same instant. A single memory with one port physically cannot serve both. There are exactly three ways out:
- Two separate memories — an instruction memory and a data memory. This is a Harvard architecture, and it is what every single-cycle design does.
- One dual-ported memory. Possible on an FPGA, where block RAM is natively dual-port, but it costs a port that you usually want for something else, and it does not scale to a real memory system.
- Give up one-instruction-per-cycle — spend one cycle fetching and another accessing data. This is exactly what a multi-cycle design is, and it is why PicoRV32’s loads cost 5 cycles.
Option 1 is not a design preference; it is the only choice that keeps CPI at 1 with ordinary single-ported memory.
flowchart TB subgraph HARV["Harvard — what single-cycle forces"] PC1["PC"] --> IMEM["instruction memory<br/>(ROM)"] ALU1["ALU address"] --> DMEM["data memory<br/>(RAM)"] IMEM -.->|"same cycle"| BOTH1["both reads<br/>concurrently"] DMEM -.->|"same cycle"| BOTH1 end subgraph VN["von Neumann — one memory"] PC2["PC"] --> MUX{"address mux"} ALU2["ALU address"] --> MUX MUX --> MEM["unified memory"] MEM -.->|"cycle 1: fetch<br/>cycle 2: data"| SEQ["two cycles<br/>minimum"] end
The memory structure a single-cycle core requires, versus the unified alternative. What it shows: with one memory, the address mux forces the fetch and the data access into different cycles; with two memories, they happen at once. The insight to take: the Harvard split is not a philosophical stance about program-versus-data separation. It is a direct mechanical consequence of insisting on CPI = 1 with single-ported memory — and it is why the first SoC you build has a boot ROM and a separate RAM rather than one flat memory, and why the memory map has to distinguish them.
Real systems blur this back. Instruction and data caches give a von Neumann machine a Harvard-like front end while sharing one main memory — a “modified Harvard” arrangement. But at the level a first core operates at, with the ROM and RAM as literal separate arrays, the split is real and visible. It is also why the very first thing that breaks when you try to run self-modifying code, or to load a program into RAM and jump to it, is that your instruction fetch cannot see writes made through the data port. RISC-V addresses this at the ISA level with the Zifencei extension’s fence.i instruction; a strict single-cycle Harvard core with a ROM cannot implement it meaningfully at all.
Building It — A Working RV32I Core
Everything below was written, run, and its output copied verbatim. Tool versions: Verilator 5.046 (2026-02-28, Fedora build), Icarus Verilog 13.0, clang 22.1.8, binutils for riscv64-linux-gnu (GCC 16.1.1 toolchain).
The top level
The control unit is quoted in full in Datapath and Control; the register file and ALU are conventional and are covered by The Register File and The Arithmetic Logic Unit. What is worth reading here is the top level, because it is where the diagram above becomes text.
module core #(
parameter IMEM_WORDS = 1024,
parameter DMEM_WORDS = 1024,
parameter RESET_PC = 32'h0000_0000
)(
input wire clk,
input wire rst_n,
output wire halted,
output wire trap_illegal
);
// ---------------- Sequential state: this is the entire architectural state
reg [31:0] pc;
// ---------------- Instruction memory (read-only, asynchronous read)
reg [31:0] imem [0:IMEM_WORDS-1];
wire [$clog2(IMEM_WORDS)-1:0] iindex = pc[$clog2(IMEM_WORDS)+1:2];
wire [31:0] instr = imem[iindex];
...
// ---------------- Execute
wire [31:0] alu_a = alu_src_a ? pc : rs1_data;
wire [31:0] alu_b = alu_src_b ? imm : rs2_data;
wire [31:0] alu_y;
alu u_alu (.a(alu_a), .b(alu_b), .op(alu_op), .y(alu_y), .zero(alu_zero));
// Branch comparator. Kept separate from the ALU so that the ALU result
// mux and the PC mux do not sit on the same chain twice.
wire beq_t = (rs1_data == rs2_data);
wire blt_t = ($signed(rs1_data) < $signed(rs2_data));
wire bltu_t = (rs1_data < rs2_data);
reg branch_taken;
always @* begin
case (funct3)
3'b000: branch_taken = beq_t; // beq
3'b001: branch_taken = ~beq_t; // bne
3'b100: branch_taken = blt_t; // blt
3'b101: branch_taken = ~blt_t; // bge
3'b110: branch_taken = bltu_t; // bltu
3'b111: branch_taken = ~bltu_t; // bgeu
default: branch_taken = 1'b0;
endcase
end
...
// ---------------- Write-back mux
assign wb_data = (wb_sel == 2'd1) ? load_data :
(wb_sel == 2'd2) ? (pc + 32'd4) : alu_y;
// ---------------- Next-PC mux
wire take_branch = branch & branch_taken;
wire [31:0] pc_plus4 = pc + 32'd4;
wire [31:0] pc_branch = pc + imm;
wire [31:0] pc_jalr = (rs1_data + imm) & ~32'd1; // spec: clear bit 0
wire [31:0] pc_next = jalr ? pc_jalr :
jump ? pc_branch : // jal: PC + J-imm
take_branch ? pc_branch : pc_plus4;
// ---------------- Halt: EBREAK (0x00100073) stops the simulation.
// Simulation convenience, not architecture -- a real core traps here.
assign halted = (instr == 32'h0010_0073);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) pc <= RESET_PC;
else if (!halted) pc <= pc_next;
end
endmodulePoints worth pausing on:
reg [31:0] pc;is the only line in the module that creates architectural sequential state apart from the two memory arrays and the register file. That is the whole machine’s state. You can print it and know everything.- The four
? :chains are the four muxes from the diagram, written as ternary cascades.alu_a,alu_b,wb_dataandpc_nextare mux A, mux B, the write-back mux and the next-PC mux respectively. The correspondence is one-to-one, which is the point of drawing the diagram first. (rs1_data + imm) & ~32'd1implements thejalrLSB clear required by the specification. This is a real conformance requirement, not an optimization; the spec’s rationale explains that “clearing the least-significant bit when calculating the JALR target address both simplifies the hardware slightly and allows the low bit of function pointers to be used to store auxiliary information.”haltedis a simulation hook, not architecture. A real core takes a breakpoint trap onebreak. Marking the boundary between “hardware I am building” and “harness that lets me observe it” in a comment is worth the two seconds — it is the kind of thing that quietly ends up in a synthesis run.- The asynchronous reset (
negedge rst_n) is FPGA-appropriate but not universal. Some fabrics prefer synchronous reset; the Verilog is written this way because it simulates identically under both tools and matches the reset style most FPGA flip-flops support natively.
The MOC’s promised artifact
The definitely-not-esp32 MOC’s Stage 2 program is “a single-cycle RV32I core that executes addi x1, x0, 42 and halts… The known answer is the register file: you can read x1 out of the simulator and check it by hand.” Here it is.
.section .text
.globl _start
_start:
addi x1, x0, 42
ebreakAssembled and disassembled with the real toolchain:
$ clang --target=riscv32 -march=rv32i -mabi=ilp32 -c t1.S -o t1.o
$ riscv64-linux-gnu-ld -m elf32lriscv -T link.ld t1.o -o t1.elf
$ riscv64-linux-gnu-objdump -d t1.elf
00000000 <_start>:
0: 02a00093 li ra,42
4: 00100073 ebreakNote that the disassembler prints li ra,42: li is the pseudo-instruction and ra is the ABI name for x1. The encoded word is 0x02a00093, which is addi x1, x0, 42 — the assembler expanded the pseudo-instruction into exactly the base instruction we want.
Running it on the core:
$ iverilog -g2012 -o sim_iv alu.v regfile.v imm_gen.v control.v core.v tb.v
$ ./sim_iv +hex=sw/t1.hex +trace
cyc1 pc=00000000 instr=02a00093
cyc2 pc=00000004 instr=00100073
HALT at pc=00000004 after 2 cycles
x1 = 0x0000002a (42)
dmem[0..3] = 00000000 00000000 00000000 00000000x1 = 0x0000002a. That is 42, read out of the register file of a processor that did not exist an hour earlier. Two instructions, two cycles.
A program that exercises the rest of the datapath
One instruction proves the wiring is not completely wrong. A program with a loop, memory traffic, a sub-word access and a function call proves the datapath. This one sums 1 to 10, round-trips the result through memory, does a byte store and zero-extended byte load, and calls a leaf function:
_start:
addi x1, x0, 42 # the canonical first instruction
addi x2, x0, 0 # sum = 0
addi x3, x0, 1 # i = 1
addi x4, x0, 11 # limit
loop:
add x2, x2, x3 # sum += i
addi x3, x3, 1 # i++
bne x3, x4, loop # 10 iterations -> sum = 55
sw x2, 0(x0) # store sum to data memory word 0
lw x6, 0(x0) # read it straight back
sub x7, x6, x2 # must be zero
bne x7, x0, fail
lui x9, 0xdead0 # U-type
auipc x10, 0 # PC-relative: x10 = address of this instruction
addi x13, x0, 0x5a
sb x13, 5(x0) # byte store into word 1, byte 1
lbu x14, 5(x0) # read it back zero-extended
jal x11, leaf # x11 = return address
ebreak
fail:
addi x8, x0, -1
ebreak
leaf:
addi x12, x0, 7
jalr x0, 0(x11) # return$ ./sim_iv +hex=sw/t2.hex
HALT at pc=00000044 after 47 cycles
x1 = 0x0000002a (42)
x2 = 0x00000037 (55)
x3 = 0x0000000b (11)
x4 = 0x0000000b (11)
x6 = 0x00000037 (55)
x9 = 0xdead0000 (-559087616)
x10 = 0x00000030 (48)
x11 = 0x00000044 (68)
x12 = 0x00000007 (7)
x13 = 0x0000005a (90)
x14 = 0x0000005a (90)
dmem[0..3] = 00000037 00005a00 00000000 00000000Every value is checkable by hand, which is exactly what the MOC’s “known answer” discipline demands:
x2 = 55— the sum 1+2+…+10. The loop ran,bnetook the branch nine times and fell through once.x6 = 55andx7never set — the store/load round trip through data memory worked, and thesubproduced zero so thebnetofailwas not taken.x9 = 0xdead0000—luiplaced the 20-bit immediate in bits 31:12 with zeros below.x10 = 0x30—auipc x10, 0yields the address of theauipcitself, and the disassembly puts it at0x30. This is the only instruction that exercises mux A.x11 = 0x44— the return address pushed byjalat0x40, i.e.0x40 + 4. The write-back mux selectedPC+4.x12 = 7— the leaf function actually ran, sojalreached it andjalr x0, 0(x11)returned.dmem[1] = 0x00005a00— the byte store went to address 5, which is byte 1 of word 1. The write strobe logic put0x5ain the right lane and left the other three bytes alone;lburead it back zero-extended intox14.
Then the same RTL was elaborated and run by the other simulator, with byte-identical results:
$ verilator --binary --timing --timescale 1ns/1ps --top-module tb -o vsim \
alu.v regfile.v imm_gen.v control.v core.v tb.v
$ ./obj_dir/vsim +hex=sw/t2.hex
HALT at pc=00000044 after 47 cycles
x1 = 0x0000002a (42)
x2 = 0x00000037 (55)
...
- Verilator: $finish at 475nsTwo independent simulators agreeing is a weak but genuinely useful check: it rules out the class of bugs where one tool’s interpretation of a Verilog construct differs from another’s — variable part-selects, $signed comparisons, and array-element part-select assignment are all places where that happens.
CPI = 1.000, measured on three programs
Because a cycle counter and an instruction count are both available in the testbench, the defining property of the microarchitecture can simply be measured. Three programs with deliberately different instruction mixes:
| Program | Mix | Instructions retired | Cycles | CPI |
|---|---|---|---|---|
t2 | loop, memory round-trip, jal/jalr, lui/auipc, byte access | 47 | 47 | 1.000 |
t3 | 40 × addi — pure ALU, zero memory traffic | 41 | 41 | 1.000 |
t4 | 20 × (addi, sw, lw) — memory-saturated | 61 | 61 | 1.000 |
Measured CPI across three instruction mixes, Icarus Verilog 13.0. What it shows: CPI is exactly 1.000 whether the program is pure arithmetic or two-thirds memory traffic. The insight to take: compare with PicoRV32, whose published per-instruction CPI ranges from 3 for an ALU operation to 5 for a load and 6 for jalr, averaging 4.100 on Dhrystone. A single-cycle core’s CPI is mix-independent because the mix has nowhere to express itself — the cost of the load was already paid, by every instruction, in the clock period. This table is the single-cycle trade-off, measured: perfect CPI, purchased with a clock nobody can raise.
Why Build It Anyway — The Golden Reference Model
If the single-cycle core is structurally slow and will be replaced in the next project stage, why build it at all rather than going straight to a pipeline? The definitely-not-esp32 MOC answers this in one sentence — it “is not a stepping stone you throw away — it is the reference model you will compare the pipelined version against for the rest of the project” — and the sentence is worth unpacking, because it makes a stronger claim than “it is a good learning exercise.”
The argument is about where correctness comes from. A pipelined core is correct if and only if it produces, for every program, the same architectural state as if the instructions had executed one at a time to completion. That is the definition of what forwarding and hazard interlocks are for. So the specification of the pipelined core is not the RISC-V manual directly — it is “behave like a machine that executes one instruction at a time.” A single-cycle core is that machine, in synthesizable RTL, in about 350 lines. It is not an approximation of the reference semantics; it is a mechanical realization of them.
That makes differential testing possible in a way that testing against the ISA manual is not.
flowchart TB PROG["test program<br/>(riscv-tests, random, or handwritten)"] PROG --> SC["single-cycle core<br/>CPI 1 · no hazards<br/>350 lines"] PROG --> PIPE["pipelined core<br/>5 stages · forwarding<br/>hazard unit · branch flush"] SC --> TRACE1["architectural trace<br/>PC, rd, value, mem writes<br/>one entry per retire"] PIPE --> TRACE2["architectural trace<br/>one entry per retire<br/>(from WB stage)"] TRACE1 --> CMP{"compare<br/>entry by entry"} TRACE2 --> CMP CMP -->|"identical"| OK["pipeline is correct<br/>for this program"] CMP -->|"first divergence"| BUG["exact instruction,<br/>exact cycle,<br/>exact wrong value"] BUG --> DBG["open the VCD at that cycle<br/>and look at the forwarding muxes"]
Differential testing against the single-cycle core. What it shows: both cores run the same program, each emits one trace entry per instruction retired, and the traces are compared. The insight to take: the payoff is the arrow labelled “first divergence”. Without a reference model, a pipeline bug presents as a wrong final answer thousands of cycles after the cause, and you bisect by hand. With one, the harness names the exact instruction where the two machines first disagreed — which is, essentially always, the instruction whose forwarding path is wrong. That reduction from “somewhere in this program” to “this instruction” is the difference between a bug you can fix in ten minutes and one that eats a weekend.
There are four further reasons that survive scrutiny.
It is small enough to be obviously right. With one register (pc) plus the register file and two memories, the entire state of the machine fits on one screen. When a riscv-tests case fails, the single-cycle core lets you determine whether the decoder is wrong or the pipeline is wrong — a distinction that is genuinely hard to make when only one core exists. Berkeley’s own framing of Sodor’s one-stage core as “essentially an ISA simulator” is exactly this property.
It separates two kinds of bug that otherwise arrive together. Decode bugs (wrong immediate shape, wrong ALU op, wrong write-back source) and timing bugs (missing forward, missing stall, missing flush) have completely different fixes and completely different symptoms — but if you write a pipeline first, they arrive simultaneously and each masks the other. Building the single-cycle core first means you retire the entire class of decode bugs before introducing any timing at all. When you then pipeline it and something breaks, you know a priori it is timing.
It gives the official test suite somewhere to land early. The rv32ui-p-* programs from The riscv-tests Suite are self-checking and do not care about microarchitecture. Running them against the single-cycle core in Stage 2 rather than Stage 4 means the decode table is validated against someone else’s understanding of the ISA before you build anything on top of it. This is the MOC’s own warning made concrete: “Your own tests encode your own misunderstandings; the official suite does not.”
It is the thing that stays comprehensible when the pipeline stops being. Once forwarding, load-use interlocks, branch flushes and CSR side effects are in, the pipelined core is no longer something you can hold in your head. The single-cycle core still is, indefinitely. That is worth keeping in the repository and in CI long after it has stopped being the core you actually run.
One honest caveat: a single-cycle core is a reference model for architectural behaviour only — final register and memory values. It says nothing about timing, interrupt latency, or the precise cycle at which a trap is taken, and it cannot be used to validate anything about Physical Memory Protection timing or interrupt response. For those you need either a golden trace from a formally specified simulator (Spike, Sail) or the real hardware. Knowing the boundary of what a reference model certifies is part of using one properly.
Single-Cycle to Multi-Cycle to Pipelined
The three classic microarchitectures are best understood as three answers to the same question: what do you do about the fact that lw is long?
timeline title What each microarchitecture buys, and what it costs Single-cycle : CPI exactly 1.000 : clock set by the lw path : two memories required : no hazards at all : smallest control logic Multi-cycle : CPI 3-6, mix-dependent : clock set by the longest SINGLE step : one memory suffices : control becomes an FSM : hardware reused across cycles Pipelined : CPI approaches 1 again : clock set by the longest STAGE : two memory ports still needed : hazards appear - forwarding, stalls, flushes : control distributed into stage registers
The progression. What it shows: each step changes what the clock period is set by. The insight to take: multi-cycle and pipelining are not two points on one axis — they are opposite moves. Multi-cycle removes hardware and pays in cycles; pipelining keeps the hardware and pays in control complexity. They happen to share the idea of putting a register in the middle of the long path, which is why they are taught in sequence, but their motivations are different: multi-cycle optimizes area, pipelining optimizes throughput.
| Single-cycle | Multi-cycle | Five-stage pipeline | |
|---|---|---|---|
| Instructions in flight | 1 | 1 | up to 5 |
| CPI | 1.000 exactly (measured above) | 3–6, instruction-dependent | ~1.1–1.5 with hazards |
| Clock period set by | the whole lw path | the longest single step | the longest stage |
| Memories needed | 2 (Harvard, forced) | 1 (can be unified) | 2 ports (I-side and D-side) |
| Control implementation | combinational function | finite state machine | decode once + stage registers + hazard unit |
| Hazards | none | none | data, control, structural |
| Extra state | none | IR, A, B, MA registers | 4 stage-register banks |
| Real example | Sodor rv32_1stage | PicoRV32, Sodor rv32_ucode | Sodor rv32_5stage, Ibex (2-stage variant) |
The three microarchitectures compared. What it shows: the columns that actually move are CPI, what sets the clock, and how much state control carries. The insight to take: the datapath row is missing from this table because it would read the same in all three columns. The elements do not change; the registers between them and the control that steers them do.
The arithmetic that makes the progression worth it
Performance is instructions × CPI × T_clk. A single-cycle core minimizes CPI at 1.000 and maximizes T_clk. A multi-cycle core does the reverse. Neither of those is obviously better — that is the honest position, and it is why multi-cycle cores like PicoRV32 remain in production. PicoRV32’s own numbers make the case: CPI 4.100 on Dhrystone, but 416–769 MHz and 761 LUTs. If your FPGA design already runs at 400 MHz and you need a small housekeeping processor that will not force a clock-domain crossing, that is a better core than a single-cycle one that tops out at a fraction of the frequency.
Pipelining is the move that is unambiguously better on throughput, because it takes the multi-cycle core’s short clock period and hands back the single-cycle core’s CPI — asymptotically. The cost is entirely in control: the hazard unit, the forwarding network, and the flush logic, none of which exist in either predecessor. That cost is why Pipeline Hazards, Operand Forwarding and Load-Use Hazard are three separate notes.
Two things about the “asymptotically” are worth being precise about, because glossy treatments skip them.
A pipeline’s CPI is never 1. Every taken branch costs a flush of the instructions fetched behind it, and every load immediately followed by a use of its result costs a mandatory bubble that forwarding cannot remove — that is precisely the Load-Use Hazard. Real CPI lands somewhere around 1.1–1.3 for typical code on a five-stage machine without branch prediction, which is why Branch Prediction is Stage 9 of the project rather than optional polish.
A pipeline’s clock is not T_single-cycle / 5. Splitting a path into five parts does not divide its delay by five, because (a) the parts are not equal — the memory stages are longer than the decode stage — and (b) each stage boundary adds a flip-flop’s t_cq + t_setup, which is pure overhead repeated five times. Ibex’s documentation is refreshingly blunt about the resulting floor: with a two-stage pipeline, “All instructions require two cycles minimum to pass down the pipeline… the maximum IPC (Instructions per Cycle) Ibex can achieve is 1”, and loads and stores “stall for at least one cycle to await a response.”
The project ordering in the definitely-not-esp32 MOC reflects all of this: build the single-cycle core (Stage 2), pipeline it and write the tests that break it (Stage 3), prove it against riscv-tests (Stage 4), and only measure and optimize once a kernel runs on it (Stage 9). Skipping to the pipeline skips the only version of the machine simple enough to be confident about.
Failure Modes and Gotchas
These are the bugs that actually happened, or that the design deliberately avoids. Decoder-specific failures live in Datapath and Control; these are the ones that come from assembling the machine.
Synchronous memory silently breaks the whole model. Symptom: every instruction executes the one before it, or loads return the previous load’s value. Cause: on an FPGA, inferring block RAM gives you a registered read — data appears one cycle after the address. A single-cycle core requires asynchronous (combinational) reads from both memories. In simulation with a plain reg [31:0] mem [0:N] array read combinationally, this never shows up; on hardware it appears immediately. Fix: either force distributed RAM / LUT RAM for small memories, or accept the extra cycle and you now have a multi-cycle core. This is the single most common way a working simulation fails to become working hardware.
Writing x0. Symptom: programs behave correctly for a while and then compare against garbage. Cause: compilers emit x0 as a destination constantly — addi x0, x0, 0 is the canonical nop, and jalr x0, 0(x1) is ret. Both appear in the test program above. If the register file does not suppress writes to index 0, nop clobbers the constant zero. RV32I requires x0 be “hardwired with all bits equal to 0” (RISC-V ISA Manual, Volume I, §2.1.1). The fix belongs inside the register file, not in the decoder.
Byte and half-word accesses done as full words. Symptom: sb destroys the three neighbouring bytes; lb returns a wrong-signed value. Cause: an easy early shortcut is to implement only lw/sw. The moment the compiler emits a char or a short, or a bool field in a struct, you need per-byte write strobes and sign/zero-extending load alignment. The core here computes wstrb = 4'b0001 << boff for sb and replicates the source byte across all four lanes, which is the standard trick — the strobe decides which lane actually lands. The measured dmem[1] = 0x00005a00 in the transcript above is that logic working.
Branch immediate decoded as an S-type. Symptom: forward branches with small offsets work; anything else jumps to garbage. Cause: S-type and B-type use the same instruction bits but assign them differently — B-type puts instr[7] at immediate bit 11 and instr[31] at bit 12, with an implicit zero at bit 0. Getting it wrong yields targets off by roughly a factor of two, which does not look like an immediate bug at all.
Store data taken from the ALU output. Symptom: memory fills with addresses. Cause: the store-data path must come straight from the register file’s rs2 port. The ALU is busy computing the address.
The ALU on the next-PC path as well as the write-back path. Symptom: nothing functional — this one is a timing bug. Cause: deriving the branch condition from the ALU’s zero flag means the ALU sits in series with the next-PC mux and with the write-back mux. The core here uses a separate comparator specifically to avoid that, which costs a few dozen LUTs and buys a shorter critical path. Whether that trade is right on your fabric is a synthesis question, not a simulation one — see Timing Closure and Fmax.
Don’t-care defaults in the decoder. Symptom: an illegal instruction corrupts state, and the failure surfaces much later. Cause and fix: the decoder’s default branch must drive every enable low. Verified in the measured control table: ebreak decodes to all-zero enables.
Assuming the simulator’s memory initialization matches hardware. Symptom: it works in simulation and reads x or garbage on the FPGA. Cause: the testbench here explicitly fills imem with nop (0x00000013) and dmem with zero before $readmemh, which papers over reads of uninitialized memory. Real block RAM powers up with whatever the bitstream loaded, and real SRAM powers up random. Initialize deliberately, and be suspicious of any test whose result depends on memory you never wrote.
Believing that CPI 1.000 means “fast”. Symptom: a design that is beautiful in simulation and disappointing on hardware. Cause: CPI is one of three terms. The measured table above shows CPI is perfect on every mix; it says nothing at all about wall-clock time, and the entire content of this note’s critical-path section is that the third term is bad. Cycles Per Instruction covers the general form of this mistake.
Alternatives and When to Choose Them
“Single-cycle” is one point in a design space whose axis is how much work happens between two clock edges. Here is the whole axis, with a real implementation at each point.
| Design | Datapath width | CPI | Relative F_max | Area | Real example |
|---|---|---|---|---|---|
| Bit-serial | 1 bit | tens (one cycle per bit, roughly) | very high | 125–239 LUT / 2.1 kGE | SERV |
| Multi-cycle | 32 bits | 3–6 | high (416–769 MHz measured) | 761–2019 LUT | PicoRV32 |
| Single-cycle | 32 bits | 1.000 | lowest | small | Sodor rv32_1stage |
| 2-stage pipeline | 32 bits | ≥1, loads stall ≥1 | medium-high | small | Ibex (default config) |
| 5-stage pipeline | 32 bits | ~1.1–1.5 | medium-high | medium | Sodor rv32_5stage |
| Superscalar / OoO | 32–64 bits, multiple issue | <1 possible | medium | large | out of scope here |
The design space, with measured figures where the projects publish them (all read 2026-09-04). What it shows: single-cycle is not merely “the slow one” — it is the only row with a CPI of exactly 1 and the only row whose clock is set by an entire instruction. The insight to take: SERV is the reductio that makes the axis legible. Making the datapath one bit wide takes the CPI to the tens and the area to 125 LUTs, and it is a genuinely useful core for applications where silicon is scarcer than time. There is no universally right point; there is a right point given a constraint.
Choose single-cycle when you are building a reference model, teaching or learning the datapath, prototyping a decoder before committing to a microarchitecture, or when the design is so small and so slow-clocked that F_max does not bind. It is also the correct first core in a project like definitely-not-esp32, for all the reasons in the reference-model section above.
Choose multi-cycle when area matters more than throughput, when you must share a single memory port between fetch and data, or when a high F_max is required so the core can be dropped into an existing clock domain. PicoRV32 exists for exactly this and says so.
Choose bit-serial when area is the binding constraint by an order of magnitude and the workload is light — SERV’s own framing is “whenever you need a bit of computation and silicon real estate is at a premium”.
Choose a pipeline when you want throughput and are prepared to build and debug a hazard unit. Keep the single-cycle core when you do.
A note on the alternative that is not a microarchitecture at all: an instruction-set simulator written in C (Spike, or your own hundred-line interpreter) is also a reference model, is faster to write, and runs orders of magnitude quicker. It is a legitimate substitute for some of what the single-cycle core provides. What it does not provide is a synthesizable artifact — you cannot put a C simulator on the FPGA, and you cannot check whether your Verilog register file behaves like your mental model of a register file by reading C. Build both if you can; the C model catches ISA misunderstandings and the RTL model catches RTL misunderstandings, and they are different mistakes.
Production Notes
Nobody ships a single-cycle processor, and that is not an argument against building one. A survey of the RV32 cores read for this note finds a bit-serial core (SERV), a multi-cycle core (PicoRV32), and a two-stage pipelined core (Ibex) in production use — and a single-cycle core (Sodor rv32_1stage) whose stated purpose is education and reference. That distribution is the honest picture: single-cycle is a tool, not a product.
Where it earns its place in a real project is the verification flow. Three concrete practices:
Keep it in the repository and in CI forever. Once the pipelined core exists, the single-cycle core becomes a test oracle. It should build and run the same test images in the same CI job, and any test that passes on one and fails on the other should be a hard failure. This is cheap — the core is 350 lines and simulates fast — and it is the only mechanism that will localize a forwarding bug to a single instruction.
Emit a common trace format from both. The trace entry needs to be architectural only: retired PC, rd index, written value, and any memory write address and value. Do not include cycle numbers — the whole point is that the two machines disagree about time and must agree about everything else. RISC-V’s ecosystem has a de facto convention for this in the RVFI (RISC-V Formal Interface) signal set, which is worth adopting rather than inventing.
Run The riscv-tests Suite against the single-cycle core in Stage 2, not Stage 4. The rv32ui-p-* programs are self-checking and microarchitecture-agnostic: each is a sequence of TEST_* macros that computes something and traps to a failure handler if the result is wrong, with the test number in a register so a failure names itself. They are the fastest available check that your decode table matches somebody else’s reading of the specification, and they will find the immediate-format bug you did not know you had. The MOC schedules them at Stage 4 as a gate before pipelining; running them earlier costs nothing and moves the decode-bug class out of the way sooner.
Do not trust simulation agreement as evidence of synthesizability. The core in this note runs identically under Verilator 5.046 and Icarus Verilog 13.0. That says nothing about whether it will meet timing, or whether the asynchronous memory reads will infer distributed RAM rather than block RAM, or whether the asynchronous reset style suits the target fabric. The Field-Programmable Gate Array and Timing Closure and Fmax notes cover what the synthesis report will tell you that no simulator can. The single most likely surprise is the one flagged in the uncertainty callout above: on a real fabric the critical path may run through the instruction memory rather than the data memory, or through the ALU’s carry chain, and the only way to find out is to read the report.
A closing point about specification versions. Everything in this note targets RV32I version 2.1, Ratified, as listed in The RISC-V Instruction Set Manual, Volume I: Unprivileged Architecture. The copy read was the intermediate release 20260903 (downloaded from the project’s GitHub release on 2026-09-04); its most recent ratified document version preface is 20260120, which lists RV32I at version 2.1 with status Ratified. The base integer instruction set has been stable for years and is not a fast-moving target — but the document that describes it publishes nightly, so cite the document version you actually read, not “the RISC-V spec”.
Uncertain
Verify: that document version 20260120 is the current ratified release of Volume I, rather than an intermediate one. Reason:
riscv.org/specifications/ratified/returns HTTP 200 with a JavaScript shell whose extracted text contains only site navigation, and the repository’s tag list contains noRatified-*tag for the unprivileged manual. The claim here rests on the PDF’s own preface structure — versions 20240411, 20250508 and 20260120 each carry a “Preface to Document Version X” stating all listed modules are ratified. To resolve: fetch the ratified-specifications listing through its backing content API, or check the RISC-V International announcement for the 20260120 release. The RV32I-is-version-2.1-and-ratified claim is directly quoted from the document and is not in doubt; only which document version is “the” ratified one is. uncertain
See Also
- Datapath and Control — the organizing split this machine is assembled from; read it for the signal vocabulary
- Instruction Decode — turning 32 bits into fields and control signals
- RISC-V Instruction Formats — R, I, S, B, U, J, and the scrambled B/J immediates
- The Register File — two read ports, one write port, and the
x0rule - The Arithmetic Logic Unit — the operations this datapath’s ALU must provide
- Classic Five-Stage Pipeline — the next microarchitecture, and what it does to this datapath
- Pipeline Hazards — the problems this design does not have
- Operand Forwarding — and Load-Use Hazard, the two hazard mechanisms a pipeline needs
- Cycles Per Instruction — the number measured at exactly 1.000 here
- Timing Closure and Fmax — the other side of the trade, and where the unresolved timing question above gets answered
- The riscv-tests Suite — the official tests to run against this core before pipelining
- Testbenches and RTL Verification — how the differential harness gets built
- Verilator and Waveform Debugging — the simulation and debug loop used throughout
- Register-Transfer Level — the abstraction the Verilog is written in
- Field-Programmable Gate Array — what happens to this Verilog on the Tang Nano 20K
- System on a Chip and The SoC Memory Map — where the ROM/RAM split from this note goes next
- Boot ROM and the Reset Vector — where
RESET_PCpoints in a real system - Branch Prediction — the Stage 9 optimization that only matters once there is a pipeline to flush
- Computer Architecture MOC — the concept hub
- definitely-not-esp32 MOC — the project this note serves, Stage 2