Parameters, generate and $clog2

Write a width once as a parameter, derive every other width from it with $clog2, and build copies of hardware with a generate loop that the Gates tab shows by name.

Lesson
R1.7
Time
25 min
Runs on
Your browser
Check
4 questions

Two copies of an adder tree went into one chip: one summed four inputs, the other eight. The output port grew with N through $clog2(N), but the partial sums inside kept a width written for four inputs. The four-input copy passed every test, and the eight-input copy lost its top bit whenever the sum reached 1024. The width lived in two places, and only one of them followed the parameter.

The same design at N = 4 and N = 8Both copies get the same stimulus in every cycle. sum4 always matches its model. sum8 matches until the true sum passes 1023, the largest value the 10-bit partial sums can hold.
01234clksum441004008301020sum883608007161016want8836080017402040
  1. Cycle 2

    Every input is 100. The eight-input sum is 800, which fits in 10 bits, so both copies are right.

  2. Cycle 3

    The true sum is 1740. The partial sums near the root hold 10 bits, so 1024 is lost: 1740 - 1024 = 716.

  3. Cycle 4

    Every input is 255. The four-input copy gives 1020, the largest sum it will ever see. The eight-input copy gives 1016 where 2040 is right.

Measured on the bench below, one line per cycle. The output port of the eight-input copy is 11 bits wide; the bit it needs was lost inside.

One number, every width derived from it

A parameter is a constant with a name, which each instance of a module may set: adder_tree #(.N(8)) u8 (...). A localparam is a constant computed inside the module that no instance can override. Write each free choice once, as a parameter, and derive everything else from it.

$clog2(x) returns the base-2 logarithm of x, rounded up. N inputs of W bits sum to at most N times (2^W - 1), which needs W + $clog2(N) bits. Four inputs of 8 bits need 10 bits; eight need 11.

The width written for one case

logic [W+1:0] s;   // W bits and two carry bits

Right while N is at most 4. At N = 8 the root drops bit 10, worth 1024.

The width derived from the parameters

localparam int OW = W + $clog2(N);
logic [OW-1:0] s;

One formula. Every instance gets the width its own N and W need.

Looks likeRuns atWhat it becomes
parameter, localparamelaborationa number fixed per instance before any hardware exists
$clog2(N), $bits(x)elaborationa number; no gates
for (genvar i ...) outside a processelaborationone named copy of its body per value of i
if (N < 2) outside a processelaborationwhich hardware exists, or none of it
for (int i ...) inside always_combeach time the block runs, in simulationsynthesis unrolls it into one copy per pass; the bounds must be constants
a variable, an if on a signalevery cyclegates and multiplexers in the chip

Elaboration is the step after reading the code: the tool fixes the parameters and builds the instances. Only the last row is still there when the chip runs.

A generate loop builds copies

A for loop with a genvar outside any process is a generate loop. The tool runs it at elaboration. Each pass creates one block with its own name: g_node[0], g_node[1], and so on. Each block has its own signal s, named g_node[3].s, so the blocks can refer to one another.

adder_tree at N = 4, after elaborationThe generate loop ran seven times, once for each node of the tree. Arrows carry data. Nodes 3 to 6 copy the inputs, nodes 1 and 2 add pairs, node 0 is the root and drives sum.
adder_tree at N = 4, after elaborationThe generate loop ran seven times, once for each node of the tree. Arrows carry data. Nodes 3 to 6 copy the inputs, nodes 1 and 2 add pairs, node 0 is the root and drives `sum`. manager N = 4, W = 8 OW = 8 + $clog2(4) =10 manager in[0] .. in[3] input ports, 8 bitseach bus g_node[3..6] g_in: s = the input,widened bus g_node[1] g_add: s = node 3 +node 4 bus g_node[2] g_add: s = node 5 +node 6 completer g_node[0] g_add: s = node 1 +node 2; drives sum sets widths andcopiesdatanodes 3, 4nodes 5, 6partial sumpartial sum
  1. Fix the numbers

    Elaboration reads N = 4 and W = 8 and computes OW = 10. The loop will run 2N - 1 = 7 times.

  2. Unroll

    Passes 3 to 6 take the g_in branch: four copies that widen an input. Passes 0 to 2 take g_add: three adders.

    Follow the calls, values and waits
    1. call OW = 10: N = 4, W = 8 → g_node[3..6]
  3. Data flows

    With every input at 255, nodes 1 and 2 each hold 510 and the root holds 1020, which fits in 10 bits.

    Follow the calls, values and waits
    1. data 255 each: in[0] .. in[3] → g_node[3..6]
    2. data 510: g_node[3..6] → g_node[1]
    3. data 1020: g_node[1] → g_node[0]
Read the diagram as text
  • N = 4, W = 8: OW = 8 + $clog2(4) = 10
  • in[0] .. in[3]: input ports, 8 bits each
  • g_node[3..6]: g_in: s = the input, widened
  • g_node[1]: g_add: s = node 3 + node 4
  • g_node[2]: g_add: s = node 5 + node 6
  • g_node[0]: g_add: s = node 1 + node 2; drives sum
  • N = 4, W = 8 → g_node[3..6]: sets widths and copies
  • in[0] .. in[3] → g_node[3..6]: data
  • g_node[3..6] → g_node[1]: nodes 3, 4
  • g_node[3..6] → g_node[2]: nodes 5, 6
  • g_node[1] → g_node[0]: partial sum
  • g_node[2] → g_node[0]: partial sum

At N = 8 the same loop builds 15 blocks and one more level of adders. The synthesized netlist keeps the names g_node[0].s to g_node[6].s.

NWCellsLevels of logic on this flow
4811919
8828125
16860731
41625236

The corrected design, measured with Yosys 0.70 on this flow at four parameter sets. Doubling N adds one level of adders, six levels of logic here. Doubling W roughly doubles both the cells and the levels.

Refuse a bad parameter, and leave macros behind

Some parameter values make no sense. With N = 1 there is nothing to add. A generate if with $error inside reports the value at elaboration. The tools do not agree on what happens next, so the legal range belongs in the brief too.

Tooladder_tree with N = 1
Yosys 0.70 (Synthesize)refused: $error encountered: adder_tree: N must be at least 2, got 1
Verilator 5.052 (Lint)a warning, USERERROR: adder_tree: N must be at least 2, got 1
Icarus Verilog 12.0 (Run)no message; it builds and runs the one-input design

Measured with the check from the lab design.

Older code uses a macro for the same job, a name defined with the define compiler directive. A macro is text substitution with no scope. It applies to every file compiled after it, until something redefines it. Which value a block sees depends on the compile order. A parameter belongs to one instance. Use parameters, and localparam for every derived value.

Design brief
PortDirectionWidthMeaning
ininput[N-1:0][W-1:0]N inputs of W bits
sumoutput[OW-1:0]sum = in[0] + in[1] + ... + in[N-1], never truncated; OW = W + $clog2(N)
Latency
combinational: sum follows the inputs in the same cycle
Parameters
N inputs, at least 2 (any N, not only powers of two); W bits per input
Corner cases
every input at 2^W - 1; N = 8 next to N = 4; N = 1 refused

The lab ships the adder tree as the team wrote it. Run it: the N = 4 copy is right in all five cycles, the N = 8 copy is wrong twice. Change logic [W+1:0] s; to logic [OW-1:0] s; and run again: zero wrong sums. Synthesize gives 119 cells. Then change the default N = 4 to 8, as an instance would: 281 cells, 25 levels.

Asked in interviews
  1. Parameter or macro, and why?

    A parameter. It belongs to one module and each instance can set its own value. A macro is global text substitution: it leaks into every file compiled after it, and two instances cannot have two values.

  2. What does $clog2(DEPTH) give you?

    The number of address bits for DEPTH entries: the base-2 log rounded up. $clog2(8) is 3, $clog2(9) is 4, $clog2(1) is 0. It is computed at elaboration and costs no gates.

  3. What happens at elaboration?

    The tool fixes every parameter, builds each instance, and unrolls generate loops into named blocks. Generate ifs choose which hardware exists. What remains is fixed hardware; nothing of the loop runs in the chip.

Try it

The N = 4 copy is right in every cycle, and so is the N = 8 copy until its true sum passes 1023. Then 1740 prints as 716 and 2040 as 1016: the partial sums hold 10 bits, and the 1024 bit falls off at the root.

