Skip to content

RTL Design Patterns · Chapter 6 · Memories & Register Files

Multi-Port Memories & Register Files

A block RAM has only one or two ports, but a real datapath needs more. A single ALU operation reads two source operands and writes one result in the same cycle, and a one-port RAM physically cannot serve that. The structure that can is a multi-port memory, and the one at the heart of every CPU and DSP is the register file: a small array with several independent read ports and a write port, classically two read and one write, a 2R1W. This lesson treats the register file as exactly that and shows how you build the extra ports, either by replicating the array or dropping to flops, why the special register zero is hardwired to zero, and how the read-during-write question returns as a real RAW hazard, where reading and writing the same register the same cycle computes on a stale operand without forwarding. Everything is built and self-checked in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design PatternsRegister FileMulti-Port Memory2R1WWrite ForwardingRAW Hazard

Chapter 6 · Section 6.3 · Memories & Register Files

1. The Engineering Problem

You are building the register file at the centre of a simple RISC datapath. Every instruction that reaches the execute stage needs the same thing from storage in the same cycle: read two source registers, feed them to the ALU, and — one cycle later, or the same cycle in a single-cycle core — write the result back to a destination register. add x5, x1, x2 reads x1 and x2 at once and writes x5. sub x9, x5, x3 reads x5 and x3 at once and writes x9. The datapath cannot stall to fetch operands one at a time; it must present both source operands to the ALU on the same clock edge, every cycle, or the pipeline throughput collapses.

Reach for the memory you just built and you hit a wall immediately. The 6.1 block RAM has one read port. You can read x1 this cycle or x2 this cycle, but not both — and you still owe a write. A simple-dual-port RAM gives you one read and one write, still one read short of what a two-operand instruction needs. This is not an exotic requirement; it is the shape of every datapath's operand storage: a CPU integer file, a floating-point file, a DSP's coefficient/accumulator bank, a GPU's vector registers. Each one must satisfy the same contract — several reads and a write, concurrently, every cycle — and a single-port memory structurally cannot.

So the register file is a multi-port memory: an array with more than the one or two ports a plain RAM offers. A classic integer file is 2R1W — two read ports (rdata1, rdata2) with independent read addresses (raddr1, raddr2), and one write port (wdata at waddr, gated by we). Two more facts fall out immediately, and they are the whole page. First, silicon does not hand you extra ports for free: a block RAM has one or two, so more read ports means you either replicate the storage (keep a mirror copy per read port, write all copies at once) or build the file from flip-flops. Second, because a read port and the write port can touch the same register on the same cycle, you inherit the 6.2 read-during-write question — but now it is a RAW hazard in a running datapath, not an abstract memory policy.

the-need.v — two operands out, one result in, every cycle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // One-port RAM CANNOT serve a two-operand instruction:
   //   reg [31:0] mem [0:31];
   //   assign a = mem[raddr1];        // reads x1 this cycle...
   //   assign b = mem[raddr2];        // ...but a 1-port RAM has ONE read port — conflict.
 
   // A REGISTER FILE is a MULTI-PORT memory: 2 read ports + 1 write port (2R1W).
   //   reg [31:0] rf [0:31];
   //   // read port 1 and read port 2 index INDEPENDENTLY, both valid at once:
   //   assign rdata1 = rf[raddr1];    // x1
   //   assign rdata2 = rf[raddr2];    // x2   <-- concurrent second read port
   //   always @(posedge clk)
   //       if (we) rf[waddr] <= wdata; // x5  <-- concurrent write port
   //   // Two questions the datapath forces: how do you BUILD 2+ read ports on a
   //   // 1-2 port RAM, and what does a read return when raddr == waddr && we?

2. Mental Model

3. Pattern Anatomy

The register file is built from one shared array, several read-address decoders, one write port, and two policy decisions (zero register, read-during-write forwarding) layered on top.

The shared array — the storage. As in 6.1, the file is a 2D array of REGS words each WIDTH bits: reg [WIDTH-1:0] rf [0:REGS-1], addressed by AW = clog2(REGS) bits. For an RV32I integer file that is 32×32 — REGS = 32, WIDTH = 32, AW = 5. This single array holds the architectural state; every port is a view onto it.

The read ports — one independent decoder each. Each read port is a selection of one word from the array by its own address. Port 1 drives rdata1 = rf[raddr1]; port 2 drives rdata2 = rf[raddr2]. These are two independent muxes over the same storage — nothing couples raddr1 and raddr2, so the two operands are read concurrently. In a small flop-based file the reads are combinational (a REGS-to-1 mux per port); in a RAM-macro file each read is a registered, one-cycle-latency access (6.2), and the reader pipelines for it.

The write port — one shared writer. A single write port writes rf[waddr] <= wdata on the clock edge, gated by we. There is exactly one, because two concurrent writes to one array is a write-port conflict (§6) — a real multi-write file needs multiple physical write ports or arbitration, which is why the classic file is nR1W.

Building extra read ports — replication vs flops. This is the load-bearing structural decision. A block RAM has one or two physical ports, so a 2R1W file mapped to block RAM keeps two mirror copies of the array: every write updates both copies, read port 1 reads copy A, read port 2 reads copy B. Each extra read port costs another full copy of the storage (write bandwidth to all copies, area for each). The alternative — for a small file — is to build it from flip-flops with a read mux per port: REGS flop-words and a REGS-to-1 mux on each read port. Flops give arbitrarily many read ports cheaply at small depth (32 words is nothing) but scale terribly with depth; replicated block RAM scales with depth but pays per read port.

Register x0 hardwired to zero. The RISC convention: register 0 is the constant 0. In RTL this is two guards — ignore writes when waddr == 0 (or never let we fire for it), and force rdata == 0 whenever a read address is 0 — so x0 is a source of zero and a bit-bucket for results you want to discard. It is an optional parameter here (ZERO_R0) because a general scratch file need not have it.

Read-during-write forwarding — the 6.2 policy, concrete. When a read port's address equals waddr and we is asserted on the same edge, the read either returns the old value (read-first) or the new wdata (write-first). A flop-based file that reads combinationally after the clocked write already returns old (the flops update at the edge, the read mux sees pre-edge state for that cycle). If the datapath needs the new value the same cycle, you add a write-forwarding bypass per read port: if (we && raddr == waddr) rdata = wdata; else rdata = rf[raddr]. Omit it where the datapath depends on it and you get the §7 RAW hazard.

The 2R1W register file — one array, two read decoders, one write port, two policies

