See it move
The idea
A register file is a small, fast memory built from flops, with independent read and write ports. CPUs use one to hold operands: read two registers and write one, all in a single cycle.
module regfile #(parameter W = 32, parameter N = 16) (
input clk, we,
input [$clog2(N)-1:0] ra1, ra2, wa,
input [W-1:0] wd,
output [W-1:0] rd1, rd2
);
reg [W-1:0] mem [0:N-1];
always_ff @(posedge clk)
if (we) mem[wa] <= wd;
assign rd1 = (we && wa == ra1) ? wd : mem[ra1];
assign rd2 = (we && wa == ra2) ? wd : mem[ra2];
endmoduleRead during write
What should a read return when it targets the address being written in that same cycle? There are two common answers:
- Old data: the read sees the value before the write.
- New data (bypass or write-through): a mux forwards the data being written, as in the code above.
Neither is universally right. Choose one, document it, and test it. The bypass adds a path from write data to read data, which also costs timing.
Cost
Each bit is a flop plus read-mux logic. That is far larger per bit than an cell, so register files are normally small: tens of entries, not thousands. Every additional read port adds another full set of read muxes.
Common mistake
Leaving reset out of your thinking. A large register file usually has no reset, to save area, so it holds x in simulation until it has been written. A read of an unwritten register then shows up as x. That is usually a useful warning of a real bug, so don't hide it by initialising everything in the testbench.
Key ideas
- Array of flops; reads are often combinational.
- Define same-cycle read and write behaviour.
- Flop arrays cost much more area than SRAM per bit.
Your exercise
Find the bug
This code has one deliberate bug. Read the symptom, then click the line you think is wrong.
The fix
assign rd = (we && wa == ra) ? wd : mem[ra];
Why it failed
Memory updates at the clock edge, so during this cycle it still holds the old value. A bypass mux checks for a matching write and forwards the incoming data directly to the read port. Whether you want this depends on your specification, but it has to be chosen deliberately.
Interview questions
coreWhat's write-through bypass?
A read of the address being written returns the new data.
basicIn a typical register file, writes happen on the clock edge but reads are combinational. What does that look like in hardware?
Each word is a row of flops with an enable. A decoder turns the write address into one enable, so only that word captures the write data at the edge. Each read port is a wide multiplexer that selects one word using the read address, with no clock involved, so the data appears in the same cycle as the address. This is why a register file can read two operands and write one result in a single CPU cycle.
basicEstimate the storage and read-mux cost of a 32-entry, 32-bit register file with two read ports.
Storage is 32 x 32 = 1024 flops. A 32-to-1 mux built from 2-input muxes needs 31 of them per bit, so one read port costs 32 x 31 = 992 two-input muxes, and two ports cost 1984. That ignores decoders and buffering, but it shows why each extra read port is expensive, and why register files stay at tens of entries.
coreIn this register file, a test writes 60 to r1 (which holds 25) and reads r1 in the same cycle. What does rd return in that cycle and in the next, and is that a bug?
reg [7:0] mem [0:3]; always @(posedge clk) if (we) mem[wa] <= wd; assign rd = mem[ra];
In the write cycle, rd returns 25, the old value, because the flops only update at the clock edge. In the next cycle it returns 60, as simulation confirms. It is a bug only if the specification promised new data on a same-cycle read. Then a bypass is needed, rd = (we && wa == ra) ? wd : mem[ra], and either way the read-during-write behaviour must be chosen, documented and tested.
coreWhat does a write-through bypass cost, apart from the extra mux?
Timing. It creates a combinational path from the write-data input, through the address comparator and the bypass mux, to the read-data output. Write data often comes late in the cycle from a result bus, and read data feeds the next stage's logic, so this can become a critical path. It also adds a comparator per read port. That is why some designs choose old-data reads and handle forwarding elsewhere.
coreIn a RISC-style register file, register 0 always reads as zero. What is wrong with this bypass?
always @(posedge clk) if (we && wa != 0) mem[wa] <= wd; wire [7:0] arr = (ra == 0) ? 8'd0 : mem[ra]; assign rd = (we && wa == ra) ? wd : arr;
The bypass forwards write data whenever the addresses match, including writes to register 0. The storage correctly ignores those writes, but a same-cycle read of register 0 returns the write data instead of zero. In simulation, writing 0xAB to register 0 while reading it returned 0xAB in that cycle and 0 in the next. The fix is to exclude address 0 from the bypass condition: (we && wa == ra && wa != 0).
coreA large register file has no reset. A testbench check reads a register that was never written. Why might this check report nothing?
if (rd != 8'd0)
$display("FAIL: got %b", rd);The unwritten register holds x in simulation. The comparison rd != 0 then evaluates to x, and an if statement treats x as false, so the error branch is skipped. In simulation this check stayed silent while rd was all x, and the same check written with !== reported a failure. Use the case-equality operators !== and === in checks, so that x and z are caught, not hidden.
advancedYou need a register file with two write ports. What new problems do you have to solve?
First, both ports can target the same address in the same cycle, so a priority or an error rule must be defined. Second, every flop now needs a mux to choose between two write data sources, plus two decoders, which adds area and delay to the write path. Bypass logic doubles too, since each read port must compare against both write addresses. Some designs avoid a second write port by banking, for example splitting registers between ports, and accepting conflicts.
advancedIn a classic five-stage CPU, write-back and decode access the register file in the same cycle. Which read-during-write behaviour is wanted there, and why?
New data. The instruction in decode may need the value that write-back is producing in that very cycle. If the register file returns old data, that instruction reads a stale operand, and the pipeline needs an extra forwarding path or a stall. Designs get new data either with a bypass mux or by writing in the first half of the cycle and reading in the second half.