Skip to content

Verilog · Chapter 6.2 · Constant Variables

localparam in Verilog — Strictly-Internal Compile-Time Constants

The parameter keyword exposes configuration knobs that a module's users can change from outside. The localparam keyword does the opposite. It declares compile-time constants that are strictly internal to the module and cannot be overridden by any external instantiation. This page is a deep look at localparam and why it matters. You will see how it protects internal constants from accidental tampering, how it derives values such as bus widths from a parameter using the clog2 system function, and how it names state-encoding constants in finite-state machines. It also explains the design discipline of keeping user-facing knobs separate from internal magic numbers. Most reusable IP blocks use both keywords together for exactly this reason.

Foundation18 min readVeriloglocalparamConstants$clog2FSM Encoding

Chapter 6 · Section 6.2 · Constant Variables

1. The Engineering Problem

A team designs a parameterised FIFO with configurable depth. The first cut:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo #(
    parameter DEPTH  = 16,
    parameter ADDR_W = 4                          // user-overridable, "should match DEPTH"
)(
    input  wire [ADDR_W-1:0] addr,
    output wire [7:0]        data_out
    // ...
);
    reg [7:0] mem [0:DEPTH-1];
    // ...
endmodule

A user instantiates it with DEPTH = 32:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fifo #(.DEPTH(32), .ADDR_W(4)) u_fifo (...);     // 32 entries, but only 4 address bits!

The result: a 32-entry memory with only 16 addressable entries. The upper 16 entries are unreachable. addr overflows from 4'hF to 4'h0 and wraps around — every write to "address 16" silently overwrites address 0. The bug is invisible in code review (both parameters are explicit) and produces wrong results at runtime.

The right design encodes the constraint in the language: ADDR_W is derived from DEPTH, not user-configurable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo #(
    parameter DEPTH = 16                          // ONLY user-overridable parameter
)(
    input  wire [ADDR_W-1:0] addr,
    output wire [7:0]        data_out
);
    localparam ADDR_W = $clog2(DEPTH);            // derived from DEPTH, cannot be overridden
 
    reg [7:0] mem [0:DEPTH-1];
    // ...
endmodule

Now the user instantiation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fifo #(.DEPTH(32)) u_fifo (...);                  // ADDR_W = $clog2(32) = 5, automatically

The user cannot override ADDR_W — it's a localparam, and any attempt produces a compile error. The address width always matches the depth. The bug is impossible.

This is the role of localparam: encode the design's internal invariants as language-enforced constraints. Where parameter says "this is a configuration knob the user can turn," localparam says "this is an internal constant the user has no business touching." The two keywords together form the design contract: knobs vs constants.

2. Anatomy of a localparam Declaration

The syntax mirrors parameter but with one difference: there is no port-list form. localparam declarations live in the module body only.

localparam-syntax.v — every legal form
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module example #(
    parameter DEPTH = 16,                         // user-overridable
    parameter MODE  = 0
)(
    input  wire clk
);
    // localparam declarations — module-body only
    localparam ADDR_W      = $clog2(DEPTH);       // derived from DEPTH
    localparam BURST_MAX   = 8;                    // internal magic number
    localparam [3:0] IDLE  = 4'h0;                 // typed FSM state
    localparam [3:0] ACTIVE = 4'h1;
    localparam [3:0] WAIT  = 4'h2;
    localparam [3:0] DONE  = 4'h3;
 
    // ... use ADDR_W, BURST_MAX, IDLE, ACTIVE, WAIT, DONE anywhere ...
endmodule

Three things to note:

  • No port-list form. Unlike parameter, localparam cannot appear in the #(...) syntax. It must be declared inside the module body.
  • No default-value override path. The right-hand-side expression is the value; no external instantiation can change it.
  • Right-hand side can reference parameters. localparam ADDR_W = $clog2(DEPTH); is the canonical pattern — derive a localparam value from a parameter so it always tracks.

The convention: ALL_CAPS names, same as parameter. The convention places localparam declarations after all parameter declarations in the module body, so the derivation order is clear.

3. Mental Model

The mental-model has three practical consequences:

  • localparam is the default choice for internal constants. If you're declaring a constant and the user has no business changing it, use localparam. Only when the constant is genuinely a knob does it become parameter.
  • Derive everything you can. A FIFO's address width, a memory's read-port count's bit width, a counter's max value — all derivable. Declaring them as localparam removes inconsistency risks.
  • Use named constants for FSM states. Raw 2'b00, 2'b01 is a code smell. Named constants IDLE, ACTIVE self-document and survive refactoring.

