Count Leading Zeros (CLZ) in Verilog: Parameterized Combinational Priority Logic
Counting leading zeros (CLZ) is a classic building block in floating-point normalization, priority encoders, logarithm approximation, and barrel-shifter control. The job is simple to state: given a binary word, how many 0 bits appear on the most-significant side before the first 1? This page delivers clean, synthesizable count leading zeros verilog code written as a parameterized combinational module, verified end-to-end with a self-checking testbench.
We keep everything in strict Verilog-2005 using reg/wire, an integer loop variable, and $clog2 to size the output. You will see the main CLZ design and a mirror-image count-trailing-zeros (CTZ) variant, so the priority-scan pattern becomes second nature for interviews and real RTL.
Parameterized CLZ module with a priority for-loop
The design scans from the MSB (W-1) down to the LSB. The first set bit it meets fixes the answer: the number of zero positions above it, W-1-i. A found flag freezes the result so lower set bits are ignored, giving true priority behaviour. An all-zero input falls through to the default W.
// Count Leading Zeros (CLZ) - combinational
// Counts zeros from the MSB side; all-zero input -> W.
module clz #(parameter W=8)(
input [W-1:0] in,
output reg [$clog2(W+1)-1:0] count
);
integer i;
reg found;
always @(*) begin
count = W[$clog2(W+1)-1:0]; // default: all-zero case
found = 1'b0;
// Scan from MSB (W-1) down to LSB (0)
for (i = W-1; i >= 0; i = i - 1) begin
if (!found && in[i]) begin
count = (W-1-i); // number of zeros above the top set bit
found = 1'b1;
end
end
end
endmodule
module tb;
reg [7:0] in;
wire [3:0] count;
// Reference leading-zero count
integer k;
integer ref_cnt;
reg ref_found;
clz #(.W(8)) dut (.in(in), .count(count));
task check;
input [7:0] v;
begin
in = v;
#1;
// compute reference
ref_cnt = 8; ref_found = 0;
for (k = 7; k >= 0; k = k - 1) begin
if (!ref_found && v[k]) begin
ref_cnt = 7 - k;
ref_found = 1;
end
end
if (count !== ref_cnt[3:0]) begin
$display("FAIL: in=0x%02h clz=%0d expected=%0d", v, count, ref_cnt);
$finish;
end else begin
$display("PASS: in=0x%02h -> leading zeros = %0d", v, count);
end
end
endtask
initial begin
check(8'h00); // 8
check(8'h01); // 7
check(8'h80); // 0
check(8'h0F); // 4
check(8'h20); // 2
check(8'hFF); // 0
check(8'h40); // 1
$display("PASS");
$finish;
end
endmodule
PASS: in=0x00 -> leading zeros = 8 PASS: in=0x01 -> leading zeros = 7 PASS: in=0x80 -> leading zeros = 0 PASS: in=0x0f -> leading zeros = 4 PASS: in=0x20 -> leading zeros = 2 PASS: in=0xff -> leading zeros = 0 PASS: in=0x40 -> leading zeros = 1 PASSEvery representative value in the spec (0x00 to 8, 0x01 to 7, 0x80 to 0, 0x0F to 4, 0x20 to 2) matches the independent reference loop, and the final
PASSline confirms the sweep.Variant: count trailing zeros (CTZ)
The trailing-zero count is the same idea with the scan direction flipped: walk from the LSB upward and the index
iof the first set bit is the answer directly. All-zero again defaults toW. Comparing the two makes the priority structure explicit.// Count Trailing Zeros (CTZ) - combinational // Counts zeros from the LSB side; all-zero input -> W. module ctz #(parameter W=8)( input [W-1:0] in, output reg [$clog2(W+1)-1:0] count ); integer i; reg found; always @(*) begin count = W[$clog2(W+1)-1:0]; // default: all-zero case found = 1'b0; // Scan from LSB (0) up to MSB (W-1) for (i = 0; i < W; i = i + 1) begin if (!found && in[i]) begin count = i[$clog2(W+1)-1:0]; found = 1'b1; end end end endmodule module tb; reg [7:0] in; wire [3:0] count; integer k; integer ref_cnt; reg ref_found; ctz #(.W(8)) dut (.in(in), .count(count)); task check; input [7:0] v; begin in = v; #1; ref_cnt = 8; ref_found = 0; for (k = 0; k < 8; k = k + 1) begin if (!ref_found && v[k]) begin ref_cnt = k; ref_found = 1; end end if (count !== ref_cnt[3:0]) begin $display("FAIL: in=0x%02h ctz=%0d expected=%0d", v, count, ref_cnt); $finish; end else begin $display("PASS: in=0x%02h -> trailing zeros = %0d", v, count); end end endtask initial begin check(8'h00); // 8 check(8'h01); // 0 check(8'h80); // 7 check(8'hF0); // 4 check(8'h20); // 5 check(8'h0C); // 2 $display("PASS"); $finish; end endmodulePASS: in=0x00 -> trailing zeros = 8
PASS: in=0x01 -> trailing zeros = 0
PASS: in=0x80 -> trailing zeros = 7
PASS: in=0xf0 -> trailing zeros = 4
PASS: in=0x20 -> trailing zeros = 5
PASS: in=0x0c -> trailing zeros = 2
PASSThe CTZ sweep checks 0x00 to 8, 0x01 to 0, 0x80 to 7, 0xF0 to 4, 0x20 to 5, and 0x0C to 2, all matching the reference.
How the CLZ logic works
The module is purely combinational: an
always @(*)block recomputescountwheneverinchanges. Two housekeeping lines run first —countis seeded toW(the all-zero answer) andfoundis cleared. Theforloop then iterates over bit positions. Because the loop boundWis a parameter known at elaboration, the synthesis tool fully unrolls it into a chain of comparisons: there is no hardware counter and no clock involved. Thefoundflag is the key to priority: once the top-most1sets it, every later iteration is gated off, so a lower set bit can never overwrite the correct answer.Sizing the output with $clog2
For a
W-bit input the result ranges from0(MSB already set) up toW(all zeros). That isW+1distinct values, so the port width must be$clog2(W+1)bits — forW=8that is$clog2(9) = 4bits, enough to represent 8. A common bug is sizing to$clog2(W), which cannot hold the all-zero result and silently wraps. WritingW[$clog2(W+1)-1:0]rather than a bareWkeeps the default assignment the exact width of the port and avoids width-mismatch warnings.Synthesis notes
A priority for-loop like this synthesizes to a priority encoder: cascaded logic where each stage depends on the higher bits. For small words this is fine, but for wide inputs the carry-chain-style depth grows linearly and can limit timing. Production CLZ units often use a logarithmic tree (split the word in halves, test whether the upper half is zero, recurse) to get
O(log W)depth. The behaviour is identical — only the gate structure differs — so this readable version is an excellent golden reference to verify a faster tree implementation against.Common pitfalls
First, forgetting the
foundguard turns the loop into a "find the last set bit" function instead of the first. Second, scanning in the wrong direction swaps CLZ for CTZ — always confirm whether bitW-1or bit0is the start. Third, never use a blocking counter inside a clocked block for this; CLZ is combinational and belongs inalways @(*). Finally, keep the testbench deterministic: these benches drive fixed vectors and check against an in-testbench reference loop, so aPASS/FAILverdict is reproducible rather than dependent on$random.Interview angle
Interviewers love CLZ because it exposes whether you understand priority logic versus a simple population count. Be ready to: (1) state the output width as
$clog2(W+1)and justify the+1; (2) explain why thefoundflag enforces priority; (3) describe the trade-off between the linear priority-encoder form and the logarithmic tree form; and (4) relate CLZ to floating-point normalization, where the leading-zero count becomes the shift amount and exponent adjustment. Being able to flip the same code into CTZ on the spot, as shown above, signals real fluency.Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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 Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres