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.
// 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
doutfor 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 agenerateblock 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 bit — ADDR+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 flowThe 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.
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
endmoduleThe 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.
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
endmoduleThe 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.
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 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
endmoduleVHDL 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.
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; 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.
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 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 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 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 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; 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.
// 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"); 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
endmoduleThe 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.
// 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 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
endmoduleIn 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).
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; 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=12is not optional: it is the one size that exercises the wrap path a power-of-two size never touches. The threesync_fifotestbenches in §4a do exactly this, and the wrap-bug testbenches in §4c isolate the pointer at DEPTH=12 and assert it never exceedsDEPTH-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
doutequals 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 (honouringfull/empty) so the pointers actually wrap several times during the run. - Flag correctness against true occupancy. Track the occupancy in the testbench and assert
emptyexactly when the model is empty andfullexactly 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, sofull/emptyfire 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=0andREG_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 undrivendout) surfaces only in the configuration that removes it. Verify the configurations you ship. - Expected waveform. After reset,
emptyis high andfulllow. As writes arrive faster than reads, occupancy climbs, the pointers wrap around the RAM, andfullasserts exactly when the count reaches DEPTH — after which furtherwris ignored (the word is not lost from the model's view because the producer must honourfull). 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.)
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- Generate Replication — Chapter 12.2; the elaboration-time replication and parameter-guarded
generatethis page uses to include or omit the optional output register and FWFT path. - Parameter Patterns — Chapter 12.1; the derived-
localparamdiscipline (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 areADDR(plus one wrap bit for the 7.2 scheme), and the RAM has DEPTH rows of WIDTH bits — alllocalparams (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 explicitptr == DEPTH-1 ? 0 : ptr+1wrap (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
generateguarded by a parameter (12.2); every optional output must be driven in both the enabled and disabled branch, or the module returnsXin 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.