Skip to content

SystemVerilog · Module 7

Parameters & Localparams

parameter vs localparam, $clog2 derivation, typed parameters, naming conventions, hierarchical sizing, and 5 debugging labs across the parameter contract.

Module 7 · Page 7.4

Write one module — use it at 8 bits, 16 bits, or 32 bits without changing a single line of RTL. Parameters are the key to truly reusable hardware. This page covers every form of parameter declaration, the strict distinction between parameter and localparam, the $clog2 pattern used in every FIFO and RAM on Earth, how to propagate widths through a UVM stack, simulation behavior at time 0, waveform predictions, five debugging labs lifted from real project post-mortems, and twelve interview-ready questions.

The Problem — Without Parameters

Imagine you need an 8-bit adder, a 16-bit adder, and a 32-bit adder. Without parameters, you write three separate modules. Same logic, different width, tripled maintenance burden.

SystemVerilog — without parameters (duplicated logic)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ Three modules — same logic, different width — maintenance nightmare
module adder_8bit  (input logic [7:0]  a, b, output logic [7:0]  sum, output logic cout);
    assign {cout, sum} = a + b;
endmodule
 
module adder_16bit (input logic [15:0] a, b, output logic [15:0] sum, output logic cout);
    assign {cout, sum} = a + b;
endmodule
 
module adder_32bit (input logic [31:0] a, b, output logic [31:0] sum, output logic cout);
    assign {cout, sum} = a + b;
endmodule
 
// A bug fix in the adder logic must be applied to all three. Every time.

Now here is the same thing written with a parameter. One module, any width:

SystemVerilog — with parameter (one module, any width)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✅ One module. Works at any width. Fix the logic once — fixed everywhere.
module adder_n #(
    parameter int WIDTH = 8     // default: 8-bit; override to any width
) (
    input  logic [WIDTH-1:0] a, b,
    output logic [WIDTH-1:0] sum,
    output logic             cout
);
    assign {cout, sum} = a + b;
endmodule
 
// Use it at any width — no new module needed
adder_n #(.WIDTH(8))  u_add8  (...);   //  8-bit instance
adder_n #(.WIDTH(16)) u_add16 (...);   // 16-bit instance
adder_n #(.WIDTH(32)) u_add32 (...);   // 32-bit instance

parameter vs localparam

SystemVerilog has two compile-time constant keywords. The distinction is about who is allowed to change the value.

adder_nparameter int WIDTH = 8Can be overridden by the parent at instantiationlocalparam HALF = WIDTH/2Locked inside — cannot be overridden externallyParent Moduleadder_n#(.WIDTH(32))u_adder(...)overridesblockedparameterexternally tunablelocalparammodule-private
Figure 1 — parameter can be overridden by the parent at instantiation time. localparam is locked inside the module — no external override allowed.
KeywordWho can change itUse it for
parameterThe parent module via #(.NAME(value)) overrideAnything the caller should tune — widths, depths, addresses, modes
localparamNobody outside the module — locked insideDerived values, internal constants, magic numbers that must not be tuned
SystemVerilog — parameter and localparam together
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo #(
    // ── Parameters: callers can override these ─────────────────
    parameter int DATA_WIDTH = 8,    // width of each data word
    parameter int DEPTH      = 16    // number of entries in the FIFO
) (
    input  logic                  clk, rst_n, wr_en, rd_en,
    input  logic [DATA_WIDTH-1:0] din,
    output logic [DATA_WIDTH-1:0] dout,
    output logic                  full, empty
);
 
    // ── Localparams: derived from parameters — internal only ────
    localparam int PTR_WIDTH = $clog2(DEPTH);     // bits needed for pointer
    localparam int HALF_FULL = DEPTH / 2;         // half-full threshold
    localparam int MAX_PTR   = DEPTH - 1;         // max valid pointer value
 
    // Storage array — width and depth driven by parameters
    logic [DATA_WIDTH-1:0] mem [0:DEPTH-1];
 
    // Pointers — width driven by localparam PTR_WIDTH
    logic [PTR_WIDTH-1:0] wr_ptr, rd_ptr;
    logic [PTR_WIDTH:0]   count;         // one extra bit for full/empty
 
    // ... FIFO logic here
    assign full  = (count == DEPTH);
    assign empty = (count == 0);
 
