fpga-debounce-eliminar-rebotes

FPGA Debounce: How to Read Push Buttons Correctly

  • 5 min

A debouncer is a digital filter that only accepts a change when the input remains stable for a specific duration.

Sooner or later, you’ll want to connect your FPGA to the real world by pressing a button. And that’s when reality hits you.

You design a counter that should add 1 every time you press a button. You test it, press the button once, and the counter jumps by 15, 20, or 50. What happened? Is the FPGA broken?

No. Welcome to the wonderful world of Mechanical Bouncing. Today, we’ll learn how to tame it using sequential logic.

The Problem: The Real World is Noisy

A push button is not an ideal digital component that instantly goes from 0 to 1. Inside, it’s two small metal pieces colliding.

When you press, these pieces bounce physically several times before settling (like a ping-pong ball hitting the floor).

  • Human Time: A bounce lasts about 10 or 20 milliseconds. To you, it’s instantaneous.
  • FPGA Time: Your FPGA runs at 12 MHz (12 million cycles per second). In 10 milliseconds, the FPGA “looks” at the button 120,000 times.

To the FPGA, that single “click” looks like a machine gun burst of 0s and 1s.

If we use that signal directly as a clock or enable for a counter, we will count every tiny bounce as a valid press.

The Strategy: A Digital Filter

To solve this, we need to implement a time filter (Debounce). The logic is simple: “I will not believe the button has changed state until the signal remains stable for X time”.

A wait time of 10 to 20 ms is typical, but you should adjust it to the push button and the response required by your application.

Synchronization and Metastability

Before filtering, there’s an important rule in FPGAs: never feed an external asynchronous signal directly into your logic.

The button is pressed by a human, unrelated to the FPGA’s clock. If the signal changes right when the clock rises (Setup/Hold time violation), we can cause Metastability (the Flip-Flop enters an unstable state that is neither 0 nor 1).

To drastically reduce the probability of metastability propagating, we pass the signal through a chain of two synchronizer flip-flops. This doesn’t mathematically eliminate the risk, but it reduces it to an acceptable level for most designs.

reg [1:0] sync_btn;
always @(posedge clk) begin
    // 2-stage shift register
    sync_btn <= {sync_btn[0], physical_button_input};
end
wire safe_button = sync_btn[1]; // Use this signal internally
Copied!

Debounce Module Implementation

Let’s create a generic module. The counter resets when the input returns to the accepted state. Only if the signal remains consecutively in the new state until the limit is reached do we update the output.

The Algorithm

  1. Compare the current (synchronized) input with our stored state.
  2. If they are different: Count how long the potential new state has been present.
  3. If they are equal: The change is not confirmed or has ended, so we reset the counter.
  4. If the counter is counting and reaches the maximum: It means the “new” signal has been stable for the entire time. It’s valid! We update the stored state.

Verilog Code

module debounce #(
    parameter CLK_FREQ = 12000000, // Clock frequency (12 MHz)
    parameter TIME_MS  = 20        // Debounce time (20 ms)
)(
    input wire clk,
    input wire rst,
    input wire btn_in,   // Physical input (noisy)
    output reg btn_out   // Clean and stable output
);

    // 1. Calculate how many cycles to count
    // Cycles = Time * Frequency
    localparam MAX_COUNT = (CLK_FREQ / 1000) * TIME_MS;
    localparam COUNT_WIDTH = (MAX_COUNT <= 1) ? 1 : $clog2(MAX_COUNT);
    reg [COUNT_WIDTH-1:0] counter;
    
    // 2. Synchronization (Chain of 2 FFs)
    reg btn_sync_0, btn_sync_1;
    always @(posedge clk) begin
        if (rst) begin
            btn_sync_0 <= 1'b0;
            btn_sync_1 <= 1'b0;
        end else begin
            btn_sync_0 <= btn_in;
            btn_sync_1 <= btn_sync_0;
        end
    end

    // 3. Filtering Machine
    always @(posedge clk) begin
        if (rst) begin
            counter <= 0;
            btn_out <= 1'b0;
        end else if (btn_sync_1 == btn_out) begin
            // If the input equals what we already have,
            // there is nothing to do. Reset the counter.
            // (We are in a stable state)
            counter <= 0;
            
        end else begin
            // The input is different from the output. Is it noise or a real change?
            // Start counting time.
            counter <= counter + 1;
            
            // If we manage to reach the limit without the input changing...
            if (counter == MAX_COUNT - 1) begin
                btn_out <= btn_sync_1; // Accept the change!
                counter <= 0;
            end
        end
    end

endmodule
Copied!

Edge Detector (One-Shot)

The previous module gives us a clean btn_out output, but it remains ‘1’ as long as you hold your finger down.

Often, in digital logic, we don’t want to know if the button “is pressed”, but if the button “has just been pressed”. We want a single pulse lasting one clock cycle, regardless of whether the user holds their finger for 1 second or 1 hour.

This is done with a rising edge detector, placed right after the debounce.

reg btn_prev;
wire btn_pressed; // 1-cycle pulse (One-shot)

always @(posedge clk) begin
    btn_prev <= btn_out; // Save the previous state
end

// Edge detected if: NOW is 1 AND BEFORE was 0
assign btn_pressed = (btn_out == 1'b1) && (btn_prev == 1'b0);
Copied!

It is very common to package the Debounce and the Edge Detector in a single module to have a btn_down signal ready to use in your state machines.