data flow
The 2R1W register file — one array, two read decoders, one write port, two policiesshared arrayrf[REGS][WIDTH]the architectural stateread port 1(raddr1)decoder/mux -> rdata1read port 2(raddr2)independent decoder/mux -> rdata2write port(waddr, we)one clocked writerwrite-forwardbypassraddr==waddr && we -> wdata (read-new)x0 = 0 guardignore writes to r0, read r0 as 0
One shared array holds the state. Two independent address decoders read two operands at once (rdata1, rdata2); a single clocked write port updates the array. Two policies sit on top: x0 is forced to zero (ignore writes to r0, read r0 as 0), and a write-forwarding bypass compares each read address to waddr so a same-register read-during-write returns the new value instead of the stale one. For >2 read ports the array is REPLICATED (one copy per read port, all written together) or the whole file is built from flops. This structure is language-independent: the same array, decoders, port, and policies exist in SystemVerilog, Verilog, and VHDL.

Depth and target close the anatomy. A small file (32 words) is naturally flops-plus-read-muxes — combinational multi-port reads, trivially many ports, and same-cycle write-forwarding is a cheap comparator. A large multi-ported storage (a big vector file, a wide GPR bank) forces replicated block RAMs, one-cycle registered reads, and forwarding logic that also has to account for the read latency. The pattern — shared array, per-port decoder, one write, zero-register and forwarding policies — is the same; only the implementation of the ports changes with depth and target.

4. Real RTL Implementation

This is the core of the page. We build three families — (a) a 2R1W register file (REGS & WIDTH generic, x0-is-zero optional), (b) a write-forwarding variant for same-register read-during-write, and (c) the no-forwarding RAW-hazard bug vs the forwarded fix — and each is shown in SystemVerilog, Verilog, and VHDL, with both an RTL block and a self-checking clocked testbench that writes and reads on both ports, including the same-register read-during-write case, and self-checks old-vs-new per its forwarding choice. The idea is identical in all three; the syntax differs, and seeing them side by side is what makes the pattern language-independent.

Two disciplines run through every block. The reads here are combinational (a small flop-based file, so both operands are available the same cycle) — a deliberate choice that makes the read-during-write question crisp and matches a real 32-entry integer file; a RAM-macro file would register the reads per 6.2. And every same-register collision is a decision, never an accident: the base file (4a) returns the old value (the flops update at the edge; the combinational read sees pre-edge state), and the forwarding variant (4b) overrides that to new.

4a. The 2R1W register file — two read ports, one write port, three ways

Start with the base file: REGS words of WIDTH bits, two combinational read ports, one clocked write port, and an optional ZERO_R0 that pins register 0 to zero. A same-register read-during-write here returns the old value — the write lands at the edge, the combinational reads see the array as it was.

