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
endmodule
After 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.

parameterlocalparam
Overridable via #( ) or defparamYesNo
Allowed in the #( ) parameter port listYesNo (in Verilog-2005; SystemVerilog allows it)
Can be computed from other parametersYesYes — its main use
Typical usePublic knob: width, depth, latencyDerived 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
endmodule
tb_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
endmodule
tb_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:

  1. 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.
  2. Elaboration order becomes subtle when multiple defparam statements target the same parameter; the standard has rules, but humans forget them.
  3. 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 always or initial block. 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'b1 and {WIDTH{1'b0}} scale with WIDTH; a stray 4'd1 or 4'b0000 does not.
  • Parameters drive generate blocks. generate if and generate for conditions 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 $error for elaboration checks, but a common trick is an initial block that calls $display and $finish when, say, DEPTH is 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.

Have questions about this topic?
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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD