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.
// ❌ 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:
// ✅ 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 instanceparameter vs localparam
SystemVerilog has two compile-time constant keywords. The distinction is about who is allowed to change the value.
| Keyword | Who can change it | Use it for |
|---|---|---|
parameter | The parent module via #(.NAME(value)) override | Anything the caller should tune — widths, depths, addresses, modes |
localparam | Nobody outside the module — locked inside | Derived values, internal constants, magic numbers that must not be tuned |
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);
endmoduleParameter 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.
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;
endmoduleDerived 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.
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
endmoduleOne 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.
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.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 Name | Meaning | Typical Values |
|---|---|---|
DATA_WIDTH | Width of each data word in bits | 8, 16, 32, 64, 128 |
ADDR_WIDTH | Width of address bus in bits | $clog2(DEPTH), 10, 12, 32 |
DEPTH | Number of entries (FIFO, RAM, buffer) | 16, 256, 1024, 4096 |
NUM_CHANNELS | Number of parallel channels or ports | 2, 4, 8, 16 |
PIPELINE_STAGES | Number of register pipeline stages | 1, 2, 3, 4 |
RESET_VAL | Reset value for registers | '0, 8'hFF, module-specific |
CLK_FREQ_HZ | Clock frequency for timing derivation | 100_000_000, 50_000_000 |
BAUD_RATE | Communication baud rate | 9600, 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.
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
| Rule | parameter | localparam |
|---|---|---|
| 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 — required | Yes — must be assigned a value |
| Can reference other parameters? | Yes — in its default value | Yes — can reference any parameter |
| Visible to parent? | Yes — part of the module interface | No — private to module |
| Where declared? | #() block or module body | Module body only (after ports) |
| Synthesisable? | Yes | Yes |
| Use for | Width, depth, mode — caller-tunable | Derived values, thresholds, magic nums |
Common Mistakes
// ════ 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 itQuick Reference — Parameters Cheat Sheet
// ── 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 arrayVerification 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.
// ── 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
endmoduleSimulation 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.
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.
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.
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.
Override Typo — Parameter Silently Defaults
SILENT DEFAULT// 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_COUNTThe 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.
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.
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.
Derived Constant Declared as parameter — Caller Breaks It
INVARIANT BROKENmodule 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 5The 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.
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.
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;
...
endmoduleRule: any constant whose value is mathematically determined by another parameter must be a localparam, never a parameter.
$clog2(1) Returns 0 — Zero-Width Vector at the Boundary
EDGE CASEmodule ram #(parameter int DEPTH = 1) (
input logic [$clog2(DEPTH)-1:0] addr, // → logic [-1:0] addr — illegal
...
);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.
$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.
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.
Untyped Parameter — Type Inferred From the Default
TYPE INFERENCEmodule 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 ✗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.
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.
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.
Two Modules, Two Names for the Same Width — Boundary Drift
INTEGRATION DRIFT// 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 truncatedTool 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.
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.
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
- Type every parameter.
parameter int WIDTH = 8;, neverparameter WIDTH = 8;. The type is what catches the bug at elaboration instead of in gate-sim. - SCREAMING_SNAKE_CASE.
DATA_WIDTH,NUM_CHANNELS,RESET_VAL. A reader scanning the file should instantly recognise constants from signals. - Default to localparam. Promote to
parameteronly when the caller has a documented reason to vary the value. External configurability is a deliberate decision, not a default. - Derive with
$clog2. Pointer widths, address widths, counter widths — every one of them should come from$clog2(DEPTH), not a hand-counted literal. - Guard
$clog2(1). Wrap it:(DEPTH > 1) ? $clog2(DEPTH) : 1, or use a packagedclog2_min1()helper. - 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.
- Never
defparam. Use named instance overrides:module_n #(.WIDTH(32)) u_inst (...);. Lint should reject anydefparam. - Document the legal range. Above every parameter, comment the valid range and any invariants:
// 1..1024, must be power of 2. Add aninitial assertthat enforces the contract. - 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.
- 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.