Switch Debounce Verilog Code: Counter-Based Debouncer

A mechanical push-button or slide switch never makes a clean 0-to-1 edge. Its metal contacts bounce for anywhere from a few hundred microseconds to several milliseconds, so a naive edge detector reads one physical press as a burst of pulses. The debounce Verilog code below fixes that in hardware: a parameterized, counter-based debouncer that accepts a new level only after the input has held steady for a programmable number of clock cycles, feeding it through a two-flop synchronizer first so an asynchronous switch cannot cause metastability.

// debounce_tb.v  --  counter-based switch debouncer + bounce-injecting bench
`timescale 1ns/1ns

module debounce #(
    parameter integer STABLE_COUNT = 16   // clk cycles the input must stay stable
)(
    input  wire clk,
    input  wire rst_n,      // active-low reset
    input  wire noisy_in,   // raw, bouncing switch line
    output reg  clean_out   // debounced, glitch-free level
);
    // counter width: enough bits to reach STABLE_COUNT-1
    localparam integer CW = (STABLE_COUNT < 2) ? 1 : $clog2(STABLE_COUNT);

    reg          sync0, sync1;   // two-FF synchronizer
    reg [CW-1:0] count;

    // 1) bring the asynchronous switch line into the clk domain
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sync0 <= 1'b0;
            sync1 <= 1'b0;
        end else begin
            sync0 <= noisy_in;
            sync1 <= sync0;
        end
    end

    // 2) require the synced input to hold a new value for STABLE_COUNT cycles
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            count     <= {CW{1'b0}};
            clean_out <= 1'b0;
        end else if (sync1 == clean_out) begin
            count <= {CW{1'b0}};              // input agrees with output: reset timer
        end else begin
            count <= count + 1'b1;            // input differs: keep timing it
            if (count == STABLE_COUNT - 1) begin
                clean_out <= sync1;           // stable long enough -> accept it
                count     <= {CW{1'b0}};
            end
        end
    end
endmodule


module tb_debounce;
    reg  clk, rst_n, noisy_in;
    wire clean_out;

    // small STABLE_COUNT so the simulation finishes quickly
    debounce #(.STABLE_COUNT(4)) dut (
        .clk(clk), .rst_n(rst_n),
        .noisy_in(noisy_in), .clean_out(clean_out)
    );

    initial clk = 1'b0;
    always  #5 clk = ~clk;                    // 100 MHz, 10 ns period

    // print every time the debounced output changes
    always @(clean_out)
        $display("[%4t ns] clean_out = %b", $time, clean_out);

    // drive 6 chattering samples, then settle on final_val
    task inject_bounce(input final_val);
        integer k;
        begin
            for (k = 0; k < 6; k = k + 1) begin
                noisy_in = $random;           // random contact chatter
                #8;
            end
            noisy_in = final_val;             // mechanical contact settles
        end
    endtask

    initial begin
        rst_n = 1'b0; noisy_in = 1'b0;
        #13 rst_n = 1'b1;

        #20 $display("[%4t ns] PRESS  : bouncing, will settle HIGH", $time);
        inject_bounce(1'b1);
        #120;

        $display("[%4t ns] RELEASE: bouncing, will settle LOW", $time);
        inject_bounce(1'b0);
        #120;

        $display("[%4t ns] done, final clean_out = %b", $time, clean_out);
        $finish;
    end
endmodule
[   0 ns] clean_out = 0
[  33 ns] PRESS  : bouncing, will settle HIGH
[  95 ns] clean_out = 1
[ 201 ns] RELEASE: bouncing, will settle LOW
[ 305 ns] clean_out = 0
[ 369 ns] done, final clean_out = 0
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpr0lnp7p0/debounce_tb.v:89: $finish called at 369 (1ns)

How the counter-based debouncer works

The design has two always blocks. The first is a classic two-flip-flop synchronizer (sync0 then sync1). A switch is asynchronous to your clock, so sampling it directly into logic risks metastability; the two stages let any metastable value settle before it reaches the counter.

The second block is the debounce timer. On every clock it compares the synchronized input sync1 with the current output clean_out:

  • If they agree, nothing has changed, so the counter is held at zero.
  • If they differ, the input is proposing a new level, so the counter increments. The moment it reaches STABLE_COUNT - 1, the new level has survived a full stable window, so clean_out latches sync1 and the counter clears.

The key property: any glitch back to the old value during the wait resets the counter to zero, so the input must hold its new state for STABLE_COUNT consecutive clocks before it is believed. That is exactly what filters contact bounce.

SignalDirectionRole
clkinputSystem clock; the whole filter is synchronous to it.
rst_ninputActive-low reset; clears the synchronizer, counter and output.
noisy_ininputRaw, bouncing line straight from the switch.
clean_outoutputRegistered, glitch-free level, one clock-stable window behind the switch.
STABLE_COUNTparameterNumber of clocks the input must stay stable before it is accepted.

The counter width is computed, not hard-coded: localparam CW = $clog2(STABLE_COUNT) sizes the register to exactly the bits needed, so you change one parameter and the hardware follows.

Choosing STABLE_COUNT for real hardware

Pick the stable window from your worst-case bounce time and clock frequency: STABLE_COUNT = t_stable × f_clk. A common, conservative target is a 10 ms guard. The table shows the counter size that implies — note how quickly the register grows, which is why computing the width matters.

ClockStable windowSTABLE_COUNTCounter bits
1 kHz tick10 ms104
1 MHz10 ms10,00014
100 MHz10 ms1,000,00020

The testbench instantiates the module with STABLE_COUNT = 4 purely so the simulation finishes in a few hundred nanoseconds. In a real FPGA you would feed the module a slow enable pulse or a large count, never four clocks at 100 MHz.

Seeing the problem: naive vs debounced

Why bother at all? The second bench drives a single button press whose contact chatters three times before settling, then counts rising edges two ways: once on the raw line and once on the debounced line. Read the simulation log — the raw count registers every bounce, while the debounced count is exactly one clean press.

// press_counter_tb.v  --  why debouncing matters: naive vs debounced press count
`timescale 1ns/1ns

