Verilog Designs · Module 30

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top