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
PASS

Every 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 PASS line 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 i of the first set bit is the answer directly. All-zero again defaults to W. 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
endmodule
PASS: 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
PASS

The 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 recomputes count whenever in changes. Two housekeeping lines run first — count is seeded to W (the all-zero answer) and found is cleared. The for loop then iterates over bit positions. Because the loop bound W is 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. The found flag is the key to priority: once the top-most 1 sets 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 from 0 (MSB already set) up to W (all zeros). That is W+1 distinct values, so the port width must be $clog2(W+1) bits — for W=8 that is $clog2(9) = 4 bits, enough to represent 8. A common bug is sizing to $clog2(W), which cannot hold the all-zero result and silently wraps. Writing W[$clog2(W+1)-1:0] rather than a bare W keeps 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 found guard 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 bit W-1 or bit 0 is the start. Third, never use a blocking counter inside a clocked block for this; CLZ is combinational and belongs in always @(*). Finally, keep the testbench deterministic: these benches drive fixed vectors and check against an in-testbench reference loop, so a PASS/FAIL verdict 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 the found flag 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 Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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