4. The Two Keywords Side-by-Side

Aspectparameterlocalparam
Compile-time constantyesyes
Overridable externallyyes (via instantiation)no
Declaration in port-list #(...)yes (Verilog-2001)no
Declaration in module bodyyes (Verilog-1995 form)yes
Default value requiredrecommendedrequired (no override mechanism)
Typical usewidth, depth, mode, encodingderived values, internal constants, FSM states
Synthesis behaviourfolded into expressionsfolded into expressions
Simulation costzero (compile-time)zero (compile-time)

The single observable difference is overridability. Everything else — synthesis, simulation, scoping — is identical.

5. The $clog2() Pattern

The canonical use case. The IEEE 1364-2005 $clog2() system function returns the ceiling of log base 2 — the number of bits needed to represent values 0 through N-1. The pattern:

clog2-pattern.v — derive address width from depth
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module memory #(
    parameter DEPTH = 16
)(
    input  wire [ADDR_W-1:0] addr,
    input  wire              wr_en,
    input  wire [7:0]        wr_data,
    output wire [7:0]        rd_data
);
    localparam ADDR_W = $clog2(DEPTH);
 
    reg [7:0] mem [0:DEPTH-1];
 
    always @(posedge clk) if (wr_en) mem[addr] <= wr_data;
    assign rd_data = mem[addr];
endmodule

For each common DEPTH, the resulting ADDR_W:

DEPTHADDR_W (= $clog2(DEPTH))Bits Needed
101-entry — no address needed
212 entries, 1 bit
424 entries, 2 bits
838 entries, 3 bits
16416 entries, 4 bits
175needs 5 bits (since 2^4 = 16 < 17)
32532 entries, 5 bits
2568256 entries, 8 bits
1024101K entries, 10 bits

The function rounds up — $clog2(17) = 5 because 17 doesn't fit in 4 bits. For powers of 2, $clog2(N) = log2(N) exactly.

5.1 Variants

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam ADDR_W = $clog2(DEPTH);                // simple address-width derivation
localparam CNT_W  = $clog2(DEPTH + 1);             // count needs one extra value (e.g., for "full")
localparam REG_CNT = NUM_REGS * REGS_PER_BANK;     // arithmetic combination of parameters
localparam BUS_WIDTH = DATA_WIDTH + ECC_BITS;      // composite width

Any expression involving parameter values and constants is legal as the right-hand side of a localparam. The elaborator computes the value at compile time.

5.2 $clog2() edge cases

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$clog2(0)    // = 0 (some tools); 1 (other tools) — implementation-defined
$clog2(1)    // = 0 (a 1-entry "memory" needs 0 address bits — only one address possible)
$clog2(2)    // = 1
$clog2(2**31) // = 31 (handles large values fine on 32-bit systems)

For DEPTH < 2, $clog2() may return 0 (in which case you'd declare a reg [-1:0] — undefined). The defensive pattern:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam ADDR_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH);   // minimum 1 bit

Most production codebases ensure DEPTH ≥ 2 at the architectural level, so the edge case rarely matters in practice. The $clog2(1) = 0 corner is the only one worth knowing about.

6. The FSM State Encoding Pattern

The second canonical use. FSM state values declared as named localparams.

rtl/fsm_encoding.v — named state-encoding
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 — named constants for readability
    localparam [1:0] IDLE   = 2'b00;
    localparam [1:0] ACTIVE = 2'b01;
    localparam [1:0] WAIT   = 2'b10;
    localparam [1:0] FINISH = 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 = FINISH;
            FINISH:             state_next = IDLE;
        endcase
    end
 
    always @(*) busy = (state_q != IDLE);
endmodule

The four localparam declarations name the states. Three benefits:

  1. Readability. if (start) state_next = ACTIVE; reads better than if (start) state_next = 2'b01;.
  2. Refactoring safety. Changing the encoding (e.g., from binary to one-hot for power optimisation) requires updating only the localparam declarations, not every reference.
  3. Code review. Typing 2'b10 instead of 2'b11 is hard to spot in code review; typing WAIT instead of FINISH is obvious.

The convention: all state names declared as localparams with explicit width ([STATE_W-1:0]). The STATE_W itself is often a localparam derived from the state count: localparam STATE_W = $clog2(NUM_STATES);.

6.1 One-hot encoding variant

For power-optimised designs (avoiding multi-bit transitions per cycle), one-hot encoding uses one bit per state:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam [3:0] IDLE   = 4'b0001;        // bit 0 = IDLE
localparam [3:0] ACTIVE = 4'b0010;        // bit 1 = ACTIVE
localparam [3:0] WAIT   = 4'b0100;        // bit 2 = WAIT
localparam [3:0] FINISH = 4'b1000;        // bit 3 = FINISH

The transition between states changes exactly two bits (one from 1 → 0, one from 0 → 1), reducing dynamic power. The localparam form makes the encoding choice swappable — change the four declarations from binary to one-hot, and the rest of the FSM code stays unchanged.

7. Visual Explanation

Three figures cover the localparam patterns.

7.1 Visual A — protected internal constant

The interface boundary. parameter is exposed; localparam is internal.

parameter vs localparam — interface boundary

data flow
parameter vs localparam — interface boundarymodule userinstantiates with overridesparameteroverridable knoblocalparamPROTECTED — cannot overridemodule logicuses both internally
The interface boundary. parameter is on the public side; the user can change it. localparam is on the protected side; no override mechanism reaches it. Both constants are used internally by the module's logic, but only parameter is exposed in the instantiation contract.

7.2 Visual B — the derived-value cascade

A parameter flowing into multiple localparams, all of which derive from it.

Parameter to localparam derivation cascadeparameter DEPTH = 16user knoblocalparam ADDR_W =$clog2(DEPTH)address width = 4localparam CNT_W =$clog2(DEPTH+1)count width = 5localparam HALF_DEPTH= DEPTH/2threshold = 812
parameter DEPTH is the user knob. Three derived localparams track it automatically: ADDR_W = $clog2(DEPTH) for the address bus width; CNT_W = $clog2(DEPTH+1) for the count register; HALF_DEPTH = DEPTH/2 for an interrupt threshold. Override DEPTH = 256 and all three localparams update at elaboration. No way for the user to break the consistency by mismatched overrides.

7.3 Visual C — FSM state encoding swap

The named-state pattern lets you swap encodings without touching FSM logic.

Binary vs one-hot FSM encodingBINARYlocalparam IDLE=2'b00,ACTIVE=2'b01, ...ONE-HOTlocalparam IDLE=4'b0001,ACTIVE=4'b0010, ...FSM logiccase (state_q) ... —unchanged12
Two encodings, same FSM. The binary version uses 2 bits and minimum-width state register. The one-hot version uses 4 bits but each transition changes only 2 bits (lower dynamic power). The localparam declarations are the only difference — the FSM's case-statement logic is identical for both. Switching encodings is a 4-line edit.

8. The Parameterised FIFO — Complete Example

A real-world FIFO with parameters for user knobs and localparams for derived values.

rtl/fifo_localparam.v — production-grade FIFO using both keywords
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`default_nettype none
 
module fifo #(
    parameter DATA_WIDTH = 8,                      // user-overridable
    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,
    output wire [CNT_W-1:0]      count
);
    // Derived constants — strictly internal
    localparam ADDR_W = $clog2(DEPTH);            // bit width to address DEPTH entries
    localparam CNT_W  = $clog2(DEPTH + 1);         // count needs DEPTH+1 representable values
 
    // Internal state
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    reg [ADDR_W-1:0]     wr_ptr_q;
    reg [ADDR_W-1:0]     rd_ptr_q;
    reg [CNT_W-1:0]      count_q;
 
    integer i;
 
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr_q <= {ADDR_W{1'b0}};
            rd_ptr_q <= {ADDR_W{1'b0}};
            count_q  <= {CNT_W{1'b0}};
            for (i = 0; i < DEPTH; i = i + 1)
                mem[i] <= {DATA_WIDTH{1'b0}};
        end else begin
            if (push && !pop) count_q <= count_q + 1'b1;
            if (!push && pop) count_q <= count_q - 1'b1;
 
            if (push) begin
                mem[wr_ptr_q] <= data_in;
                wr_ptr_q <= (wr_ptr_q == DEPTH-1) ? {ADDR_W{1'b0}} : wr_ptr_q + 1'b1;
            end
 
            if (pop) begin
                rd_ptr_q <= (rd_ptr_q == DEPTH-1) ? {ADDR_W{1'b0}} : rd_ptr_q + 1'b1;
            end
        end
    end
 
    assign data_out = mem[rd_ptr_q];
    assign full     = (count_q == DEPTH);
    assign empty    = (count_q == {CNT_W{1'b0}});
    assign count    = count_q;
endmodule
 
// Instantiation examples:
//   fifo #(.DATA_WIDTH(8),  .DEPTH(4))    u_uart_rx_fifo (...);
//   fifo #(.DATA_WIDTH(32), .DEPTH(64))   u_dma_buffer   (...);
//   fifo #(.DATA_WIDTH(64), .DEPTH(256))  u_packet_fifo  (...);

Two parameters expose the user knobs (data width, depth). Two localparams derive from DEPTH (address width, count width). The user cannot override ADDR_W or CNT_W because they're localparams — the language enforces the constraint.

The instantiations are simple: only the two parameters that matter to the user (data width, depth). Everything else follows automatically.

9. Simulation and Synthesis Behavior

localparam is resolved at elaboration time, identical to parameter. The elaborator substitutes the value into every expression that uses it; the simulator and synthesis tool see only the resolved constant.

The right-hand-side expression of a localparam can be any expression involving constants and parameters — $clog2(), arithmetic, conditional (? :), bitwise operators. The elaborator evaluates it once.

10. Common Mistakes

Five pitfalls that catch engineers using localparam.

10.1 Using parameter for derived values

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo #(
    parameter DEPTH  = 16,
    parameter ADDR_W = 4                          // BUG: should be localparam
);

Exposing ADDR_W as a parameter invites the §1 mismatch bug. Use localparam ADDR_W = $clog2(DEPTH); instead.

10.2 Using localparam for genuinely user-configurable values

The opposite mistake. A FIFO's DATA_WIDTH is genuinely user-configurable; using localparam forces every variant to hardcode it. Use parameter for user knobs.

10.3 Forgetting that localparam cannot be overridden

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo;
    localparam DEPTH = 16;
endmodule
 
// Trying to override
fifo #(.DEPTH(32)) u_fifo (...);                  // COMPILE ERROR

The compiler errors out: "DEPTH is a localparam and cannot be overridden." If you want the value to be overridable, change localparam to parameter.

10.4 Circular derivation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam A = B + 1;
localparam B = A * 2;                              // circular dependency

The elaborator cannot resolve A and B simultaneously; compilation fails. Make sure every localparam's right-hand side references only parameters, constants, and previously-declared localparams.

10.5 Forward reference

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fifo;
    localparam ADDR_W = $clog2(DEPTH);             // BUG: DEPTH not declared yet
    parameter DEPTH = 16;
endmodule

The forward reference produces a compile error or undefined behaviour. Declare parameters before any localparam that references them; this is the convention every working codebase enforces.

11. Industry Use Cases

Three patterns where localparam is essential.

11.1 Derived widths in parameterised IP

Every parameterised IP block (FIFO, register file, arbiter, decoder) has localparams deriving from its parameters. The pattern is universal: a DEPTH parameter and a localparam ADDR_W = $clog2(DEPTH); derivation, ensuring the address width always matches the depth.

11.2 FSM state encoding

Every FSM uses localparam for its state values. The named-state pattern is the standard idiom across every working RTL house. Mixing binary, gray, and one-hot encodings within a project is supported by switching only the localparam declarations — the FSM logic stays unchanged.

11.3 Module-internal constants

Protocol-defined constants (e.g., AHB burst lengths, AXI4 burst sizes, USB descriptor values) that come from external specifications are localparam — the protocol fixes them; no instantiation should override them. Exposing them as parameter would mislead users into thinking they have flexibility they don't.

12. Debugging Lab

Three localparam debug post-mortems

13. Coding Guidelines

  1. Use localparam for derived values. Any constant computed from a parameter should be localparam — the user shouldn't be able to override it independently.
  2. Use localparam for FSM state encoding. Named states (IDLE, ACTIVE, DONE) survive code-review and refactoring better than raw binary literals.
  3. Use localparam for module-internal magic numbers. Constants the user has no business overriding belong as localparam, not parameter.
  4. Declare localparams after parameters. Convention: parameters first, then localparams that derive from them.
  5. ALL_CAPS naming for localparam. Same convention as parameter.
  6. Typed localparams for FSM states. localparam [STATE_W-1:0] IDLE = 2'b00; makes the width explicit and prevents mismatched-width assignments.
  7. Avoid circular dependencies. Each localparam's right-hand side should reference only parameters, constants, and previously-declared localparams.
  8. Don't try to override a localparam. Use parameter if you want overridable; use localparam only when you don't.

14. Interview Q&A

15. Exercises

Three exercises that turn localparam usage into reflex.

Exercise 1 — Spot the misuse

For each declaration, recommend parameter, localparam, or "remove entirely."

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module memory (
    parameter DATA_WIDTH = 8,                      // ?
    parameter NUM_ENTRIES = 16,                    // ?
    parameter ADDR_W      = 4,                     // ?
    parameter signed ZERO = 32'h0000_0000,         // ?
    parameter CACHE_LINE_SIZE = 8,                 // ?  (always 8 in this design)
    localparam HALF_ENTRIES = NUM_ENTRIES / 2,    // ?
    localparam [1:0] IDLE = 2'b00                  // ?
);

Hints. User-configurable → parameter. Derived or design-constant → localparam. Used-as-constant-zero values rarely belong as parameter.

Exercise 2 — Convert FSM to named states

Convert the following FSM to use localparam-named state constants. The current encoding uses raw 3-bit binary values.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module traffic_light (
    input  wire clk, rst_n,
    output reg  green, yellow, red
);
    reg [2:0] state_q;
 
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) state_q <= 3'b000;
        else case (state_q)
            3'b000: state_q <= 3'b001;
            3'b001: state_q <= 3'b010;
            3'b010: state_q <= 3'b100;
            3'b100: state_q <= 3'b000;
        endcase
    end
 
    always @(*) begin
        green  = (state_q == 3'b001);
        yellow = (state_q == 3'b010);
        red    = (state_q == 3'b100);
    end
endmodule

Hints. Identify the four states and name them (RESET, GREEN, YELLOW, RED). Declare each as localparam [2:0]. Replace every raw 3'bXXX with the named version.

Exercise 3 — Parameterise a register file

Write a register file module with DATA_WIDTH and NUM_REGS as parameters and ADDR_W as a derived localparam. The module should:

  • Have a synchronous write port.
  • Have an asynchronous read port.
  • Reset every register to zero in the reset path.

Hints. Use localparam ADDR_W = $clog2(NUM_REGS); so the address bus width tracks NUM_REGS. Declare the storage as reg [DATA_WIDTH-1:0] rf [0:NUM_REGS-1];. Use a generate loop or integer for loop in the reset path.

16. Summary

localparam declares compile-time constants that cannot be overridden externally — the strict-internal counterpart to parameter. The keyword exists to protect internal constants from accidental tampering and to encode design invariants as language-enforced constraints.

The three canonical uses:

  • Derived values. localparam ADDR_W = $clog2(DEPTH); derives from a parameter, always tracks it, cannot be overridden independently.
  • FSM state encoding. localparam IDLE = 2'b00; declares named state constants for readability and refactoring safety.
  • Internal magic numbers. localparam BURST_MAX = 16; for constants the user has no business overriding.

The syntax:

  • Module-body only (no port-list form).
  • Right-hand side can be any expression involving parameters, constants, and previously-declared localparams.
  • Convention: ALL_CAPS names, declared after all parameters.

The keyword choice is interface design:

  • parameter — user-facing knob, part of the public API.
  • localparam — strictly internal, protected from external override.

Both produce identical hardware after synthesis — they fold into expressions at elaboration time. The simulator and synthesis tool see only the resolved constants.

The day-to-day discipline:

  • Default to localparam. Use parameter only when the value is genuinely a user knob.
  • Derive everything you can. A parameter for the user knob; one or more localparams for the derived widths and counts.
  • Use named state constants for FSMs. Raw binary literals are a code smell.
  • Declare parameters before localparams. Forward references produce compile errors.

The next chapter (Chapter 7) covers compiler directives — the `-prefixed constructs (`define, `ifdef, `include, `timescale) that configure compilation rather than describe hardware. Together with parameter and localparam, they form the compile-time configuration vocabulary of the language.