endmodule

Parameter Syntax — Every Form You'll See

Parameters are declared in a separate #( ... ) block that appears before the port list. This is called the parameter port list.

SystemVerilog — all parameter declaration forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module param_showcase #(
 
    // ── Form 1: typed integer parameter (PREFERRED) ─────────────
    parameter int          WIDTH      = 8,
 
    // ── Form 2: typed with explicit range ──────────────────────
    parameter int unsigned DEPTH      = 256,
 
    // ── Form 3: untyped (legacy style — type inferred) ──────────
    parameter              ADDR_W     = 10,
 
    // ── Form 4: logic vector parameter ──────────────────────────
    parameter logic [7:0]  INIT_VAL   = 8'hFF,
 
    // ── Form 5: string parameter (useful for mode selection) ────
    parameter string       MODE       = "SYNC",
 
    // ── Form 6: real parameter (for timing/analog models) ───────
    parameter real         CLK_PERIOD = 10.0    // last param: no comma
 
) (
    input  logic [WIDTH-1:0]  din,
    output logic [WIDTH-1:0]  dout
);
    // Localparams derived from parameters
    localparam int HALF_W   = WIDTH / 2;
    localparam int ADDR_MAX = (1 << ADDR_W) - 1;
 
    assign dout = din;
endmodule

Derived Parameters with $clog2 and Expressions

The real power of parameters comes when you derive other constants from them using localparam and built-in functions. The most important is $clog2(N) — it gives you the number of bits needed to represent N values (ceiling of log base 2), which is exactly what you need for pointers and address buses.

DEPTH parameter → $clog2(DEPTH) = PTR_WIDTHDEPTH=44 entries$clog2(4) = 2ptr needs 2 bits → [1:0]10DEPTH=1616 entries$clog2(16) = 4ptr needs 4 bits → [3:0]DEPTH=256256 entries$clog2(256)=8ptr needs 8 bits → [7:0]DEPTH=10241K entries$clog2(1024)=10ptr needs 10 bits → [9:0]
Figure 2 — $clog2(DEPTH) automatically computes the minimum pointer width. Change DEPTH and the pointer resizes itself — no manual update needed.
SystemVerilog — derived localparams using $clog2 and expressions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module ram_1port #(
    parameter int DATA_WIDTH = 32,
    parameter int DEPTH      = 1024
) (
    input  logic                   clk, we,
    input  logic [$clog2(DEPTH)-1:0] addr,    // width auto-computed
    input  logic [DATA_WIDTH-1:0]    wdata,
    output logic [DATA_WIDTH-1:0]    rdata
);
 
    // ── Derive all other constants from the two parameters ──────
    localparam int ADDR_W    = $clog2(DEPTH);    // 10 bits for 1024 entries
    localparam int BYTE_EN_W = DATA_WIDTH / 8;   // 4 byte-enables for 32-bit data
    localparam int MEM_BYTES = DEPTH * BYTE_EN_W; // total RAM bytes
 
    // Storage — sized entirely by parameters
    logic [DATA_WIDTH-1:0] mem [0:DEPTH-1];
 
    always_ff @(posedge clk) begin
        if (we)    mem[addr] <= wdata;
        rdata <= mem[addr];
    end
 
endmodule

One Module — Many Configurations

The whole point of parameters is that you write the module once and instantiate it at different configurations. Each instance gets its own complete, independent hardware — sized correctly for its role.

SystemVerilog — one FIFO module, four different configurations
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module chip_top (
    input logic clk, rst_n
    // ... other ports
);
 
    // ── Four FIFOs from ONE module definition ──────────────────────
 
    // UART TX buffer: 8-bit data, 16-entry depth
    fifo #(
        .DATA_WIDTH(8),
        .DEPTH(16)
    ) u_uart_tx_buf (
        .clk(clk), .rst_n(rst_n)
        // ... signal connections
    );
 
    // CPU data cache line buffer: 32-bit data, 256-entry depth
    fifo #(
        .DATA_WIDTH(32),
        .DEPTH(256)
    ) u_cpu_cache_buf (
        .clk(clk), .rst_n(rst_n)
        // ...
    );
 
    // DMA scatter buffer: 64-bit data, 512-entry depth
    fifo #(
        .DATA_WIDTH(64),
        .DEPTH(512)
    ) u_dma_scatter (
        .clk(clk), .rst_n(rst_n)
        // ...
    );
 
    // Video line buffer: 16-bit pixels, 1920-entry depth (one HD row)
    fifo #(
        .DATA_WIDTH(16),
        .DEPTH(1920)
    ) u_vid_line_buf (
        .clk(clk), .rst_n(rst_n)
        // ...
    );
 
endmodule
 
// Result: four completely different hardware blocks — one module written once.
fifomoduledefinitionwritten onceu_uart_tx_buf8-bit × 1616 bytes RAMu_cpu_cache_buf32-bit × 2561 KB RAMu_dma_scatter64-bit × 512 → 4 KBOne RTL fileBug fixed once → fixed in all instancesEach instance sized exactly rightNo copy-paste drift
Figure 3 — One fifo module generates four completely different hardware blocks. Fix a bug once — it is automatically fixed in all four instances.

Parameter Naming Conventions & Common Patterns

The industry has settled on a few strong conventions for parameter names. Following them makes your code immediately readable to any engineer anywhere.

Parameter NameMeaningTypical Values
DATA_WIDTHWidth of each data word in bits8, 16, 32, 64, 128
ADDR_WIDTHWidth of address bus in bits$clog2(DEPTH), 10, 12, 32
DEPTHNumber of entries (FIFO, RAM, buffer)16, 256, 1024, 4096
NUM_CHANNELSNumber of parallel channels or ports2, 4, 8, 16
PIPELINE_STAGESNumber of register pipeline stages1, 2, 3, 4
RESET_VALReset value for registers'0, 8'hFF, module-specific
CLK_FREQ_HZClock frequency for timing derivation100_000_000, 50_000_000
BAUD_RATECommunication baud rate9600, 115200, 921600

Default Values — The Smart Way to Use Them

Every parameter has a default value. If the parent does not override a parameter, the default is used. This lets you make a module usable with zero configuration for the common case, while still being fully configurable for edge cases.

SystemVerilog — default parameters: common case vs custom
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module uart_tx #(
    parameter int    DATA_BITS  = 8,             // standard UART: 8 data bits
    parameter int    STOP_BITS  = 1,             // standard: 1 stop bit
    parameter int    CLK_HZ     = 50_000_000,    // 50 MHz default clock
    parameter int    BAUD_RATE  = 115_200        // common baud rate
) (
    input  logic clk, rst_n, tx_valid,
    input  logic [DATA_BITS-1:0] tx_data,
    output logic tx_out, tx_busy
);
    localparam int CLK_DIV = CLK_HZ / BAUD_RATE;  // e.g. 434 for 50 MHz / 115200
    // ... TX logic using CLK_DIV
endmodule
 
 
// ── Instantiate with defaults — zero configuration ─────────────
uart_tx u_uart (     // uses: 8 data bits, 1 stop, 50 MHz, 115200 baud
    .clk(clk), .rst_n(rst_n),
    .tx_valid(valid), .tx_data(byte_out),
    .tx_out(serial_out), .tx_busy(busy)
);
 
// ── Custom instance: 100 MHz clock, 9600 baud ──────────────────
uart_tx #(
    .CLK_HZ   (100_000_000),
    .BAUD_RATE(9_600)
) u_slow_uart (
    .clk(clk), .rst_n(rst_n),
    .tx_valid(valid), .tx_data(byte_out),
    .tx_out(serial_out2), .tx_busy(busy2)
);

