Your Button Is Bouncing: Here’s the Debouncer Every FPGA Project Needs

You wire up a button, write five lines of SystemVerilog, load the bitstream — and the LED goes crazy. Every press toggles it three, five, sometimes ten times. You slow down, press more carefully. Still wrong. The hardware looks fine. The code looks fine. The problem is physics.

A mechanical button is a metal contact closing against another metal contact. At the microscopic level, that contact bounces — making and breaking the connection dozens of times in the span of a few milliseconds. Your FPGA clock runs at 100 MHz. It sees every single bounce as a separate, valid press. This post shows you exactly what is happening and builds a reusable debouncer module that eliminates the problem permanently.

1. The Physics of Bounce

When a button is pressed, the contacts do not close cleanly in one motion. The metal flexes, vibrates, and makes intermittent contact for roughly 5 to 20 milliseconds before settling into a stable state. The same thing happens on release. An oscilloscope on the button pin shows a clean-looking press at human timescales but reveals a dense burst of transitions at higher resolution.

The diagram below captures this behavior at clock resolution. What looks like one button press to your finger is a storm of rising and falling edges to the FPGA.

bounce_diagram.sv
// Button bounce — what the oscilloscope actually shows // // Ideal (what you expect): // _______ ___________________________ // |__________________| <– one clean press // // Reality (what the FPGA sees): // _______ _ __ _ _ ___________________________ // |_| || |_| || |__| <– dozens of transitions // |<—- 5–20 ms —->| // // At 100 MHz: 1 clock cycle = 10 ns // A 10 ms bounce window = 1,000,000 clock cycles // Every transition above is a separate rising/falling edge to the FPGA. localparam int CLK_FREQ = 100_000_000; // 100 MHz localparam int DEBOUNCE_MS = 20; // filter window: 20 ms localparam int SAMPLE_DIV = CLK_FREQ / (DEBOUNCE_MS * 1000); // 5000 cycles per sample

2. Why the FPGA Counts Every Bounce

The fundamental mismatch is one of timescales. Human reaction time is measured in hundreds of milliseconds. The bounce window is measured in milliseconds. The FPGA clock period is measured in nanoseconds. The FPGA operates six orders of magnitude faster than the bounce settles.

The naive approach — connecting the button pin directly to a counter or state machine — looks correct in simulation (where the button is an ideal step function) but fails immediately on hardware. The code below demonstrates the problem: what appears to be a simple counter becomes a bounce counter, incrementing hundreds of times per physical press.

btn_naive.sv
// WRONG — direct button read without debouncing module btn_naive ( input logic clk, input logic btn_raw, // directly from pin — bouncing signal output logic led ); logic [7:0] press_count; always_ff @(posedge clk) begin if (btn_raw) press_count <= press_count + 1; // increments on every bounce end // One physical press → press_count jumps by 50–200, not 1 assign led = press_count[0]; // flickers unpredictably endmodule

3. The Shift Register Debouncer

The solution is to stop reading the button on every clock cycle and instead sample it at a much lower rate — once every few milliseconds. After each sample, the value is shifted into a small shift register. The output only changes when all N slots in the shift register agree: all ones means the button is definitively pressed, all zeros means it is definitively released. Any mixed pattern — which occurs during a bounce — leaves the output unchanged.

This works because the bounce window (5–20 ms) is much shorter than the debounce window (N × sample period). By the time N consecutive samples have been taken, the contacts have long since settled. The module below is fully parameterized: drop it into any project and adjust CLK_FREQ and DEBOUNCE_MS to match your hardware.

