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.
📋 Port Description & Function Table
| Port | Dir | Width | Description |
|---|---|---|---|
| clk | in | 1 | Clock — active rising edge |
| rst_n | in | 1 | Synchronous active-low reset — clears count to 0 |
| en | in | 1 | Clock enable — counter only updates when en=1 |
| up_dn | in | 1 | Direction: 1 = count up, 0 = count down |
| load | in | 1 | Synchronous load — captures d_in at clock edge (Impl 2+) |
| d_in | in | N | Parallel load data (Impl 2+) |
| count | out | N | Current counter value |
| tc_up | out | 1 | Terminal count up — asserts when count = 2N−1 and en=1 and up_dn=1 |
| tc_dn | out | 1 | Terminal count down — asserts when count = 0 and en=1 and up_dn=0 |
Function Table
| rst_n | en | load | up_dn | count (next) | Operation |
|---|---|---|---|---|---|
| 0 | x | x | x | 0 | Synchronous reset — clear to 0 |
| 1 | x | 1 | x | d_in | Synchronous load — count = d_in |
| 1 | 0 | 0 | x | count | Hold — count unchanged |
| 1 | 1 | 0 | 1 | count + 1 | Count Up |
| 1 | 1 | 0 | 0 | count − 1 | Count 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)
🔌 Block Diagram
⚫ 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.
// ============================================================ // 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
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.
// ============================================================ // 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
// 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).
// ============================================================ // 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
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.
// ============================================================ // 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
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!)
🧪 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.
// ============================================================ // 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
| Phase | Action | Checks |
|---|---|---|
| 1 — Reset | rst_n=0 → clock edge | count=0 on all DUTs |
| 2 — Up sweep | up_dn=1, en=1, count 0→7 | count increments correctly each cycle |
| 3 — Max + TC | count up to 15, then one more | tc_up=1 at count=15, wraps to 0 |
| 4 — Hold | en=0 for two cycles | count unchanged (holds at 0) |
| 5 — Load + Down | load d_in=5, then count down to 0 | load works, down count 5→4→…→0 |
| 6 — Underflow | count down from 0 | tc_dn=1 at count=0, wraps to 15 |
| 7 — Direction change | toggle up_dn mid-sequence | counter reverses immediately on next edge |
| 8 — Load value | load d_in=9, then count up | count=9, then 10 |
| Gray check | 16 consecutive up-count cycles | exactly 1 bit changes in gray_count each step |
