Skip to content

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

One module. The WIDTH parameter defaults to 32 but can be overridden at each instantiation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
end

Three problems with the reg approach:

  1. Not actually constant. A reg is a procedural-assignment target; nothing prevents a stray FIFO_DEPTH = 8'h20; somewhere from reassigning it. The "constant" can be silently modified by buggy code.
  2. 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.
  3. 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 parameter only for genuinely configurable values. Width, depth, mode flags, encoding choices. Things the module's user might want to control.
  • Use localparam for derived values. localparam ADDR_W = $clog2(DEPTH); is the canonical pattern — DEPTH is a parameter (overridable); ADDR_W is a localparam derived from it (not overridable, because changing it independently would break the design).
  • Don't use parameter for module-internal magic numbers. A constant like BURST_MAX = 16 that the user has no business changing should be localparam. Using parameter for it would mislead callers into thinking it's a knob.

4. The Two Keywords at a Glance

Aspectparameterlocalparam
Compile-time constantyesyes
Overridable at instantiationyesno
Typical usewidth, depth, mode, encodingderived values, internal constants
Synthesis behaviourfolded into expressionsfolded into expressions
Introduction dateVerilog-1995Verilog-2001
Scopingmodule-level (or generate block)module-level (or generate block)
Value can change at simulation timenono
Default value requiredrecommended (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.

parameter-syntax.v — every legal form
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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;
    // ...
endmodule

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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];
    // ...
endmodule

Each 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_WIDTHWIDTHW) — only the override syntax connects them.

6. The localparam Keyword

Declared inside a module. Cannot be overridden. Used for values that should be fixed.

localparam-syntax.v — fixed compile-time constants
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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 ...
endmodule

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

fsm-state-encoding.v — localparam for state names
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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);
endmodule

The 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 flow
parameter vs localparam — module interface visibilitymoduleencapsulates parametersparameter XOVERRIDABLE by parentlocalparam YFIXED at module levelparent modulecan override parameter Xlogic insideuses X and Y in expressions
The parameter is publicly overridable — the parent module connects to it via the instantiation syntax (#(.X(value))). The localparam is fixed — no external override path exists. Both are constants inside the module's logic, but the parameter exposes a knob to the user.

7.2 Visual B — the parameterisation pattern

A single source module produces N variant instances via parameter overrides.

Parameterisation patternup_counter.vsingle source moduleinstance 1WIDTH=8 (UART counter)instance 2WIDTH=48 (timestamp)instance 3WIDTH=20 (APB watchdog)12
The parameterisation pattern: one source module + N parameter overrides = N different hardware variants. Each instantiation produces a separate physical block in the netlist (8-bit, 32-bit, 48-bit counters), but all share the same source code. Bug fixes apply once and propagate to every instance.

7.3 Visual C — derived localparam

A parameter and its derived localparams. The user overrides DEPTH; the localparams automatically follow.

parameter and derived localparamparameter DEPTH = 16user-overridablelocalparam ADDR_W =$clog2(DEPTH)= 4 (derived)localparam CNT_W =$clog2(DEPTH+1)= 5 (derived)reg [ADDR_W-1:0] addraddress width follows12
parameter DEPTH is the user-overridable knob. localparam ADDR_W = $clog2(DEPTH) is derived — it tracks DEPTH automatically. Override DEPTH = 64 at instantiation, and ADDR_W becomes 6 without further user intervention. The localparam is the protection against inconsistency: users cannot override ADDR_W to a value that doesn't match DEPTH.

8. The Parameterised FIFO — Canonical Example

A single FIFO module that supports any data width and any depth via parameters and derived localparams.

rtl/parameterised_fifo.v — production-grade FIFO
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`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 (...);
endmodule

Three users instantiate this FIFO at different sizes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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;
    // ...
endmodule

The defparam override is forbidden in modern code for three reasons:

  1. Action-at-a-distance. The override happens in top, but the affected parameter is in sub_module. Reading the sub_module's declaration doesn't reveal the override; you have to search the entire codebase for defparam statements affecting this instance.
  2. Hierarchical naming required. The path u_sub.WIDTH ties the override to the specific instance name; renaming the instance in top breaks the override silently.
  3. Removed from SystemVerilog. SystemVerilog (Verilog's successor) deprecated defparam and most synthesis flows reject it entirely. Code using defparam will not survive a migration to SystemVerilog.

The canonical alternative is the instantiation-time override introduced in §5.1:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sub_module #(.WIDTH(16)) u_sub (...);     // clear, scope-correct, modern

Every 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module bad #(parameter BURST_MAX = 16) (...);   // BURST_MAX is internal — should be localparam

A 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"

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg [7:0] FIFO_DEPTH = 8'h10;    // not actually constant — can be overwritten

Use 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 default

The 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

  1. Use parameter for genuinely user-configurable values. Width, depth, mode, encoding.
  2. Use localparam for derived values and internal constants. $clog2()-based widths, state-encoding names, magic numbers that users have no business changing.
  3. Always use named overrides at instantiation. #(.PARAM(value)), never #(value).
  4. Never use defparam. It's deprecated, action-at-a-distance, and rejected by SystemVerilog.
  5. Name constants in ALL_CAPS. Convention: WIDTH, DEPTH, IDLE_STATE, MAX_COUNT.
  6. Provide default values for every parameter. Even if every instantiation overrides them, the default documents the module's intended use.
  7. Use localparam for FSM state encoding. Named states (IDLE, BUSY, DONE) survive code-review better than raw 2'b00, 2'b01, 2'b11.
  8. 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
1FIFO depth (user wants to choose)
2FIFO address width (= log2 of depth)
3UART baud rate (configurable per chip)
4Boot vector address (= 0x1000 — chip-fixed)
5Number of pipeline stages (per-instance configurable)
6FSM 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 en is asserted; resets to RESET_VAL on !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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;  // ?
endmodule

What 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 as localparam $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:

  • parameter for user configuration, localparam for derived and internal.
  • Named overrides at every instantiation, never ordered.
  • defparam is forbidden.
  • ALL_CAPS naming 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.