Mod-N Counter in Verilog: Parameterized Code + Mod-10 TB

A mod-N counter cycles through exactly N states, 0 up to N-1, then rolls back to zero, which makes it the building block behind clock dividers, BCD digits, baud generators and state sequencers. Writing it once as a parameterized module means a single, tested design can become a mod-6, mod-10 or mod-256 counter just by changing one number. This tutorial builds that module from scratch, gives it a terminal-count flag, and verifies it with a runnable mod-10 testbench in Icarus Verilog.

The parameterized Mod-N counter

Here is the complete design plus a decade (mod-10) testbench. The parameter N sets the modulus, and WIDTH is derived from it with $clog2 so the register is always exactly wide enough to hold N-1. The tc output (“terminal count”) pulses high on the final state, which is how downstream logic knows a full cycle has elapsed. Run it with iverilog -g2005 -o sim mod_n_counter_tb.v && vvp sim.

// mod_n_counter.v  -- parameterized Mod-N counter with terminal-count output
// Counts 0, 1, ... , N-1, then wraps back to 0. `tc` is high on the last state.
module mod_n_counter #(
    parameter N     = 10,                 // modulus: number of distinct states
    parameter WIDTH = (N <= 2) ? 1 : $clog2(N)
)(
    input  wire             clk,
    input  wire             rst_n,        // active-low synchronous-free async reset
    input  wire             en,           // count enable
    output reg  [WIDTH-1:0] count,
    output wire             tc            // terminal count: count == N-1
);
    assign tc = (count == (N-1));

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= {WIDTH{1'b0}};
        else if (en) begin
            if (tc)
                count <= {WIDTH{1'b0}};
            else
                count <= count + 1'b1;
        end
    end
endmodule

// tb_mod10.v -- testbench exercising the counter as a mod-10 (decade) counter
module tb_mod10;
    localparam N = 10;

    reg             clk;
    reg             rst_n;
    reg             en;
    wire [3:0]      count;
    wire            tc;

    integer         wraps;

    mod_n_counter #(.N(N)) dut (
        .clk   (clk),
        .rst_n (rst_n),
        .en    (en),
        .count (count),
        .tc    (tc)
    );

    // 10 ns clock
    initial clk = 1'b0;
    always #5 clk = ~clk;

    initial begin
        rst_n  = 1'b0;
        en     = 1'b0;
        wraps  = 0;
        #12;
        rst_n  = 1'b1;   // release reset
        en     = 1'b1;   // start counting
    end

    // Log every rising edge and tally the complete decades.
    always @(posedge clk) begin
        if (rst_n && en) begin
            $display("t=%0t  count=%0d  tc=%b", $time, count, tc);
            if (tc) wraps = wraps + 1;
        end
    end

    initial begin
        // Run long enough to see two full 0..9 cycles.
        #230;
        $display("Completed %0d full decade wraps (expected 2).", wraps);
        $finish;
    end
endmodule
t=15  count=0  tc=0
t=25  count=1  tc=0
t=35  count=2  tc=0
t=45  count=3  tc=0
t=55  count=4  tc=0
t=65  count=5  tc=0
t=75  count=6  tc=0
t=85  count=7  tc=0
t=95  count=8  tc=0
t=105  count=9  tc=1
t=115  count=0  tc=0
t=125  count=1  tc=0
t=135  count=2  tc=0
t=145  count=3  tc=0
t=155  count=4  tc=0
t=165  count=5  tc=0
t=175  count=6  tc=0
t=185  count=7  tc=0
t=195  count=8  tc=0
t=205  count=9  tc=1
t=215  count=0  tc=0
t=225  count=1  tc=0
Completed 2 full decade wraps (expected 2).
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpco7g4wjl/mod_n_counter_tb.v:72: $finish called at 230 (1s)

How the code works

The whole design hinges on four ideas. Read them in order and the module stops looking like boilerplate.

  • Modulus and width. N is the count of distinct states. The register width is computed, not typed: $clog2(N) returns the number of bits needed to index N values. For N = 10 that is 4 bits (a 4-bit register reaches 15, and we use 0 through 9). The ternary guard keeps WIDTH at least 1 so tiny moduli still elaborate.
  • Terminal count. tc is a pure combinational comparison, count == N-1. It is high for one clock on the last state and nowhere else, so you can chain counters (feed tc into the next stage’s enable) or trigger a one-shot event per cycle.
  • The counted sequence. On every enabled rising edge the counter asks one question: am I on the terminal count? If yes, wrap to zero; otherwise add one. That single branch is what turns an ordinary binary counter into a modulo-N counter.
  • Reset and enable. The active-low rst_n forces count to zero asynchronously. en gates counting, so when it is low the counter simply holds its value instead of free-running.

Because the width tracks the modulus automatically, the same source handles a wide range of counters without edits:

Modulus N$clog2(N)Count bitsCounts
6330-5
10440-9
16440-15
100770-99

Reusing the module: a mod-6 counter

Since the width is derived from N, retargeting the decade counter to a mod-6 counter is a one-line change: instantiate it with .N(6). Nothing inside the module moves, the register simply shrinks to 3 bits and the sequence becomes 0 through 5. This is the payoff of parameterization, one verified block serving every modulus you need.

// Same mod_n_counter reused with a different modulus -- no RTL changes needed.
module mod_n_counter #(
    parameter N     = 10,
    parameter WIDTH = (N <= 2) ? 1 : $clog2(N)
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [WIDTH-1:0] count,
    output wire             tc
);
    assign tc = (count == (N-1));

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= {WIDTH{1'b0}};
        else if (en) begin
            if (tc)
                count <= {WIDTH{1'b0}};
            else
                count <= count + 1'b1;
        end
    end
endmodule

// tb_mod6.v -- the one module instantiated as a mod-6 counter (count 0..5)
module tb_mod6;
    reg        clk;
    reg        rst_n;
    reg        en;
    wire [2:0] count;   // ceil(log2(6)) = 3 bits
    wire       tc;

    mod_n_counter #(.N(6)) dut (
        .clk(clk), .rst_n(rst_n), .en(en), .count(count), .tc(tc)
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    initial begin
        rst_n = 1'b0; en = 1'b0;
        #12 rst_n = 1'b1; en = 1'b1;
        #150 $finish;
    end

    always @(posedge clk)
        if (rst_n && en)
            $display("t=%0t  count=%0d  tc=%b%s",
                     $time, count, tc, tc ? "  <-- wrap next" : "");
endmodule
t=15  count=0  tc=0               
t=25  count=1  tc=0               
t=35  count=2  tc=0               
t=45  count=3  tc=0               
t=55  count=4  tc=0               
t=65  count=5  tc=1  <-- wrap next
t=75  count=0  tc=0               
t=85  count=1  tc=0               
t=95  count=2  tc=0               
t=105  count=3  tc=0               
t=115  count=4  tc=0               
t=125  count=5  tc=1  <-- wrap next
t=135  count=0  tc=0               
t=145  count=1  tc=0               
t=155  count=2  tc=0               
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpfnm_de9v/mod6_counter_tb.v:44: $finish called at 162 (1s)

Add a self-checking testbench

Eyeballing a log is fine for a first look, but a testbench that checks itself scales to regression runs and catches a broken edit instantly. The version below keeps an expected value in software, predicts the sequence, and compares it against the hardware output on every edge, printing a single PASS or FAIL line. Note the #1 delay after each edge: it lets the non-blocking update settle before sampling, so you read the new count rather than the old one.

// mod_n_counter.v (same design) + a self-checking mod-10 testbench
module mod_n_counter #(
    parameter N     = 10,
    parameter WIDTH = (N <= 2) ? 1 : $clog2(N)
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [WIDTH-1:0] count,
    output wire             tc
);
    assign tc = (count == (N-1));

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= {WIDTH{1'b0}};
        else if (en) begin
            if (tc)
                count <= {WIDTH{1'b0}};
            else
                count <= count + 1'b1;
        end
    end
endmodule

// tb_selfcheck.v -- predicts the expected value and fails loudly on a mismatch
module tb_selfcheck;
    localparam N = 10;

    reg        clk, rst_n, en;
    wire [3:0] count;
    wire       tc;

    reg  [3:0] expected;
    integer    errors;
    integer    i;

    mod_n_counter #(.N(N)) dut (
        .clk(clk), .rst_n(rst_n), .en(en), .count(count), .tc(tc)
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    initial begin
        errors   = 0;
        expected = 4'd1;   // counter holds 0 in reset; first enabled edge -> 1
        rst_n    = 1'b0;
        en       = 1'b0;
        @(negedge clk);
        rst_n = 1'b1;
        en    = 1'b1;

        // Check 25 counted edges against the predicted sequence.
        for (i = 0; i < 25; i = i + 1) begin
            @(posedge clk);
            #1;  // let the non-blocking update settle before sampling
            if (count !== expected) begin
                $display("MISMATCH at edge %0d: got %0d, expected %0d",
                         i, count, expected);
                errors = errors + 1;
            end
            if (tc !== (expected == N-1)) begin
                $display("TC WRONG at edge %0d: tc=%b expected=%b",
                         i, tc, (expected == N-1));
                errors = errors + 1;
            end
            expected = (expected == N-1) ? 4'd0 : expected + 4'd1;
        end

        if (errors == 0)
            $display("PASS: mod-10 sequence and terminal count verified over 25 edges.");
        else
            $display("FAIL: %0d error(s) detected.", errors);
        $finish;
    end
endmodule
PASS: mod-10 sequence and terminal count verified over 25 edges.
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp2hmosff4/mod10_selfcheck_tb.v:75: $finish called at 256 (1s)

Common mistakes

  • Hardcoding the width. Declaring reg [3:0] count and later bumping N to 20 silently truncates the register, so the counter wraps at 15 instead of 19. Let $clog2 size it from the parameter.
  • Comparing against N instead of N-1. The sequence is 0 to N-1, so terminal count is N-1. Resetting when count == N adds an extra state and gives you a mod-(N+1) counter, or hangs if the register cannot even represent N.
  • Forgetting the wrap. Writing only count <= count + 1 with no terminal-count branch produces a plain binary counter that wraps at 2^WIDTH, not at N.
  • Blocking assignments in a clocked block. Sequential logic must use non-blocking (<=). Blocking (=) across edges invites simulation races and can diverge from the synthesized flip-flops.
  • Ignoring the sample timing in the testbench. Reading count in the same delta as the clock edge returns the old value; sample after a small delay or on the opposite edge.

Quick check

How many bits does count need for a mod-12 counter?

Four bits. $clog2(12) is 4, and a 4-bit register reaches 15, which comfortably holds the states 0 through 11.

Why is terminal count compared against N-1 and not N?

The counter visits exactly N values starting at 0, so the last one is N-1. Comparing against N would either add a spurious extra state or never match if the register is too narrow to hold N.

Instantiate the module with .N(16). What is WIDTH and the count range?

$clog2(16) is 4, so WIDTH is 4 and the counter runs 0 to 15, which is exactly 16 states. Powers of two use all of their bits.

Why use non-blocking assignments in the clocked always block?

Non-blocking assignments sample every right-hand side first and update afterward, which models real flip-flop behavior and avoids ordering-dependent simulation races between registers that update on the same edge.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.

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