HomeFPGA from ScratchDay 10
DAY 10 · REAL WORLD DESIGN

Debouncing a Push Button

By EcrioniX · Updated Jun 11, 2026

You connect a push button to your FPGA. You press it once. Your counter jumps by 17. What went wrong? Button bounce — the mechanical reality of every physical switch, and the first real-world signal integrity problem every digital designer encounters. This lesson explains why it happens and how to fix it properly in Verilog.

1. Why buttons bounce

A mechanical button has springy metal contacts. When you press it, the contacts bounce against each other 5–50 times over 1–20 ms before settling into a stable state. Your FPGA at 100 MHz samples that input millions of times during the bounce window and sees each bounce as a separate press.

2. The fix: counter-based debounce

The idea: only accept a new button state if the input has been stable for N consecutive cycles. Any glitch shorter than N cycles is ignored.

3. Port table

PortDirWidthMeaning
clkinput1system clock
rstinput1synchronous reset
btn_rawinput1raw bouncy button signal from the FPGA pin
btn_cleanoutput1debounced stable button state
btn_pulseoutput1one-cycle rising-edge pulse — fires once per press

4. debounce.v — design

debounce.v — counter-based debouncer + edge detector
// Button debouncer: requires input stable for STABLE_COUNT cycles
// Also provides a one-cycle rising-edge pulse (btn_pulse)
module debounce #(parameter STABLE_COUNT = 2_000_000) (
    input  wire clk,
    input  wire rst,
    input  wire btn_raw,    // raw button input (may bounce)
    output reg  btn_clean,  // debounced stable output
    output wire btn_pulse   // one-cycle pulse on rising edge of btn_clean
);
    reg [20:0] cnt;         // up to 2M, needs 21 bits
    reg        last_raw;    // previous raw sample

    always @(posedge clk) begin
        if (rst) begin
            cnt       <= 21'd0;
            btn_clean <= 1'b0;
            last_raw  <= 1'b0;
        end else begin
            if (btn_raw != last_raw) begin
                cnt <= 21'd0;          // input changed — restart timer
            end else if (cnt < STABLE_COUNT - 1) begin
                cnt <= cnt + 1;        // still counting stability
            end else begin
                btn_clean <= btn_raw;  // stable for long enough — accept
            end
            last_raw <= btn_raw;
        end
    end

    // Rising-edge detector on btn_clean
    reg btn_clean_r;
    always @(posedge clk) btn_clean_r <= btn_clean;
    assign btn_pulse = btn_clean & ~btn_clean_r;
endmodule

5. Testbench — with bounce simulation

tb_debounce.v — testbench
`timescale 1ns/1ps
module tb_debounce;
    reg  clk=0, rst=1, btn_raw=0;
    wire btn_clean, btn_pulse;
    // Small STABLE_COUNT=5 for fast simulation
    debounce #(.STABLE_COUNT(5)) dut(.clk(clk),.rst(rst),
        .btn_raw(btn_raw),.btn_clean(btn_clean),.btn_pulse(btn_pulse));
    always #5 clk=~clk;
    integer errors=0, pulses=0;
    // Monitor pulse
    always @(posedge clk) if(btn_pulse) begin pulses=pulses+1; $display("pulse #%0d at t=%0t",pulses,$time);end
    initial begin
        @(posedge clk); rst=0;
        // Simulate bounce: 3 rapid glitches then stable press
        btn_raw=1; repeat(2) @(posedge clk);
        btn_raw=0; @(posedge clk);
        btn_raw=1; repeat(2) @(posedge clk);
        btn_raw=0; @(posedge clk);
        btn_raw=1; repeat(8) @(posedge clk); // now stable for >5 cycles
        #1;
        if(btn_clean!==1) begin $display("FAIL: btn_clean should be 1");errors=errors+1;end
        else $display("ok   btn_clean=1 after stable press");
        // Release button
        btn_raw=0; repeat(8) @(posedge clk); #1;
        if(btn_clean!==0) begin $display("FAIL: btn_clean should be 0");errors=errors+1;end
        else $display("ok   btn_clean=0 after release");
        if(pulses!=1) begin $display("FAIL: expected 1 pulse, got %0d",pulses);errors=errors+1;end
        else $display("ok   exactly 1 pulse fired");
        if(errors==0) $display("ALL TESTS PASSED"); else $display("%0d FAILED",errors);
        $finish;
    end
endmodule
expected output
pulse #1 at t=185
ok   btn_clean=1 after stable press
ok   btn_clean=0 after release
ok   exactly 1 pulse fired
ALL TESTS PASSED

🎯 Day 10 takeaways

FAQ

What is button bounce?

Rapid on/off transitions when a mechanical switch makes contact, lasting 1–20 ms. An FPGA sees each bounce as a separate press without debouncing.

How do you debounce in Verilog?

Count consecutive cycles where the input is stable. Only update the output when the count reaches the stability threshold (e.g. 2M cycles = 20 ms at 100 MHz).

What is an edge detector?

A circuit that produces a one-cycle pulse on a signal transition. Rising edge: pulse = current & ~previous.

Previous
← Day 9: Counters & clock dividers

← Full roadmap