Skip to content

RTL Design Patterns · Chapter 12 · Parameterized & Reusable RTL

Parameterized FIFO & Memory

Every project needs a FIFO and a block of on-chip memory, usually several at different sizes. The wasteful path copies last project's FIFO, hand-edits the widths, and rediscovers the same wrap and full-empty bugs in each copy. The engineering path builds them once as parameterized, drop-in IP: a single module that exposes WIDTH and DEPTH, derives its own pointer and address widths, gates optional features at elaboration, and infers a dense RAM instead of a wall of flip-flops, so the same source becomes a small buffer or a large one with two numbers changed. You then confront the subtlety that separates real reusable IP from copy-paste: a pointer that wraps at a power of two while DEPTH is not, which silently corrupts data at sizes the author never tested, plus the explicit-wrap and elaboration-guard fixes, all in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design PatternsParameterizationFIFOMemory InferenceGenerateReusable IP

Chapter 12 · Section 12.3 · Parameterized & Reusable RTL

1. The Engineering Problem

You are three blocks into a new datapath and you need a FIFO. Not a special FIFO — an ordinary synchronous one, a producer on one side, a consumer on the other. You have written this exact thing a dozen times. So you open last project's FIFO, the depth-16, 32-bit one that worked, and you start editing. This one needs to be 8 bits wide and 64 deep. You change the data width in five places. You change the pointer width. You change the compare that decides full. You change the RAM declaration. You think you got them all.

Two weeks later, in system simulation, the FIFO drops words under a specific burst pattern — and only in this instance, not the one two blocks over that you copied from the same original. The difference is a single width you missed editing, or a full/empty compare that was fine at depth 16 and is subtly wrong at depth 64. You did not write a new bug; you re-imported an old design and mutated it by hand, and hand-mutation is exactly where these bugs live. Multiply that by every FIFO and every memory in the chip — and there are many; the FIFO and the on-chip memory are the two most-reused blocks in any design — and copy-paste becomes the single largest source of avoidable RTL defects.

The fix is not "be more careful when editing." It is to stop editing. You build the FIFO and the memory once, as parameterized IP: one module, WIDTH and DEPTH as parameters, everything else derived from them, and you drop that one module in wherever you need a FIFO — 8x64 here, 32x16 there, 64x4096 in the packet buffer — with two numbers changed and nothing re-derived by hand. This page is how you write that module so it is genuinely reusable across the whole range of sizes it advertises, and not just at the one size you happened to test.

the-need.v — one reusable module, not a dozen hand-edited copies
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // WRONG — copy last project's FIFO and hand-edit widths for every instance:
   //   reg [31:0] mem [0:15];         // was 32x16... now edit to 8x64 by hand,
   //   reg [3:0]  wptr, rptr;         // ...and this width... and the full compare...
   //   // miss ONE edit and this instance silently drops data. Repeat per FIFO.
 
   // RIGHT — one parameterized module; instantiate at any size, derive the rest:
   //   sync_fifo #(.WIDTH(8),  .DEPTH(64))  rx_buf (...);   // 8 x 64
   //   sync_fifo #(.WIDTH(32), .DEPTH(16))  cmd_q  (...);   // 32 x 16
   //   sync_fifo #(.WIDTH(64), .DEPTH(4096)) pkt_buf(...);  // 64 x 4096
   //   // ADDR = clog2(DEPTH) derived; RAM inferred; full/empty correct at ALL sizes.

2. Mental Model

3. Pattern Anatomy

A reusable FIFO/memory is the FIFO of 7.1 and the RAM of 6.1, re-expressed so that nothing inside the body is a fixed number. Take the parts in turn.

The parameter set — the module's public contract. A well-formed reusable FIFO exposes a small, orthogonal set of knobs and nothing else:

  • WIDTH — the word size in bits (the only thing that scales the datapath).
  • DEPTH — the number of entries (the only thing that scales the storage).
  • Optional, parameter-gated features — a first-word-fall-through path (FWFT, from 7.5), a registered output stage (REG_OUTPUT, an extra pipeline register on dout for timing), and a programmable ALMOST_FULL threshold (a second flag that asserts when occupancy reaches ALMOST_FULL, for early back-pressure). Each is a parameter the instantiator sets; each maps to a generate block that is present or absent accordingly.

Everything else is derived, not a parameter.

Deriving the pointer / address widths — and the extra wrap bit. From DEPTH alone: the address width is ADDR = $clog2(DEPTH) — the number of bits needed to index DEPTH rows. In the occupancy-count FIFO (7.1) each pointer is ADDR bits; in the pointer-comparison FIFO (7.2) each pointer carries one extra bitADDR+1 bits — whose top bit is the wrap flag that distinguishes "the pointers are equal because the FIFO is empty" from "equal because it is full." All of these are localparams (12.1): computed once, from DEPTH, so the instantiator can never desynchronize a pointer width from the depth it is supposed to index.

Power-of-two DEPTH vs not — the subtlety that bites. When DEPTH is a power of two, the pointer's natural binary rollover is the modulo-DEPTH wrap: an ADDR-bit counter counts 0 … DEPTH-1 and rolls back to 0 for free, and the compare that decides full/empty can mask with & (DEPTH-1). This is elegant and it is why almost every published FIFO assumes a power-of-two depth. But the instant DEPTH is not a power of two — DEPTH=12, so ADDR=4 and the counter runs 0 … 15 — the natural rollover overshoots: the pointer visits addresses 12, 13, 14, 15 that the RAM does not have, and & (DEPTH-1) no longer computes modulo-DEPTH. A reusable block that advertises arbitrary DEPTH must therefore either wrap the pointer explicitly at DEPTH (ptr == DEPTH-1 ? 0 : ptr+1) and size the RAM to exactly DEPTH, or forbid non-power-of-two DEPTH with an elaboration check. Silently doing neither is the signature bug of this page (§7).

The RAM-inference coding style — a memory, not flops. The storage is a 2D array — mem [DEPTH] of WIDTH-bit words — written by a clocked, write-enabled write and read by a registered (synchronous) read (6.1). That template is the admission ticket to a block RAM tile. Two things break it and force the tool back onto individual flip-flops plus a read mux: an asynchronous read (assign dout = mem[addr], zero latency — no RAM tile can do that), and a reset on the array (block RAMs have no reset on their contents, so resetting mem disqualifies inference). A reusable memory must pick the inferring style deliberately, or a 4096-deep FIFO becomes a quarter-million flip-flops.

Generate-guarded optional features. The optional register/FWFT/almost-full logic lives inside generate … if (PARAM) … endgenerate. When the parameter requests the feature, the block is elaborated and every one of its signals is driven; when it does not, the block is absent and the feature's output is driven by the complementary branch (e.g. dout comes straight from the RAM read instead of through the extra register). The discipline from 12.2 applies exactly: every optional output is driven in every configuration — a feature left half-wired when disabled is an undriven net (§6).

Parameter sanity — an elaboration contract. A reusable module states and checks its legal range: DEPTH >= 2 (a one-entry FIFO is degenerate), WIDTH >= 1, and — if the pointer scheme requires it — DEPTH a power of two. These checks belong at elaboration (an initial/assert in SV, an if generate error, a VHDL assert … severity failure) so an illegal instantiation fails loudly at compile, not silently at run time.

