Verilog Designs — Up-Down Counter — VLSI Trainers
Verilog Designs · Module 30

Up-Down Counter

Four implementations of a synchronous up-down counter — basic 4-bit, with synchronous load, parameterised N-bit, and Gray-code variant — with function tables, count sequences, waveforms, and an exhaustive self-checking testbench covering reset, up-count, overflow, hold, underflow, down-count, direction change, and load.

🔄 Introduction & Theory

An up-down counter is a synchronous sequential circuit that increments or decrements its stored count by 1 on each active clock edge, depending on a direction control signal. It is one of the most widely used building blocks in digital systems, appearing in address generators, PWM period counters, event counters, and FSM state sequencers.

▲
Up Count
When up_dn = 1, count increments by 1 each clock. At maximum value (2N−1), wraps to 0 (roll-over) or holds at max (saturate).
▼
Down Count
When up_dn = 0, count decrements by 1 each clock. At 0, wraps to 2N−1 (roll-under) or holds at 0 (saturate).
🔄
Synchronous Reset
Reset clears the counter to 0 at the next rising clock edge. Only the clock edge triggers the update.
📋
Load & Hold
Optional synchronous load presets the counter to any value. Hold (enable=0) freezes the count without reset.
Applications: up-down counters are used in PWM duty-cycle generators (triangle-wave carrier), ADC successive-approximation registers, bidirectional ring buffers (FIFO read/write pointer difference), motor encoder interfaces, and as the core of PID controller integrators in FPGA signal processing.

📋 Port Description & Function Table

PortDirWidthDescription
clkin1Clock — active rising edge
rst_nin1Synchronous active-low reset — clears count to 0
enin1Clock enable — counter only updates when en=1
up_dnin1Direction: 1 = count up, 0 = count down
loadin1Synchronous load — captures d_in at clock edge (Impl 2+)
d_ininNParallel load data (Impl 2+)
countoutNCurrent counter value
tc_upout1Terminal count up — asserts when count = 2N−1 and en=1 and up_dn=1
tc_dnout1Terminal count down — asserts when count = 0 and en=1 and up_dn=0

Function Table

rst_nenloadup_dncount (next)Operation
0xxx0Synchronous reset — clear to 0
1x1xd_inSynchronous load — count = d_in
100xcountHold — count unchanged
1101count + 1Count Up
1100count − 1Count Down

Priority order: reset > load > hold (en=0) > count up/down. All transitions occur at the rising clock edge only.

Count Sequence (4-bit, wrap-around)

Up direction (up_dn=1):
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15→0
Down direction (up_dn=0):
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0→15

🔌 Block Diagram

Fig 1 — Up-down counter block diagram with all ports
Up-Down Counter N=4 (0..15) clk rst_n en up_dn load / d_in count[N-1:0] tc_up tc_dn clk▶ rst en dir ld Q tc+ tc- ▲ ▼

⚫ Implementation 1 — Basic 4-bit Up-Down Counter

The foundational implementation: synchronous reset, clock enable, bidirectional count, and terminal count flags. No load input. Clean priority structure: reset > hold > count.

1
updn_counter_basic
4-bit · Sync reset · Enable · Up/Down · tc_up and tc_dn flags
Basic 4-bit
// ============================================================
// Module   : updn_counter_basic
// Function : 4-bit synchronous up-down counter
// Ports    : clk, rst_n (sync reset), en (enable),
//            up_dn (1=up, 0=down), count[3:0],
//            tc_up (max reached), tc_dn (zero reached)
// ============================================================
`timescale 1ns/1ps
`default_nettype none