parameter vs localparam — Complete Rules

Ruleparameterlocalparam
Can be overridden at instantiation?Yes — with #(.PARAM(val))No — locked inside module
Can be overridden with defparam?Yes (legacy, avoid)No — illegal
Has a default value?Yes — requiredYes — must be assigned a value
Can reference other parameters?Yes — in its default valueYes — can reference any parameter
Visible to parent?Yes — part of the module interfaceNo — private to module
Where declared?#() block or module bodyModule body only (after ports)
Synthesisable?YesYes
Use forWidth, depth, mode — caller-tunableDerived values, thresholds, magic nums

Common Mistakes

SystemVerilog — parameter mistakes and fixes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ MISTAKE 1: Using parameter for an internal constant ════════
module bad1 #(parameter int HALF = WIDTH/2);    // ❌ caller could break this
// ✅ FIX: use localparam for derived/internal constants
module good1 #(parameter int WIDTH = 32);
    localparam int HALF = WIDTH / 2;            // ✅ internal — can't be broken
 
 
// ════ MISTAKE 2: Hardcoding widths instead of using parameters ══
module bad2 (input logic [7:0] din, output logic [7:0] dout);   // ❌ hardcoded
// ✅ FIX: parameterise the width
module good2 #(parameter int W = 8) (input logic [W-1:0] din, output logic [W-1:0] dout);
 
 
// ════ MISTAKE 3: Forgetting to use $clog2 for pointer width ═════
module bad3 #(parameter int DEPTH = 256);
    logic [7:0] ptr;                 // ❌ hardcoded width — breaks when DEPTH changes
// ✅ FIX: derive width with $clog2
module good3 #(parameter int DEPTH = 256);
    localparam int PTR_W = $clog2(DEPTH);
    logic [PTR_W-1:0] ptr;           // ✅ auto-sizes with DEPTH
 
 
// ════ MISTAKE 4: Untyped parameter — silent type confusion ══════
module bad4 #(parameter WIDTH = 8);              // ❌ type inferred — fragile
// ✅ FIX: always specify the type
module good4 #(parameter int WIDTH = 8);         // ✅ explicit type — tool checks it

Quick Reference — Parameters Cheat Sheet

SystemVerilog — parameters quick reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Declare parameters (in #() before port list) ───────────────
module my_mod #(
    parameter int          WIDTH      = 8,       // integer
    parameter int unsigned DEPTH      = 16,      // unsigned integer
    parameter logic [7:0]  INIT       = 8'h00,   // logic vector
    parameter string       MODE       = "SYNC",  // string
    parameter real         CLK_PERIOD = 10.0     // real
) ( /* port list */ );
 
// ── Declare localparams (inside module body) ───────────────────
    localparam int ADDR_W   = $clog2(DEPTH);    // from function
    localparam int HALF_W   = WIDTH / 2;        // from expression
    localparam int MAX_ADDR = DEPTH - 1;        // from arithmetic
 
// ── Override at instantiation (named style) ────────────────────
my_mod #(
    .WIDTH(32),
    .DEPTH(1024)
) u_inst ( /* port connections */ );
 
// ── Use default (no # block needed) ───────────────────────────
my_mod u_default ( /* port connections */ );
 
// ── Key system functions ───────────────────────────────────────
// $clog2(N)   — ceiling log2, minimum bits to hold N values
// $bits(type) — number of bits in a type
// $size(arr)  — number of elements in an array

Verification Usage — Parameters Inside the Testbench

The DUT and the testbench must agree on every width that crosses the boundary between them. When the DUT is parameterised, the testbench classes that talk to it — sequence items, drivers, monitors, scoreboards — must be parameterised in lockstep. A mismatch is a silent killer: a 32-bit driver feeding a 64-bit DUT produces the wrong stimulus on every cycle, and the scoreboard happily compares partial values.