Parameterized FIFO/memory — two knobs in, everything else derived

data flow
Parameterized FIFO/memory — two knobs in, everything else derivedWIDTH, DEPTH (+opts)the module's public contractADDR =clog2(DEPTH)localparam widths, +1 wrap bit (7.2)wrap decisionpow2 rollover vs explicit ptr==DEPTH-1 wrapRAM-inferring mem2D array, clocked write, REGISTERED readfull / empty /almostderived from DEPTH; correct at any sizegenerate-guardedoptsFWFT / REG_OUTPUT elaborated in or out
Two parameters go in — WIDTH and DEPTH — and everything else is a consequence: ADDR is derived by clog2(DEPTH), the pointers get an extra wrap bit for the 7.2 full/empty scheme, the wrap is either free (power-of-two rollover) or explicit (ptr == DEPTH-1 ? 0 : ptr+1) for arbitrary depth, the storage infers a RAM from the registered-read template, the flags derive from DEPTH, and optional features are elaborated in or out by generate. Change the two numbers and a correct module rebuilds itself. This is language-independent: the derived width, the wrap decision, the RAM template, and the generate guard are the same in SystemVerilog, Verilog, and VHDL.

The through-line of the anatomy is a single rule: the only free numbers are WIDTH and DEPTH; every width, every pointer, every flag, and every RAM dimension is derived from them and correct across the module's whole advertised range — and the wrap and RAM-inference decisions are made deliberately, not inherited from a copy.

4. Real RTL Implementation

This is the core of the page. We build three things — (a) a fully parameterized synchronous FIFO (WIDTH, DEPTH; ADDR derived via $clog2; a RAM-inferring array; one generate-guarded optional feature; correct full/empty via an occupancy count), (b) a parameterized RAM-inferring memory, and (c) the non-power-of-two depth wrap bug — buggy vs fixed — and each is shown in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking testbench. The FIFO testbench sweeps WIDTH and DEPTH, including a non-power-of-two DEPTH, and asserts in-order, exactly-once behaviour with correct flags — because a parameterized block must be verified generic, not assumed generic.

One discipline runs through every block: the pointer wraps at DEPTH, explicitly, so the FIFO is correct whether or not DEPTH is a power of two — and the RAM is sized to exactly DEPTH. That single decision is what makes the module honest about the range it advertises.

4a. Fully parameterized synchronous FIFO — three ways

The FIFO exposes WIDTH and DEPTH; derives ADDR = $clog2(DEPTH); stores in a RAM-inferring array; keeps an occupancy count for full/empty (the obviously-correct scheme from 7.1); wraps each pointer explicitly at DEPTH so a non-power-of-two DEPTH is safe; and gates one optional feature — a registered output stage REG_OUTPUT — behind a generate. The SystemVerilog form uses an unpacked array for mem and a clocked registered read, the canonical RAM template.

