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:

SignalDirPurpose
clkinClock; the counter updates on the rising edge.
rst_ninAsynchronous reset, active low. Forces count to 0 immediately.
eninCount enable. When low, the counter holds its value.
loadinSynchronous parallel load of data_in.
up_downin1 counts up, 0 counts down.
data_inin4-bit value loaded when load is high.
countoutCurrent 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
endmodule
time 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 of rst_n. Because rst_n is in the list and the first if tests 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 load and en are both high on the same edge, load wins and data_in is captured; the increment is skipped that cycle. If none of the conditions is true, there is no else, so count simply keeps its value and holds.
  • Load is synchronous. load is 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'd1 adds or subtracts 1. Writing 4'd1 keeps 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
endmodule
t=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_n out of the sensitivity list. If you write always @(posedge clk) and still test !rst_n inside, 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 count from more than one block. A reg assigned in two always blocks 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/else accordingly; 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.

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