SystemVerilog — parameterised UVM stack synchronised with the DUT
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Shared package — one source of truth for widths ───────────
package apb_cfg_pkg;
    localparam int ADDR_W = 32;
    localparam int DATA_W = 64;     // change once → propagates everywhere
endpackage
 
// ── Parameterised interface — sized by the package constants ──
import apb_cfg_pkg::*;
interface apb_if #(
    parameter int AW = ADDR_W,
    parameter int DW = DATA_W
) (input logic clk);
    logic [AW-1:0] paddr;
    logic [DW-1:0] pwdata, prdata;
    logic          pwrite, psel, penable, pready;
endinterface
 
// ── Sequence item — same parameter contract as the DUT ────────
class apb_xact #(parameter int AW = ADDR_W, parameter int DW = DATA_W)
    extends uvm_sequence_item;
 
    rand logic [AW-1:0] paddr;
    rand logic [DW-1:0] pwdata;
    bit                 pwrite;
 
    // $bits(paddr) returns AW at elaboration — usable in constraints
    constraint addr_word_aligned { paddr[1:0] == 2'b00; }
 
    `uvm_object_param_utils(apb_xact#(AW, DW))
endclass
 
// ── Driver — same widths reach into vif transactions ──────────
class apb_driver #(parameter int AW = ADDR_W, parameter int DW = DATA_W)
    extends uvm_driver #(apb_xact#(AW, DW));
 
    virtual apb_if#(AW, DW) vif;
 
    task run_phase(uvm_phase phase);
        forever begin
            apb_xact#(AW, DW) tr;
            seq_item_port.get_next_item(tr);
            vif.paddr  <= tr.paddr;
            vif.pwdata <= tr.pwdata;
            vif.pwrite <= tr.pwrite;
            @(posedge vif.clk);
            seq_item_port.item_done();
        end
    endtask
endclass
 
// ── TB top — wires the DUT's parameters through to every TB component
module tb_top;
    import apb_cfg_pkg::*;
 
    logic clk;
    initial begin clk = 0; forever #5 clk = ~clk; end
 
    // Same parameter values flow into interface, DUT, and config_db
    apb_if#(.AW(ADDR_W), .DW(DATA_W)) intf(.clk(clk));
 
    apb_slave_dut #(.ADDR_W(ADDR_W), .DATA_W(DATA_W)) dut (.apb(intf));
 
    initial begin
        uvm_config_db#(virtual apb_if#(ADDR_W, DATA_W))::set(null, "*", "vif", intf);
        run_test();
    end
endmodule

Simulation Behavior — How Parameters Appear at Time 0

Parameters do not exist in the simulator's signal database. They are resolved during elaboration — the phase between compilation and time 0 — and their values become part of the static netlist. By the time the simulator's Active region fires for the first time, every WIDTH reference has been replaced by its concrete integer.

SystemVerilog — observing parameter resolution at time 0
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module probe #(
    parameter int    WIDTH = 8,
    parameter int    DEPTH = 16,
    parameter string MODE  = "SYNC"
) ( input logic clk );
 
    localparam int PTR_W = $clog2(DEPTH);
 
    initial begin
        $display("[%0t] %m: WIDTH=%0d DEPTH=%0d PTR_W=%0d MODE=%s",
                 $time, WIDTH, DEPTH, PTR_W, MODE);
    end
endmodule
 
// Two instances, two configurations
probe #(.WIDTH(8),  .DEPTH(16))                       u_p1(.clk(clk));
probe #(.WIDTH(32), .DEPTH(1024), .MODE("ASYNC"))     u_p2(.clk(clk));
 
// Expected $display output at time 0 — once per instance:
//   [0] top.u_p1: WIDTH=8  DEPTH=16   PTR_W=4  MODE=SYNC
//   [0] top.u_p2: WIDTH=32 DEPTH=1024 PTR_W=10 MODE=ASYNC
//
// Note: PTR_W differs between instances even though the source code says
// $clog2(DEPTH) once — elaboration resolved it twice, once per instance.

Waveform Analysis — Parameter-Driven Counter Rollover

The most visible consequence of a parameter change is the shape of the waveform — the counter rolls over at a different value, the FIFO empties at a different cycle, the address bus widens. Predicting the waveform from the parameter set is a junior-to-mid skill jump.

SystemVerilog — parameterised counter under test
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module counter_n #(
    parameter int MAX_COUNT = 16
) (
    input  logic clk, rst_n, en,
    output logic [$clog2(MAX_COUNT)-1:0] cnt,
    output logic                         wrap
);
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)        cnt <= '0;
        else if (en) begin
            if (cnt == MAX_COUNT - 1) cnt <= '0;
            else                       cnt <= cnt + 1;
        end
    end
 
    assign wrap = (cnt == MAX_COUNT - 1) && en;
endmodule
 
// Two instances driven by the same clk/en at different MAX_COUNT
counter_n #(.MAX_COUNT(16))  u_cnt16  (.clk, .rst_n, .en, .cnt(c16),  .wrap(w16));
counter_n #(.MAX_COUNT(256)) u_cnt256 (.clk, .rst_n, .en, .cnt(c256), .wrap(w256));

Waveform of the two counters running side by side. u_cnt16's cnt is 4 bits wide ($clog2(16) = 4) and wraps every 16 cycles; u_cnt256's is 8 bits and wraps every 256.

Expected waveform — same RTL, different MAX_COUNT
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cycle           0   1   2   3  ...  13  14  15  16  17  18  ...
clk           __│‾│_│‾│_│‾│_│‾│_  _│‾│_│‾│_│‾│_│‾│_│‾│_│‾│_
en            ──┘‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
 
u_cnt16:
  cnt[3:0]      0   1   2   3  ...  13  14  15   0   1   2  ...   ← wraps at 16
  wrap        __________________  ________│‾‾‾│______________
 
u_cnt256:
  cnt[7:0]      0   1   2   3  ...  13  14  15  16  17  18  ...  255   0
  wrap        __________________  ____________________________ ... ____│‾‾‾│___ ← wraps at 256
 
Key observation: same source, different shape.
  - cnt[3:0]   is 4 bits because $clog2(16)  = 4
  - cnt[7:0]   is 8 bits because $clog2(256) = 8
  - The wrap pulse on u_cnt16  fires 16× more often than on u_cnt256.

Industry Insights — How Senior Teams Use Parameters

Debugging Academy — 5 Real Parameter Bugs

Each lab is a real failure mode that shipped at least once in a real project. Buggy code, what the symptom looked like, why it happened, and the fix.

1

Override Typo — Parameter Silently Defaults

SILENT DEFAULT
Buggy Code
Typo'd parameter name — silently falls back to default
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Module declares MAX_COUNT
module counter_n #(parameter int MAX_COUNT = 16) (...);
 
// Parent: typo'd parameter name — falls back to the default
counter_n #(.MAX_CONT(1024)) u_cnt (.clk, .rst_n, .en, .cnt, .wrap);
//          ^^^^^^^^ should be MAX_COUNT
Symptom

The counter wraps at 16, not 1024. The downstream protocol times out because the sequencer expected 1024 cycles between wrap pulses. The first regression failure is a single test, and only on the configuration that needs the larger count — so the bug looks random.

Root Cause

SystemVerilog does not error on an unknown parameter name in a named override list unless the tool is configured to. Most tools warn (-pedanticerrors in Questa, +lint=PCWM in VCS), but those warnings often drown in a clean build. The override is silently dropped and the module instantiates with its default.

Fix

Two-step fix. First, correct the name. Second, turn the warning into an error in CI: vcs +lint=PCWM-L -error=PCWM-L or vsim -pedanticerrors. From that day, the bug cannot recur silently. Optional: add an $info at initial time that prints the resolved parameter value, so future builds make the mistake visible at time 0.

2

Derived Constant Declared as parameter — Caller Breaks It

INVARIANT BROKEN
Buggy Code
PTR_W exposed as parameter — caller overrides it incorrectly
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo #(
    parameter int DEPTH = 16,
    parameter int PTR_W = $clog2(DEPTH)   // ← should be localparam!
) (...);
    logic [PTR_W-1:0] wr_ptr, rd_ptr;
    ...
endmodule
 
// Integrator does this:
fifo #(.DEPTH(32), .PTR_W(8)) u_fifo (...);  // PTR_W=8 but DEPTH=32 needs only 5
Symptom

The pointer is 8 bits when it should be 5. Upper 3 bits of the pointer hold X for the first cycles, then garbage. The wrap math (wr_ptr == DEPTH - 1) compares 8-bit against 5-bit literal — wrap never fires; the FIFO overruns into unallocated memory locations.

Root Cause

Putting PTR_W in the #() parameter list exposed it to the caller. The original author intended it as "derived from DEPTH" but didn't move it to a localparam inside the module body. The integrator assumed every parameter was tunable and "helpfully" set it.

Fix
Move derived constants into localparam — inside the module body
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo #(
    parameter int DEPTH = 16                  // only contract surface
) (...);
    localparam int PTR_W = $clog2(DEPTH);    // ← internal, can't be overridden
    logic [PTR_W-1:0] wr_ptr, rd_ptr;
    ...
endmodule

Rule: any constant whose value is mathematically determined by another parameter must be a localparam, never a parameter.

3

$clog2(1) Returns 0 — Zero-Width Vector at the Boundary

EDGE CASE
Buggy Code
DEPTH=1 makes $clog2(DEPTH)-1 evaluate to -1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module ram #(parameter int DEPTH = 1) (
    input  logic [$clog2(DEPTH)-1:0] addr,  // → logic [-1:0] addr — illegal
    ...
);
Symptom

Synthesis errors with "illegal vector range [-1:0]" or a similarly cryptic elaboration error. Some simulators clamp to a 1-bit signal and run; gate-sim then mismatches RTL-sim.

Root Cause

$clog2(1) returns 0 per IEEE 1800-2017 §20.8 — the ceiling of log₂(1) is 0. Subtracting 1 gives -1. The expression is then used as a vector range, which the LRM disallows.

Fix
Guard $clog2 with a min-1 expression
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam int AW = (DEPTH > 1) ? $clog2(DEPTH) : 1;
input logic [AW-1:0] addr;

Always guard $clog2 against single-entry depths. Standard library functions like vlsim_pkg::clog2_min1(N) in big-team packages encode this exact guard.

4

Untyped Parameter — Type Inferred From the Default

TYPE INFERENCE
Buggy Code
Untyped parameter — type inferred from default literal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module pkt #(
    parameter WIDTH = 8'h08    // ← no type — inferred as logic[7:0] from default
) ( ... );
    logic [WIDTH-1:0] payload;    // 7-bit vector?  No — see below
endmodule
 
// Caller intends 32:
pkt #(.WIDTH(32))  u_pkt (...);   // tool truncates 32 to fit 8-bit → WIDTH = 8'h20 = 32 ✓
pkt #(.WIDTH(256)) u_big (...);   // 256 truncates to 8 bits → WIDTH = 8'h00 = 0  ✗
Symptom

The 32-bit instance works. The 256-bit instance silently elaborates to a 0-bit payload — the simulator prints garbled output, the synthesis tool reports a zero-width signal warning that gets buried in a clean log.

Root Cause

Untyped parameter inherits the type of its default value. 8'h08 is an 8-bit literal — so WIDTH is typed as 8-bit logic. Overrides are silently truncated to fit. There is no type error because the assignment is "compatible" by SystemVerilog's standard width-extension rules.

Fix

Always type your parameters: parameter int WIDTH = 8;. The int keyword makes the parameter 32-bit signed; overrides are checked against that and an attempt to assign a string or a struct triggers an elaboration error instead of silent truncation. Make this a lint rule (Spyglass parameter_type_missing) and reject merges that violate it.

5

Two Modules, Two Names for the Same Width — Boundary Drift

INTEGRATION DRIFT
Buggy Code
Two teams pick two names for the same bus width
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Team A's bridge — uses W
module axi_to_apb_a #(parameter int W = 64) (
    input  logic [W-1:0] s_axi_wdata,
    output logic [W-1:0] m_apb_pwdata
);
 
// Team B's bridge — uses DATA_W
module apb_to_periph_b #(parameter int DATA_W = 32) (
    input  logic [DATA_W-1:0] s_apb_pwdata,
    output logic [DATA_W-1:0] m_periph_data
);
 
// SoC integrator wires them up:
axi_to_apb_a    #(.W(64))      u_bridge_a (.s_axi_wdata(axi_wd),    .m_apb_pwdata(internal));
apb_to_periph_b #(.DATA_W(32)) u_bridge_b (.s_apb_pwdata(internal), .m_periph_data(periph_d));
// internal is 64 bits going in, 32 bits going out — upper 32 bits silently truncated
Symptom

Tool prints "width mismatch: 64 to 32 in port connection." Team has a lint config that downgrades width-mismatch from error to warning because there are ~500 of them in legacy modules. The warning drowns. The peripheral receives the lower 32 bits; the upper 32 disappear without trace.

Root Cause

Two teams chose two different parameter names for the same conceptual width. There is no single source of truth. Each team's value is "right" for their module. The integration code is "right" for the two values it sees. The system is wrong.

Fix

Centralise the canonical widths in a shared package (soc_widths_pkg::AXI_DATA_W, soc_widths_pkg::APB_DATA_W). Every module imports the package and uses the canonical name as the default for its own parameter. SoC integration declares one widths package; every block inherits it. Upgrade lint to fail on width-mismatch (no exceptions), and run the full RTL through the linter once to surface all 500 instances at once — most can be fixed by exposing existing parameters; a few require new parameters.

Interview Q&A — 12 Questions Engineers Actually Ask

Real questions drawn from technical interviews at semiconductor companies. Try to answer each before reading the response.

parameter is a compile-time constant that the parent module can override at instantiation via #(.NAME(value)). localparam is a compile-time constant locked inside the module — no external override allowed. Choose parameter when the caller should be able to vary the value (widths, depths, modes). Choose localparam for derived values, internal thresholds, and any "magic number" that the caller has no business changing. The default state should be localparam; promotion to parameter is an explicit contractual commitment that the value is part of the module's public interface.

Best Practices — Rules to Walk Away With

  1. Type every parameter. parameter int WIDTH = 8;, never parameter WIDTH = 8;. The type is what catches the bug at elaboration instead of in gate-sim.
  2. SCREAMING_SNAKE_CASE. DATA_WIDTH, NUM_CHANNELS, RESET_VAL. A reader scanning the file should instantly recognise constants from signals.
  3. Default to localparam. Promote to parameter only when the caller has a documented reason to vary the value. External configurability is a deliberate decision, not a default.
  4. Derive with $clog2. Pointer widths, address widths, counter widths — every one of them should come from $clog2(DEPTH), not a hand-counted literal.
  5. Guard $clog2(1). Wrap it: (DEPTH > 1) ? $clog2(DEPTH) : 1, or use a packaged clog2_min1() helper.
  6. Propagate parameters through the testbench. Every UVM component that talks to the DUT must accept the same parameters. A single config package as the source of truth is the cleanest pattern.
  7. Never defparam. Use named instance overrides: module_n #(.WIDTH(32)) u_inst (...);. Lint should reject any defparam.
  8. Document the legal range. Above every parameter, comment the valid range and any invariants: // 1..1024, must be power of 2. Add an initial assert that enforces the contract.
  9. Treat the parameter list as a versioned API. Adding a parameter with a default is backward-compatible. Renaming or removing one is a breaking change — version-bump and announce.
  10. Make override mismatch a CI error, not a warning. Configure your simulator and synthesis tool to fail the build on unknown parameter overrides and port width mismatches. The silent-default failure mode (Lab 1) cannot survive this rule.