module updn_counter_basic (
  input      clk,
  input      rst_n,    // synchronous active-low reset
  input      en,       // clock enable
  input      up_dn,    // 1=count up, 0=count down
  output reg [3:0] count,
  output     tc_up,    // terminal count: count at max (15)
  output     tc_dn     // terminal count: count at min (0)
);

  always @(posedge clk) begin
    if (!rst_n)
      count <= 4'b0000;           // synchronous reset
    else if (en) begin
      if (up_dn)
        count <= count + 1'b1;   // count up (wraps 15->0)
      else
        count <= count - 1'b1;   // count down (wraps 0->15)
    end
    // else: en=0 → count holds (implicit)
  end

  // Terminal count flags (combinational)
  assign tc_up = en & up_dn  & (&count);  // count==4'hF
  assign tc_dn = en & ~up_dn & ~(|count); // count==4'h0

endmodule
`default_nettype wire
Terminal count flags explained: tc_up uses &count (reduction AND) which is 1 only when all bits are 1 (count=15). tc_dn uses ~(|count) (reduction OR, then invert) which is 1 only when all bits are 0 (count=0). Both are gated by en and the direction signal so they assert only when the terminal value is actively being counted through, not when the counter is held.

🔵 Implementation 2 — With Synchronous Load

Extends the basic counter with a synchronous parallel load input. When load=1 at a rising clock edge, the counter captures d_in regardless of up_dn or en. This allows the counter to be preset to any value — useful for modulo-N counting and PWM period setting.

2
updn_counter_load
4-bit · Sync reset · Enable · Sync load · Up/Down · TC flags
With Load
// ============================================================
// Module   : updn_counter_load
// Adds     : synchronous parallel load (load + d_in[3:0])
// Priority : rst_n > load > enable/direction
// ============================================================
`timescale 1ns/1ps
`default_nettype none

module updn_counter_load (
  input           clk, rst_n, en, up_dn,
  input           load,     // synchronous load strobe
  input  [3:0]   d_in,     // parallel load data
  output reg [3:0] count,
  output          tc_up, tc_dn
);

  always @(posedge clk) begin
    if      (!rst_n)  count <= 4'b0;
    else if (load)    count <= d_in;    // preset to any value
    else if (en)
      count <= up_dn ? count+1 : count-1;
  end

  assign tc_up = en & up_dn  & (&count);
  assign tc_dn = en & ~up_dn & ~(|count);

endmodule
`default_nettype wire
Fig 2 — Modulo-N counting using synchronous load: counts 0..N-1 then reloads
// Count 0..9 repeatedly (BCD decade counter):
// When tc_up asserts (count=9 with en=1, up_dn=1),
// assert load=1 with d_in=0 to reset to 0 next cycle.

always @(posedge clk)
  if (tc_up) load_en <= 1;  // trigger reload at overflow

// Or: use tc_up as a carry-out to the next digit in a
// multi-digit BCD display counter.

🟠 Implementation 3 — Parameterised N-bit Counter

The parameterised version scales to any bit width using a single parameter N. All counter operations, terminal count detection, and port widths automatically adapt. The default is N=8 (256 states).

3
updn_counter_n
Parameterised N-bit · Any width · Auto-scaling TC flags · Default N=8
N-bit Param
// ============================================================
// Module   : updn_counter_n
// Param    : N = counter bit-width (default 8 = 0..255)
// All widths and comparisons auto-scale with N
// ============================================================
`timescale 1ns/1ps
`default_nettype none

