Verilog Parameters: parameter, localparam, and Overrides
A Verilog parameter is a named constant that is fixed at elaboration time — the stage where the simulator or synthesis tool builds the design hierarchy, before any simulation cycle runs. Because each instance of a module can receive different parameter values, one piece of RTL can produce many different circuits: a 4-bit counter here, a 32-bit counter there, from the same source file. This is the single most important reuse mechanism in Verilog, and it shows up constantly in interviews: expect questions like “what is the difference between parameter and localparam?” or “how do you override a parameter without editing the module?”
Declaring parameters
Verilog-2001/2005 gives you two places to declare a parameter. The modern style puts overridable parameters in a parameter port list using #( ... ) right after the module name. The older style declares them in the module body. Both are legal; the port-list style is preferred because it tells the reader at a glance which knobs the module exposes.
// Modern style: parameter port list (preferred)
module fifo #(
parameter DATA_W = 8,
parameter DEPTH = 16
) (
input wire clk,
input wire [DATA_W-1:0] din
/* ... */
);
// Older style: body declaration (still overridable)
module fifo_old (clk, din /* ... */);
parameter DATA_W = 8;
parameter DEPTH = 16;
/* ... */A parameter can hold an integer, a sized value like 4'hF, a real number, or a string. You can also give it an explicit type and range, e.g. parameter integer DEPTH = 16;. Every parameter must have a default value — that default is what an instance gets when the instantiating code does not override it.
A WIDTH-generic counter
Here is the classic example you should be able to write from memory. The counter below works for any width because every internal expression is written in terms of WIDTH instead of a hard-coded number. Note the reset value {WIDTH{1'b0}} — a replication expression that produces the right number of zero bits no matter what WIDTH is. Writing 4'b0 there would quietly break the 8-bit instance.
The testbench instantiates the same module twice: once with the default WIDTH = 4 and once overridden to 8 using the named override syntax #(.WIDTH(8)). After 18 enabled clock cycles the 4-bit instance has wrapped around (18 mod 16 = 2) while the 8-bit instance is still counting — two genuinely different circuits from one module.
module counter #(
parameter WIDTH = 4
) (
input wire clk,
input wire rst,
input wire en,
output reg [WIDTH-1:0] count
);
always @(posedge clk) begin
if (rst)
count <= {WIDTH{1'b0}};
else if (en)
count <= count + 1'b1;
end
endmodule
module tb_counter;
reg clk = 1'b0;
reg rst = 1'b1;
reg en = 1'b0;
wire [3:0] c4;
wire [7:0] c8;
// Default WIDTH = 4
counter u_c4 (.clk(clk), .rst(rst), .en(en), .count(c4));
// WIDTH overridden to 8 at instantiation
counter #(.WIDTH(8)) u_c8 (.clk(clk), .rst(rst), .en(en), .count(c8));
always #5 clk = ~clk;
initial begin
repeat (2) @(posedge clk);
rst = 1'b0;
en = 1'b1;
repeat (18) @(posedge clk);
#1;
$display("After 18 enabled clock cycles:");
$display(" 4-bit counter = %0d (wrapped past 15)", c4);
$display(" 8-bit counter = %0d (still counting)", c8);
$finish;
end
endmoduleAfter 18 enabled clock cycles: 4-bit counter = 2 (wrapped past 15) 8-bit counter = 18 (still counting) /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpjz87b5tt/param_counter.v:41: $finish called at 196 (1s)
Overriding parameters with #( )
The instantiation override comes in two flavours:
- Named association —
counter #(.WIDTH(8)) u1 (...);. Each override names the parameter it targets. Order does not matter, and you can skip parameters you want to leave at their defaults. - Positional association —
counter #(8) u1 (...);. Values are matched to parameters in declaration order. This breaks silently if someone later reorders or inserts a parameter in the module, so use it only for modules with a single parameter, if at all.
One rule trips up many beginners: with positional association you cannot skip a parameter in the middle. If a module declares #(parameter A = 1, parameter B = 2) and you want to override only B, you must write #(.B(5)) — there is no positional way to say “default, then 5”. Named association avoids the whole problem, which is why coding guidelines in most VLSI teams mandate it.
parameter vs localparam
A localparam is also an elaboration-time constant, but it cannot be overridden from outside the module. That restriction is the whole point. Use it for constants that are derived from other parameters or that encode internal facts (FSM state encodings, field offsets) which would corrupt the design if a careless instantiation changed them.
parameter | localparam | |
|---|---|---|
Overridable via #( ) or defparam | Yes | No |
Allowed in the #( ) parameter port list | Yes | No (in Verilog-2005; SystemVerilog allows it) |
| Can be computed from other parameters | Yes | Yes — its main use |
| Typical use | Public knob: width, depth, latency | Derived or internal constant: address width, state codes |
The standard illustration is a memory: the user chooses DEPTH, and the address width follows from it as $clog2(DEPTH) (ceiling log base 2, available in Verilog-2005). Making the address width a localparam guarantees it always matches the depth — nobody can instantiate a 64-deep RAM with a 4-bit address bus. Watch the log below: the same source yields a 16×8 RAM with a 4-bit address and a 64×16 RAM with a 6-bit address.
module sync_ram #(
parameter DEPTH = 16, // user-tunable
parameter DATA_W = 8 // user-tunable
) (
input wire clk,
input wire we,
input wire [$clog2(DEPTH)-1:0] addr,
input wire [DATA_W-1:0] wdata,
output reg [DATA_W-1:0] rdata
);
// Derived constant: callers cannot override this
localparam ADDR_W = $clog2(DEPTH);
reg [DATA_W-1:0] mem [0:DEPTH-1];
always @(posedge clk) begin
if (we)
mem[addr] <= wdata;
rdata <= mem[addr];
end
initial
$display("%m: DEPTH=%0d DATA_W=%0d -> ADDR_W=%0d",
DEPTH, DATA_W, ADDR_W);
endmodule
module tb_sync_ram;
reg clk = 1'b0;
reg we = 1'b0;
reg [5:0] addr_a; // wide enough for both instances
reg [15:0] wdata;
wire [7:0] rdata_small;
wire [15:0] rdata_big;
sync_ram u_small ( // DEPTH=16, DATA_W=8 (defaults)
.clk(clk), .we(we),
.addr(addr_a[3:0]), .wdata(wdata[7:0]), .rdata(rdata_small)
);
sync_ram #(.DEPTH(64), .DATA_W(16)) u_big (
.clk(clk), .we(we),
.addr(addr_a), .wdata(wdata), .rdata(rdata_big)
);
always #5 clk = ~clk;
initial begin
// write one location in each RAM, then read it back
@(negedge clk);
we = 1'b1; addr_a = 6'd9; wdata = 16'hA5C3;
@(negedge clk);
we = 1'b0;
@(negedge clk); // read cycle completes
$display("small RAM [9] = %h", rdata_small);
$display("big RAM [9] = %h", rdata_big);
$finish;
end
endmoduletb_sync_ram.u_small: DEPTH=16 DATA_W=8 -> ADDR_W=4 tb_sync_ram.u_big: DEPTH=64 DATA_W=16 -> ADDR_W=6 small RAM [9] = c3 big RAM [9] = a5c3 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpjybp6kq0/param_localparam.v:56: $finish called at 30 (1s)
The %m format specifier prints the hierarchical path of the instance, which is a handy trick for confirming which instance received which configuration.
defparam: the legacy override
Before Verilog-2001 introduced the #( ) port list, parameters were overridden with defparam: a separate statement that reaches into an instance by hierarchical name and rewrites one of its parameters. It still works, and you will meet it in older codebases and in some gate-level netlists, so you should be able to read it.
module pulse_gen #(
parameter PERIOD = 10,
parameter TAG = "default"
) (
input wire clk,
output reg pulse
);
reg [7:0] cnt = 8'd0;
always @(posedge clk) begin
if (cnt == PERIOD - 1) begin
cnt <= 8'd0;
pulse <= 1'b1;
end else begin
cnt <= cnt + 8'd1;
pulse <= 1'b0;
end
end
initial
$display("%m configured: PERIOD=%0d TAG=%s", PERIOD, TAG);
endmodule
module tb_pulse_gen;
reg clk = 1'b0;
wire p_fast, p_slow;
pulse_gen u_fast (.clk(clk), .pulse(p_fast));
pulse_gen u_slow (.clk(clk), .pulse(p_slow));
// Legacy hierarchical override -- avoid in new code
defparam u_fast.PERIOD = 4;
defparam u_fast.TAG = "fast";
defparam u_slow.PERIOD = 8;
defparam u_slow.TAG = "slow";
integer fast_pulses = 0;
integer slow_pulses = 0;
always #5 clk = ~clk;
always @(posedge clk) begin
if (p_fast) fast_pulses = fast_pulses + 1;
if (p_slow) slow_pulses = slow_pulses + 1;
end
initial begin
repeat (33) @(posedge clk);
$display("fast pulses seen: %0d", fast_pulses);
$display("slow pulses seen: %0d", slow_pulses);
$finish;
end
endmoduletb_pulse_gen.u_fast configured: PERIOD=4 TAG=fast tb_pulse_gen.u_slow configured: PERIOD=8 TAG=slow fast pulses seen: 7 slow pulses seen: 3 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmprs2pdl9p/param_defparam.v:51: $finish called at 325 (1s)
The simulation confirms both overrides took effect: the instance running with PERIOD = 4 produces roughly twice as many pulses as the one with PERIOD = 8. So why avoid defparam in new RTL? Three reasons:
- The configuration lives far from the instantiation — sometimes in a different file — so a reader looking at the instance has no idea its parameters were changed.
- Elaboration order becomes subtle when multiple
defparamstatements target the same parameter; the standard has rules, but humans forget them. - Many synthesis flows and lint rule decks restrict or flag it, and SystemVerilog has deprecated the cross-hierarchy form. Named
#( )overrides do everything you need, locally and explicitly.
Rules and gotchas worth remembering
- Parameters are constants, not variables. You cannot assign to a parameter from an
alwaysorinitialblock. If a value must change during simulation, it is a signal, not a parameter. - Parameters disappear in hardware. Synthesis evaluates them and bakes the result into the netlist. A parameterized module costs nothing extra in area or timing compared to a hard-coded one.
- Size your literals against the parameter. Expressions like
count + 1'b1and{WIDTH{1'b0}}scale withWIDTH; a stray4'd1or4'b0000does not. - Parameters drive
generateblocks.generate ifandgenerate forconditions must be elaboration-time constants, and parameters are exactly that — this is how you include or exclude whole blocks of hardware per instance. - Guard illegal values. Verilog-2005 has no
$errorfor elaboration checks, but a common trick is aninitialblock that calls$displayand$finishwhen, say,DEPTHis not a power of two. In SystemVerilog you would use an elaboration assertion instead. - Don’t confuse parameters with
`define. A macro is global text substitution handled by the preprocessor; a parameter is scoped to its module and can differ per instance. For per-instance configuration, macros are the wrong tool.
Quick check
Can a localparam be overridden using #( ) at instantiation?
No. A localparam is fixed inside the module and cannot be overridden by #( ) or defparam. That is precisely why derived constants such as ADDR_W = $clog2(DEPTH) are declared as localparam — the tool guarantees they always stay consistent with the parameters they are computed from.
A module declares #(parameter A = 1, parameter B = 2). How do you override only B?
Use named association: mymod #(.B(5)) u1 (...);. Positional association cannot skip A, so #(5) would override A instead — a classic silent bug, and the main argument for always using named overrides.
Does a parameterized module consume more gates after synthesis than a hard-coded one?
No. Parameters exist only at elaboration time. Synthesis substitutes the final values and optimizes the netlist exactly as if the numbers had been written by hand, so parameterization is free in area and timing.
Why is defparam discouraged in new RTL?
Because it rewrites an instance’s parameters from a distance using hierarchical names: the configuration is separated from the instantiation, multiple defparam statements can interact in order-dependent ways, and SystemVerilog has deprecated the cross-hierarchy form. The in-line #(.NAME(value)) override is local, explicit, and covers every use case.
Share your question in comments or talk to our mentor team for batch guidance.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses