4-Bit Up/Down Counter Verilog Code (With Testbench)
A 4-bit up/down counter is one of the first sequential circuits every RTL designer builds, because a single block ties together clocking, reset, enable and parallel load. This tutorial gives you working, synthesizable 4 bit counter Verilog code: a synchronous counter that counts up or down, loads a parallel value, and clears on an asynchronous active-low reset, plus a self-checking testbench. Everything is copy-paste ready and runs in Icarus Verilog, industry-standard simulators, or any Verilog-2005 simulator.
To build and simulate the file below, use iverilog -g2005 -o sim counter.v && vvp sim.
The complete Verilog code
The design is a single always block clocked on the rising edge of clk. Before the code, here is what each port does:
| Signal | Dir | Purpose |
|---|---|---|
clk | in | Clock; the counter updates on the rising edge. |
rst_n | in | Asynchronous reset, active low. Forces count to 0 immediately. |
en | in | Count enable. When low, the counter holds its value. |
load | in | Synchronous parallel load of data_in. |
up_down | in | 1 counts up, 0 counts down. |
data_in | in | 4-bit value loaded when load is high. |
count | out | Current 4-bit count (0 to 15). |
// 4-bit synchronous up/down counter
// Features: count enable, synchronous load, asynchronous active-low reset
module counter_4bit (
input wire clk, // clock
input wire rst_n, // asynchronous reset, active low
input wire en, // count enable
input wire load, // synchronous parallel load
input wire up_down, // 1 = count up, 0 = count down
input wire [3:0] data_in, // value loaded when load = 1
output reg [3:0] count // current count value
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0000; // async clear
else if (load)
count <= data_in; // sync load wins over count
else if (en)
count <= up_down ? count + 4'd1
: count - 4'd1; // up or down
// else: hold current value
end
endmodule
// Testbench: drives reset, load, up, down and hold
module counter_4bit_tb;
reg clk, rst_n, en, load, up_down;
reg [3:0] data_in;
wire [3:0] count;
counter_4bit dut (
.clk(clk), .rst_n(rst_n), .en(en), .load(load),
.up_down(up_down), .data_in(data_in), .count(count)
);
// 10 ns clock period
always #5 clk = ~clk;
initial begin
$display(" time rstn en ld ud din | count");
$monitor("%5t %b %b %b %b %0d | %0d",
$time, rst_n, en, load, up_down, data_in, count);
// Initialise and apply async reset
clk = 0; rst_n = 0; en = 0; load = 0; up_down = 1; data_in = 4'd0;
#12 rst_n = 1; // release reset between edges
// Count up for 5 clocks: 0->1->2->3->4->5
en = 1; up_down = 1;
#50;
// Synchronous load of 9
en = 0; load = 1; data_in = 4'd9;
#10 load = 0;
// Count down for 4 clocks: 9->8->7->6->5
en = 1; up_down = 0;
#40;
// Hold for 2 clocks (enable low)
en = 0;
#20;
// Asynchronous reset asserted mid-operation
rst_n = 0;
#3 rst_n = 1;
#10;
$display("Simulation complete.");
$finish;
end
endmoduletime rstn en ld ud din | count
0 0 0 0 1 0 | 0
12 1 1 0 1 0 | 0
15 1 1 0 1 0 | 1
25 1 1 0 1 0 | 2
35 1 1 0 1 0 | 3
45 1 1 0 1 0 | 4
55 1 1 0 1 0 | 5
62 1 0 1 1 9 | 5
65 1 0 1 1 9 | 9
72 1 1 0 0 9 | 9
75 1 1 0 0 9 | 8
85 1 1 0 0 9 | 7
95 1 1 0 0 9 | 6
105 1 1 0 0 9 | 5
112 1 0 0 0 9 | 5
132 0 0 0 0 9 | 0
135 1 0 0 0 9 | 0
Simulation complete.
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpb19fxcy9/counter_4bit.v:72: $finish called at 145 (1s)How the code works
The whole behaviour lives in one clocked process, so reading it top to bottom tells you exactly how the flip-flops behave.
- The sensitivity list makes reset asynchronous.
always @(posedge clk or negedge rst_n)wakes the block on either a rising clock edge or a falling edge ofrst_n. Becauserst_nis in the list and the firstiftests it, the counter clears the instant reset drops low, without waiting for a clock. That is the defining feature of an async reset. - The if/else chain sets priority. The order is reset > load > enable > hold. So if
loadandenare both high on the same edge,loadwins anddata_inis captured; the increment is skipped that cycle. If none of the conditions is true, there is noelse, socountsimply keeps its value and holds. - Load is synchronous.
loadis tested inside the clocked block, not in the sensitivity list, so the parallel value is captured only on a clock edge, exactly like a real register with a mux on its input. - Direction is one ternary.
up_down ? count + 4'd1 : count - 4'd1adds or subtracts 1. Writing4'd1keeps the literal 4 bits wide so the addition stays inside the register width. - Nonblocking assignment (
<=) models flip-flops. All updates are scheduled together at the end of the time step, which matches how real registers clock simultaneously and avoids simulation races between signals.
In the testbench, always #5 clk = ~clk; produces a 10 ns clock, and $monitor prints a line whenever any watched signal changes. The stimulus releases reset at #12 (deliberately between edges, to show the async clear), counts up, loads 9, counts down, holds with en = 0, then yanks rst_n low mid-count to prove the async reset fires immediately.
Watch it wrap around
A 4-bit register holds 0 to 15, so counting past the ends wraps modulo-16: 15 + 1 = 0 (overflow) and 0 - 1 = 15 (underflow). This shorter testbench loads 14 and drives the counter through both wrap points.
// Same 4-bit up/down counter, shown wrapping around its 4-bit range
module counter_4bit (
input wire clk,
input wire rst_n,
input wire en,
input wire load,
input wire up_down,
input wire [3:0] data_in,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0000;
else if (load)
count <= data_in;
else if (en)
count <= up_down ? count + 4'd1 : count - 4'd1;
end
endmodule
module counter_4bit_tb;
reg clk, rst_n, en, load, up_down;
reg [3:0] data_in;
wire [3:0] count;
counter_4bit dut (
.clk(clk), .rst_n(rst_n), .en(en), .load(load),
.up_down(up_down), .data_in(data_in), .count(count)
);
always #5 clk = ~clk;
initial begin
$monitor("t=%0t up_down=%b count=%2d (0x%h)",
$time, up_down, count, count);
clk = 0; rst_n = 0; en = 0; load = 0; up_down = 1; data_in = 0;
#12 rst_n = 1;
// Load 14, then count up: 14 -> 15 -> 0 (overflow wrap)
load = 1; data_in = 4'd14; #10 load = 0;
en = 1; up_down = 1; #30;
// Now count down from 1 through 0 -> 15 (underflow wrap)
up_down = 0; #30;
$display("Done: 4-bit counter wraps modulo-16.");
$finish;
end
endmodulet=0 up_down=1 count= 0 (0x0) t=15 up_down=1 count=14 (0xe) t=25 up_down=1 count=15 (0xf) t=35 up_down=1 count= 0 (0x0) t=45 up_down=1 count= 1 (0x1) t=52 up_down=0 count= 1 (0x1) t=55 up_down=0 count= 0 (0x0) t=65 up_down=0 count=15 (0xf) t=75 up_down=0 count=14 (0xe) Done: 4-bit counter wraps modulo-16. /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp4h3usnn_/counter_wrap.v:49: $finish called at 82 (1s)
The log shows the count reach 15, roll to 0, climb to 1, then reverse and drop through 0 back to 15. No special overflow logic is needed; the fixed 4-bit width does it for free.
Common mistakes to avoid
- Leaving
rst_nout of the sensitivity list. If you writealways @(posedge clk)and still test!rst_ninside, you have built a synchronous reset, not an async one. For a true async reset, the reset edge must be in the list. - Mixing blocking and nonblocking in a clocked block. Use
<=throughout sequential logic. Using=for registers invites simulation-versus-synthesis mismatches. - Driving
countfrom more than one block. Aregassigned in twoalwaysblocks is a multiple-driver error. Keep every bit of a register in exactly one process. - Forgetting that load should usually outrank enable. Decide the priority you want and order the
if/elseaccordingly; the first matching branch wins. - Assuming the counter saturates. It wraps, it does not stop at 15 or 0. If you need saturation, add an explicit guard before incrementing.
Quick check
What makes the reset in this counter asynchronous?
rst_n appears in the always block’s sensitivity list as negedge rst_n, so count clears the moment rst_n goes low, without waiting for a clock edge.
If load and en are both high on a clock edge, what happens?
load comes first in the if/else chain, so it wins: data_in is captured and the increment or decrement is skipped for that cycle.
What value follows 1111 when counting up?
0000. A 4-bit register wraps modulo-16, so 15 + 1 overflows back to 0 with no extra logic.
Why use nonblocking assignments (<=) inside the clocked block?
They schedule all register updates to take effect together at the end of the time step, modeling real flip-flop behaviour and avoiding race conditions between signals.
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