Change the width of s to [OW-1:0] and run again: every line matches its model and the summary reads 0 wrong sums twice. The design at its default N = 4 synthesizes to the same 119 cells either way, because there W + 2 and OW are both 10.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e0 N=4: 4 (want 4) N=8: 8 (want 8) e1 N=4: 100 (want 100) N=8: 360 (want 360) e2 N=4: 400 (want 400) N=8: 800 (want 800) e3 N=4: 830 (want 830) N=8: 716 (want 1740) wrong e4 N=4: 1020 (want 1020) N=8: 1016 (want 2040) wrong N=4: 0 wrong sums, N=8: 2 wrong sums

Original design · 119 cells · 0 flip-flops · 0 latches · 19 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic [3:0][7:0] in4;                   // N = 4, W = 8
  logic [7:0][7:0] in8;                   // N = 8, W = 8
  logic [9:0]      sum4;                  // 8 + $clog2(4) = 10 bits
  logic [10:0]     sum8;                  // 8 + $clog2(8) = 11 bits
  int want4, want8, wrong4 = 0, wrong8 = 0;
  // Input j of both copies gets base + step * j; the model adds them up.
  task automatic fill(input int base, input int step);
    want4 = 0; want8 = 0;
    for (int j = 0; j < 4; j++) begin in4[j] = 8'(base + step * j); want4 += in4[j]; end
    for (int j = 0; j < 8; j++) begin in8[j] = 8'(base + step * j); want8 += in8[j]; end
  endtask
  adder_tree #(.N(4), .W(8)) u4 (.in(in4), .sum(sum4));
  adder_tree #(.N(8), .W(8)) u8 (.in(in8), .sum(sum8));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (sum4 != want4) wrong4++;
    if (sum8 != want8) wrong8++;
    $display("e%0d N=4: %0d (want %0d)  N=8: %0d (want %0d)%s", edge_n, sum4, want4, sum8, want8,
             (sum4 == want4 && sum8 == want8) ? "" : "  wrong");
  endtask
  initial begin
    fill(1, 0);   tick();                 // every input 1
    fill(10, 10); tick();                 // 10, 20, 30, ...
    fill(100, 0); tick();                 // every input 100
    fill(200, 5); tick();                 // 200, 205, 210, ...
    fill(255, 0); tick();                 // every input at its largest value
    $display("N=4: %0d wrong sums, N=8: %0d wrong sums", wrong4, wrong8);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe module sizes an address from its DEPTH parameter. The bench builds it with 5, 8 and 9 entries and prints the address width of each. What is the output line?
module ptr #(parameter int DEPTH = 8) (output int bits);
  localparam int AW = $clog2(DEPTH);   // address width for DEPTH entries
  logic [AW-1:0] addr;
  assign bits = $bits(addr);
endmodule
module tb;
  int a, b, c;
  ptr #(.DEPTH(5)) p5 (.bits(a));
  ptr #(.DEPTH(8)) p8 (.bits(b));
  ptr #(.DEPTH(9)) p9 (.bits(c));
  initial begin
    #1 $display("%0d %0d %0d", a, b, c);
    $finish(0);
  end
endmodule
Spot the bugEach option is the body of all_ones, which sets y when every bit of the W-bit input x is 1. The bench builds it with W = 16 and W = 4. Which body fails?
ConceptA generate loop for (genvar i = 0; i < 8; i++) holds one adder. When does the loop run, and what does the chip contain?
ConceptA team sets the data width with a macro, WIDTH, defined as 8 in a header. A second block needs 16 bits, so they define WIDTH as 16 in that block's file. What goes wrong?

Remember

  • Write each free choice once as a parameter and derive every other width from it with localparam and $clog2; a width written twice will someday disagree.
  • $clog2(x) is the base-2 log rounded up: 8 entries need 3 address bits, 9 need 4. N inputs of W bits sum to W + $clog2(N) bits.
  • Generate loops and generate ifs run at elaboration: they build named copies of hardware (g_node[3].s) and choose which hardware exists; nothing of the loop runs in the chip.
  • Check a parameter's legal range with $error in a generate if; Yosys refuses, Verilator warns, Icarus 12.0 says nothing, so state the range in the brief as well.
  • Prefer parameters to macros: a macro has no scope, so the value a block sees depends on the compile order.