module updn_counter_n #(parameter N = 8) (
  input              clk, rst_n, en, up_dn,
  input              load,
  input  [N-1:0]  d_in,
  output reg [N-1:0] count,
  output             tc_up,  // count == {N{1'b1}}
  output             tc_dn   // count == {N{1'b0}}
);

  always @(posedge clk) begin
    if      (!rst_n) count <= {N{1'b0}};
    else if (load)   count <= d_in;
    else if (en)
      count <= up_dn ? count+1 : count-1;
  end

  // Reduction operators scale with N automatically
  assign tc_up = en & up_dn  & (&count);   // all 1s
  assign tc_dn = en & ~up_dn & ~(|count);  // all 0s

endmodule
`default_nettype wire
Instantiation examples: updn_counter_n #(.N(4)) u0 (...); — 4-bit, 0..15. updn_counter_n #(.N(16)) u1 (...); — 16-bit, 0..65535. updn_counter_n #(.N(32)) u2 (...); — 32-bit address counter. The {N{1'b1}} and {N{1'b0}} replication expressions scale the reset value and TC detection to any width with zero code changes.

🟣 Implementation 4 — Gray-Code Up-Down Counter

A Gray-code counter produces an output where only one bit changes per clock edge (unit-distance property). This eliminates glitches on the count output — critical for asynchronous FIFO pointers and reliable multi-bit bus sampling across clock domain crossings.

4
updn_counter_gray
Gray-code output · Unit-distance · Single-bit transitions · CDC-safe pointers
Gray Code
// ============================================================
// Module   : updn_counter_gray
// Method   : Binary counter internally, Gray encoded on output
// Property : Only 1 bit changes per clock (unit-distance)
// Use case : Async FIFO read/write pointers, CDC-safe counters
// ============================================================
`timescale 1ns/1ps
`default_nettype none

module updn_counter_gray #(parameter N = 4) (
  input              clk, rst_n, en, up_dn,
  output reg [N-1:0] bin_count,   // internal binary count
  output     [N-1:0] gray_count   // Gray-encoded output
);

  // ── Binary counter (internal) ─────────────────────────────
  always @(posedge clk) begin
    if      (!rst_n) bin_count <= {N{1'b0}};
    else if (en)
      bin_count <= up_dn ? bin_count+1 : bin_count-1;
  end

  // ── Binary to Gray conversion (combinational) ─────────────
  // MSB is the same; each other bit = bin[i] XOR bin[i+1]
  assign gray_count = bin_count ^ (bin_count >> 1);

endmodule
`default_nettype wire
Fig 3 — 4-bit binary vs Gray count sequence (only 1 bit changes each step)
Dec   Binary   Gray      Bits changed in Gray
 0    0000     0000
 1    0001     0001      bit 0
 2    0010     0011      bit 1
 3    0011     0010      bit 0
 4    0100     0110      bit 2
 5    0101     0111      bit 0
 6    0110     0101      bit 1
 7    0111     0100      bit 0
 8    1000     1100      bit 3
 9    1001     1101      bit 0
10    1010     1111      bit 1
11    1011     1110      bit 0
12    1100     1010      bit 2
13    1101     1011      bit 0
14    1110     1001      bit 1
15    1111     1000      bit 0
       wraps: 0000      bit 3 (only 1 bit!)
Why Gray code matters for CDC: When a multi-bit binary counter crosses a clock domain, multiple bits can transition simultaneously. A CDC synchroniser may sample a transitioning bus and see an invalid intermediate code (e.g., binary 0111→1000 has 4 bits changing simultaneously, and a sampling in the middle could read 0000 or 1111 — both incorrect). Gray code guarantees only one bit changes per step, so a cross-domain sample can only be off by one count — an acceptable error for FIFO depth calculation.

🧪 Comprehensive Testbench

The testbench verifies all four counter functions in sequence: reset, up-count sweep, overflow wrap, hold, down-count sweep, underflow wrap, direction change mid-count, and synchronous load. All three binary counter variants are checked simultaneously.

TB
updn_counter_tb
All implementations · 8 test phases · TC flag verification · Gray unit-distance check
Testbench
// ============================================================
// Testbench  : updn_counter_tb
// DUTs       : updn_counter_basic, updn_counter_load,
//              updn_counter_n (#N=4), updn_counter_gray (#N=4)
// Test phases: reset, up-sweep, overflow, hold, down-sweep,
//              underflow, direction-change, load
// ============================================================
`timescale 1ns/1ps
`default_nettype none

module updn_counter_tb;

  reg clk=0, rst_n=1, en=0, up_dn=1, load=0;
  reg [3:0] d_in=0;

  wire [3:0] cnt_b, cnt_l, cnt_n, cnt_g_bin, cnt_g_gray;
  wire tc_up_b, tc_dn_b, tc_up_l, tc_dn_l;

  updn_counter_basic           u_basic (.clk(clk),.rst_n(rst_n),.en(en),.up_dn(up_dn),.count(cnt_b),.tc_up(tc_up_b),.tc_dn(tc_dn_b));
  updn_counter_load            u_load  (.clk(clk),.rst_n(rst_n),.en(en),.up_dn(up_dn),.load(load),.d_in(d_in),.count(cnt_l),.tc_up(tc_up_l),.tc_dn(tc_dn_l));
  updn_counter_n #(.N(4))      u_n     (.clk(clk),.rst_n(rst_n),.en(en),.up_dn(up_dn),.load(load),.d_in(d_in),.count(cnt_n),.tc_up(),.tc_dn());
  updn_counter_gray #(.N(4))  u_gray  (.clk(clk),.rst_n(rst_n),.en(en),.up_dn(up_dn),.bin_count(cnt_g_bin),.gray_count(cnt_g_gray));

  always #5 clk = ~clk;

  initial begin $dumpfile("updn_cnt.vcd"); $dumpvars(0,updn_counter_tb); end

  integer pass_cnt=0, fail_cnt=0, test_num=0;
  reg [3:0] exp, prev_gray;

  task clk_tick; @(posedge clk); #1; endtask

  task check;
    input [3:0] expected;
    input [255:0] msg;
    begin
      test_num++;
      if(cnt_b===expected && cnt_l===expected && cnt_n===expected) begin
        $display("  PASS [%2d] %s | count=%0d",test_num,msg,cnt_b);
        pass_cnt++;
      end else begin
        $display("  FAIL [%2d] %s | basic=%0d load=%0d n=%0d exp=%0d",
          test_num,msg,cnt_b,cnt_l,cnt_n,expected);
        fail_cnt++;
      end
    end
  endtask

  task check_tc;
    input exp_tc_up, exp_tc_dn;
    input [255:0] msg;
    begin
      test_num++;
      if(tc_up_b===exp_tc_up && tc_dn_b===exp_tc_dn) begin
        $display("  PASS [%2d] TC: %s | tc_up=%b tc_dn=%b",test_num,msg,tc_up_b,tc_dn_b);
        pass_cnt++;
      end else begin
        $display("  FAIL [%2d] TC: %s | got=%b%b exp=%b%b",
          test_num,msg,tc_up_b,tc_dn_b,exp_tc_up,exp_tc_dn);
        fail_cnt++;
      end
    end
  endtask

  initial begin
    $display("\n======================================================");
    $display("  Up-Down Counter Testbench");
    $display("======================================================");

    // Phase 1: Reset
    $display("\n  --- Phase 1: Synchronous Reset ---");
    rst_n=0; en=1; up_dn=1; clk_tick;
    check(4'd0, "After reset: count=0");

    // Phase 2: Up count 0..7
    $display("\n  --- Phase 2: Up Count 0..7 ---");
    rst_n=1; up_dn=1; en=1;
    begin : up_loop
      integer i;
      for(i=1; i<=7; i=i+1) begin
        clk_tick;
        check(i, "Up count");
      end
    end

    // Phase 3: Count to 15, check tc_up
    $display("\n  --- Phase 3: Count to Max, TC flag ---");
    begin : max_loop
      integer j;
      for(j=8; j<=15; j=j+1) begin clk_tick; check(j,"count"); end
    end
    check_tc(1,0, "tc_up=1 at count=15");
    clk_tick; check(4'd0, "Overflow: 15->0 wrap");

    // Phase 4: Hold (en=0)
    $display("\n  --- Phase 4: Hold (en=0) ---");
    en=0; clk_tick; check(4'd0,"Hold: count stays 0");
    clk_tick; check(4'd0,"Hold: count stays 0");

    // Phase 5: Down count from 5
    $display("\n  --- Phase 5: Down Count from 5 ---");
    load=1; d_in=4'd5; en=1; clk_tick;
    load=0; up_dn=0;
    check(4'd5,"After load=5");
    begin : dn_loop
      integer k;
      for(k=4; k>=0; k=k-1) begin clk_tick; check(k,"Down count"); end
    end

    // Phase 6: Underflow 0->15
    $display("\n  --- Phase 6: Underflow (0->15) ---");
    check_tc(0,1,"tc_dn=1 at count=0");
    clk_tick; check(4'd15,"Underflow: 0->15 wrap");

    // Phase 7: Direction change mid-count
    $display("\n  --- Phase 7: Direction Change ---");
    up_dn=1; clk_tick; // now going up
    check(4'd0,"15+1=0 (up from 15)");
    clk_tick; check(4'd1,"Up: 1");
    clk_tick; check(4'd2,"Up: 2");
    up_dn=0; clk_tick; check(4'd1,"Dir change: 2-1=1");
    up_dn=1; clk_tick; check(4'd2,"Dir change: 1+1=2");

    // Phase 8: Load to specific value
    $display("\n  --- Phase 8: Synchronous Load ---");
    load=1; d_in=4'd9; clk_tick; load=0;
    check(4'd9,"Load 9");
    clk_tick; check(4'd10,"Up from 9: 10");

    // Gray unit-distance check
    $display("\n  --- Gray code unit-distance check ---");
    rst_n=0; clk_tick; rst_n=1; up_dn=1;
    prev_gray = cnt_g_gray;
    begin : gray_loop
      integer g;
      for(g=0; g<16; g=g+1) begin
        clk_tick; test_num++;
        if($countones(cnt_g_gray ^ prev_gray)==1) begin
          $display("  PASS Gray unit-dist bin=%04b gray=%04b (1 bit changed)",cnt_g_bin,cnt_g_gray);
          pass_cnt++;
        end else begin
          $display("  FAIL Gray unit-dist: %d bits changed!",$countones(cnt_g_gray^prev_gray));
          fail_cnt++;
        end
        prev_gray = cnt_g_gray;
      end
    end

    $display("\n======================================================");
    $display("  RESULTS: %0d / %0d PASS  |  %0d FAIL",pass_cnt,test_num,fail_cnt);
    $display("======================================================");
    if(fail_cnt==0) $display("  ALL TESTS PASSED\n");
    else $fatal(1,"  %0d FAILURE(S)\n",fail_cnt);
    #20; $finish;
  end
endmodule
`default_nettype wire

Test Phase Summary

PhaseActionChecks
1 — Resetrst_n=0 → clock edgecount=0 on all DUTs
2 — Up sweepup_dn=1, en=1, count 0→7count increments correctly each cycle
3 — Max + TCcount up to 15, then one moretc_up=1 at count=15, wraps to 0
4 — Holden=0 for two cyclescount unchanged (holds at 0)
5 — Load + Download d_in=5, then count down to 0load works, down count 5→4→…→0
6 — Underflowcount down from 0tc_dn=1 at count=0, wraps to 15
7 — Direction changetoggle up_dn mid-sequencecounter reverses immediately on next edge
8 — Load valueload d_in=9, then count upcount=9, then 10
Gray check16 consecutive up-count cyclesexactly 1 bit changes in gray_count each step

📈 Simulation Waveform

Fig 4 — Up-down counter waveform: reset, up-sweep, overflow, hold, direction change
clk rst_n en up_dn count tc_up tc_dn 0 1 2 3 4 5 6 7 8 9 10 0 1 0 1 0 (hold) 1 1 (up) 0 (dn) 0 1 2 3 0 (OVF) 0 (hold) 15 (UNF) 14 13 tc_up {"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/astra/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]}
Scroll to Top