module debounce #(
    parameter integer STABLE_COUNT = 4    // clk cycles the input must stay stable
)(
    input  wire clk,
    input  wire rst_n,
    input  wire noisy_in,
    output reg  clean_out
);
    localparam integer CW = (STABLE_COUNT < 2) ? 1 : $clog2(STABLE_COUNT);

    reg          sync0, sync1;
    reg [CW-1:0] count;

    always @(posedge clk or negedge rst_n)
        if (!rst_n) {sync1, sync0} <= 2'b00;
        else        {sync1, sync0} <= {sync0, noisy_in};

    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
            count     <= {CW{1'b0}};
            clean_out <= 1'b0;
        end else if (sync1 == clean_out) begin
            count <= {CW{1'b0}};
        end else begin
            count <= count + 1'b1;
            if (count == STABLE_COUNT - 1) begin
                clean_out <= sync1;
                count     <= {CW{1'b0}};
            end
        end
endmodule


module tb_press_counter;
    reg  clk, rst_n, noisy_in;
    wire clean;

    reg       naive_q, clean_q;
    reg [7:0] naive_cnt, clean_cnt;
    integer   k;

    debounce #(.STABLE_COUNT(4)) dut (
        .clk(clk), .rst_n(rst_n), .noisy_in(noisy_in), .clean_out(clean));

    initial clk = 1'b0;
    always  #5 clk = ~clk;

    // count rising edges on the RAW line (naive) and on the CLEAN line
    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
            naive_q <= 1'b0; clean_q <= 1'b0;
            naive_cnt <= 8'd0; clean_cnt <= 8'd0;
        end else begin
            naive_q <= noisy_in;
            clean_q <= clean;
            if ( noisy_in & ~naive_q) naive_cnt <= naive_cnt + 1'b1;
            if ( clean    & ~clean_q) clean_cnt <= clean_cnt + 1'b1;
        end

    // one make/break bounce: HIGH then LOW, each 12 ns (well under the
    // 40 ns debounce window), so the raw line never settles mid-bounce
    task one_bounce;
        begin
            noisy_in = 1'b1; #12;
            noisy_in = 1'b0; #12;
        end
    endtask

    initial begin
        rst_n = 1'b0; noisy_in = 1'b0;
        #13 rst_n = 1'b1;

        // ONE physical button press, but the contact chatters several times
        #20;
        for (k = 0; k < 3; k = k + 1) one_bounce;
        noisy_in = 1'b1;              // finally settles pressed
        #120;

        $display("one physical press with contact bounce:");
        $display("  naive rising-edge count     = %0d", naive_cnt);
        $display("  debounced rising-edge count = %0d", clean_cnt);
        $finish;
    end
endmodule
one physical press with contact bounce:
  naive rising-edge count     = 4
  debounced rising-edge count = 1
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpc25qg5u8/press_counter_tb.v:85: $finish called at 225 (1ns)

That gap is the entire reason this debounce Verilog code exists. Wire the raw line into a menu counter, a state machine, or an interrupt and every physical press fires several times; route it through clean_out and you get one event per press.

Adapting it to your design

Two tweaks cover most real uses. First, if a million-cycle counter feels wasteful, drive the module from a slow enable instead: generate a tick every, say, 1 ms and gate both always blocks with it, so a tiny STABLE_COUNT still spans several milliseconds. Second, when downstream logic wants an event rather than a level, register clean_out once more and AND the new value with the inverted old one; the result is a one-clock-wide pulse on each press. The second bench already does exactly this to count presses, so you can lift that rising-edge detector straight into your own module.

Everything stays on a single clock and nothing is hard-coded, which keeps the block easy to reuse across boards: change STABLE_COUNT, or the enable rate, and the same source retargets from a 1 kHz housekeeping tick to a 100 MHz fabric clock without edits.

Common mistakes

  • Skipping the synchronizer. The switch is asynchronous to clk. Sampling noisy_in straight into the counter invites metastability; always keep the two sync flops.
  • Shipping the simulation value. STABLE_COUNT = 4 is for a fast sim only. At 100 MHz that is 40 ns of filtering — thousands of times too short for real bounce. Size it from your clock and worst-case bounce time.
  • Driving a clock with the switch. Never use noisy_in (or even clean_out) as a clock. Keep everything on one clock and treat the button as data.
  • Making the output combinational. If clean_out is a wire of the raw input, glitches pass straight through. It must be a register updated only after the stable window.
  • Off-by-one on the window. Compare against STABLE_COUNT - 1 and reset the counter when the input matches the output. Resetting on the wrong condition lets a mid-settle glitch restart or prematurely accept the level.
  • Forgetting the one-shot. clean_out is a clean level, not a pulse. If you need a single-cycle “pressed” event, add a rising-edge detector on clean_out, as the second bench does.

Quick check

Why does a mechanical switch need debouncing at all?

Its metal contacts physically bounce as they open or close, making and breaking the circuit many times over a few milliseconds. Each make/break is a real electrical edge, so one press looks like many to downstream logic.

What does STABLE_COUNT control, and how do you turn it into real time?

It is the number of clock cycles the input must stay unchanged before the output updates. In time that is STABLE_COUNT / f_clk (equivalently STABLE_COUNT × clock period). For a 10 ms guard at 100 MHz you need 1,000,000 cycles.

Why are there two flip-flops before the counter?

The switch is asynchronous to the clock, so a direct sample can land in a metastable state. The two-flop synchronizer gives any metastable value a full clock to resolve before the counter uses it.

In this design, exactly when does clean_out change?

Only after the synchronized input has differed from clean_out for STABLE_COUNT consecutive clocks (the counter reaches STABLE_COUNT - 1). Any return to the old value before then resets the counter, so a lone glitch is ignored.

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