Verilog · Chapter 6 · Constant Variables
Constant Variables in Verilog (parameter and localparam)
Every reusable RTL block has compile-time constants, such as the width of a counter, the depth of a FIFO, the number of pipeline stages, or the encoding of an FSM. Verilog provides two keywords for these. A parameter declares a constant that can be overridden at module instantiation, allowing the same module to be built as 8-bit, 32-bit, or 64-bit. A localparam declares a constant that is fixed at the module level, used for derived values that should not be tampered with. The two keywords are the foundation of parameterised RTL design, and every real-world IP block uses both extensively. This chapter overview frames the distinction between them, introduces the parameter-override patterns, and points to the deep drills on parameter and localparam.
Foundation16 min readVerilogparameterlocalparamConstantsGenerics
Chapter 6 · Constant Variables
1. The Engineering Problem
A senior engineer ships a 32-bit counter as an IP block. A week later, three different teams ask for variants:
- Team A needs an 8-bit counter for a UART byte counter.
- Team B needs a 48-bit counter for a timestamp accumulator.
- Team C needs a 20-bit counter for an APB watchdog.
The naive solution is to copy-paste the original 32-bit module three times, change [31:0] to [7:0], [47:0], [19:0] in each copy, and maintain four parallel codebases. Each bug fix has to be applied four times; each new feature has to be reimplemented four times. Within six months, the four counters drift apart and become subtly different — the maintenance debt compounds.
The right solution is a single parameterised counter:
module up_counter #(parameter WIDTH = 32) (
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count_q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count_q <= {WIDTH{1'b0}};
else if (en) count_q <= count_q + {{(WIDTH-1){1'b0}}, 1'b1};
end
endmoduleOne module. The WIDTH parameter defaults to 32 but can be overridden at each instantiation:
up_counter #(.WIDTH(8)) u_uart (.clk(clk), .rst_n(rst_n), .en(uart_byte), .count_q(uart_count));
up_counter #(.WIDTH(48)) u_time (.clk(clk), .rst_n(rst_n), .en(1'b1), .count_q(timestamp));
up_counter #(.WIDTH(20)) u_apb (.clk(clk), .rst_n(rst_n), .en(apb_wd_en), .count_q(apb_wdog));Three instantiations, three different widths, one source of truth. Bug fixes apply once and propagate to every instance. New features land everywhere atomically. The parameter keyword is the mechanism that makes this work — and the localparam keyword (covered in 6.2) is its companion for values that should be derived from parameter but never overridden.
This chapter overview is about the constant-variable family — what each keyword does, when to reach for each, and why parameterised RTL is the foundation of every real-world IP block.
2. Why Constants Have Their Own Keywords
Verilog could in principle use reg or wire with a fixed initial value to declare "this is a constant" — and beginners often try exactly that:
reg [7:0] FIFO_DEPTH = 8'h10; // tries to be a constant
always @(*) begin
for (i = 0; i < FIFO_DEPTH; i = i + 1) ... // uses FIFO_DEPTH as if constant
endThree problems with the reg approach:
- Not actually constant. A
regis a procedural-assignment target; nothing prevents a strayFIFO_DEPTH = 8'h20;somewhere from reassigning it. The "constant" can be silently modified by buggy code. - Not overridable at instantiation. The value is hard-coded in the source; the same module cannot be instantiated as 4-deep, 16-deep, and 256-deep without copy-paste.
- Not synthesisable as a constant. The synthesis tool may generate flops for the
reg, treating it as a regular register; the explicit constant intent is lost.
The parameter and localparam keywords solve all three:
- Truly constant at compile time. Any attempt to assign a value after declaration produces a compile error.
- Overridable (
parameter) or strictly fixed (localparam) — the keyword declares the override-ability. - Synthesised as a constant. The value is folded into every expression that uses it; no flop is generated; the synthesis tool propagates the constant through arithmetic.
The two keywords are simulation and synthesis primitives that the language has had since Verilog-1995 — they predate localparam (Verilog-2001) but the design pattern is universal across every RTL house.
3. Mental Model
The mental-model has three practical consequences:
- Use
parameteronly for genuinely configurable values. Width, depth, mode flags, encoding choices. Things the module's user might want to control. - Use
localparamfor derived values.localparam ADDR_W = $clog2(DEPTH);is the canonical pattern —DEPTHis aparameter(overridable);ADDR_Wis alocalparamderived from it (not overridable, because changing it independently would break the design). - Don't use
parameterfor module-internal magic numbers. A constant likeBURST_MAX = 16that the user has no business changing should belocalparam. Usingparameterfor it would mislead callers into thinking it's a knob.
4. The Two Keywords at a Glance
| Aspect | parameter | localparam |
|---|---|---|
| Compile-time constant | yes | yes |
| Overridable at instantiation | yes | no |
| Typical use | width, depth, mode, encoding | derived values, internal constants |
| Synthesis behaviour | folded into expressions | folded into expressions |
| Introduction date | Verilog-1995 | Verilog-2001 |
| Scoping | module-level (or generate block) | module-level (or generate block) |
| Value can change at simulation time | no | no |
| Default value required | recommended (parameter X = 8;) | required (localparam Y = 16;) |
The single observable difference is overridability. Everything else — synthesis behaviour, simulation behaviour, scoping rules — is identical.
5. The parameter Keyword
Declared inside a module with a default value. The default applies if the parent doesn't override.
`default_nettype none
module example #(
parameter WIDTH = 8, // simple parameter
parameter DEPTH = 16,
parameter MODE = 0, // mode flag
parameter [7:0] DEFAULT_VAL = 8'hAA // typed parameter
)(
input wire clk,
output reg [WIDTH-1:0] q
);
// ... use WIDTH, DEPTH, MODE, DEFAULT_VAL anywhere ...
endmodule
// Legacy in-body parameter declaration (Verilog-1995 style)
module legacy_style (...);
parameter WIDTH = 8;
parameter DEPTH = 16;
// ...
endmoduleThe two forms — port-list #(parameter ...) (Verilog-2001) and in-body parameter (Verilog-1995) — are functionally equivalent. Modern code uses the port-list form for module-public parameters; in-body declarations are sometimes used for legacy compatibility or for parameters introduced late in the module.
5.1 Parameter override at instantiation
Two override syntaxes:
// Named override (preferred — clear and order-independent)
up_counter #(.WIDTH(16), .RESET_VAL(8'h00)) u_cnt (...);
// Ordered override (legacy — order must match declaration order)
up_counter #(16, 8'h00) u_cnt (...);The named form (.WIDTH(16)) is preferred in every modern codebase — adding a new parameter to the module doesn't break existing instantiations, and the name documents the intent at the call site. The ordered form is fragile and forbidden in most style guides.
The 6.1 parameter sub-page covers the override mechanics in depth.
5.2 Parameter passing through hierarchy
A common pattern: pass a parameter down through several levels of hierarchy:
module top #(parameter DATA_WIDTH = 32) (...);
fifo #(.WIDTH(DATA_WIDTH)) u_fifo (...);
endmodule
module fifo #(parameter WIDTH = 8) (...);
fifo_storage #(.W(WIDTH)) u_storage (...);
endmodule
module fifo_storage #(parameter W = 8) (...);
reg [W-1:0] mem [0:15];
// ...
endmoduleEach level declares its own parameter and forwards it to the next. The override at the top (DATA_WIDTH = 32) propagates down through every level. The parameter names can differ at each level (here DATA_WIDTH → WIDTH → W) — only the override syntax connects them.
6. The localparam Keyword
Declared inside a module. Cannot be overridden. Used for values that should be fixed.
`default_nettype none
module fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16
)(
input wire clk,
input wire rst_n,
input wire push,
input wire pop,
input wire [DATA_WIDTH-1:0] data_in,
output wire [DATA_WIDTH-1:0] data_out,
output wire full,
output wire empty
);
// Derived constant — address width is log2(DEPTH)
localparam ADDR_W = $clog2(DEPTH);
localparam CNT_W = $clog2(DEPTH + 1); // count needs one extra value for "full"
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg [ADDR_W-1:0] wr_ptr, rd_ptr;
reg [CNT_W-1:0] count;
// ... FIFO logic ...
endmoduleThe ADDR_W and CNT_W are derived from DEPTH. Declaring them as localparam ensures that an instantiation cannot accidentally override them — if it could, the FIFO would have inconsistent address widths and break.
6.1 The $clog2() system function
The IEEE 1364-2005 $clog2() function returns the ceiling of log2(N) — the number of bits needed to represent values 0 through N-1. It is the canonical companion to localparam:
parameter DEPTH = 16;
localparam ADDR_W = $clog2(DEPTH); // = 4 (since 2^4 = 16)
parameter DEPTH = 17;
localparam ADDR_W = $clog2(DEPTH); // = 5 (since 2^4 = 16 < 17 ≤ 32 = 2^5)$clog2(0) is typically 0 (depends on tool), and $clog2(1) is 0 (a 1-entry "memory" needs 0 address bits). For arbitrary DEPTH, $clog2(DEPTH) is the address-width formula.
6.2 Using localparam for state encoding
The other canonical use: declaring FSM state names as named constants.
`default_nettype none
module simple_fsm (
input wire clk, rst_n, start, done,
output reg busy
);
// FSM state encoding as localparam — fixed at the module level
localparam IDLE = 2'b00;
localparam ACTIVE = 2'b01;
localparam WAIT = 2'b10;
localparam DONE = 2'b11;
reg [1:0] state_q, state_next;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state_q <= IDLE;
else state_q <= state_next;
end
always @(*) begin
state_next = state_q;
case (state_q)
IDLE: if (start) state_next = ACTIVE;
ACTIVE: state_next = WAIT;
WAIT: if (done) state_next = DONE;
DONE: state_next = IDLE;
endcase
end
always @(*) busy = (state_q != IDLE);
endmoduleThe four localparam declarations name the states. Without them, the code would use 2'b00, 2'b01, etc. directly — readable but error-prone (typing 2'b10 instead of 2'b11 is hard to spot in code review). The named localparams document the design and survive code-review.
7. Visual Explanation
Three figures cover the parameter-vs-localparam distinction and the parameterisation pattern.
7.1 Visual A — parameter vs localparam
The keyword choice as an access-control contract.
parameter vs localparam — module interface visibility
data flow7.2 Visual B — the parameterisation pattern
A single source module produces N variant instances via parameter overrides.
7.3 Visual C — derived localparam
A parameter and its derived localparams. The user overrides DEPTH; the localparams automatically follow.
8. The Parameterised FIFO — Canonical Example
A single FIFO module that supports any data width and any depth via parameters and derived localparams.
`default_nettype none
module sync_fifo #(
parameter DATA_WIDTH = 8, // user-overridable
parameter DEPTH = 16 // user-overridable
)(
input wire clk,
input wire rst_n,
input wire push,
input wire pop,
input wire [DATA_WIDTH-1:0] data_in,
output reg [DATA_WIDTH-1:0] data_out,
output wire full,
output wire empty,
output wire [ADDR_W-1:0] count // exposed to user
);
// Derived constants — track DEPTH automatically
localparam ADDR_W = $clog2(DEPTH);
localparam CNT_W = $clog2(DEPTH + 1); // +1 for "full" representation
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg [ADDR_W-1:0] wr_ptr_q, rd_ptr_q;
reg [CNT_W-1:0] count_q;
// ... FIFO logic ...
// Instantiation example:
// sync_fifo #(.DATA_WIDTH(32), .DEPTH(256)) u_fifo (...);
endmoduleThree users instantiate this FIFO at different sizes:
// 8-bit × 4-deep UART RX FIFO
sync_fifo #(.DATA_WIDTH(8), .DEPTH(4)) u_uart_fifo (...);
// 32-bit × 64-deep DMA buffer
sync_fifo #(.DATA_WIDTH(32), .DEPTH(64)) u_dma_fifo (...);
// 64-bit × 8-deep instruction queue
sync_fifo #(.DATA_WIDTH(64), .DEPTH(8)) u_iq_fifo (...);Same source. Three different gate counts. One bug fix benefits all.
The pattern scales across every parameterisable block in the design — registers, FIFOs, arbiters, decoders, datapath stages. Every reusable IP block follows the same template.
9. The Forbidden Cousin — defparam
Verilog-1995 introduced a third constant-related construct — defparam — that allows parameter override from outside the instantiation:
// Top module
module top;
sub_module u_sub (...);
// defparam override — DEPRECATED, do not use in new code
defparam u_sub.WIDTH = 16;
endmodule
// Sub module
module sub_module;
parameter WIDTH = 8;
// ...
endmoduleThe defparam override is forbidden in modern code for three reasons:
- Action-at-a-distance. The override happens in
top, but the affected parameter is insub_module. Reading the sub_module's declaration doesn't reveal the override; you have to search the entire codebase fordefparamstatements affecting this instance. - Hierarchical naming required. The path
u_sub.WIDTHties the override to the specific instance name; renaming the instance intopbreaks the override silently. - Removed from SystemVerilog. SystemVerilog (Verilog's successor) deprecated
defparamand most synthesis flows reject it entirely. Code usingdefparamwill not survive a migration to SystemVerilog.
The canonical alternative is the instantiation-time override introduced in §5.1:
sub_module #(.WIDTH(16)) u_sub (...); // clear, scope-correct, modernEvery parameter override should use this form. defparam is a Verilog wart that survived 25 years of language evolution and should not appear in any new code.
10. Simulation and Synthesis Behavior
Parameters and localparams are resolved at elaboration time — before simulation starts and before synthesis maps to gates. The elaborator walks the module hierarchy, applies every parameter override, computes derived localparams, and substitutes the resulting values into every expression that uses them.
By the time simulation starts, the parameters are constants. The simulator never allocates storage for them; they appear as literal values in the compiled simulation code. The synthesis tool sees the same constants and produces gates with the values baked in (e.g., a reg [WIDTH-1:0] with WIDTH = 32 produces 32 flops; no width inference is needed).
11. Common Mistakes
Five pitfalls that catch engineers using constant variables.
11.1 Using parameter for module-internal magic numbers
module bad #(parameter BURST_MAX = 16) (...); // BURST_MAX is internal — should be localparamA constant like BURST_MAX that no instantiator should be allowed to change should be localparam, not parameter. Exposing it as parameter invites users to override it to invalid values; the module's behaviour with BURST_MAX = 1000 is probably broken.
11.2 Using localparam for genuinely configurable values
The reverse mistake. A FIFO's DEPTH should be parameter (so users can change it); declaring it as localparam forces every variant to be a separate source-code copy.
11.3 Using reg initial values as "constants"
reg [7:0] FIFO_DEPTH = 8'h10; // not actually constant — can be overwrittenUse localparam FIFO_DEPTH = 8'h10; instead. The localparam is guaranteed-constant; the reg is a runtime variable that happens to have an initial value.
11.4 Using defparam for override
Forbidden. Use the instantiation-time override #(.PARAM(value)) instead. defparam is deprecated; SystemVerilog removed it; most synthesis tools reject it.
11.5 Mismatched parameter override count
module sub_module #(parameter WIDTH = 8, parameter DEPTH = 16) (...);
// Instantiation
sub_module #(.WIDTH(32)) u_sub (...); // only one parameter overridden — DEPTH stays at default 16
sub_module #(32) u_sub2 (...); // ordered form — overrides WIDTH, DEPTH stays at defaultThe named-override form leaves un-mentioned parameters at their defaults; this is intentional and safe. The ordered-override form is fragile — adding a new parameter to the module breaks every existing ordered-style instantiation. Always use the named form.
12. Industry Use Cases
Three patterns where parameterisation is essential:
12.1 Reusable IP blocks
Every commercial IP block (Synopsys DesignWare, Cadence IP, ARM cores, Xilinx Vivado IP) is parameterised. The user picks the FIFO depth, register count, port width, instruction-set variant — all via parameters.
12.2 Customer-configurable RTL
A chip company sells the same RTL to multiple customers, each with slightly different requirements (one customer needs a 64-bit data bus, another 32-bit). Parameters allow shipping one source with per-customer configurations.
12.3 Synthesis-time optimisation
A heavily-used module (e.g., a multiplier) may be parameterised for width. The synthesis tool optimises each instance for the exact width — a 4-bit multiplier is faster and smaller than an 8-bit instantiation. Parameterised RTL gives the synthesis tool the freedom to optimise per-instance.
13. Debugging Lab
Three constant-variable debug post-mortems
14. Coding Guidelines
- Use
parameterfor genuinely user-configurable values. Width, depth, mode, encoding. - Use
localparamfor derived values and internal constants.$clog2()-based widths, state-encoding names, magic numbers that users have no business changing. - Always use named overrides at instantiation.
#(.PARAM(value)), never#(value). - Never use
defparam. It's deprecated, action-at-a-distance, and rejected by SystemVerilog. - Name constants in
ALL_CAPS. Convention:WIDTH,DEPTH,IDLE_STATE,MAX_COUNT. - Provide default values for every
parameter. Even if every instantiation overrides them, the default documents the module's intended use. - Use
localparamfor FSM state encoding. Named states (IDLE,BUSY,DONE) survive code-review better than raw2'b00,2'b01,2'b11. - Use
$clog2()for address-width derivation. The canonical idiom:localparam ADDR_W = $clog2(DEPTH);.
15. Interview Q&A
16. Exercises
Three exercises that turn the parameter-vs-localparam choice into reflex.
Exercise 1 — Pick the keyword
For each value, recommend parameter or localparam.
| # | Value description |
|---|---|
| 1 | FIFO depth (user wants to choose) |
| 2 | FIFO address width (= log2 of depth) |
| 3 | UART baud rate (configurable per chip) |
| 4 | Boot vector address (= 0x1000 — chip-fixed) |
| 5 | Number of pipeline stages (per-instance configurable) |
| 6 | FSM state encoding values (IDLE = 2'b00, etc.) |
Hints. User-configurable → parameter. Derived or internal → localparam.
Exercise 2 — Parameterise a counter
Write a Verilog module up_counter that supports any width and any reset value. Specifications:
- Parameters:
WIDTH(default 8),RESET_VAL(default 0). - Inputs:
wire clk, wire rst_n, wire en. - Output:
reg [WIDTH-1:0] count_q. - Behaviour: counts up by 1 on each posedge clk when
enis asserted; resets toRESET_VALon!rst_n.
Hints. Use named parameter overrides at instantiation. Use {WIDTH{1'b0}} for "all zeros" expressions if you want to avoid hard-coded widths.
Exercise 3 — Identify the misuse
The following module is supposed to be a parameterised dual-port FIFO. Identify each parameter / localparam choice that's wrong, and recommend the fix.
module dual_fifo (
input wire clk,
input wire [DATA_WIDTH-1:0] data_in,
output wire [DATA_WIDTH-1:0] data_out
);
parameter DATA_WIDTH = 8; // ?
parameter DEPTH = 16; // ?
parameter ADDR_W = 4; // ?
localparam BURST_MAX = 8; // ?
localparam WIDTH = DATA_WIDTH; // ?
endmoduleWhat to produce. (a) For each declaration, is parameter or localparam the right choice? (b) Show the corrected module. (c) Identify which declarations are redundant.
17. Summary
Verilog has two keywords for compile-time constants:
parameter— overridable at module instantiation. Use for user-configurable values: width, depth, mode flags, encoding choices. Default values are optional but recommended.localparam— fixed at the module level. Use for derived values (like$clog2(DEPTH)) and internal constants that should never be overridden. Default values are required.
Both keywords are resolved at elaboration time — by the time simulation or synthesis starts, the values are constants. No simulation overhead; no synthesis surprise.
The override mechanism:
- Named override (preferred):
module #(.PARAM(value)) instance (...)— clear, robust. - Ordered override (legacy):
module #(value1, value2) instance (...)— fragile, forbidden in modern style. defparam— deprecated; never use in new code.
The canonical patterns:
- Width and depth as
parameter. Address width aslocalparam $clog2(DEPTH). - FSM state names as
localparam. Self-documenting; survives refactoring. - Single source module + N parameter overrides = N hardware variants. The foundation of reusable IP design.
The day-to-day discipline:
parameterfor user configuration,localparamfor derived and internal.- Named overrides at every instantiation, never ordered.
defparamis forbidden.ALL_CAPSnaming for constants.WIDTH,DEPTH,IDLE,MAX_COUNT.- Always provide default values for
parameter. Documents the module's intended use.
The two deep sub-pages drill into each keyword: 6.1 parameter covers the override mechanics, hierarchical parameter passing, and the defparam migration; 6.2 localparam covers the derived-value patterns and the FSM state-encoding idiom. After Chapter 6, Chapter 7 picks up with compiler directives — the `-prefixed constructs (`define, `ifdef, `include, `timescale) that configure compilation rather than describe hardware.
Related Tutorials
- Scalar vs Vector vs Arrays — Chapter 5.2.4; the dimensions that parameter-derived widths configure.
- reg — Chapter 5.2.1; the runtime-variable counterpart to compile-time constants.
- Variables & Data Types — Chapter 5; the broader variable family.
- RTL Designing — Chapter 3; the broader RTL framing that motivates parameterised reuse.
- Number Representation — Chapter 4.4; literal syntax for parameter values.