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.
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.
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.
| Port | Dir | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | system clock |
| rst | input | 1 | synchronous reset |
| btn_raw | input | 1 | raw bouncy button signal from the FPGA pin |
| btn_clean | output | 1 | debounced stable button state |
| btn_pulse | output | 1 | one-cycle rising-edge pulse — fires once per press |
// 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`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
endmodulepulse #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
pulse = clean & ~clean_r gives exactly one cycle per press.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.
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).
A circuit that produces a one-cycle pulse on a signal transition. Rising edge: pulse = current & ~previous.