Verilog · lesson 12

Parameters and Generate

Making one module work at any width with parameter, replicating hardware with a generate loop, and sizing a vector automatically with $clog2.

10 min read · Free, no login

One module, many widths

Every module in this course so far has a fixed width baked into its port declarations โ€” lesson 7's counter is permanently 4 bits, lesson 2's half adder is permanently 1 bit. A parameter removes that limitation: it declares a named constant that an instance can override at instantiation time, letting the exact same module description become a 4-bit adder in one place and a 64-bit adder somewhere else, with no code duplicated and no copy to keep in sync.

A parameterized, bit-width-independent ripple-carry adder
module full_adder (input wire a, input wire b, input wire cin, output wire sum, output wire cout);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (cin & (a ^ b));
endmodule

module ripple_adder #(
    parameter WIDTH = 4
) (
    input  wire [WIDTH-1:0] a,
    input  wire [WIDTH-1:0] b,
    input  wire             cin,
    output wire [WIDTH-1:0] sum,
    output wire             cout
);
    wire [WIDTH:0] carry;
    assign carry[0] = cin;
    assign cout = carry[WIDTH];

    genvar i;
    generate
        for (i = 0; i < WIDTH; i = i + 1) begin : stage
            full_adder fa (.a(a[i]), .b(b[i]), .cin(carry[i]), .sum(sum[i]), .cout(carry[i+1]));
        end
    endgenerate
endmodule

The #(parameter WIDTH = 4) block, written right after the module name, declares WIDTH with a default value of 4 โ€” every port declaration below it is written in terms of WIDTH rather than a hard-coded number, which is what makes the whole module scale. An instance overrides the default the same way a port list is connected, using the #( ) syntax:

ripple_adder #(.WIDTH(4)) add4 (.a(a4), .b(b4), .cin(1'b0), .sum(sum4), .cout(cout4));
ripple_adder #(.WIDTH(8)) add8 (.a(a8), .b(b8), .cin(1'b0), .sum(sum8), .cout(cout8));

These two instances come from the exact same source file, with no copy-pasting, and genuinely behave as different-width adders:

WIDTH=4: f + 1 = {cout=1, sum=0}
WIDTH=8: ff + 01 = {cout=1, sum=00}

Both results are a correct overflow: 0xF + 0x1 = 0x10, which needs 5 bits and does not fit in a 4-bit sum, so sum reads 0 with cout correctly flagging the carry out โ€” exactly lesson 4's width-truncation rule, now happening on purpose and handled correctly because cout exists specifically to catch it. The 8-bit instance shows the identical pattern one bit wider (0xFF + 0x01 = 0x100). Synthesizing the 4-bit instance confirms the generated hardware actually scales with WIDTH too: the netlist contains exactly four full adders' worth of logic, not some fixed amount.

The generate loop: building hardware, not looping over time

generate/for/endgenerate looks like an ordinary software loop, and this is exactly the thing to unlearn about it immediately: it does not execute at simulation time the way an initial block's loop does. It runs at elaboration time โ€” before simulation or synthesis even begins โ€” and its only job is to stamp out a fixed number of copies of whatever is inside it, based on the loop bound, which must be a constant (a parameter or a plain number) known at that point, never a value that changes while the design is running. genvar i declares the loop's index specifically for this elaboration-time use; it is not a hardware signal and has no width or storage of its own. The begin : stage label is not optional styling โ€” naming a generate block gives each stamped-out copy of the hardware inside it a distinct hierarchical name (stage[0].fa, stage[1].fa, and so on), which is what a waveform viewer or a synthesis report actually shows when asked to look inside a specific stage.

The adder above could have been written by hand-instantiating four (or eight, or sixty-four) separate full_adder copies with explicit indices, the way a module written before generate existed (Verilog-2001 added it) would have had to. generate is exactly that manual repetition, done once in the source and expanded automatically to match WIDTH โ€” the same value used to size the ports is the value that decides how many times the loop body is stamped out, so the two can never drift out of sync the way two separately hand-maintained copies eventually would.

Sizing a vector automatically with $clog2

A very common parameterization need is an address or index vector sized to exactly cover some number of entries โ€” a DEPTH-entry memory needs an address bus wide enough to reach every entry and no wider. $clog2(N) computes exactly that: the ceiling of log base 2 of N, which is the minimum number of bits needed to represent N distinct values.

DEPTH=64 -> $clog2(DEPTH)=6 bits needed
$clog2(1)=0  $clog2(2)=1  $clog2(5)=3  $clog2(8)=3  $clog2(9)=4

A 64-entry memory needs exactly 6 address bits (26 = 64, addresses 0 through 63), which $clog2(64) gives directly. The values worth noticing specifically are the edge cases: $clog2(8) = 3 (an exact power of two needs exactly that many bits, with every address used), while $clog2(9) = 4 (one entry past a power of two needs a whole extra bit, since 3 bits can only reach 8 distinct addresses). Writing [$clog2(DEPTH)-1:0] on an address port, with DEPTH itself a parameter, means changing one number (DEPTH) automatically resizes the address bus to match โ€” exactly the kind of single-source-of-truth parameterization this lesson's adder example did for bit width.

Common mistakes

Using a variable that changes during simulation as a generate loop bound is the first โ€” the bound has to be resolvable at elaboration time, before the design even starts running, so only a parameter, a localparam, or a plain constant is legal there. The second is forgetting the named label on a generate block (begin : stage) โ€” the code still compiles without it in most tools, but the generated hierarchy becomes much harder to navigate in a waveform viewer or a synthesis report once there is more than one instance to tell apart. The third is hard-coding a width or a depth as a bare number somewhere inside a module that is otherwise fully parameterized โ€” the moment one number is missed, the module silently stops actually scaling with its own parameter for that one calculation, and nothing catches the mismatch until a non-default width is actually used and produces a wrong result.

Interview questions

What is the difference between a generate loop and an ordinary procedural loop inside an initial or always block? A generate loop runs at elaboration time, before simulation begins, and its only effect is to stamp out a fixed number of copies of hardware โ€” its bound must be a compile-time constant. A procedural loop runs during simulation itself, as part of the behavior being modeled, and its bound can depend on a signal's current value.

What does $clog2 compute, and what is it typically used for? The ceiling of log base 2 of its argument โ€” the minimum number of bits needed to represent that many distinct values. It is commonly used to size an address or index vector from a DEPTH or SIZE parameter, so the two stay automatically in sync as the parameter changes.

Why does a parameterized module need a named label on each generate block? Each stamped-out copy of the block's hardware gets a distinct hierarchical name built from that label and its loop index โ€” without a label, the generated instances are far harder to identify individually in a waveform viewer or a synthesis report, especially once a module is instantiated with more than one copy per stage.

Key takeaways

  • A parameter lets one module description serve many bit widths, overridden per instance with #(.PARAM(value)) โ€” confirmed here with the identical adder module correctly producing both a 4-bit and an 8-bit overflow from the same source file.
  • A generate/for loop runs at elaboration time, not simulation time, and its bound must be a compile-time constant โ€” its only job is replicating hardware a fixed number of times.
  • Label every generate block (begin : name) so each replicated instance gets a distinct, navigable hierarchical name.
  • $clog2(N) computes the minimum bits needed to address N entries, automatically keeping an address bus width in sync with a DEPTH parameter rather than requiring it to be maintained by hand.