The contract is signed — time to cut the first metal. We start with the piece every other block
will lean on: the register file, the CPU's 32-word scratchpad. It is the busiest
memory on the chip: in the machine we are building it will be read twice and written once
every single cycle, forever. You already know its software face from the
x0 pinned to zero — and its place beside the ALU from
x0 stays zero.
How many ports does the register file need? Don't guess — read the contract.
add x3, x1, x2 must, in one cycle, read x1 and x2
and write x3. That is the worst case in our subset, so the register file is a
2R1W memory: two read ports (traditionally named for the fields that drive them,
rs1 and rs2) and one write port (rd). Three instructions can
touch it at once — two readers and a writer — and none may wait, because in a CPU "wait" means a
stalled machine.
The two kinds of port work on different clocks of logic:
rs1 and
rdata1 settles later that same cycle — no clock edge involved. A read port is
nothing more than a 32-way mux selecting one of 32 flip-flop outputs.wdata is captured into register
rd at the rising edge, and only if the control unit asserts we
(the signal we will later call RegWrite). This is our
Here is the whole thing — genuinely all of it. After five modules of RTL, a 32×32 2R1W register file with a hardwired zero comes to about a dozen lines:
Read the x0 defence carefully — it is belt and braces. The read ports
force zero out whenever the address is 0, and the write port refuses
to store when rd is 0. Either alone would almost work; together they make
x0 == 0 an invariant no instruction sequence can break. Synthesis enjoys the braces
too: register 0 is never written and never read as storage, so the optimizer quietly deletes its 32
flip-flops.
Drop the rd != 5'd0 guard and everything looks fine — until a program runs
jal x0, loop (a plain jump: "discard the return address"). Your register file
dutifully stores the return address into register 0… and if the read path returns
regs[0] rather than forced zero, x0 is suddenly, silently,
not zero. Every later "load constant" (addi x5, x0, 7), every NOP, every
"compare against zero" is now poisoned — the program doesn't crash at the store, it goes subtly
insane hundreds of instructions later. This is a classic CPU bug class: the violation and the
symptom are far apart. Tuck that away — in the final lesson we will write an assertion whose only
job is to catch exactly this.
One timing subtlety decides whether next lesson's CPU is buggy or correct. Suppose in one cycle the
write port is storing 42 into x7 while a read port is reading x7.
What comes out? Trace the hardware: the read mux taps the flip-flops' current outputs, all
cycle long; the write only lands at the edge that ends the cycle. So the read sees the
old value, and the new one becomes visible the following cycle. For our
single-cycle CPU this is exactly right: an instruction reads its sources before its own
result exists — reads-see-old matches program order perfectly. (File the question away, though:
when the pipeline arrives, reader and writer will be different instructions sharing a
cycle, and this same question becomes a genuine hazard with a whole lesson of consequences.)
Now the executable twin — the same 2R1W behaviour in the two-phase style we will grow into a full
CPU next lesson: reads are plain functions (combinational), and the write produces the
next state (the edge). Watch cycle 0 read the old value of the register it is writing, and
cycles 1–2 bounce off x0:
Five cycles, every rule on display: combinational reads, the edge-committed write, reads-see-old on
a same-cycle collision, we gating the port, and x0 shrugging off a write.
Keep read and write in your head — next lesson they get sandwiched
between a decoder and an ALU, and the machine starts to run.
Our register file is built from flip-flops — 31 real registers × 32 bits ≈ a thousand flops plus
two 32-way muxes. At this size that is the right call: cheap, fast, and the synthesis tool handles
it without ceremony. But the approach does not scale. Multiply the muxes and the fan-out and you
can feel it: every extra read port replicates a 32-way, 32-bit mux; every extra entry deepens it.
Big register files — a GPU's, say — abandon flip-flops for dense SRAM banks and start playing
scheduling games to fake many ports out of few. If you want to see how monstrous the same idea can
get, peek at the
If registers are this cheap, why stop at 32? Because the register file is cheap but the register fields are not: each register number costs 5 bits of every instruction, and an R-type already spends 15 of its 32 bits naming registers. Doubling to 64 registers would spend 18 bits — squeezing opcodes and immediates — for gains that fall off a cliff once the compiler has "enough" places to keep loop variables. RISC-V's answer is 32 (and even a 16-register embedded variant, RV32E, for tiny cores). The sweet spot is an ISA-level trade-off between encoding space, register-file area, and how many live values compilers actually juggle — decided in 1980s RISC research and barely moved since.