sync_fifo.sv — parameterized FIFO: derived ADDR, inferred RAM, explicit wrap, generate-guarded output register
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_fifo #(
       parameter int WIDTH      = 8,                       // word size (only datapath knob)
       parameter int DEPTH      = 16,                      // entries (only storage knob)
       parameter bit REG_OUTPUT = 0,                       // optional: extra output register
       localparam int ADDR = (DEPTH > 1) ? $clog2(DEPTH) : 1   // DERIVED, never re-entered
   )(
       input  logic             clk,
       input  logic             rst,                       // synchronous, active-high
       input  logic             wr,                        // push request
       input  logic [WIDTH-1:0] din,
       input  logic             rd,                        // pop request
       output logic [WIDTH-1:0] dout,
       output logic             full,
       output logic             empty
   );
       // ---- elaboration-time parameter sanity (fails LOUD at compile) ----
       initial begin
           assert (DEPTH >= 2) else $fatal(1, "DEPTH must be >= 2");
           assert (WIDTH >= 1) else $fatal(1, "WIDTH must be >= 1");
       end
 
       // ---- RAM-inferring storage: 2D array, sized to EXACTLY DEPTH ----
       logic [WIDTH-1:0] mem [DEPTH];                      // no reset on the array (keeps it a RAM)
       logic [ADDR-1:0]  wptr, rptr;                       // pointers index EXACTLY DEPTH rows
       logic [ADDR:0]    count;                            // 0..DEPTH occupancy (needs ADDR+1 bits)
 
       assign full  = (count == DEPTH[ADDR:0]);
       assign empty = (count == 0);
 
       // explicit wrap at DEPTH: correct whether or not DEPTH is a power of two
       function automatic logic [ADDR-1:0] wrap (input logic [ADDR-1:0] p);
           wrap = (p == ADDR'(DEPTH-1)) ? '0 : p + 1'b1;
       endfunction
 
       logic do_wr, do_rd;
       assign do_wr = wr && !full;                          // gate the write on full
       assign do_rd = rd && !empty;                         // gate the read on empty
 
       logic [WIDTH-1:0] rdata;                             // raw registered read from the RAM
 
       always_ff @(posedge clk) begin
           if (rst) begin
               wptr  <= '0;
               rptr  <= '0;
               count <= '0;
           end else begin
               if (do_wr) begin
                   mem[wptr] <= din;                        // clocked write-enabled write
                   wptr      <= wrap(wptr);                 // explicit wrap
               end
               if (do_rd) rptr <= wrap(rptr);              // explicit wrap
               // occupancy: +1 write-only, -1 read-only, unchanged on both/neither
               count <= count + (do_wr ? 1 : 0) - (do_rd ? 1 : 0);
           end
       end
 
       always_ff @(posedge clk)
           rdata <= mem[rptr];                             // REGISTERED read → infers a RAM
 
       // ---- generate-guarded optional feature: an extra output register ----
       generate
           if (REG_OUTPUT) begin : g_reg_out
               logic [WIDTH-1:0] dout_r;
               always_ff @(posedge clk) dout_r <= rdata;   // one more pipeline stage on dout
               assign dout = dout_r;                       // driven in THIS configuration
           end else begin : g_no_reg
               assign dout = rdata;                        // driven in the OTHER configuration
           end
       endgenerate
   endmodule

The load-bearing lines: localparam int ADDR = $clog2(DEPTH) derives the address width from DEPTH so it can never be out of sync; mem [DEPTH] sizes the RAM to exactly DEPTH and takes no reset, keeping it inferable as a block RAM; the wrap function wraps at DEPTH rather than by binary rollover, so DEPTH=12 works; and the generate drives dout in both the REG_OUTPUT and the plain configuration, never leaving it half-wired. The occupancy count gives an obviously-correct full/empty at any DEPTH. The testbench sweeps WIDTH and DEPTH — including the non-power-of-two DEPTH=12 — and checks that every word read out matches the word written, in order, exactly once.

sync_fifo_tb.sv — sweep WIDTH and DEPTH (incl. DEPTH=12); assert in-order, exactly-once, correct flags
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_fifo_tb;
       // Elaborate several configurations. DEPTH=12 is the non-power-of-two case.
       localparam int NCFG = 3;
       int W   [NCFG] = '{8, 16, 32};
       int D   [NCFG] = '{16, 12, 8};                     // 12 is NOT a power of two
 
       task automatic run (input int WIDTH, input int DEPTH);
           logic             clk = 0, rst, wr, rd, full, empty;
           logic [63:0]      din, dout;                    // 64 wide, use low WIDTH bits
           int               errors = 0;
           int               model  [$];                   // golden queue = the FIFO's meaning
           // (instantiate via a generate-selected wrapper in real code; shown inline for clarity)
           // ... assume sync_fifo #(WIDTH,DEPTH) dut bound to these signals ...
           begin
               // reset
               rst = 1; wr = 0; rd = 0; repeat (2) @(posedge clk);
               rst = 0;
               // push DEPTH+4 words but honour full; pop honouring empty; interleave
               for (int i = 0; i < DEPTH*3; i++) begin
                   wr  = (!full)  && ($urandom & 1);
                   rd  = (!empty) && ($urandom & 1);
                   din = $urandom & ((1 << WIDTH) - 1);
                   if (wr && !full)  model.push_back(din); // golden model records the push
                   @(posedge clk);
                   #1;
                   if (rd && !empty) begin                 // account for 1-cycle read latency in real DUT
                       int exp = model.pop_front();        // FIFO order: first in, first out
                       if (dout !== exp[WIDTH-1:0]) begin
                           $error("W=%0d D=%0d: dout=%h exp=%h (order/exactly-once)", WIDTH, DEPTH, dout, exp);
                           errors++;
                       end
                   end
               end
               if (errors == 0) $display("PASS: WIDTH=%0d DEPTH=%0d in-order exactly-once", WIDTH, DEPTH);
               else             $display("FAIL: WIDTH=%0d DEPTH=%0d %0d mismatches", WIDTH, DEPTH, errors);
           end
       endtask
 
       initial begin
           for (int c = 0; c < NCFG; c++) run(W[c], D[c]);  // SWEEP width and depth, incl. DEPTH=12
           $finish;
       end
   endmodule

The Verilog form is identical in intent; a localparam ADDR = clog2(DEPTH) via a constant function (Verilog-2001 has no $clog2 in every tool, so a small function is the portable idiom), a reg [WIDTH-1:0] mem [0:DEPTH-1] array, an explicit wrap, and a generate around the optional output register.

sync_fifo.v — parameterized FIFO in Verilog (constant-function ADDR, inferred RAM, explicit wrap)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_fifo #(
       parameter WIDTH      = 8,
       parameter DEPTH      = 16,
       parameter REG_OUTPUT = 0
   )(
       input  wire             clk,
       input  wire             rst,
       input  wire             wr,
       input  wire [WIDTH-1:0] din,
       input  wire             rd,
       output wire [WIDTH-1:0] dout,
       output wire             full,
       output wire             empty
   );
       // DERIVE the address width from DEPTH with a constant function (portable clog2)
       function integer clog2;
           input integer value;
           integer i;
           begin
               clog2 = 0;
               for (i = value - 1; i > 0; i = i >> 1) clog2 = clog2 + 1;
           end
       endfunction
       localparam ADDR = (DEPTH > 1) ? clog2(DEPTH) : 1;
 
       reg [WIDTH-1:0] mem [0:DEPTH-1];                    // RAM-inferring array, sized to DEPTH
       reg [ADDR-1:0]  wptr, rptr;
       reg [ADDR:0]    count;
       reg [WIDTH-1:0] rdata;
 
       assign full  = (count == DEPTH);
       assign empty = (count == 0);
 
       wire do_wr = wr && !full;
       wire do_rd = rd && !empty;
 
       // explicit wrap at DEPTH: nextp(p) = (p == DEPTH-1) ? 0 : p+1
       always @(posedge clk) begin
           if (rst) begin
               wptr  <= 0; rptr <= 0; count <= 0;
           end else begin
               if (do_wr) begin
                   mem[wptr] <= din;                       // clocked write-enabled write
                   wptr      <= (wptr == DEPTH-1) ? 0 : wptr + 1'b1;
               end
               if (do_rd)
                   rptr <= (rptr == DEPTH-1) ? 0 : rptr + 1'b1;
               count <= count + (do_wr ? 1'b1 : 1'b0) - (do_rd ? 1'b1 : 1'b0);
           end
       end
 
       always @(posedge clk)
           rdata <= mem[rptr];                             // REGISTERED read → RAM
 
       // generate-guarded optional output register; dout driven in BOTH branches
       generate
           if (REG_OUTPUT) begin : g_reg_out
               reg [WIDTH-1:0] dout_r;
               always @(posedge clk) dout_r <= rdata;
               assign dout = dout_r;
           end else begin : g_no_reg
               assign dout = rdata;
           end
       endgenerate
   endmodule
sync_fifo_tb.v — self-checking; sweep DEPTH incl. non-power-of-two; print PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_fifo_tb;
       parameter WIDTH = 16;
       parameter DEPTH = 12;                               // NON-power-of-two on purpose
       reg              clk, rst, wr, rd;
       reg  [WIDTH-1:0] din;
       wire [WIDTH-1:0] dout;
       wire             full, empty;
       integer          errors, i;
       reg  [WIDTH-1:0] model [0:1023];                    // golden circular queue
       integer          mh, mt, mcount;                    // head, tail, count of the model
 
       sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut
           (.clk(clk), .rst(rst), .wr(wr), .din(din), .rd(rd),
            .dout(dout), .full(full), .empty(empty));
 
       initial clk = 0;  always #5 clk = ~clk;
 
       initial begin
           errors = 0; mh = 0; mt = 0; mcount = 0;
           rst = 1; wr = 0; rd = 0;
           @(posedge clk); @(posedge clk); rst = 0;
           for (i = 0; i < DEPTH*4; i = i + 1) begin
               wr  = (!full)  && ($random & 1);
               rd  = (!empty) && ($random & 1);
               din = $random;
               @(negedge clk);
               if (wr && !full) begin                      // golden push
                   model[mt] = din; mt = (mt+1) % 1024; mcount = mcount + 1;
               end
               if (rd && !empty) begin                     // golden pop, compare (order + exactly-once)
                   if (dout !== model[mh]) begin
                       $display("FAIL: D=%0d dout=%h exp=%h", DEPTH, dout, model[mh]);
                       errors = errors + 1;
                   end
                   mh = (mh+1) % 1024; mcount = mcount - 1;
               end
           end
           if (errors == 0) $display("PASS: WIDTH=%0d DEPTH=%0d in-order exactly-once", WIDTH, DEPTH);
           else             $display("FAIL: %0d mismatches at DEPTH=%0d", errors, DEPTH);
           $finish;
       end
   endmodule

VHDL expresses the parameterization with generics and numeric_std. ADDR is derived with integer(ceil(log2(real(DEPTH)))); the storage is a type ram_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0); the wrap is explicit; and the optional register is a if REG_OUTPUT generate. Parameter sanity is an assert … severity failure in the architecture body.