debouncer.sv
module debouncer #( parameter int CLK_FREQ = 100_000_000, parameter int DEBOUNCE_MS = 20, parameter int N = 4 // consecutive samples required )( input logic clk, input logic rst, input logic btn_raw, // bouncing input from pin output logic btn_clean // stable debounced output ); localparam int SAMPLE_DIV = CLK_FREQ / (DEBOUNCE_MS * 1000); // cycles between samples logic [$clog2(SAMPLE_DIV)-1:0] sample_cnt; logic [N1:0] shift_reg; always_ff @(posedge clk or posedge rst) begin if (rst) begin sample_cnt <= ‘0; shift_reg <= ‘0; btn_clean <= 1’b0; end else begin if (sample_cnt == SAMPLE_DIV 1) begin sample_cnt <= ‘0; shift_reg <= {shift_reg[N2:0], btn_raw}; // shift in new sample if (shift_reg == ‘1) btn_clean <= 1’b1; // N consecutive highs else if (shift_reg == ‘0) btn_clean <= 1’b0; // N consecutive lows // else: mixed — hold previous value (bounce still in progress) end else sample_cnt <= sample_cnt + 1; end end endmodule
Choosing DEBOUNCE_MS: A value of 20 ms is safe for most tactile buttons, which bounce for 5–10 ms. Going shorter risks letting a bounce slip through; going much longer makes the button feel sluggish. If you are debouncing a reed switch or rotary encoder, use 5 ms or less — those contacts settle much faster.

4. Edge Detection: From Level to Pulse

The debouncer outputs a level signal: it is high for as long as the button is held down. For most applications — toggling an LED, triggering a state machine transition, incrementing a counter — you want a single-cycle pulse that fires exactly once per press, regardless of how long the button is held. That requires edge detection.

The edge detector stores the previous cycle’s value of the debounced signal and compares it to the current value. A rising edge — current is high, previous was low — produces a one-cycle pulse on btn_press. The combinational assign ensures the pulse appears in the same cycle the edge is detected, with no added latency.

edge_detect.sv
module edge_detect ( input logic clk, input logic rst, input logic level, // debounced level signal output logic rising_pulse // high for exactly 1 cycle on rising edge ); logic prev; always_ff @(posedge clk or posedge rst) begin if (rst) prev <= 1’b0; else prev <= level; end assign rising_pulse = level & ~prev; // high only on the transition cycle endmodule
Why you need edge detection: If you connect btn_clean directly to a counter’s enable input, the counter increments on every clock cycle the button is held — potentially thousands of times per press. Edge detection ensures the counter increments exactly once per physical press, no matter how long the button is held.

5. Putting It Together

The top-level module chains the two building blocks: raw button pin → debouncer → edge detector → application logic. The example below toggles an LED on each clean button press. The debouncer handles the electrical noise; the edge detector converts the stable level into a precise trigger. Every project that reads a button should use this exact pattern.

btn_top.sv
module btn_top #( parameter int CLK_FREQ = 100_000_000, parameter int DEBOUNCE_MS = 20 )( input logic clk, input logic rst, input logic btn, // physical button pin (active high) output logic led // toggles on each clean press ); logic btn_clean; logic btn_press; debouncer #(CLK_FREQ, DEBOUNCE_MS) u_deb ( .clk (clk), .rst (rst), .btn_raw (btn), .btn_clean(btn_clean) ); edge_detect u_edge ( .clk (clk), .rst (rst), .level (btn_clean), .rising_pulse (btn_press) ); always_ff @(posedge clk or posedge rst) begin if (rst) led <= 1’b0; else if (btn_press) led <= ~led; // exactly one toggle per physical press end endmodule

Final Thoughts: One Module, Every Project

Button debouncing is a solved problem. The shift register debouncer above is 25 lines, fully parameterized, and works correctly on any FPGA at any clock frequency. Write it once, add it to your project library, and never think about bounce again.

The deeper lesson is about the gap between simulation and hardware. In simulation, btn_raw is a perfect step function — it transitions once and stays there. On hardware, it is a burst of noise that your 100 MHz clock captures in full detail. Any design that reads a mechanical signal without debouncing is correct in simulation and broken on the board. Debounce every button, every switch, every mechanical contact — no exceptions.


Happy coding.
fpgawizard.com

error: Selection is disabled!