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
endmodulet=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.
Nis the count of distinct states. The register width is computed, not typed:$clog2(N)returns the number of bits needed to indexNvalues. ForN = 10that is 4 bits (a 4-bit register reaches 15, and we use 0 through 9). The ternary guard keepsWIDTHat least 1 so tiny moduli still elaborate. - Terminal count.
tcis 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 (feedtcinto 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_nforcescountto zero asynchronously.engates 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 bits | Counts |
|---|---|---|---|
| 6 | 3 | 3 | 0-5 |
| 10 | 4 | 4 | 0-9 |
| 16 | 4 | 4 | 0-15 |
| 100 | 7 | 7 | 0-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" : "");
endmodulet=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
endmodulePASS: 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] countand later bumpingNto 20 silently truncates the register, so the counter wraps at 15 instead of 19. Let$clog2size 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 whencount == Nadds an extra state and gives you a mod-(N+1) counter, or hangs if the register cannot even representN. - Forgetting the wrap. Writing only
count <= count + 1with 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
countin 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.
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