sync_fifo.vhd — parameterized FIFO in VHDL (generics, numeric_std, derived ADDR, explicit wrap)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   use ieee.math_real.all;                                 -- for log2/ceil at elaboration
 
   entity sync_fifo is
       generic (
           WIDTH      : positive := 8;                     -- word size
           DEPTH      : positive := 16;                    -- entries
           REG_OUTPUT : boolean  := false                  -- optional output register
       );
       port (
           clk   : in  std_logic;
           rst   : in  std_logic;
           wr    : in  std_logic;
           din   : in  std_logic_vector(WIDTH-1 downto 0);
           rd    : in  std_logic;
           dout  : out std_logic_vector(WIDTH-1 downto 0);
           full  : out std_logic;
           empty : out std_logic
       );
   end entity;
 
   architecture rtl of sync_fifo is
       -- DERIVED address width; pointers index EXACTLY DEPTH rows
       constant ADDR : positive := integer(ceil(log2(real(DEPTH))));
       type ram_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       signal mem   : ram_t;                               -- no reset on the array → stays a RAM
       signal wptr  : integer range 0 to DEPTH-1 := 0;
       signal rptr  : integer range 0 to DEPTH-1 := 0;
       signal cnt   : integer range 0 to DEPTH   := 0;
       signal rdata : std_logic_vector(WIDTH-1 downto 0);
       signal do_wr, do_rd : std_logic;
   begin
       -- elaboration-time parameter sanity (fails LOUD at compile)
       assert DEPTH >= 2 report "DEPTH must be >= 2" severity failure;
       assert WIDTH >= 1 report "WIDTH must be >= 1" severity failure;
 
       full   <= '1' when cnt = DEPTH else '0';
       empty  <= '1' when cnt = 0     else '0';
       do_wr  <= '1' when (wr = '1' and cnt <  DEPTH) else '0';
       do_rd  <= '1' when (rd = '1' and cnt >  0)     else '0';
 
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   wptr <= 0; rptr <= 0; cnt <= 0;
               else
                   if do_wr = '1' then
                       mem(wptr) <= din;                   -- clocked write-enabled write
                       if wptr = DEPTH-1 then wptr <= 0; else wptr <= wptr + 1; end if;  -- explicit wrap
                   end if;
                   if do_rd = '1' then
                       if rptr = DEPTH-1 then rptr <= 0; else rptr <= rptr + 1; end if;  -- explicit wrap
                   end if;
                   -- occupancy update
                   if    do_wr = '1' and do_rd = '0' then cnt <= cnt + 1;
                   elsif do_wr = '0' and do_rd = '1' then cnt <= cnt - 1;
                   end if;
               end if;
               rdata <= mem(rptr);                         -- REGISTERED read → infers a RAM
           end if;
       end process;
 
       -- generate-guarded optional output register; dout driven in BOTH branches
       g_reg : if REG_OUTPUT generate
           process (clk) begin
               if rising_edge(clk) then dout <= rdata; end if;
           end process;
       end generate;
       g_noreg : if not REG_OUTPUT generate
           dout <= rdata;
       end generate;
   end architecture;
sync_fifo_tb.vhd — self-checking; DEPTH=12 (non-power-of-two); assert order + exactly-once
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity sync_fifo_tb is
   end entity;
 
   architecture sim of sync_fifo_tb is
       constant WIDTH : positive := 16;
       constant DEPTH : positive := 12;                    -- NON-power-of-two on purpose
       signal clk, rst, wr, rd, full, empty : std_logic := '0';
       signal din, dout : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       type   qarr is array (0 to 1023) of std_logic_vector(WIDTH-1 downto 0);
   begin
       dut : entity work.sync_fifo
           generic map (WIDTH => WIDTH, DEPTH => DEPTH)
           port map (clk => clk, rst => rst, wr => wr, din => din, rd => rd,
                     dout => dout, full => full, empty => empty);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable model : qarr;
           variable mh, mt : integer := 0;
           variable errors : integer := 0;
           variable lfsr   : unsigned(WIDTH-1 downto 0) := to_unsigned(1, WIDTH);
       begin
           rst <= '1'; wr <= '0'; rd <= '0';
           wait until rising_edge(clk); wait until rising_edge(clk);
           rst <= '0';
           for i in 0 to DEPTH*4 - 1 loop
               lfsr := lfsr(WIDTH-2 downto 0) & (lfsr(WIDTH-1) xor lfsr(WIDTH-3));  -- cheap PRNG
               din  <= std_logic_vector(lfsr);
               wr   <= '1' when (full  = '0' and lfsr(0) = '1') else '0';
               rd   <= '1' when (empty = '0' and lfsr(1) = '1') else '0';
               wait until falling_edge(clk);
               if wr = '1' and full = '0' then
                   model(mt) := std_logic_vector(lfsr); mt := (mt + 1) mod 1024;   -- golden push
               end if;
               if rd = '1' and empty = '0' then
                   assert dout = model(mh)                                          -- order + exactly-once
                       report "FIFO order/exactly-once mismatch at DEPTH=12" severity error;
                   if dout /= model(mh) then errors := errors + 1; end if;
                   mh := (mh + 1) mod 1024;
               end if;
           end loop;
           if errors = 0 then report "PASS: DEPTH=12 in-order exactly-once" severity note;
           else               report "FAIL: mismatches at DEPTH=12" severity error; end if;
           wait;
       end process;
   end architecture;

4b. Parameterized RAM-inferring memory — three ways

The FIFO's storage is a memory; here it stands alone as reusable single-port RAM IP — WIDTH and DEPTH generic, a clocked write-enabled write, and the crux, a registered read so the tool infers a block RAM rather than a flop array (6.1). The same source is a 512x8 table or a 4096x64 buffer.

sp_ram.sv — parameterized single-port RAM (registered read → infers a block RAM)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sp_ram #(
       parameter int WIDTH = 8,
       parameter int DEPTH = 1024,
       localparam int ADDR = $clog2(DEPTH)                 // derived address width
   )(
       input  logic             clk,
       input  logic             we,                        // write-enable (the block-RAM native port)
       input  logic [ADDR-1:0]  addr,
       input  logic [WIDTH-1:0] din,
       output logic [WIDTH-1:0] dout
   );
       logic [WIDTH-1:0] mem [DEPTH];                      // 2D array; NO reset on it → stays a RAM
       always_ff @(posedge clk) begin
           if (we) mem[addr] <= din;                       // clocked write-enabled write
           dout <= mem[addr];                              // REGISTERED read → 1-cycle latency → RAM
       end
   endmodule
sp_ram_tb.sv — write then read back each address; account for 1-cycle read latency
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sp_ram_tb;
       localparam int WIDTH = 8, DEPTH = 16, ADDR = $clog2(DEPTH);
       logic clk = 0, we;
       logic [ADDR-1:0]  addr;
       logic [WIDTH-1:0] din, dout;
       int   errors = 0;
       logic [WIDTH-1:0] golden [DEPTH];
 
       sp_ram #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (.*);
       always #5 clk = ~clk;
 
       initial begin
           for (int a = 0; a < DEPTH; a++) begin           // write every address
               golden[a] = $urandom; we = 1; addr = a; din = golden[a];
               @(posedge clk);
           end
           we = 0;
           for (int a = 0; a < DEPTH; a++) begin           // read back; data is valid NEXT edge
               addr = a; @(posedge clk); #1;
               assert (dout === golden[a])
                   else begin $error("addr=%0d dout=%h exp=%h", a, dout, golden[a]); errors++; end
           end
           if (errors == 0) $display("PASS: sp_ram read-back matches at WIDTH=%0d DEPTH=%0d", WIDTH, DEPTH);
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule
sp_ram.v — parameterized single-port RAM in Verilog (registered read → block RAM)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sp_ram #(
       parameter WIDTH = 8,
       parameter DEPTH = 1024,
       parameter ADDR  = 10                                // = clog2(DEPTH); set by instantiator
   )(
       input  wire             clk,
       input  wire             we,
       input  wire [ADDR-1:0]  addr,
       input  wire [WIDTH-1:0] din,
       output reg  [WIDTH-1:0] dout
   );
       reg [WIDTH-1:0] mem [0:DEPTH-1];                    // 2D array → block RAM (no reset on it)
       always @(posedge clk) begin
           if (we) mem[addr] <= din;                       // clocked write-enabled write
           dout <= mem[addr];                              // REGISTERED read → RAM
       end
   endmodule