regfile_2r1w.sv — 2R1W file, REGS/WIDTH generic, optional x0-is-zero
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module regfile_2r1w #(
       parameter int REGS    = 32,                 // number of registers
       parameter int WIDTH   = 32,                 // word width
       parameter bit ZERO_R0 = 1,                  // 1 = register 0 hardwired to zero (RISC)
       localparam int AW = (REGS > 1) ? $clog2(REGS) : 1
   )(
       input  logic             clk,
       input  logic             we,                // write enable
       input  logic [AW-1:0]    waddr,             // write address
       input  logic [WIDTH-1:0] wdata,             // write data
       input  logic [AW-1:0]    raddr1,            // read port 1 address
       input  logic [AW-1:0]    raddr2,            // read port 2 address
       output logic [WIDTH-1:0] rdata1,            // read port 1 data  (operand A)
       output logic [WIDTH-1:0] rdata2             // read port 2 data  (operand B)
   );
       // Small file => flops + a read mux per port. Both reads are combinational,
       // so BOTH operands are valid the same cycle (what a two-operand ALU needs).
       logic [WIDTH-1:0] rf [REGS];
 
       // ONE clocked write port. x0 guard: never let a write to reg 0 stick.
       always_ff @(posedge clk) begin
           if (we && !(ZERO_R0 && waddr == '0))
               rf[waddr] <= wdata;
       end
 
       // TWO independent read decoders over the SAME array. Reads are pre-edge
       // (old value on a same-register write) — no forwarding here, by design.
       always_comb begin
           rdata1 = (ZERO_R0 && raddr1 == '0) ? '0 : rf[raddr1];
           rdata2 = (ZERO_R0 && raddr2 == '0) ? '0 : rf[raddr2];
       end
   endmodule

The x0 guard is two lines of intent: a write to register 0 is dropped (so the flop for x0, if it exists, never changes), and a read of address 0 returns a hard '0 regardless of storage. The two always_comb reads are independent — nothing ties raddr1 to raddr2 — which is the multi-port property. The testbench writes several registers, reads two different ones concurrently, then hits the same-register read-during-write and asserts it returns the old value (this file's chosen policy), and if ZERO_R0, asserts writes to x0 are ignored.

regfile_2r1w_tb.sv — clocked TB: concurrent reads, RDW=old, x0 ignored
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module regfile_2r1w_tb;
       localparam int REGS = 32, WIDTH = 32, AW = 5;
       logic clk = 0, we;
       logic [AW-1:0]    waddr, raddr1, raddr2;
       logic [WIDTH-1:0] wdata, rdata1, rdata2;
       int errors = 0;
       always #5 clk = ~clk;
 
       regfile_2r1w #(.REGS(REGS), .WIDTH(WIDTH), .ZERO_R0(1)) dut (.*);
 
       task automatic wr(input [AW-1:0] a, input [WIDTH-1:0] d);
           @(negedge clk); we = 1; waddr = a; wdata = d; @(negedge clk); we = 0;
       endtask
 
       initial begin
           we = 0; waddr = 0; wdata = 0; raddr1 = 0; raddr2 = 0;
           wr(5, 32'hAAAA_0005);  wr(1, 32'h1111_0001);  wr(2, 32'h2222_0002);
           // Two DIFFERENT registers read concurrently on the two ports:
           raddr1 = 1; raddr2 = 2; #1;
           assert (rdata1 === 32'h1111_0001 && rdata2 === 32'h2222_0002)
               else begin $error("concurrent 2-port read wrong: %h %h", rdata1, rdata2); errors++; end
           // Same-register READ-DURING-WRITE: read x5 on port 1 the cycle we write x5.
           @(negedge clk); we = 1; waddr = 5; wdata = 32'hDEAD_0005; raddr1 = 5;
           #1;  // combinational read observes the OLD value this cycle (read-first)
           assert (rdata1 === 32'hAAAA_0005)
               else begin $error("RDW should read OLD: got %h", rdata1); errors++; end
           @(negedge clk); we = 0; #1;   // next cycle the new value is visible
           assert (rdata1 === 32'hDEAD_0005)
               else begin $error("post-write read wrong: %h", rdata1); errors++; end
           // x0 must ignore writes and read as zero:
           @(negedge clk); we = 1; waddr = 0; wdata = 32'hFFFF_FFFF; @(negedge clk); we = 0;
           raddr2 = 0; #1;
           assert (rdata2 === '0) else begin $error("x0 not zero: %h", rdata2); errors++; end
           if (errors == 0) $display("PASS: 2R1W concurrent reads, RDW=old, x0=0 all correct");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent: reg array, a clocked write with the x0 guard, two combinational reads with a zero-select. Verilog has no bit parameter, so ZERO_R0 is a plain integer parameter used as a flag.

regfile_2r1w.v — 2R1W file in Verilog (flop array, two combinational reads)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module regfile_2r1w #(
       parameter REGS    = 32,
       parameter WIDTH   = 32,
       parameter ZERO_R0 = 1,                       // 1 = register 0 hardwired to zero
       parameter AW      = 5                         // = clog2(REGS); set by instantiator
   )(
       input  wire              clk,
       input  wire              we,
       input  wire [AW-1:0]     waddr,
       input  wire [WIDTH-1:0]  wdata,
       input  wire [AW-1:0]     raddr1,
       input  wire [AW-1:0]     raddr2,
       output wire [WIDTH-1:0]  rdata1,
       output wire [WIDTH-1:0]  rdata2
   );
       reg [WIDTH-1:0] rf [0:REGS-1];
 
       // ONE clocked write port; drop writes to register 0 when ZERO_R0.
       always @(posedge clk)
           if (we && !(ZERO_R0 && waddr == {AW{1'b0}}))
               rf[waddr] <= wdata;
 
       // TWO independent combinational read ports; read reg 0 as 0 when ZERO_R0.
       assign rdata1 = (ZERO_R0 && raddr1 == {AW{1'b0}}) ? {WIDTH{1'b0}} : rf[raddr1];
       assign rdata2 = (ZERO_R0 && raddr2 == {AW{1'b0}}) ? {WIDTH{1'b0}} : rf[raddr2];
   endmodule
regfile_2r1w_tb.v — self-checking clocked TB (concurrent reads, RDW=old, x0=0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module regfile_2r1w_tb;
       parameter REGS = 32, WIDTH = 32, AW = 5;
       reg  clk, we;
       reg  [AW-1:0]    waddr, raddr1, raddr2;
       reg  [WIDTH-1:0] wdata;
       wire [WIDTH-1:0] rdata1, rdata2;
       integer errors;
       initial clk = 0;  always #5 clk = ~clk;
 
       regfile_2r1w #(.REGS(REGS), .WIDTH(WIDTH), .ZERO_R0(1), .AW(AW)) dut
           (.clk(clk), .we(we), .waddr(waddr), .wdata(wdata),
            .raddr1(raddr1), .raddr2(raddr2), .rdata1(rdata1), .rdata2(rdata2));
 
       task wr; input [AW-1:0] a; input [WIDTH-1:0] d; begin
           @(negedge clk); we = 1; waddr = a; wdata = d; @(negedge clk); we = 0;
       end endtask
 
       initial begin
           errors = 0; we = 0; waddr = 0; wdata = 0; raddr1 = 0; raddr2 = 0;
           wr(5, 32'hAAAA0005);  wr(1, 32'h11110001);  wr(2, 32'h22220002);
           raddr1 = 1; raddr2 = 2; #1;                       // two different regs at once
           if (rdata1 !== 32'h11110001 || rdata2 !== 32'h22220002) begin
               $display("FAIL: concurrent read %h %h", rdata1, rdata2); errors = errors + 1; end
           @(negedge clk); we = 1; waddr = 5; wdata = 32'hDEAD0005; raddr1 = 5; #1;  // RDW
           if (rdata1 !== 32'hAAAA0005) begin
               $display("FAIL: RDW should read OLD, got %h", rdata1); errors = errors + 1; end
           @(negedge clk); we = 0; #1;                       // new value visible next cycle
           if (rdata1 !== 32'hDEAD0005) begin
               $display("FAIL: post-write read %h", rdata1); errors = errors + 1; end
           @(negedge clk); we = 1; waddr = 0; wdata = 32'hFFFFFFFF; @(negedge clk); we = 0;
           raddr2 = 0; #1;
           if (rdata2 !== {WIDTH{1'b0}}) begin
               $display("FAIL: x0 not zero: %h", rdata2); errors = errors + 1; end
           if (errors == 0) $display("PASS: 2R1W concurrent reads, RDW=old, x0=0 all correct");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the file is an array type of std_logic_vector, the write is a rising_edge(clk) process, and the two reads are concurrent conditional assignments. to_integer(unsigned(raddr1)) turns an address into an index; the x0 guard uses a comparison of the address to zero.

regfile_2r1w.vhd — 2R1W file in VHDL (array + rising_edge, two reads)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity regfile_2r1w is
       generic (
           REGS    : positive := 32;
           WIDTH   : positive := 32;
           ZERO_R0 : boolean  := true;                 -- register 0 hardwired to zero
           AW      : positive := 5                     -- = clog2(REGS)
       );
       port (
           clk    : in  std_logic;
           we     : in  std_logic;
           waddr  : in  std_logic_vector(AW-1 downto 0);
           wdata  : in  std_logic_vector(WIDTH-1 downto 0);
           raddr1 : in  std_logic_vector(AW-1 downto 0);
           raddr2 : in  std_logic_vector(AW-1 downto 0);
           rdata1 : out std_logic_vector(WIDTH-1 downto 0);
           rdata2 : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of regfile_2r1w is
       type rf_t is array (0 to REGS-1) of std_logic_vector(WIDTH-1 downto 0);
       signal rf : rf_t;
       constant ZERO : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       -- ONE clocked write port; drop writes to register 0 when ZERO_R0.
       write_port : process (clk)
       begin
           if rising_edge(clk) then
               if we = '1' and not (ZERO_R0 and unsigned(waddr) = 0) then
                   rf(to_integer(unsigned(waddr))) <= wdata;
               end if;
           end if;
       end process;
 
       -- TWO independent combinational read ports; read reg 0 as 0 when ZERO_R0.
       rdata1 <= ZERO when (ZERO_R0 and unsigned(raddr1) = 0)
                 else rf(to_integer(unsigned(raddr1)));
       rdata2 <= ZERO when (ZERO_R0 and unsigned(raddr2) = 0)
                 else rf(to_integer(unsigned(raddr2)));
   end architecture;
regfile_2r1w_tb.vhd — self-checking clocked TB (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity regfile_2r1w_tb is
   end entity;
 
   architecture sim of regfile_2r1w_tb is
       constant WIDTH : positive := 32;
       constant AW    : positive := 5;
       signal clk    : std_logic := '0';
       signal we     : std_logic := '0';
       signal waddr, raddr1, raddr2 : std_logic_vector(AW-1 downto 0) := (others => '0');
       signal wdata, rdata1, rdata2 : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
 
       procedure wr(signal clk_s : in  std_logic;
                    signal we_s  : out std_logic;
                    signal wa    : out std_logic_vector(AW-1 downto 0);
                    signal wd    : out std_logic_vector(WIDTH-1 downto 0);
                    a : integer; d : std_logic_vector(WIDTH-1 downto 0)) is
       begin
           wait until falling_edge(clk_s);
           we_s <= '1'; wa <= std_logic_vector(to_unsigned(a, AW)); wd <= d;
           wait until falling_edge(clk_s);
           we_s <= '0';
       end procedure;
   begin
       clk <= not clk after 5 ns;
 
       dut : entity work.regfile_2r1w
           generic map (REGS => 32, WIDTH => WIDTH, ZERO_R0 => true, AW => AW)
           port map (clk => clk, we => we, waddr => waddr, wdata => wdata,
                     raddr1 => raddr1, raddr2 => raddr2, rdata1 => rdata1, rdata2 => rdata2);
 
       stim : process
       begin
           wr(clk, we, waddr, wdata, 5, x"AAAA0005");
           wr(clk, we, waddr, wdata, 1, x"11110001");
           wr(clk, we, waddr, wdata, 2, x"22220002");
           -- two DIFFERENT registers read concurrently on the two ports:
           raddr1 <= std_logic_vector(to_unsigned(1, AW));
           raddr2 <= std_logic_vector(to_unsigned(2, AW));
           wait for 1 ns;
           assert rdata1 = x"11110001" and rdata2 = x"22220002"
               report "concurrent 2-port read wrong" severity error;
           -- same-register READ-DURING-WRITE: read x5 the cycle we write x5 -> OLD.
           wait until falling_edge(clk);
           we <= '1'; waddr <= std_logic_vector(to_unsigned(5, AW)); wdata <= x"DEAD0005";
           raddr1 <= std_logic_vector(to_unsigned(5, AW));
           wait for 1 ns;
           assert rdata1 = x"AAAA0005" report "RDW should read OLD value" severity error;
           wait until falling_edge(clk); we <= '0'; wait for 1 ns;
           assert rdata1 = x"DEAD0005" report "post-write read wrong" severity error;
           report "regfile_2r1w self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. Write-forwarding — same-register read-during-write returns the new value

The base file returns the old value on a same-register collision. When the datapath needs the just-written value the same cycle — a back-to-back dependent instruction, a forwarding path — you add a write-forwarding bypass per read port: compare the read address to waddr, and when they collide with we, mux wdata onto that read port instead of the stale array value. This is exactly the 6.2 bypass, applied per read port.

regfile_2r1w_fwd.sv — write-forwarding: same-register RDW returns NEW value
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module regfile_2r1w_fwd #(
       parameter int REGS    = 32,
       parameter int WIDTH   = 32,
       parameter bit ZERO_R0 = 1,
       localparam int AW = (REGS > 1) ? $clog2(REGS) : 1
   )(
       input  logic             clk, we,
       input  logic [AW-1:0]    waddr, raddr1, raddr2,
       input  logic [WIDTH-1:0] wdata,
       output logic [WIDTH-1:0] rdata1, rdata2
   );
       logic [WIDTH-1:0] rf [REGS];
 
       always_ff @(posedge clk)
           if (we && !(ZERO_R0 && waddr == '0)) rf[waddr] <= wdata;
 
       // Per-port write-forward: collide with the write this cycle -> return NEW wdata.
       // (x0 still reads 0; a forwarded write to x0 is dropped, so it stays 0.)
       function automatic logic [WIDTH-1:0] rd(input logic [AW-1:0] a);
           if (ZERO_R0 && a == '0)                 return '0;               // x0 = 0
           else if (we && a == waddr)              return wdata;           // FORWARD new
           else                                    return rf[a];           // array (old)
       endfunction
 
       always_comb begin
           rdata1 = rd(raddr1);
           rdata2 = rd(raddr2);
       end
   endmodule

The forwarding is per read port because each port has its own address to compare, and the x0 guard is checked first so a forwarded write to register 0 never leaks a non-zero value. The testbench repeats the base file's checks but now asserts the same-register read-during-write returns the new value on the forwarded port — and that the other port, reading a different register, is unaffected.

regfile_2r1w_fwd_tb.sv — assert RDW returns NEW on the forwarded port
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module regfile_2r1w_fwd_tb;
       localparam int WIDTH = 32, AW = 5;
       logic clk = 0, we;
       logic [AW-1:0]    waddr, raddr1, raddr2;
       logic [WIDTH-1:0] wdata, rdata1, rdata2;
       int errors = 0;
       always #5 clk = ~clk;
 
       regfile_2r1w_fwd #(.WIDTH(WIDTH), .ZERO_R0(1)) dut (.*);
 
       initial begin
           we = 0; waddr = 0; wdata = 0; raddr1 = 0; raddr2 = 0;
           @(negedge clk); we = 1; waddr = 7; wdata = 32'h0707_0707; @(negedge clk); we = 0;
           @(negedge clk); we = 1; waddr = 3; wdata = 32'h0303_0303; @(negedge clk); we = 0;
           // Read-during-write on x7 (port 1) while port 2 reads a settled x3:
           @(negedge clk); we = 1; waddr = 7; wdata = 32'hBEEF_0007; raddr1 = 7; raddr2 = 3;
           #1;
           assert (rdata1 === 32'hBEEF_0007)                 // FORWARDED new value
               else begin $error("forward failed: rdata1=%h", rdata1); errors++; end
           assert (rdata2 === 32'h0303_0303)                 // other port unaffected
               else begin $error("port2 disturbed: rdata2=%h", rdata2); errors++; end
           @(negedge clk); we = 0;
           if (errors == 0) $display("PASS: write-forward returns NEW on same-register RDW");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog forwarding variant folds the same compare into the combinational read expression — no function needed, just a nested conditional per read port. The comparison we && raddr1 == waddr is the whole bypass.

regfile_2r1w_fwd.v — write-forwarding in Verilog (per-port bypass mux)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module regfile_2r1w_fwd #(
       parameter WIDTH   = 32,
       parameter ZERO_R0 = 1,
       parameter AW      = 5
   )(
       input  wire              clk, we,
       input  wire [AW-1:0]     waddr, raddr1, raddr2,
       input  wire [WIDTH-1:0]  wdata,
       output wire [WIDTH-1:0]  rdata1, rdata2
   );
       reg [WIDTH-1:0] rf [0:(1<<AW)-1];
 
       always @(posedge clk)
           if (we && !(ZERO_R0 && waddr == {AW{1'b0}})) rf[waddr] <= wdata;
 
       // Per-port: x0 first, then FORWARD wdata on a same-register write, else array.
       assign rdata1 = (ZERO_R0 && raddr1 == {AW{1'b0}}) ? {WIDTH{1'b0}} :
                       (we && raddr1 == waddr)           ? wdata          : rf[raddr1];
       assign rdata2 = (ZERO_R0 && raddr2 == {AW{1'b0}}) ? {WIDTH{1'b0}} :
                       (we && raddr2 == waddr)           ? wdata          : rf[raddr2];
   endmodule
regfile_2r1w_fwd_tb.v — self-checking; RDW returns NEW on forwarded port
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module regfile_2r1w_fwd_tb;
       parameter WIDTH = 32, AW = 5;
       reg  clk, we;
       reg  [AW-1:0]    waddr, raddr1, raddr2;
       reg  [WIDTH-1:0] wdata;
       wire [WIDTH-1:0] rdata1, rdata2;
       integer errors;
       initial clk = 0;  always #5 clk = ~clk;
 
       regfile_2r1w_fwd #(.WIDTH(WIDTH), .ZERO_R0(1), .AW(AW)) dut
           (.clk(clk), .we(we), .waddr(waddr), .wdata(wdata),
            .raddr1(raddr1), .raddr2(raddr2), .rdata1(rdata1), .rdata2(rdata2));
 
       initial begin
           errors = 0; we = 0; waddr = 0; wdata = 0; raddr1 = 0; raddr2 = 0;
           @(negedge clk); we = 1; waddr = 7; wdata = 32'h07070707; @(negedge clk); we = 0;
           @(negedge clk); we = 1; waddr = 3; wdata = 32'h03030303; @(negedge clk); we = 0;
           @(negedge clk); we = 1; waddr = 7; wdata = 32'hBEEF0007; raddr1 = 7; raddr2 = 3; #1;
           if (rdata1 !== 32'hBEEF0007) begin
               $display("FAIL: forward failed rdata1=%h", rdata1); errors = errors + 1; end
           if (rdata2 !== 32'h03030303) begin
               $display("FAIL: port2 disturbed rdata2=%h", rdata2); errors = errors + 1; end
           @(negedge clk); we = 0;
           if (errors == 0) $display("PASS: write-forward returns NEW on same-register RDW");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the bypass is a nested conditional signal assignment per read port: x0 guard, then we = '1' and raddr = waddr forwards wdata, else the array. Same three-way selection, VHDL when/else spelling.

regfile_2r1w_fwd.vhd — write-forwarding in VHDL (per-port when/else bypass)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity regfile_2r1w_fwd is
       generic ( WIDTH : positive := 32; ZERO_R0 : boolean := true; AW : positive := 5 );
       port (
           clk    : in  std_logic;
           we     : in  std_logic;
           waddr  : in  std_logic_vector(AW-1 downto 0);
           wdata  : in  std_logic_vector(WIDTH-1 downto 0);
           raddr1 : in  std_logic_vector(AW-1 downto 0);
           raddr2 : in  std_logic_vector(AW-1 downto 0);
           rdata1 : out std_logic_vector(WIDTH-1 downto 0);
           rdata2 : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of regfile_2r1w_fwd is
       type rf_t is array (0 to 2**AW-1) of std_logic_vector(WIDTH-1 downto 0);
       signal rf : rf_t;
       constant ZERO : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       write_port : process (clk)
       begin
           if rising_edge(clk) then
               if we = '1' and not (ZERO_R0 and unsigned(waddr) = 0) then
                   rf(to_integer(unsigned(waddr))) <= wdata;
               end if;
           end if;
       end process;
 
       -- x0 first, then FORWARD wdata on a same-register write, else the array.
       rdata1 <= ZERO  when (ZERO_R0 and unsigned(raddr1) = 0) else
                 wdata when (we = '1' and raddr1 = waddr)      else
                 rf(to_integer(unsigned(raddr1)));
       rdata2 <= ZERO  when (ZERO_R0 and unsigned(raddr2) = 0) else
                 wdata when (we = '1' and raddr2 = waddr)      else
                 rf(to_integer(unsigned(raddr2)));
   end architecture;
regfile_2r1w_fwd_tb.vhd — self-checking; RDW returns NEW on forwarded port
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity regfile_2r1w_fwd_tb is
   end entity;
 
   architecture sim of regfile_2r1w_fwd_tb is
       constant WIDTH : positive := 32;
       constant AW    : positive := 5;
       signal clk    : std_logic := '0';
       signal we     : std_logic := '0';
       signal waddr, raddr1, raddr2 : std_logic_vector(AW-1 downto 0) := (others => '0');
       signal wdata, rdata1, rdata2 : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       clk <= not clk after 5 ns;
 
       dut : entity work.regfile_2r1w_fwd
           generic map (WIDTH => WIDTH, ZERO_R0 => true, AW => AW)
           port map (clk => clk, we => we, waddr => waddr, wdata => wdata,
                     raddr1 => raddr1, raddr2 => raddr2, rdata1 => rdata1, rdata2 => rdata2);
 
       stim : process
       begin
           wait until falling_edge(clk);
           we <= '1'; waddr <= std_logic_vector(to_unsigned(7, AW)); wdata <= x"07070707";
           wait until falling_edge(clk); we <= '0';
           wait until falling_edge(clk);
           we <= '1'; waddr <= std_logic_vector(to_unsigned(3, AW)); wdata <= x"03030303";
           wait until falling_edge(clk); we <= '0';
           -- read-during-write on x7 (port 1) while port 2 reads settled x3:
           wait until falling_edge(clk);
           we <= '1'; waddr <= std_logic_vector(to_unsigned(7, AW)); wdata <= x"BEEF0007";
           raddr1 <= std_logic_vector(to_unsigned(7, AW));
           raddr2 <= std_logic_vector(to_unsigned(3, AW));
           wait for 1 ns;
           assert rdata1 = x"BEEF0007" report "forward failed (rdata1)"   severity error;
           assert rdata2 = x"03030303" report "port2 disturbed (rdata2)"  severity error;
           wait until falling_edge(clk); we <= '0';
           report "regfile_2r1w_fwd self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. The no-forwarding RAW-hazard bug — buggy vs fixed, in all three HDLs

Now the signature failure, shown as buggy vs fixed RTL. The bug is a register file with no write-forwarding dropped into a datapath whose instructions can be back-to-back dependent. A producing instruction writes x5; the very next instruction reads x5 on the same cycle the write lands. Without forwarding the read returns the old x5 — a stale operand — and the dependent instruction computes on the wrong value. It passes every test that reads and writes different registers; it fails only on the back-to-back same-register case.

rf_bug.sv — BUGGY (no forwarding, stale operand) vs FIXED (forward)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: a plain read-old file used where the datapath needs read-new.
   //   When raddr1 == waddr && we (back-to-back dependent instr), rdata1 returns
   //   the OLD x5 -> the consumer computes on a STALE operand (a RAW hazard).
   module rf_raw_bad #(parameter WIDTH = 32, parameter AW = 5)(
       input  logic clk, we,
       input  logic [AW-1:0]    waddr, raddr1,
       input  logic [WIDTH-1:0] wdata,
       output logic [WIDTH-1:0] rdata1
   );
       logic [WIDTH-1:0] rf [1<<AW];
       always_ff @(posedge clk) if (we) rf[waddr] <= wdata;
       assign rdata1 = rf[raddr1];              // no bypass -> same-cycle read is STALE
   endmodule
 
   // FIXED: forward wdata when the read collides with the write this cycle.
   module rf_raw_good #(parameter WIDTH = 32, parameter AW = 5)(
       input  logic clk, we,
       input  logic [AW-1:0]    waddr, raddr1,
       input  logic [WIDTH-1:0] wdata,
       output logic [WIDTH-1:0] rdata1
   );
       logic [WIDTH-1:0] rf [1<<AW];
       always_ff @(posedge clk) if (we) rf[waddr] <= wdata;
       // Bypass: same-register read-during-write returns the NEW value.
       assign rdata1 = (we && raddr1 == waddr) ? wdata : rf[raddr1];
   endmodule
rf_bug_tb.sv — the back-to-back dependent case exposes the stale operand
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rf_bug_tb;
       localparam int WIDTH = 32, AW = 5;
       logic clk = 0, we;
       logic [AW-1:0]    waddr, raddr1;
       logic [WIDTH-1:0] wdata, rdata1;
       int errors = 0;
       always #5 clk = ~clk;
 
       // Point the DUT at rf_raw_good to PASS; at rf_raw_bad to see the stale operand.
       rf_raw_good #(.WIDTH(WIDTH), .AW(AW)) dut
           (.clk(clk), .we(we), .waddr(waddr), .raddr1(raddr1), .wdata(wdata), .rdata1(rdata1));
 
       initial begin
           we = 0; waddr = 0; raddr1 = 0; wdata = 0;
           // seed x5 = OLD:
           @(negedge clk); we = 1; waddr = 5; wdata = 32'h0000_1111; @(negedge clk); we = 0;
           // Independent regs first (this ALWAYS passes, even on the buggy file):
           raddr1 = 5; #1;
           assert (rdata1 === 32'h0000_1111) else begin $error("settled read wrong"); errors++; end
           // Back-to-back dependent: producer writes x5, consumer reads x5 SAME cycle:
           @(negedge clk); we = 1; waddr = 5; wdata = 32'h0000_2222; raddr1 = 5; #1;
           assert (rdata1 === 32'h0000_2222)     // must be NEW; buggy file returns 0x1111
               else begin $error("RAW hazard: stale operand rdata1=%h (want 2222)", rdata1); errors++; end
           @(negedge clk); we = 0;
           if (errors == 0) $display("PASS: dependent instruction reads the forwarded (new) operand");
           else             $display("FAIL: %0d mismatches (stale operand — RAW hazard)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with reg arrays and an assign read. The single (we && raddr1 == waddr) comparison is the entire difference between a stale operand and a correct one.

rf_bug.v — BUGGY (stale operand) vs FIXED (forward) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: no bypass — same-register read-during-write returns the OLD value.
   module rf_raw_bad #(parameter WIDTH = 32, parameter AW = 5)(
       input  wire clk, we,
       input  wire [AW-1:0]    waddr, raddr1,
       input  wire [WIDTH-1:0] wdata,
       output wire [WIDTH-1:0] rdata1
   );
       reg [WIDTH-1:0] rf [0:(1<<AW)-1];
       always @(posedge clk) if (we) rf[waddr] <= wdata;
       assign rdata1 = rf[raddr1];              // STALE on a same-cycle dependent read
   endmodule
 
   // FIXED: forward wdata on a same-register collision -> read-new.
   module rf_raw_good #(parameter WIDTH = 32, parameter AW = 5)(
       input  wire clk, we,
       input  wire [AW-1:0]    waddr, raddr1,
       input  wire [WIDTH-1:0] wdata,
       output wire [WIDTH-1:0] rdata1
   );
       reg [WIDTH-1:0] rf [0:(1<<AW)-1];
       always @(posedge clk) if (we) rf[waddr] <= wdata;
       assign rdata1 = (we && raddr1 == waddr) ? wdata : rf[raddr1];   // bypass
   endmodule
rf_bug_tb.v — self-checking; the dependent read exposes the stale operand
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rf_bug_tb;
       parameter WIDTH = 32, AW = 5;
       reg  clk, we;
       reg  [AW-1:0]    waddr, raddr1;
       reg  [WIDTH-1:0] wdata;
       wire [WIDTH-1:0] rdata1;
       integer errors;
       initial clk = 0;  always #5 clk = ~clk;
 
       // Bind to rf_raw_good to PASS; to rf_raw_bad to observe the stale operand.
       rf_raw_good #(.WIDTH(WIDTH), .AW(AW)) dut
           (.clk(clk), .we(we), .waddr(waddr), .raddr1(raddr1), .wdata(wdata), .rdata1(rdata1));
 
       initial begin
           errors = 0; we = 0; waddr = 0; raddr1 = 0; wdata = 0;
           @(negedge clk); we = 1; waddr = 5; wdata = 32'h00001111; @(negedge clk); we = 0;
           raddr1 = 5; #1;                                    // settled read (always passes)
           if (rdata1 !== 32'h00001111) begin
               $display("FAIL: settled read %h", rdata1); errors = errors + 1; end
           @(negedge clk); we = 1; waddr = 5; wdata = 32'h00002222; raddr1 = 5; #1;  // RAW
           if (rdata1 !== 32'h00002222) begin
               $display("FAIL: RAW hazard stale operand rdata1=%h (want 2222)", rdata1);
               errors = errors + 1; end
           @(negedge clk); we = 0;
           if (errors == 0) $display("PASS: dependent instruction reads the forwarded operand");
           else             $display("FAIL: %0d mismatches (stale operand)", errors);
           $finish;
       end
   endmodule

In VHDL the bug is a read that is a plain rf(to_integer(...)) with no bypass; the fix adds the wdata when (we = '1' and raddr1 = waddr) arm. The when others-style completeness of a conditional signal assignment makes the fix total by construction.

rf_bug.vhd — BUGGY (no bypass) vs FIXED (forward) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: plain read — same-register read-during-write returns the OLD value.
   entity rf_raw_bad is
       generic ( WIDTH : positive := 32; AW : positive := 5 );
       port ( clk, we : in std_logic;
              waddr, raddr1 : in std_logic_vector(AW-1 downto 0);
              wdata  : in  std_logic_vector(WIDTH-1 downto 0);
              rdata1 : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of rf_raw_bad is
       type rf_t is array (0 to 2**AW-1) of std_logic_vector(WIDTH-1 downto 0);
       signal rf : rf_t;
   begin
       process (clk) begin
           if rising_edge(clk) then
               if we = '1' then rf(to_integer(unsigned(waddr))) <= wdata; end if;
           end if;
       end process;
       rdata1 <= rf(to_integer(unsigned(raddr1)));         -- STALE on same-cycle read
   end architecture;
 
   -- FIXED: forward wdata on a same-register collision -> read-new.
   entity rf_raw_good is
       generic ( WIDTH : positive := 32; AW : positive := 5 );
       port ( clk, we : in std_logic;
              waddr, raddr1 : in std_logic_vector(AW-1 downto 0);
              wdata  : in  std_logic_vector(WIDTH-1 downto 0);
              rdata1 : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of rf_raw_good is
       type rf_t is array (0 to 2**AW-1) of std_logic_vector(WIDTH-1 downto 0);
       signal rf : rf_t;
   begin
       process (clk) begin
           if rising_edge(clk) then
               if we = '1' then rf(to_integer(unsigned(waddr))) <= wdata; end if;
           end if;
       end process;
       rdata1 <= wdata when (we = '1' and raddr1 = waddr)   -- bypass -> NEW value
                 else rf(to_integer(unsigned(raddr1)));
   end architecture;
rf_bug_tb.vhd — self-checking; the dependent read exposes the stale operand
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity rf_bug_tb is
   end entity;
 
   architecture sim of rf_bug_tb is
       constant WIDTH : positive := 32;
       constant AW    : positive := 5;
       signal clk    : std_logic := '0';
       signal we     : std_logic := '0';
       signal waddr, raddr1 : std_logic_vector(AW-1 downto 0) := (others => '0');
       signal wdata, rdata1 : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       clk <= not clk after 5 ns;
 
       -- Bind to rf_raw_good to PASS; to rf_raw_bad to observe the stale operand.
       dut : entity work.rf_raw_good
           generic map (WIDTH => WIDTH, AW => AW)
           port map (clk => clk, we => we, waddr => waddr, raddr1 => raddr1,
                     wdata => wdata, rdata1 => rdata1);
 
       stim : process
       begin
           wait until falling_edge(clk);
           we <= '1'; waddr <= std_logic_vector(to_unsigned(5, AW)); wdata <= x"00001111";
           wait until falling_edge(clk); we <= '0';
           raddr1 <= std_logic_vector(to_unsigned(5, AW));
           wait for 1 ns;                                   -- settled read (always passes)
           assert rdata1 = x"00001111" report "settled read wrong" severity error;
           -- back-to-back dependent: write x5 and read x5 the SAME cycle -> want NEW.
           wait until falling_edge(clk);
           we <= '1'; waddr <= std_logic_vector(to_unsigned(5, AW)); wdata <= x"00002222";
           raddr1 <= std_logic_vector(to_unsigned(5, AW));
           wait for 1 ns;
           assert rdata1 = x"00002222"
               report "RAW hazard: stale operand (want 2222)" severity error;
           wait until falling_edge(clk); we <= '0';
           report "rf_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a register file is a multi-port memory, and if the datapath reads a register the same cycle it is written, the read must be forwarded — or the dependent instruction computes on a stale operand.

5. Verification Strategy

A register file is a small multi-port memory, so its correctness is what each port returns given the write history and the addresses this cycle — and the one collision that hides the bugs is the same-register read-during-write. The single invariant that captures a correct read-new file is:

Each read port returns the most-recently-written value of its addressed register — including a write landing this very cycle when forwarding is enabled — and register 0 (if ZERO_R0) always returns 0 and never stores a write.

Everything below is that invariant, made checkable, and it maps directly onto the clocked testbenches in §4.

  • Write, then read back on both ports (self-checking). Write a distinct pattern to several registers, then drive raddr1 and raddr2 and assert each returns what was written. Because reads are combinational here, the check settles the same cycle; a RAM-macro file registers the reads and the testbench must pipeline the compare by the 6.2 read latency. The three §4a testbenches do exactly this: SV assert (rdata1 === …), Verilog if (rdata1 !== exp) $display("FAIL"), VHDL assert rdata1 = … severity error.
  • Read two DIFFERENT registers simultaneously. The multi-port property: drive raddr1 != raddr2 and assert both operands come out correct on the same edge. A file that accidentally shares a decoder (or serializes the reads) fails here; this is the check that proves the two read ports are genuinely independent.
  • Same-register read-during-write — assert old or new per your policy. Drive raddr1 == waddr with we asserted and assert the decided result: the base file (4a) must return the old value, the forwarding file (4b) the new value. This is the corner the whole page is about; if it is not directly tested, the RAW hazard of §7 ships undetected. Also assert the other read port (reading a different register) is undisturbed by the collision on the first.
  • x0-is-zero, when ZERO_R0. Assert two properties: a write to register 0 is ignored (write 0xFFFF_FFFF to x0, read it back as 0), and a read of address 0 returns 0 regardless of storage. With forwarding, also assert a forwarded write to x0 still reads 0 — the zero guard must sit ahead of the bypass.
  • Parameter-generic verification (REGS, WIDTH). A parameterized file must be verified generic. Re-run the write/read-back and the read-during-write checks for WIDTH = 8, 16, 32 and REGS = 8, 16, 32 (so AW changes), and with ZERO_R0 both on and off. A file that passes at 32×32 can still be wrong at a non-standard depth if the address width or the zero guard was hardcoded.
  • Expected waveform. With combinational reads, rdata1/rdata2 track their addressed words immediately as the addresses change. On a same-register collision the forwarded file shows rdata jump to wdata the same cycle we is high, while the read-old file shows rdata hold the old value that cycle and only step to the new value the cycle after we. The visual signature of the RAW bug is a dependent read that shows the previous value exactly on the back-to-back cycle.

6. Common Mistakes

Read-during-write not forwarded when the datapath needs read-new. The signature register-file bug (full post-mortem in §7). A plain read-old file is used in a datapath whose instructions can be back-to-back dependent, so a register read on the same cycle as its write returns the stale value — a RAW hazard. The fix is the per-port write-forwarding bypass (we && raddr == waddr forwards wdata), or a pipeline that inserts a bubble. Whether you need forwarding is a datapath decision; if you skip it, you must guarantee no dependent instruction reads a register the cycle it is written.

Trying to infer 3+ read ports from a 1-2 port block RAM. A block RAM physically has one or two ports; writing three combinational reads and expecting them all to map to one block RAM does not work — the tool cannot build a third port that is not there. For extra read ports you must replicate the array (one mirror copy per read port, every write fanned out to all copies) or build the file from flops. Assuming the tool will "just add a port" gives you either a mapping failure or a silent fallback to distributed RAM/flops (ties 6.1).

Forgetting x0-is-zero. On a RISC file, register 0 must read as 0 and swallow writes. Omit the guard and a write to x0 sticks, so later reads of x0 return garbage instead of the architectural constant 0 — and every addi x0, ... "discard" and every "read zero" idiom breaks. The guard is two comparisons (drop writes to waddr == 0, force rdata == 0 on raddr == 0), and the zero-read guard must sit ahead of any forwarding so a bypass never leaks a non-zero value onto x0.

Async read where a sync (registered) read was intended. For a large multi-ported file mapped to a RAM macro, a combinational read cannot infer the macro — it degrades to a giant flop-and-mux array (exactly the 6.1 failure), blowing area and timing. A small 32-entry flop file wants combinational reads (many ports, cheap); a large file needs registered reads to hit the macro. Match the read style to the target and depth, and pipeline the reader for the latency you chose.

Write-port conflicts — two writes to one array the same cycle. The classic file is nR1W: one write port. Wiring two write sources into one array (two if (we) rf[...] <= ... on the same array, or two drivers) is a conflict — the last assignment wins in sim and the hardware is ambiguous or needs true dual-write ports/arbitration the RTL did not provide. If a design genuinely needs two writes per cycle, that is a 2W file (multiple physical write ports or banking), not a second always block bolted onto a 1W array.

7. DebugLab

The instruction that read the wrong operand — a register file with no write-forwarding

The engineering lesson: a register file is a multi-port memory, and a read that lands on the same cycle as a write to the same register must be forwarded — or the datapath reads a stale operand, a RAW hazard. The tell is a bug that depends on instruction adjacency ("the fault disappears if I separate the two instructions"): adjacency-dependence in operand values means the storage is returning the pre-write value on a same-cycle collision. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: decide the same-register read-during-write policy explicitly (forward per read port when the datapath needs read-new, or guarantee no dependent read coincides with its write), and test the collision directly — drive a read address equal to the write address with we high and assert the intended old-or-new value, because the independent-register tests will never exercise it.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "a register file is a small multi-port memory" habit.

Exercise 1 — Choose the port count

For each datapath, state the read/write port count the register file needs and one line of why: (a) a classic single-issue RISC integer core executing one two-operand instruction per cycle; (b) a fused multiply-add unit computing d = a*b + c in one cycle; (c) a two-wide superscalar core issuing two independent two-operand instructions per cycle and retiring two results. (Hint: count the operands read and results written per cycle, and remember write ports are the expensive ones.)

Exercise 2 — Replicate or flop

You must build a 4R1W register file. For (i) a 32-entry, 32-bit integer file and (ii) a 1024-entry, 32-bit vector file, decide whether to build it from flip-flops or from replicated block RAMs, and justify each with the resource cost (how many copies of the storage, how much write fan-out, and how the read ports are formed). Then state what goes wrong if you pick the other option for each.

Exercise 3 — Trace the RAW hazard

A 2R1W file has no write-forwarding and combinational reads. Given the instruction pair add x5, x1, x2 (writes x5) immediately followed by sub x9, x5, x3 (reads x5), trace exactly what value the SUB's x5 read port returns on the cycle the ADD's result is written, and why. Then give two distinct fixes — one in the register file, one in the pipeline — and name the cost of each (area/logic vs a stall cycle).

Exercise 4 — Place the x0 guard

A colleague adds write-forwarding to a RISC-V register file but puts the forwarding bypass ahead of the x0-is-zero guard, so the read port is rdata1 = (raddr1 == 0) ? 0 : (we && raddr1 == waddr) ? wdata : rf[raddr1] — wait, that ordering is actually correct. Now describe the bug in the reversed ordering (forward first, then zero-guard), give the exact instruction that exposes it (a write to x0 on the same cycle a port reads x0), and state the one-line rule for where the zero guard must sit relative to the bypass.

Continue in RTL Design Patterns (forward links unlock as they ship):

  • ROM & Initialization — Chapter 6.4; the read-only cousin — a memory with an initialized array and no write port, the same array viewed as constants.
  • Dual-Port RAM — Chapter 6.5; the true two-port memory whose second port and replication trick this file's extra read ports build on.
  • Synchronous FIFO Architecture — Chapter 7.1; a FIFO's data store is a simple-dual-port memory, and its read-during-write at the boundary is the same collision question this page answers.

Backward / method (this chapter and before):

  • Inferring RAM — Chapter 6.1; the block-RAM inference template a register file specializes into a multi-port array (and the flop-vs-RAM choice this page extends).
  • Read Timing & Read-During-Write — Chapter 6.2; the read latency and the same-address collision policy this page turns into a per-port RAW-hazard forwarding decision.
  • Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; each read port is a REGS-to-1 mux over the array, and the forwarding bypass is a 2:1 mux — selection is the whole read path.
  • Decoders — Chapter 1.2; the address decode behind each read/write port that turns a register index into a word select.
  • FSM + Datapath — Chapter 4.6; the control/datapath split in which a register file is the datapath's operand storage.
  • ALU Construction — Chapter 5; the ALU the register file feeds — two operands out, one result back, the loop this file closes.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Physical Data Types — the array-of-vectors (reg [W-1:0] rf [0:REGS-1]) that is the register file's storage.
  • Blocking and Non-Blocking Assignments — why the clocked write uses non-blocking (<=) and the combinational read path uses blocking / continuous assignment.
  • Case Statements — the selection behind each read port's decode and the forwarding bypass mux.
  • Generate Blocks — the elaboration-time replication used to build extra read ports (mirror copies) and parameterize the file.

11. Summary

  • A register file is a small multi-port memory. A datapath must read several operands and write a result every cycle, which a single-port RAM cannot do. The classic file is 2R1W — two independent read ports (raddr1/rdata1, raddr2/rdata2) reading the same array concurrently, plus one clocked write port (we/waddr/wdata) — so both operands come out on the same edge.
  • Extra read ports are not free — replicate or flop. A block RAM has one or two physical ports; more read ports means keeping a mirror copy of the array per read port (every write fanned out to all copies) or building a small file from flip-flops with a read mux per port. Flops give many ports cheaply at small depth; replicated block RAMs scale with depth but cost per read port.
  • x0 is hardwired to zero. On a RISC file, drop writes to register 0 and force reads of address 0 to all-zeros — and the zero guard must sit ahead of any write-forwarding so a bypass never leaks a non-zero value onto x0.
  • Same-register read-during-write is a RAW hazard. When a read address equals the write address with we this cycle, a plain flop file returns the old value; a back-to-back dependent instruction then reads a stale operand (the §7 DebugLab). Fix it with a per-port write-forwarding bypass (we && raddr == waddr forwards wdata) or a pipeline bubble — and test the collision directly, because independent-register tests never exercise it.
  • Verify it as a multi-port memory. Write and read back on both ports; read two different registers simultaneously (proving the ports are independent); drive the same-register read-during-write and assert old or new per your forwarding choice; assert x0 ignores writes and reads 0; and re-run generic over REGS and WIDTH — self-checking clocked testbenches shown here in SystemVerilog, Verilog, and VHDL.