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.
- Cycle 2
Every input is 100. The eight-input sum is 800, which fits in 10 bits, so both copies are right.
- Cycle 3
The true sum is 1740. The partial sums near the root hold 10 bits, so 1024 is lost: 1740 - 1024 = 716.
- 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 like | Runs at | What it becomes |
|---|---|---|
parameter, localparam | elaboration | a number fixed per instance before any hardware exists |
$clog2(N), $bits(x) | elaboration | a number; no gates |
for (genvar i ...) outside a process | elaboration | one named copy of its body per value of i |
if (N < 2) outside a process | elaboration | which hardware exists, or none of it |
for (int i ...) inside always_comb | each time the block runs, in simulation | synthesis unrolls it into one copy per pass; the bounds must be constants |
a variable, an if on a signal | every cycle | gates 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.
sum.- Fix the numbers
Elaboration reads N = 4 and W = 8 and computes OW = 10. The loop will run 2N - 1 = 7 times.
- Unroll
Passes 3 to 6 take the
g_inbranch: four copies that widen an input. Passes 0 to 2 takeg_add: three adders.Follow the calls, values and waits
- call OW = 10: N = 4, W = 8 → g_node[3..6]
- 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
- data 255 each: in[0] .. in[3] → g_node[3..6]
- data 510: g_node[3..6] → g_node[1]
- 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.
| N | W | Cells | Levels of logic on this flow |
|---|---|---|---|
| 4 | 8 | 119 | 19 |
| 8 | 8 | 281 | 25 |
| 16 | 8 | 607 | 31 |
| 4 | 16 | 252 | 36 |
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.
| Tool | adder_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.
| Port | Direction | Width | Meaning |
|---|---|---|---|
| in | input | [N-1:0][W-1:0] | N inputs of W bits |
| sum | output | [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
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.
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.
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.
Synthesize the design to measure its cells and inspect the mapped gates.
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 sumsOriginal 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
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
$errorin 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.