sp_ram_tb.v — self-checking; write all, read back (1-cycle latency), PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sp_ram_tb;
       parameter WIDTH = 8, DEPTH = 16, ADDR = 4;
       reg              clk, we;
       reg  [ADDR-1:0]  addr;
       reg  [WIDTH-1:0] din;
       wire [WIDTH-1:0] dout;
       reg  [WIDTH-1:0] golden [0:DEPTH-1];
       integer          a, errors;
 
       sp_ram #(.WIDTH(WIDTH), .DEPTH(DEPTH), .ADDR(ADDR)) dut
           (.clk(clk), .we(we), .addr(addr), .din(din), .dout(dout));
       initial clk = 0;  always #5 clk = ~clk;
 
       initial begin
           errors = 0;
           for (a = 0; a < DEPTH; a = a + 1) begin
               golden[a] = $random; we = 1; addr = a[ADDR-1:0]; din = golden[a];
               @(posedge clk);
           end
           we = 0;
           for (a = 0; a < DEPTH; a = a + 1) begin
               addr = a[ADDR-1:0]; @(posedge clk); #1;     // data valid the cycle after addr
               if (dout !== golden[a]) begin
                   $display("FAIL: addr=%0d dout=%h exp=%h", a, dout, golden[a]);
                   errors = errors + 1;
               end
           end
           if (errors == 0) $display("PASS: sp_ram read-back matches");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule
sp_ram.vhd — parameterized single-port RAM in VHDL (generics, registered read → block RAM)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity sp_ram is
       generic ( WIDTH : positive := 8; DEPTH : positive := 1024 );
       port (
           clk  : in  std_logic;
           we   : in  std_logic;
           addr : in  std_logic_vector(natural(ceil(log2(real(DEPTH))))-1 downto 0);
           din  : in  std_logic_vector(WIDTH-1 downto 0);
           dout : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of sp_ram is
       type ram_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       signal mem : ram_t;                                 -- 2D array, no reset → stays a RAM
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if we = '1' then
                   mem(to_integer(unsigned(addr))) <= din; -- clocked write-enabled write
               end if;
               dout <= mem(to_integer(unsigned(addr)));    -- REGISTERED read → block RAM
           end if;
       end process;
   end architecture;
sp_ram_tb.vhd — self-checking; write all, read back (1-cycle latency), assert report severity
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity sp_ram_tb is
   end entity;
 
   architecture sim of sp_ram_tb is
       constant WIDTH : positive := 8;
       constant DEPTH : positive := 16;
       constant ADDR  : positive := 4;                     -- = clog2(DEPTH)
       signal clk, we : std_logic := '0';
       signal addr    : std_logic_vector(ADDR-1 downto 0)  := (others => '0');
       signal din, dout : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       type gt is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
   begin
       dut : entity work.sp_ram
           generic map (WIDTH => WIDTH, DEPTH => DEPTH)
           port map (clk => clk, we => we, addr => addr, din => din, dout => dout);
       clk <= not clk after 5 ns;
 
       stim : process
           variable golden : gt;
       begin
           for a in 0 to DEPTH-1 loop                       -- write every address
               golden(a) := std_logic_vector(to_unsigned((a*37+1) mod 2**WIDTH, WIDTH));
               we <= '1'; addr <= std_logic_vector(to_unsigned(a, ADDR)); din <= golden(a);
               wait until rising_edge(clk);
           end loop;
           we <= '0';
           for a in 0 to DEPTH-1 loop                       -- read back (data valid next edge)
               addr <= std_logic_vector(to_unsigned(a, ADDR));
               wait until rising_edge(clk); wait for 1 ns;
               assert dout = golden(a)
                   report "sp_ram read-back mismatch" severity error;
           end loop;
           report "sp_ram self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. The non-power-of-two depth wrap bug — buggy vs fixed, in all three HDLs

Pattern (c) is the signature failure, shown as buggy vs fixed RTL in each language, then dramatized in §7. The bug: the "reusable" FIFO advances its pointer by natural binary rollover — an ADDR-bit counter that wraps at 2^ADDR — which equals modulo-DEPTH only when DEPTH is a power of two. Set DEPTH=12, and ADDR=4, so the pointer runs 0 … 15 while the RAM only has 12 slots: it addresses non-existent entries and the full/empty count desynchronizes from the real occupancy. The fix is either an explicit wrap at DEPTH (and size the RAM to DEPTH) or a power-of-two elaboration guard.

fifo_wrap_bug.sv — BUGGY (binary rollover) vs FIXED (explicit wrap at DEPTH)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: pointer is a plain ADDR-bit counter → wraps at 2^ADDR, NOT at DEPTH.
   //        Correct at DEPTH=16 (ADDR=4, 2^4==16). At DEPTH=12 it visits 12..15,
   //        which mem[0:11] does not have, and the wrap is wrong → dropped/duplicated data.
   module fifo_ptr_bad #(
       parameter int WIDTH = 8,
       parameter int DEPTH = 16,
       localparam int ADDR = $clog2(DEPTH)
   )(
       input  logic clk, input logic do_wr, input logic do_rd,
       output logic [ADDR-1:0] wptr, output logic [ADDR-1:0] rptr
   );
       always_ff @(posedge clk) begin
           if (do_wr) wptr <= wptr + 1'b1;   // BINARY ROLLOVER: wraps at 2^ADDR (=16), not DEPTH
           if (do_rd) rptr <= rptr + 1'b1;   // fine at DEPTH=16, BROKEN at DEPTH=12
       end
   endmodule
 
   // FIXED: wrap EXPLICITLY at DEPTH; size the RAM to DEPTH. Correct for ANY DEPTH.
   module fifo_ptr_good #(
       parameter int WIDTH = 8,
       parameter int DEPTH = 16,
       localparam int ADDR = $clog2(DEPTH)
   )(
       input  logic clk, input logic do_wr, input logic do_rd,
       output logic [ADDR-1:0] wptr, output logic [ADDR-1:0] rptr
   );
       always_ff @(posedge clk) begin
           if (do_wr) wptr <= (wptr == ADDR'(DEPTH-1)) ? '0 : wptr + 1'b1;  // wrap AT DEPTH
           if (do_rd) rptr <= (rptr == ADDR'(DEPTH-1)) ? '0 : rptr + 1'b1;
       end
   endmodule
 
   // ALTERNATIVE FIX (power-of-two-only IP): forbid non-power-of-two DEPTH at elaboration.
   //   initial assert ((DEPTH & (DEPTH-1)) == 0) else $fatal(1, "DEPTH must be a power of two");
fifo_wrap_bug_tb.sv — run the pointer at DEPTH=12; the buggy counter overshoots to 12..15
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fifo_wrap_bug_tb;
       localparam int DEPTH = 12;                          // NON-power-of-two: exposes the bug
       localparam int ADDR  = $clog2(DEPTH);               // = 4 → counter can reach 15 > 11
       logic clk = 0, do_wr = 1, do_rd = 0;
       logic [ADDR-1:0] wptr;  logic [ADDR-1:0] rptr;
       int   errors = 0;
 
       // Bind to fifo_ptr_good to PASS; to fifo_ptr_bad to see the overshoot.
       fifo_ptr_good #(.DEPTH(DEPTH)) dut (.clk(clk), .do_wr(do_wr), .do_rd(do_rd), .wptr(wptr), .rptr(rptr));
       always #5 clk = ~clk;
 
       initial begin
           for (int i = 0; i < 3*DEPTH; i++) begin
               @(posedge clk); #1;
               // Invariant a reusable FIFO must hold: the pointer NEVER exceeds DEPTH-1.
               assert (wptr <= DEPTH-1)
                   else begin $error("wptr=%0d overshot DEPTH-1=%0d (non-pow2 wrap bug)", wptr, DEPTH-1); errors++; end
           end
           if (errors == 0) $display("PASS: wptr stayed within 0..%0d at DEPTH=%0d", DEPTH-1, DEPTH);
           else             $display("FAIL: %0d overshoots (binary rollover assumed pow2 DEPTH)", errors);
           $finish;
       end
   endmodule

The Verilog pair is the same story with reg pointers. Sweeping the pointer at DEPTH=12 and asserting wptr <= DEPTH-1 is what exposes the overshoot — the value 12 is the whole bug.

fifo_wrap_bug.v — BUGGY (binary rollover) vs FIXED (explicit wrap) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: plain counter → wraps at 2^ADDR. Passes at DEPTH=16, breaks at DEPTH=12.
   module fifo_ptr_bad #(
       parameter WIDTH = 8, parameter DEPTH = 16, parameter ADDR = 4
   )(
       input  wire clk, input wire do_wr, input wire do_rd,
       output reg [ADDR-1:0] wptr, output reg [ADDR-1:0] rptr
   );
       always @(posedge clk) begin
           if (do_wr) wptr <= wptr + 1'b1;   // binary rollover at 2^ADDR, NOT DEPTH
           if (do_rd) rptr <= rptr + 1'b1;
       end
   endmodule
 
   // FIXED: explicit wrap at DEPTH → correct for any DEPTH, power-of-two or not.
   module fifo_ptr_good #(
       parameter WIDTH = 8, parameter DEPTH = 16, parameter ADDR = 4
   )(
       input  wire clk, input wire do_wr, input wire do_rd,
       output reg [ADDR-1:0] wptr, output reg [ADDR-1:0] rptr
   );
       always @(posedge clk) begin
           if (do_wr) wptr <= (wptr == DEPTH-1) ? 0 : wptr + 1'b1;  // wrap AT DEPTH
           if (do_rd) rptr <= (rptr == DEPTH-1) ? 0 : rptr + 1'b1;
       end
   endmodule
fifo_wrap_bug_tb.v — self-checking; DEPTH=12; assert pointer never exceeds DEPTH-1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fifo_wrap_bug_tb;
       parameter DEPTH = 12, ADDR = 4;                     // 2^4=16 > 12: the overshoot window
       reg              clk, do_wr, do_rd;
       wire [ADDR-1:0]  wptr, rptr;
       integer          i, errors;
 
       // Bind to fifo_ptr_good to PASS; to fifo_ptr_bad to observe the overshoot.
       fifo_ptr_good #(.DEPTH(DEPTH), .ADDR(ADDR)) dut
           (.clk(clk), .do_wr(do_wr), .do_rd(do_rd), .wptr(wptr), .rptr(rptr));
       initial clk = 0;  always #5 clk = ~clk;
 
       initial begin
           errors = 0; do_wr = 1; do_rd = 0;
           for (i = 0; i < 3*DEPTH; i = i + 1) begin
               @(posedge clk); #1;
               if (wptr > DEPTH-1) begin                   // reusable FIFO invariant
                   $display("FAIL: wptr=%0d overshot DEPTH-1=%0d", wptr, DEPTH-1);
                   errors = errors + 1;
               end
           end
           if (errors == 0) $display("PASS: wptr in 0..%0d at DEPTH=%0d", DEPTH-1, DEPTH);
           else             $display("FAIL: %0d overshoots (binary rollover assumed pow2)", errors);
           $finish;
       end
   endmodule

In VHDL the bug is subtler to write because a bounded integer range 0 to DEPTH-1 would catch it at run time — so the buggy version uses an unsigned vector of width ADDR that rolls over at 2^ADDR. The fix compares against DEPTH-1 explicitly (or uses the bounded integer range that makes the overshoot impossible by construction).

fifo_wrap_bug.vhd — BUGGY (unsigned rollover at 2^ADDR) vs FIXED (explicit wrap at DEPTH)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: ADDR-bit unsigned pointer rolls over at 2^ADDR, not at DEPTH.
   entity fifo_ptr_bad is
       generic ( DEPTH : positive := 16; ADDR : positive := 4 );
       port ( clk, do_wr, do_rd : in std_logic;
              wptr, rptr : out std_logic_vector(ADDR-1 downto 0) );
   end entity;
   architecture rtl of fifo_ptr_bad is
       signal w, r : unsigned(ADDR-1 downto 0) := (others => '0');
   begin
       process (clk) begin
           if rising_edge(clk) then
               if do_wr = '1' then w <= w + 1; end if;     -- rolls at 2^ADDR (16), NOT DEPTH (12)
               if do_rd = '1' then r <= r + 1; end if;
           end if;
       end process;
       wptr <= std_logic_vector(w);  rptr <= std_logic_vector(r);
   end architecture;
 
   -- FIXED: wrap EXPLICITLY at DEPTH-1 → correct for any DEPTH.
   entity fifo_ptr_good is
       generic ( DEPTH : positive := 16; ADDR : positive := 4 );
       port ( clk, do_wr, do_rd : in std_logic;
              wptr, rptr : out std_logic_vector(ADDR-1 downto 0) );
   end entity;
   architecture rtl of fifo_ptr_good is
       signal w, r : integer range 0 to DEPTH-1 := 0;      -- bounded: overshoot impossible
   begin
       process (clk) begin
           if rising_edge(clk) then
               if do_wr = '1' then
                   if w = DEPTH-1 then w <= 0; else w <= w + 1; end if;   -- wrap AT DEPTH
               end if;
               if do_rd = '1' then
                   if r = DEPTH-1 then r <= 0; else r <= r + 1; end if;
               end if;
           end if;
       end process;
       wptr <= std_logic_vector(to_unsigned(w, ADDR));
       rptr <= std_logic_vector(to_unsigned(r, ADDR));
   end architecture;
