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.
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 | 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 |
| 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.
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.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.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.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!)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
| 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 |