RTL Design Patterns · Chapter 12 · Parameterized & Reusable RTL
Configurable IP & Elaboration Assertions
A reusable IP block advertises a range of settings such as any width, any depth, and a few feature flags, and an integrator two teams away picks the numbers. But not every combination the syntax permits is one the hardware can actually be. A depth of zero is no FIFO, a width below one is no bus, and an almost-full threshold at or above the depth is a flag that can never assert. Left unchecked, an illegal set does not error. It elaborates quietly into degenerate hardware that synthesizes clean and then hangs or corrupts data far away in the SoC. This lesson shows how an IP owns its parameter contract and enforces it at elaboration, before any simulation or synthesis, with fatal-severity assertions, so a bad parameter becomes a crisp build error at the point of misuse. Built and self-check-verified in SystemVerilog, Verilog, and VHDL.
Advanced18 min readRTL Design PatternsConfigurable IPElaboration AssertionsParameter ValidationReusable IPSynthesis
Chapter 12 · Section 12.5 · Parameterized & Reusable RTL
1. The Engineering Problem
You ship a configurable synchronous FIFO — the drop-in IP from 12.3. It exposes WIDTH, DEPTH, an ALMOST_FULL threshold, and a couple of feature flags, and it is used in nineteen places across the SoC. It works. Then a new integrator, wiring a low-rate status queue, instantiates it with DEPTH = 0 — they meant "I don't need buffering here" and typed zero. Or they set WIDTH = 0 for a queue that only carries a valid pulse. Or they set ALMOST_FULL = 16 on a DEPTH = 8 FIFO, reasoning about a different block. Nothing errors. The design elaborates, synthesizes clean, and passes their block-level smoke test.
Three weeks later the SoC-level simulation deadlocks under load, in a corner far from that FIFO. The bug hunt burns days: a producer stalls because a FIFO it feeds reports full forever; a consumer stalls because the same FIFO also reports empty forever. Eventually someone traces the dead FIFO back to DEPTH = 0 — an "IP" that elaborated into a zero-entry queue that is simultaneously full and empty, a $clog2(0)-wide pointer that is degenerate, a RAM with no rows. The IP did exactly what the RTL said. The RTL never said DEPTH = 0 was illegal, so the tool built the nonsense it was asked for.
This is not a made-up puzzle; it is the failure mode of every configurable block whose parameter contract lives only in a comment or a datasheet. The parameter space that the syntax permits is larger than the parameter space the hardware supports, and the difference is a minefield: DEPTH = 0, WIDTH < 1, a non-power-of-two depth where the pointer scheme requires a power of two, ALMOST_FULL >= DEPTH, two mutually-exclusive feature flags both set. Each is a legal expression and an illegal configuration. The integrator, who sets the parameters, is the one who must be told — at their build, immediately, precisely — and the only place to tell them before broken hardware exists is elaboration.
// A configurable FIFO instantiated by an integrator two teams away:
// sync_fifo #(.WIDTH(0), .DEPTH(0)) status_q (...); // "no buffering, valid only"
// sync_fifo #(.WIDTH(8), .DEPTH(8), .ALMOST_FULL(16)) rx_q (...); // threshold > depth
// WRONG — no parameter contract. These elaborate SILENTLY into:
// * a zero-width datapath (WIDTH=0) -> a bus that carries nothing
// * a zero-entry FIFO (DEPTH=0) -> full AND empty forever -> deadlock
// * an ALMOST_FULL flag that can NEVER assert (threshold >= DEPTH)
// ...synthesizes clean, hangs three levels up, costs the integrator days.
// RIGHT — the IP validates its own contract at ELABORATION and refuses to
// build an illegal instance, with a precise message at the point of misuse.
// (the pattern this page builds: fatal-severity elaboration assertions)2. Mental Model
3. Pattern Anatomy
A configurable-IP guard has three parts: the contract (what combinations are legal), the check (the condition, evaluated on the elaboration-time parameters), and the refusal (a fatal-severity stop with a precise message). The parts are the same in every HDL; only the spelling of the refusal differs.
The contract — what to check. A reusable block's legal parameter space is a small set of predicates over its parameters and their derived values:
- Bounds —
DEPTH >= 2(a zero- or one-entry FIFO is degenerate),WIDTH >= 1(a zero-width datapath carries nothing). These are the floor of the range the IP advertises. - Derived-value domain — the guard must protect the math the IP does on its parameters, not just the parameters.
ADDR = $clog2(DEPTH)is only meaningful forDEPTH >= 1;$clog2(0)is a degenerate domain error. Check the parameter before the derived localparam consumes it. - Power-of-two-if-required — if the pointer scheme relies on natural binary rollover (12.3), the IP must either handle arbitrary DEPTH or require a power-of-two DEPTH and reject anything else, so the wrap bug can never be instantiated.
- Ratio / threshold constraints —
ALMOST_FULL < DEPTH(a threshold at or above DEPTH is a flag that can never assert),ALMOST_FULL >= 1. Relationships between parameters, not just each in isolation. - Feature-flag consistency — flags that are mutually exclusive (a FWFT path and a registered-output stage that contradict) must not both be set; flags that are required-together (a mode that needs a companion enable) must both be set. A contradictory pair is an illegal configuration.
- Port-width vs parameter agreement — where a port width is fixed but a parameter implies a different width, the disagreement is a build error, caught at elaboration rather than as a silent truncation.
The check — where it lives. The predicate is evaluated on constant, elaboration-time values (parameters and localparams), so it can run before any clock edge. In SystemVerilog it sits in an initial block or, better, a generate region so it is truly compile-time. In VHDL it is a concurrent (or declarative-region) assert. In Verilog it is a generate/initial guard. The key is that it reads only parameters, so the tool can evaluate it while elaborating.
The refusal — fatal severity, precise message. The stop must halt the build and name the fault:
- SystemVerilog:
$fatal(or$errorpromoted to fatal) inside aninitial/generateregion, or anif (BAD) generatethat instantiates a$fatal— elaboration aborts. - VHDL:
assert <condition> report "..." severity failure;—failurestops elaboration;error/warning/notedo not. - Verilog-2001: a
generate/initialguard that prints and calls$finish. Caveat: not every tool guarantees a pre-elaboration hard stop from$finish, so the portable fallback is ageneratethat instantiates an illegal construct (e.g. an out-of-range index) on the bad condition, forcing the elaborator itself to error.
The parameter-contract pipeline — check at elaboration, refuse if illegal
data flowWhy this belongs to configurable IP specifically: a fixed, single-instance module has no parameter contract to violate — its widths are what they are. The instant a block advertises a range, the space of illegal-but-syntactically-valid configurations opens up, and the integrator who chooses within that range is a different engineer, at a different build, than the one who wrote the block. The guard is how the IP author speaks to that future integrator: these are the settings I support; here, at your build, is the one you got wrong. Without it, the only feedback the integrator gets is a deadlock in someone else's simulation, weeks later.
4. Real RTL Implementation
This is the core of the page. We harden the configurable FIFO of 12.3 with a parameter contract checked at elaboration, and show it three ways — (a) the guarded IP with its legal-corner testbench, (b) the illegal parameter sets each guard rejects, and (c) the no-guard bug vs the guarded fix — in SystemVerilog, Verilog, and VHDL. The idea is identical in all three: evaluate a predicate over the parameters at compile time and refuse to elaborate on failure, with fatal severity and a precise message. Only the spelling of the refusal differs.
One discipline runs through every block: the check reads only parameters and derived localparams, and its severity stops the build. A check that reads a signal is a runtime check (too late); a check whose severity is warning/note/$display is not a guard at all.
4a. The guarded configurable FIFO — elaboration assertions, three ways
Start with the IP. The FIFO exposes WIDTH, DEPTH, and ALMOST_FULL; it derives ADDR = $clog2(DEPTH); and before any of that math or storage is built, a generate-region check validates the contract. In SystemVerilog the cleanest form is a generate block with an initial $fatal (or an if generate that hard-stops) — evaluated at elaboration, so an illegal instance never produces a netlist.
module sync_fifo #(
parameter int WIDTH = 8, // datapath width (legal: >= 1)
parameter int DEPTH = 16, // entries (legal: >= 2)
parameter int ALMOST_FULL = DEPTH-2 // early-full mark (legal: 1 .. DEPTH-1)
)(
input logic clk,
input logic rst, // synchronous, active-high
input logic wr_en,
input logic [WIDTH-1:0] wdata,
input logic rd_en,
output logic [WIDTH-1:0] rdata,
output logic full,
output logic empty,
output logic almost_full
);
// ---- PARAMETER CONTRACT, CHECKED AT ELABORATION ------------------------
// A generate region is compile-time: this fires while the tool resolves
// parameters, BEFORE any simulation cycle or synthesized gate. $fatal halts
// elaboration, so an illegal instance never becomes hardware. The message
// names the parameter so the INTEGRATOR sees exactly what they got wrong.
generate
if (WIDTH < 1) // zero-width datapath is no bus
initial $fatal(1, "sync_fifo: WIDTH must be >= 1 (got %0d)", WIDTH);
if (DEPTH < 2) // zero/one entry is degenerate
initial $fatal(1, "sync_fifo: DEPTH must be >= 2 (got %0d)", DEPTH);
if (ALMOST_FULL < 1 || ALMOST_FULL >= DEPTH) // threshold must be able to assert
initial $fatal(1, "sync_fifo: ALMOST_FULL must be 1..DEPTH-1 (got %0d, DEPTH=%0d)",
ALMOST_FULL, DEPTH);
endgenerate
// Derived widths — SAFE now, because DEPTH >= 2 was enforced above, so
// $clog2(DEPTH) is well defined (never $clog2(0)).
localparam int ADDR = $clog2(DEPTH);
logic [WIDTH-1:0] mem [DEPTH]; // RAM-inferring array (6.1)
logic [ADDR:0] wptr, rptr; // +1 wrap bit for full/empty (7.2)
always_ff @(posedge clk) begin
if (rst) begin
wptr <= '0; rptr <= '0;
end else begin
if (wr_en && !full) begin mem[wptr[ADDR-1:0]] <= wdata; wptr <= wptr + 1'b1; end
if (rd_en && !empty) rptr <= rptr + 1'b1;
end
end
assign rdata = mem[rptr[ADDR-1:0]];
assign empty = (wptr == rptr);
assign full = (wptr[ADDR-1:0] == rptr[ADDR-1:0]) && (wptr[ADDR] != rptr[ADDR]);
assign almost_full = ((wptr - rptr) >= ALMOST_FULL[ADDR:0]); // occupancy >= threshold
endmoduleThe three generate-if blocks are the whole contract: each names one predicate, and $fatal gives it teeth. Note the ordering — DEPTH >= 2 is enforced before localparam ADDR = $clog2(DEPTH), so the derived-value math is never evaluated on an illegal DEPTH (this is the $clog2(0)-domain guard from §3). The legal-corner testbench elaborates the IP at the edges of its advertised range — minimum and maximum WIDTH and DEPTH, threshold at both ends — confirms it builds, and self-checks that it actually works there.
module sync_fifo_tb;
// Elaborate the IP at a LEGAL corner and prove it builds AND works.
// (Re-run with #(.WIDTH(1),.DEPTH(2)) and #(.WIDTH(64),.DEPTH(1024)) etc.
// — every legal corner must elaborate clean and pass.)
localparam int WIDTH = 8, DEPTH = 4, ALMOST_FULL = 3;
logic clk = 0, rst = 1, wr_en = 0, rd_en = 0;
logic [WIDTH-1:0] wdata, rdata;
logic full, empty, almost_full;
int errors = 0;
sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH), .ALMOST_FULL(ALMOST_FULL)) dut (.*);
always #5 clk = ~clk;
initial begin
@(negedge clk); rst = 0;
// Legal-corner invariant #1: empty at reset, never full-and-empty at once.
assert (empty && !full) else begin $error("reset: expected empty"); errors++; end
// Write the FIFO full, then read it back — data must return in order.
for (int i = 0; i < DEPTH; i++) begin
@(negedge clk); wr_en = 1; wdata = 8'hA0 + i;
end
@(negedge clk); wr_en = 0;
assert (full && !empty) else begin $error("expected full after DEPTH writes"); errors++; end
for (int i = 0; i < DEPTH; i++) begin
@(negedge clk); rd_en = 1;
assert (rdata === (8'hA0 + i))
else begin $error("read %0d: got %h", i, rdata); errors++; end
end
@(negedge clk); rd_en = 0;
assert (empty && !full) else begin $error("expected empty after draining"); errors++; end
if (errors == 0) $display("PASS: sync_fifo legal corner elaborates and works");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog form carries the same contract, but the refusal is different because Verilog-2001 has no $fatal. The portable pattern is a generate-if that on the illegal condition both prints a precise message and — the load-bearing part — instantiates an illegal construct so the elaborator itself errors and refuses to build. An initial $display + $finish alone is a weaker guard (see the caveat comment): $finish is a simulation control, and not every tool hard-stops elaboration on it, so the bad-index instantiation is what makes the refusal portable.
module sync_fifo #(
parameter WIDTH = 8, // legal: >= 1
parameter DEPTH = 16, // legal: >= 2
parameter ALMOST_FULL = 14 // legal: 1 .. DEPTH-1
)(
input wire clk,
input wire rst, // synchronous, active-high
input wire wr_en,
input wire [WIDTH-1:0] wdata,
input wire rd_en,
output wire [WIDTH-1:0] rdata,
output wire full,
output wire empty,
output wire almost_full
);
// ---- PARAMETER CONTRACT, CHECKED AT ELABORATION ------------------------
// Verilog-2001 has no $fatal. Portable refusal: on the illegal condition,
// print a precise message AND instantiate an illegal construct (a param whose
// value is out of a legal range for a builtin) so the ELABORATOR errors and
// refuses to build. $display + $finish alone is a weaker guard — $finish is a
// sim control and not every tool hard-stops elaboration on it.
localparam BAD_PARAMS = (WIDTH < 1) || (DEPTH < 2)
|| (ALMOST_FULL < 1) || (ALMOST_FULL >= DEPTH);
generate
if (BAD_PARAMS) begin : illegal_config
initial begin
$display("FATAL sync_fifo: illegal params WIDTH=%0d DEPTH=%0d ALMOST_FULL=%0d",
WIDTH, DEPTH, ALMOST_FULL);
$finish;
end
// Force the elaborator to error even on tools that ignore $finish here:
reg [-1:0] force_elaboration_error; // negative range is illegal -> build fails
end
endgenerate
localparam ADDR = $clog2(DEPTH); // safe: DEPTH >= 2 enforced above
reg [WIDTH-1:0] mem [0:DEPTH-1];
reg [ADDR:0] wptr, rptr;
always @(posedge clk) begin
if (rst) begin
wptr <= 0; rptr <= 0;
end else begin
if (wr_en && !full) begin mem[wptr[ADDR-1:0]] <= wdata; wptr <= wptr + 1'b1; end
if (rd_en && !empty) rptr <= rptr + 1'b1;
end
end
assign rdata = mem[rptr[ADDR-1:0]];
assign empty = (wptr == rptr);
assign full = (wptr[ADDR-1:0] == rptr[ADDR-1:0]) && (wptr[ADDR] != rptr[ADDR]);
assign almost_full = ((wptr - rptr) >= ALMOST_FULL);
endmodule module sync_fifo_tb;
parameter WIDTH = 8, DEPTH = 4, ALMOST_FULL = 3; // a legal corner
reg clk, rst, wr_en, rd_en;
reg [WIDTH-1:0] wdata;
wire [WIDTH-1:0] rdata;
wire full, empty, almost_full;
integer i, errors;
sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH), .ALMOST_FULL(ALMOST_FULL)) dut
(.clk(clk), .rst(rst), .wr_en(wr_en), .wdata(wdata), .rd_en(rd_en),
.rdata(rdata), .full(full), .empty(empty), .almost_full(almost_full));
initial begin clk = 0; forever #5 clk = ~clk; end
initial begin
errors = 0; rst = 1; wr_en = 0; rd_en = 0; wdata = 0;
@(negedge clk); rst = 0;
if (!(empty && !full)) begin $display("FAIL reset: not empty"); errors = errors + 1; end
for (i = 0; i < DEPTH; i = i + 1) begin
@(negedge clk); wr_en = 1; wdata = 8'hA0 + i[7:0];
end
@(negedge clk); wr_en = 0;
if (!(full && !empty)) begin $display("FAIL: expected full"); errors = errors + 1; end
for (i = 0; i < DEPTH; i = i + 1) begin
@(negedge clk); rd_en = 1;
if (rdata !== (8'hA0 + i[7:0])) begin
$display("FAIL read %0d: got %h", i, rdata); errors = errors + 1;
end
end
@(negedge clk); rd_en = 0;
if (!(empty && !full)) begin $display("FAIL: expected empty"); errors = errors + 1; end
if (errors == 0) $display("PASS: sync_fifo legal corner elaborates and works");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleVHDL expresses the elaboration guard most directly: a concurrent assert ... severity failure in the architecture body fires during elaboration, and failure is the one severity that stops the build (error/warning/note only print). Because generics are elaboration-time constants, the assertion evaluates before any process runs. This is the idiom the SV $fatal and Verilog illegal-construct forms are emulating.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all; -- for ceil/log2 on the derived width
entity sync_fifo is
generic (
WIDTH : positive := 8; -- 'positive' already forbids <= 0,
DEPTH : positive := 16; -- but the range/ratio rules below
ALMOST_FULL : positive := 14 -- still need an explicit contract
);
port (
clk, rst, wr_en, rd_en : in std_logic;
wdata : in std_logic_vector(WIDTH-1 downto 0);
rdata : out std_logic_vector(WIDTH-1 downto 0);
full, empty, almost_full : out std_logic
);
end entity;
architecture rtl of sync_fifo is
-- Derived address width. 'positive' guarantees DEPTH >= 1, so log2 is defined.
constant ADDR : natural := integer(ceil(log2(real(DEPTH))));
type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
signal mem : mem_t;
signal wptr, rptr : unsigned(ADDR downto 0) := (others => '0'); -- +1 wrap bit
begin
-- ---- PARAMETER CONTRACT, CHECKED AT ELABORATION -----------------------
-- Concurrent asserts on GENERICS fire during elaboration, before any process
-- runs. severity FAILURE stops the build; error/warning/note only print, so
-- FAILURE is what makes these true guards. The report string names the fault.
assert DEPTH >= 2
report "sync_fifo: DEPTH must be >= 2" severity failure;
assert ALMOST_FULL < DEPTH
report "sync_fifo: ALMOST_FULL must be < DEPTH" severity failure;
-- (WIDTH >= 1 and ALMOST_FULL >= 1 are already enforced by 'positive'.)
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
wptr <= (others => '0'); rptr <= (others => '0');
else
if wr_en = '1' and full = '0' then
mem(to_integer(wptr(ADDR-1 downto 0))) <= wdata;
wptr <= wptr + 1;
end if;
if rd_en = '1' and empty = '0' then
rptr <= rptr + 1;
end if;
end if;
end if;
end process;
rdata <= mem(to_integer(rptr(ADDR-1 downto 0)));
empty <= '1' when wptr = rptr else '0';
full <= '1' when (wptr(ADDR-1 downto 0) = rptr(ADDR-1 downto 0))
and (wptr(ADDR) /= rptr(ADDR)) else '0';
almost_full <= '1' when (wptr - rptr) >= ALMOST_FULL else '0';
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 := 8;
constant DEPTH : positive := 4; -- a legal corner
constant AF : positive := 3;
signal clk, rst, wr_en, rd_en : std_logic := '0';
signal wdata, rdata : std_logic_vector(WIDTH-1 downto 0);
signal full, empty, almost_full : std_logic;
begin
dut : entity work.sync_fifo
generic map (WIDTH => WIDTH, DEPTH => DEPTH, ALMOST_FULL => AF)
port map (clk => clk, rst => rst, wr_en => wr_en, wdata => wdata,
rd_en => rd_en, rdata => rdata, full => full,
empty => empty, almost_full => almost_full);
clk <= not clk after 5 ns;
stim : process
begin
rst <= '1'; wait until falling_edge(clk); rst <= '0';
assert empty = '1' and full = '0'
report "reset: expected empty" severity error;
for i in 0 to DEPTH-1 loop -- fill
wr_en <= '1';
wdata <= std_logic_vector(to_unsigned(16#A0# + i, WIDTH));
wait until falling_edge(clk);
end loop;
wr_en <= '0';
assert full = '1' and empty = '0'
report "expected full after DEPTH writes" severity error;
for i in 0 to DEPTH-1 loop -- drain, checking order
rd_en <= '1'; wait until falling_edge(clk);
assert rdata = std_logic_vector(to_unsigned(16#A0# + i, WIDTH))
report "read mismatch" severity error;
end loop;
rd_en <= '0';
assert empty = '1' and full = '0'
report "expected empty after draining" severity error;
report "sync_fifo legal-corner self-check complete" severity note;
wait;
end process;
end architecture;4b. The illegal parameter sets the guard rejects
The contract earns its keep on the instances it refuses. Each of the following instantiations is legal syntax and an illegal configuration; with the guards of 4a in place, every one fails to elaborate with a precise message, at the integrator's build, before any hardware exists. Without the guards (§4c), each elaborates silently into the degenerate hardware noted in the comment.
// Every instance below is caught by sync_fifo's elaboration $fatal guards.
// The message printed at the INTEGRATOR's build names the offending parameter.
// (1) DEPTH = 0 -> a zero-entry FIFO: full AND empty forever -> SoC deadlock.
sync_fifo #(.WIDTH(8), .DEPTH(0)) bad_depth0 (.*);
// => FATAL "sync_fifo: DEPTH must be >= 2 (got 0)" [never $clog2(0)]
// (2) WIDTH = 0 -> a zero-width datapath: a bus that carries nothing.
sync_fifo #(.WIDTH(0), .DEPTH(16)) bad_width0 (.*);
// => FATAL "sync_fifo: WIDTH must be >= 1 (got 0)"
// (3) ALMOST_FULL >= DEPTH -> a threshold flag that can NEVER assert.
sync_fifo #(.WIDTH(8), .DEPTH(8), .ALMOST_FULL(16)) bad_thresh (.*);
// => FATAL "sync_fifo: ALMOST_FULL must be 1..DEPTH-1 (got 16, DEPTH=8)"
// (4) DEPTH = 1 -> one entry is still degenerate for a producer/consumer FIFO.
sync_fifo #(.WIDTH(8), .DEPTH(1)) bad_depth1 (.*);
// => FATAL "sync_fifo: DEPTH must be >= 2 (got 1)"
// Contrast — every LEGAL corner elaborates clean:
sync_fifo #(.WIDTH(1), .DEPTH(2), .ALMOST_FULL(1)) ok_min (.*);
sync_fifo #(.WIDTH(64), .DEPTH(1024),.ALMOST_FULL(1000))ok_max (.*);In VHDL the same illegal sets trip the severity failure asserts (and the positive subtype rejects WIDTH = 0 / DEPTH = 0 even earlier, at the generic bind). The point is identical: an illegal configuration is a compile error at the point of misuse, not a runtime surprise downstream.
-- (1) DEPTH = 0 is rejected by the 'positive' generic subtype at bind time;
-- DEPTH = 1 is rejected by assert DEPTH >= 2 ... severity failure.
bad1 : entity work.sync_fifo generic map (WIDTH => 8, DEPTH => 1, ALMOST_FULL => 1) ...;
-- => FAILURE "sync_fifo: DEPTH must be >= 2"
-- (2) ALMOST_FULL >= DEPTH is rejected by assert ALMOST_FULL < DEPTH severity failure.
bad2 : entity work.sync_fifo generic map (WIDTH => 8, DEPTH => 8, ALMOST_FULL => 16) ...;
-- => FAILURE "sync_fifo: ALMOST_FULL must be < DEPTH"
-- Legal corners elaborate clean:
ok : entity work.sync_fifo generic map (WIDTH => 1, DEPTH => 2, ALMOST_FULL => 1) ...;4c. The no-guard bug vs the guarded fix — in all three HDLs
The signature failure is a "configurable" IP with no parameter contract at all: it accepts whatever the syntax allows and elaborates the nonsense into hardware. Here is the buggy-vs-fixed contrast, the same story in each language: the buggy module omits the guard and lets DEPTH = 0 build a degenerate FIFO; the fixed module adds the fatal-severity elaboration assertion that rejects it.
// BUGGY: no parameter guard. DEPTH=0 makes ADDR=$clog2(0) degenerate, mem has
// zero rows, and full/empty are BOTH true forever -> the integrator's
// producer and consumer both stall, three levels up, weeks later.
module fifo_bad #(parameter int WIDTH = 8, parameter int DEPTH = 16) (/* ...ports... */);
localparam int ADDR = $clog2(DEPTH); // $clog2(0) on DEPTH=0 -> degenerate
logic [WIDTH-1:0] mem [DEPTH]; // DEPTH=0 -> empty array
// ...pointers, full/empty... elaborates SILENTLY into broken hardware.
endmodule
// FIXED: the IP owns its contract. A compile-time generate check with $fatal
// rejects the illegal DEPTH at the integrator's build, with a message,
// BEFORE ADDR or mem is ever built.
module fifo_good #(parameter int WIDTH = 8, parameter int DEPTH = 16) (/* ...ports... */);
generate
if (WIDTH < 1) initial $fatal(1, "fifo: WIDTH must be >= 1 (got %0d)", WIDTH);
if (DEPTH < 2) initial $fatal(1, "fifo: DEPTH must be >= 2 (got %0d)", DEPTH);
endgenerate
localparam int ADDR = $clog2(DEPTH); // now guaranteed DEPTH >= 2
logic [WIDTH-1:0] mem [DEPTH]; // now guaranteed >= 2 rows
// ...rest of the FIFO... an illegal instance can no longer elaborate.
endmodule // BUGGY: no guard; DEPTH=0 elaborates into a zero-row RAM and a full==empty FIFO.
module fifo_bad #(parameter WIDTH = 8, parameter DEPTH = 16) (/* ...ports... */);
localparam ADDR = $clog2(DEPTH);
reg [WIDTH-1:0] mem [0:DEPTH-1]; // DEPTH=0 -> reversed/empty range
// ...silently builds broken hardware.
endmodule
// FIXED: generate guard prints a precise message AND forces an elaboration error
// (illegal negative range) so the build cannot proceed on bad params.
module fifo_good #(parameter WIDTH = 8, parameter DEPTH = 16) (/* ...ports... */);
localparam BAD = (WIDTH < 1) || (DEPTH < 2);
generate
if (BAD) begin : illegal
initial $display("FATAL fifo: illegal WIDTH=%0d DEPTH=%0d", WIDTH, DEPTH);
reg [-1:0] stop; // negative range -> elaborator errors
end
endgenerate
localparam ADDR = $clog2(DEPTH);
reg [WIDTH-1:0] mem [0:DEPTH-1];
// ...an illegal instance can no longer elaborate.
endmodule -- BUGGY: no contract beyond the subtype. If DEPTH could reach 1, the FIFO is a
-- one-entry degenerate queue; nothing stops it elaborating.
architecture bad of fifo is
constant ADDR : natural := integer(ceil(log2(real(DEPTH))));
type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
signal mem : mem_t;
begin
-- ...builds whatever DEPTH was given, degenerate or not.
end architecture;
-- FIXED: concurrent asserts with severity FAILURE stop elaboration on an illegal
-- combination, naming the fault at the integrator's build.
architecture good of fifo is
constant ADDR : natural := integer(ceil(log2(real(DEPTH))));
type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
signal mem : mem_t;
begin
assert DEPTH >= 2
report "fifo: DEPTH must be >= 2" severity failure;
assert ALMOST_FULL < DEPTH
report "fifo: ALMOST_FULL must be < DEPTH" severity failure;
-- ...an illegal instance can no longer elaborate.
end architecture;Across all three languages the lesson is one sentence: a configurable IP validates its own parameter contract at elaboration with a fatal-severity refusal — an SV $fatal/generate-guard, a VHDL assert ... severity failure, a Verilog generate + illegal-construct guard — so an illegal parameter set is a crisp compile error at the point of misuse, never silent broken hardware.
5. Verification Strategy
A configurable IP has two things to verify, and one of them is unusual: you verify the hardware at its legal corners and you verify the guards themselves — that an illegal parameter set actually fails to build. The second is a build-time test, not a runtime one: the "pass" condition for an illegal instance is that the compile fails.
The invariant: for every parameter set inside the advertised range the IP elaborates and works; for every illegal set outside it, the IP refuses to elaborate. A guard that lets an illegal set through, or that rejects a legal one, is itself a bug.
- Legal-corner elaboration + self-check (positive tests). Elaborate the IP at the edges of its range — minimum and maximum WIDTH and DEPTH, threshold at
1and atDEPTH-1, each feature-flag combination — and run the self-checking FIFO testbench from §4a at each corner: write it full, drain it, and assert data returns in order andfull/emptybehave. This is the width/DEPTH sweep of 12.1–12.4 applied as a legal-range sweep; a form that passes atDEPTH=16can still be wrong atDEPTH=2or a non-power-of-twoDEPTH, so every corner must be built and checked, not assumed. - Illegal-set elaboration-failure test (negative tests). For each guard, add an instance with the illegal parameter to a separate, expected-to-fail compile target, and assert that the build fails with the guard's message. This is a documented negative test:
DEPTH=0,WIDTH=0,ALMOST_FULL >= DEPTH, a contradictory flag pair — each must trip the elaboration assertion. Because a passing negative test is a failed compile, it lives in the build/regression script (a target that must error), not in a simulation run. Confirm both directions: the illegal set fails, and the corresponding legal set does not. - Guard-severity check. Verify the refusal actually stops the build. A guard written with
$errorwithout a stop, orseverity warning/note, or a bare$display, will print and let the degenerate instance through — so the negative test above must confirm a non-zero exit / halted elaboration, not merely that a message appeared. A message with a proceeding build is not a guard. - Derived-value domain check. Include the corner where a naive derived value would be out of domain —
DEPTH=1andDEPTH=0feeding$clog2/log2— and confirm the bound guard fires before the derived localparam is evaluated, so the tool never computes$clog2(0). Ordering is part of the contract. - Conceptual invariants. State the properties in plain terms: the IP never elaborates a zero-entry or zero-width instance; the ALMOST_FULL flag is always assertable (
1 <= ALMOST_FULL < DEPTH); no two mutually-exclusive feature flags are ever both active in an elaborated instance. These are exactly the predicates the guards check, restated as always-true properties of any instance that did elaborate. (Full SystemVerilog Assertions are out of scope; these are conceptual and enforced structurally by the elaboration guards.) - Expected build/waveform behaviour. A correct legal instance elaborates with no assertion message and, on a waveform, fills and drains cleanly —
fullafterDEPTHwrites,emptyafterDEPTHreads, never both at once. A correct illegal instance produces no waveform at all: the compile stops with the guard's message and the log shows a fatal, which is the intended and only correct outcome.
6. Common Mistakes
No elaboration guard at all — the signature mistake. A "configurable" IP with no parameter contract accepts every value the syntax permits and elaborates the illegal ones into degenerate hardware: DEPTH = 0 builds a zero-entry FIFO that is simultaneously full and empty (deadlock), WIDTH = 0 builds a zero-width bus that carries nothing, ALMOST_FULL >= DEPTH builds a flag that never asserts. Each synthesizes clean and fails mysteriously downstream, weeks and levels away from the parameter that caused it. The fix is the whole page: validate the contract at elaboration with a fatal-severity refusal.
Writing the check as a runtime assertion. An assertion inside a clocked process or an always @(posedge clk) — assert (DEPTH >= 2) firing on a clock edge — is a runtime check. It fires only in simulation, so it misses synthesis entirely, and for a parameter the integrator sets it may never fire at all if that path is not simulated. Parameter validation must be elaboration-time (a generate region / concurrent assert on constants), not sequential logic, because the value is known at compile time and the check must run before hardware exists.
The wrong severity — a guard that waves the build through. severity warning/note, an $error with no stop, or a bare $display("...") prints a message and then lets the illegal instance elaborate anyway. A guard is only a guard if it halts the build: $fatal (SV), severity failure (VHDL), a $finish/illegal-construct (Verilog). Choosing the polite severity turns the contract into a log line nobody reads.
Checking one bound but not the contradictory combination. Guarding DEPTH >= 2 and WIDTH >= 1 individually but forgetting the relationship between parameters — ALMOST_FULL < DEPTH, or two feature flags that must not both be set — lets a per-parameter-legal-but-jointly-illegal set through. DEPTH = 8, ALMOST_FULL = 16 has two individually-fine numbers and a contradictory pair. The contract is the conjunction of all predicates, including the cross-parameter ones, not a checklist of isolated bounds.
Forgetting the derived-value domain. Deriving ADDR = $clog2(DEPTH) (or log2(real(DEPTH))) before guarding DEPTH >= 1 evaluates the math on an out-of-domain value — $clog2(0) — which is degenerate or tool-dependent, and the resulting error, if any, is cryptic instead of naming the real fault. Order the guard before the derived localparam so the illegal parameter is rejected with a precise message before any derived value consumes it.
Relying on positive/subtype alone in VHDL and omitting the ratio guards. VHDL's positive subtype rejects WIDTH = 0 and DEPTH = 0 at generic bind — useful, but it does not catch DEPTH = 1 (degenerate but positive), ALMOST_FULL >= DEPTH, or a contradictory flag pair. The subtype is a floor, not the whole contract; the cross-parameter and >= 2 rules still need explicit assert ... severity failure.
7. DebugLab
The status queue that deadlocked the SoC — a configurable FIFO with no parameter contract
The engineering lesson: a configurable IP owns its parameter contract — validate every legal range and every illegal combination at ELABORATION with fatal-severity assertions, so a bad parameter is a crisp compile error at the point of misuse, never silent broken hardware discovered downstream. The tell is a system-level failure (a deadlock, a corruption) that traces back not to a logic bug but to a parameter an integrator set — which means the IP exported its contract as a comment instead of enforcing it in RTL. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: check the parameter contract at compile time with a fatal-severity refusal (SV $fatal/generate-guard, VHDL assert ... severity failure, Verilog generate + illegal-construct), ordered before any derived-value math; and add a negative build test per guard — an instance that must fail to elaborate — so the contract is verified, not just written.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "the IP owns its parameter contract" habit.
Exercise 1 — Write the contract before the code
You are about to write a configurable shift-register-based delay line with parameters WIDTH (data width), DEPTH (number of stages), and an optional TAP parameter (a mid-line tap position). Before writing any RTL, enumerate the full legal-range contract as a list of predicates — every single-parameter bound, every derived-value domain condition, and every cross-parameter relationship (think about where TAP must sit relative to DEPTH). State, for each predicate, the degenerate hardware that results if it is violated and left unchecked.
Exercise 2 — Pick the refusal per language
For each of your Exercise 1 predicates, write the elaboration guard three ways: SystemVerilog (a generate region with $fatal), VHDL (a concurrent assert ... severity failure), and Verilog-2001 (a generate guard that prints and forces an elaboration error). For the Verilog case, explain in one line why $display + $finish alone is not a sufficient guard and what you add to make it portable. Confirm each guard is ordered so no derived value is computed on an out-of-domain parameter.
Exercise 3 — Break it on paper, two ways
Given a configurable FIFO with guards for DEPTH >= 2 and WIDTH >= 1 but no guard on ALMOST_FULL, describe (i) an integrator instantiation that is legal syntax, passes every existing guard, and still produces a feature that silently does nothing, and (ii) the exact downstream symptom the integrator would eventually observe. Then state the single elaboration predicate that closes the gap, and explain why per-parameter bounds alone could never have caught it.
Exercise 4 — Design the negative test suite
You have hardened the FIFO with a full parameter contract. Design the verification that proves the contract, not just the hardware. Specify (i) the positive legal-corner sweep (which corners, and what each functionally checks), and (ii) the negative build-test suite (one illegal instance per guard). For the negative suite, explain what a "pass" looks like at the build level, why it cannot live in an ordinary simulation run, and how you would additionally confirm that each guard's severity actually halted the build rather than merely printing a message.
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- Width-Generic Datapath — Chapter 12.4; a datapath that is correct at every width is the other half of a trustworthy configurable block — verify the range, then guard the range.
- Synchronous FIFO Architecture — Chapter 7.1; the FIFO whose full/empty logic the DEPTH=0 guard protects from degenerating into a permanently full-and-empty queue.
- FIFO Full/Empty Logic — Chapter 7.2; exactly why a zero-entry FIFO reports full and empty at once — the degenerate case the elaboration guard forbids.
- CDC Design Rules — Chapter 11; a companion contract-at-build discipline — structural rules an integrator must not violate, caught early rather than downstream.
Backward / in-track dependencies (the configurable IP this page validates):
- Parameter Patterns — Chapter 12.1; the
parameter/localparamand derived-width discipline whose legal ranges this page turns into an enforced contract. - Generate Replication — Chapter 12.2; the
generateregion that hosts a compile-time$fatalguard, and the flag-gated features whose consistency the contract checks. - Parameterized FIFO & Memory — Chapter 12.3; the WIDTH/DEPTH FIFO that is the subject here — the elaboration check is the guard that stops its non-power-of-two and DEPTH=0 failure modes at build.
Backward / method:
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses — the parameter contract is the interface the IP exposes, made executable.
- What Is an RTL Design Pattern? — Chapter 0.1; why a configurable-IP guard earns the name "pattern" — a recurring problem, a reusable form, a synthesis reality, and a signature failure.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Parameters — the
parameter/localparamvalues the elaboration check evaluates its predicates over. - Generate Blocks — the elaboration-time region that hosts the compile-time guard in SystemVerilog and Verilog.
- System Tasks & Functions —
$fatal/$error/$finishand$clog2, the tasks the guard and the derived-width math are built from. - Display, Monitor, Strobe & Write — the
$displayused to print the guard's precise, integrator-facing message.
11. Summary
- A configurable IP owns its parameter contract. It advertises a range of parameters, but only some combinations are legal; the space the syntax permits is larger than the space the hardware supports. The IP — not a datasheet, not the integrator — must state and enforce which combinations are legal.
- Validate at ELABORATION, not runtime. A parameter is a compile-time constant, so its check belongs in a
generateregion (SV/Verilog) or a concurrentassert(VHDL) that fires while the tool builds the module tree — before any simulation cycle and before synthesis emits a gate. A runtime assertion is too late and, for a parameter the integrator sets, may never fire. - The refusal must be fatal.
$fatal(SystemVerilog),severity failure(VHDL), or a$finish+ illegal-construct (Verilog) halt the build;warning/note/$displaymerely print and let the broken instance elaborate. A guard is only a guard if it stops the build, and its message must name the offending parameter so the integrator can act. - Check the whole contract, in order. Single-parameter bounds (
DEPTH >= 2,WIDTH >= 1), derived-value domains (guardDEPTH >= 1before$clog2(DEPTH)), power-of-two-if-required, cross-parameter ratios (ALMOST_FULL < DEPTH), and feature-flag consistency (mutually-exclusive / required-together). The contract is the conjunction of all predicates — a per-parameter checklist misses the contradictory combinations. - Verify the guards, not just the hardware — across every HDL. Positive tests elaborate and functionally self-check the IP at every legal corner (min/max WIDTH and DEPTH, threshold at both ends); negative build tests confirm each illegal set fails to elaborate with its message (a passing negative test is a failed compile). Written this way — an SV
$fatal/generate-guard, a VHDLassert ... severity failure, a Verilog generate + illegal-construct guard — an illegal parameter set is a crisp compile error at the point of misuse, never silent broken hardware discovered downstream. This closes Chapter 12: a reusable block is only trustworthy when its parameter contract is enforced, not merely documented.