fifo_wrap_bug_tb.vhd — self-checking; DEPTH=12; assert pointer stays within 0..DEPTH-1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity fifo_wrap_bug_tb is
   end entity;
 
   architecture sim of fifo_wrap_bug_tb is
       constant DEPTH : positive := 12;                    -- NON-power-of-two
       constant ADDR  : positive := 4;                     -- 2^4=16 > 12
       signal clk, do_wr, do_rd : std_logic := '0';
       signal wptr, rptr : std_logic_vector(ADDR-1 downto 0);
   begin
       -- Bind to fifo_ptr_good to PASS; to fifo_ptr_bad to observe the overshoot.
       dut : entity work.fifo_ptr_good
           generic map (DEPTH => DEPTH, ADDR => ADDR)
           port map (clk => clk, do_wr => do_wr, do_rd => do_rd, wptr => wptr, rptr => rptr);
       clk <= not clk after 5 ns;
 
       stim : process
           variable errors : integer := 0;
       begin
           do_wr <= '1'; do_rd <= '0';
           for i in 0 to 3*DEPTH - 1 loop
               wait until rising_edge(clk); wait for 1 ns;
               assert to_integer(unsigned(wptr)) <= DEPTH-1     -- reusable FIFO invariant
                   report "wptr overshot DEPTH-1 (non-pow2 wrap bug)" severity error;
               if to_integer(unsigned(wptr)) > DEPTH-1 then errors := errors + 1; end if;
           end loop;
           if errors = 0 then report "PASS: wptr within 0..DEPTH-1 at DEPTH=12" severity note;
           else               report "FAIL: overshoots at DEPTH=12" severity error; end if;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a reusable pointer must wrap at DEPTH, not at a power of two — wrap it explicitly (ptr == DEPTH-1 ? 0 : ptr+1) or forbid non-power-of-two DEPTH at elaboration, and verify by sweeping a non-power-of-two depth.

5. Verification Strategy

A parameterized FIFO/memory has a verification obligation a fixed one does not: it must be proven correct across the parameter range it advertises, not at one convenient size. The single invariant that captures a correct FIFO is:

Every word read out equals the word written, in the order written, exactly once — and the flags (full, empty) track the true occupancy — at every legal WIDTH and DEPTH, including a DEPTH that is not a power of two.

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

  • Sweep WIDTH and DEPTH — including a non-power-of-two DEPTH (self-checking). Instantiate the FIFO at several configurations — e.g. (8,16), (16,12), (32,8) — and run each through a golden-model check. DEPTH=12 is not optional: it is the one size that exercises the wrap path a power-of-two size never touches. The three sync_fifo testbenches in §4a do exactly this, and the wrap-bug testbenches in §4c isolate the pointer at DEPTH=12 and assert it never exceeds DEPTH-1.
  • The order + exactly-once invariant (golden queue). The cleanest FIFO check is a reference queue: push each written word onto a software queue on a real write, pop it on a real read, and assert the DUT's dout equals the queue's front. If they ever diverge, the FIFO reordered, dropped, or duplicated a word — the three ways a FIFO can be wrong. Do this while randomly interleaving writes and reads (honouring full/empty) so the pointers actually wrap several times during the run.
  • Flag correctness against true occupancy. Track the occupancy in the testbench and assert empty exactly when the model is empty and full exactly when it holds DEPTH words. A wrong wrap shows up here too: at a non-power-of-two DEPTH the buggy pointer desynchronizes the count from reality, so full/empty fire at the wrong moments — drive the FIFO to exactly DEPTH and to exactly zero and check the flags at those boundaries.
  • The memory actually INFERS a RAM (synthesis intent). Functional simulation cannot see flop-vs-RAM — that is a synthesis property. The verification obligation is (i) to keep the coding style RAM-inferring (registered read, no reset on the array) and (ii) to read the synthesis report: a deep FIFO/RAM should map to N block-RAM tiles, and a per-bit flip-flop explosion (thousands of registers for a 4096x64 buffer) is the visible signature of a broken read style or a reset on mem. Treat the report line as a checked assertion, not a hope.
  • Every optional-feature configuration (generate paths). Each parameter-gated feature doubles the elaboration space. Re-run the golden-model check with REG_OUTPUT=0 and REG_OUTPUT=1 (and any FWFT/ALMOST_FULL option), because the disabled path and the enabled path are different RTL — a feature left half-wired when disabled (an undriven dout) surfaces only in the configuration that removes it. Verify the configurations you ship.
  • Expected waveform. After reset, empty is high and full low. As writes arrive faster than reads, occupancy climbs, the pointers wrap around the RAM, and full asserts exactly when the count reaches DEPTH — after which further wr is ignored (the word is not lost from the model's view because the producer must honour full). At a non-power-of-two DEPTH the correct waveform still wraps cleanly at DEPTH; the buggy variant's write pointer visibly jumps to an address >= DEPTH, and the golden-queue compare fails right there.

6. Common Mistakes

DEPTH not a power of two breaking pointer wrap. The signature bug. A pointer advanced by natural binary rollover — a plain ADDR-bit counter, or a mask & (DEPTH-1) — wraps at 2^ADDR, which equals modulo-DEPTH only when DEPTH is a power of two. Advertise arbitrary DEPTH, set DEPTH=12 (ADDR=4), and the pointer runs 0 … 15: it addresses RAM slots 12–15 that do not exist and the full/empty math desynchronizes, so the FIFO overwrites or drops data. The fix is to wrap at DEPTH explicitly (ptr == DEPTH-1 ? 0 : ptr+1) and size the RAM to DEPTH — or to forbid non-power-of-two DEPTH with an elaboration check. Pick one deliberately; do not inherit binary rollover from a copy and assume it generalizes. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4c.)

A coding style that INFERS FLOPS instead of a RAM. Two coding choices silently disqualify block-RAM inference (6.1): an asynchronous read (assign dout = mem[addr], zero latency — no RAM tile can do that) and a reset applied to the array (if (rst) mem <= '{default:0} — block RAMs have no reset on their contents). Either forces the tool to build the storage out of individual flip-flops plus a read mux, so a 4096x64 buffer becomes a quarter-million flops and blows the area budget. Keep the read registered and never reset the array — reset the pointers and the count, not mem.

Hardcoded widths inside the 'parameterized' FIFO. A module that takes WIDTH and DEPTH as parameters but still contains a literal [31:0], a [3:0] pointer, or a mem [0:15] is not parameterized — it is a fixed design wearing a parameter list, and it breaks silently at every size except the one those literals happen to match. Every width must be derived from the parameters (ADDR = $clog2(DEPTH), [WIDTH-1:0], mem [DEPTH]); a leftover magic number is a 12.1 leak, and it is exactly the kind of missed edit copy-paste introduces.

An optional feature left half-wired when disabled. A generate-gated feature — an output register, an FWFT path, an almost-full flag — must drive its outputs in both configurations (12.2). If the enabled branch drives dout through an extra register but the disabled branch forgets to drive dout from the RAM read, then dout is an undriven net whenever the feature is off — the FIFO returns X/Z in exactly the configuration that removes the feature. Every optional output needs a complementary branch that drives it when the feature is absent.

Skipping the non-power-of-two DEPTH in verification. The wrap bug hides perfectly if you only ever test DEPTH=16. A parameterized FIFO that passes at every power-of-two size and is never run at DEPTH=12 has been tested, not verified — the one size that exercises the wrap path was skipped. Always include a non-power-of-two DEPTH in the sweep; it is the single most valuable configuration for a reusable FIFO.

7. DebugLab

The FIFO that only broke at depth 12 — a binary pointer wrap assumed a power-of-two depth

The engineering lesson: reusable memory IP must handle the FULL parameter range it advertises — derive every width, pick a RAM-inferring style, drive every optional feature in every configuration, and decide power-of-two-only (with an elaboration check) versus arbitrary-depth (with an explicit wrap) deliberately, not by inheriting a coincidence from the one size you tested. The tell here is a bug that lives in a module yet appears in one instance and not another: instance-dependence in a module that is supposed to be size-independent means the module is correct only at certain sizes, and the sizes it fails at are exactly the ones outside its author's test set. The habit that makes it impossible is to verify across the advertised range — sweep WIDTH and DEPTH and always include a non-power-of-two DEPTH — so no size the module claims to support goes untested.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "derive it, verify it across the range" habit.

Exercise 1 — Find the magic numbers

You are handed a FIFO whose header reads #(parameter WIDTH=8, parameter DEPTH=16) but whose body contains reg [3:0] wptr;, mem[0:15], and a compare count == 16. List every place the module is not actually parameterized, state the exact DEPTH values at which each hardcoded number silently breaks, and rewrite each as a derived width or a localparam in terms of WIDTH/DEPTH. (Hint: ADDR = $clog2(DEPTH); the literal 16 is DEPTH.)

Exercise 2 — Power-of-two-only vs arbitrary-depth: choose and defend

For a given FIFO, decide between (a) supporting arbitrary DEPTH with an explicit ptr == DEPTH-1 ? 0 : ptr+1 wrap, and (b) supporting only power-of-two DEPTH with a natural-rollover pointer and an elaboration guard. State the hardware cost of each (the explicit-wrap compare vs the free rollover), the usability cost of each (who is allowed to instantiate it), and one concrete situation where you would pick each. Then write the one-line elaboration check that makes choice (b) safe rather than silently wrong.

Exercise 3 — Break the RAM inference on paper

Take the parameterized single-port RAM of §4b. Describe the two independent code changes that would each, on its own, stop the array from inferring a block RAM, and say what the synthesized hardware becomes in each case and why (what the tool can no longer map). Then estimate the flip-flop count for a 4096x64 buffer if inference fails, and name the one line in the synthesis report you would check to confirm the memory mapped to block-RAM tiles rather than registers.

Exercise 4 — Verify a feature you can't see in simulation

Your parameterized FIFO offers REG_OUTPUT (an extra output register) and must, at DEPTH up to 4096, infer block RAM. Two of its correctness properties — "the disabled REG_OUTPUT branch still drives dout" and "the storage inferred a RAM, not flops" — are invisible or awkward in a plain functional sim. For each, describe exactly how you would gain confidence: which configuration(s) to elaborate, what to assert, and — for the RAM one — what artifact outside simulation you must inspect. (Hint: one is a generate-configuration sweep; the other is a synthesis-report check.)

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

  • Generate Replication — Chapter 12.2; the elaboration-time replication and parameter-guarded generate this page uses to include or omit the optional output register and FWFT path.
  • Parameter Patterns — Chapter 12.1; the derived-localparam discipline (ADDR = $clog2(DEPTH)) that keeps every width a consequence of WIDTH and DEPTH instead of a third magic number.
  • Width-Generic Datapath — Chapter 12; the same parameterization applied to an arithmetic datapath rather than storage.
  • Configurable-IP Assertions — Chapter 12; the elaboration-time parameter sanity checks (DEPTH power-of-two, DEPTH >= 2) formalized as reusable-IP guardrails.

The blocks this page parameterizes:

  • Synchronous FIFO Architecture — Chapter 7.1; the circular-buffer FIFO (RAM + wrapping pointers + full/empty gating) that this page turns into fully generic IP.
  • FIFO Full/Empty Logic — Chapter 7.2; the pointer-comparison full/empty boundary with the extra wrap bit, the scheme whose pointer width this page derives from DEPTH.
  • FIFO Depth Calculation — Chapter 7.4; how you choose the DEPTH parameter this page makes configurable — the sizing that feeds the parameter.
  • First-Word Fall-Through FIFO — Chapter 7.5; the FWFT read path that becomes one of this page's generate-guarded optional features.
  • Inferring RAM — Chapter 6.1; the registered-read coding style that makes the FIFO's storage map to a block RAM instead of a flop array.
  • Dual-Port RAM — Chapter 6; the simple-dual-port memory (independent write and read ports) the FIFO's storage is built from.
  • Register Files — Chapter 6; a closely-related parameterized memory (width x depth, multiple read ports) that reuses the same derive-and-infer discipline.

Backward / method:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applies to reusable IP (the parameters are the interface contract).
  • What Is an RTL Design Pattern? — Chapter 0.1; why a parameterized, reusable block earns the name "pattern" — a recurring problem, a configurable form, a synthesis reality, and a signature failure.

11. Summary

  • A reusable FIFO/memory has exactly two free numbers — WIDTH and DEPTH — and everything else is derived. The address width is ADDR = $clog2(DEPTH), the pointers are ADDR (plus one wrap bit for the 7.2 scheme), and the RAM has DEPTH rows of WIDTH bits — all localparams (12.1), never a third magic number the instantiator can desynchronize. A leftover literal is a fixed design wearing a parameter list.
  • The pointer must wrap at DEPTH, not at a power of two. Natural binary rollover wraps at 2^ADDR, which equals modulo-DEPTH only when DEPTH is a power of two; advertise arbitrary DEPTH and a DEPTH like 12 makes the pointer overshoot to non-existent slots and desynchronizes full/empty — the signature bug in §7. Fix it with an explicit ptr == DEPTH-1 ? 0 : ptr+1 wrap (and RAM sized to DEPTH), or forbid non-power-of-two DEPTH with an elaboration check — deliberately, not by inheriting a coincidence.
  • The storage must INFER a RAM. A 2D array, a clocked write-enabled write, and a registered read (6.1) map to a block-RAM tile; an asynchronous read or a reset on the array forces individual flip-flops plus a read mux, turning a 4096x64 buffer into a quarter-million flops. Reset the pointers and count, never mem, and confirm inference in the synthesis report.
  • Optional features are elaborated in or out with generate — and driven in every configuration. A registered output, an FWFT path, an almost-full flag each live in a generate guarded by a parameter (12.2); every optional output must be driven in both the enabled and disabled branch, or the module returns X in the configuration that removes it.
  • Verify across the whole advertised range, not one convenient size. Sweep WIDTH and DEPTH — always including a non-power-of-two DEPTH — run a golden-queue check for in-order/exactly-once under interleaved writes and reads, check the flags at the occupancy boundaries, re-run every optional-feature configuration, and read the synthesis report to confirm the RAM inferred. "It passed at DEPTH=16" is tested, not verified — self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.