fpga-debounce-eliminar-rebotes

Debounce en FPGA: cómo leer pulsadores correctamente

  • 5 min

Un debouncer es un filtro digital que solo acepta un cambio cuando la entrada permanece estable durante un tiempo determinado.

Tarde o temprano, querrás conectar tu FPGA al mundo real pulsando un botón. Y ahí es donde la realidad te da una bofetada.

Diseñas un contador que debería sumar 1 cada vez que pulsas un botón. Lo pruebas, pulsas el botón una vez, y el contador suma 15, 20 o 50 de golpe. ¿Qué ha pasado? ¿Está rota la FPGA?

No. Bienvenido al maravilloso mundo del Rebote Mecánico (Bouncing). Hoy vamos a aprender a domarlo usando lógica secuencial.

El problema: el mundo real es ruidoso

Un pulsador no es un componente digital ideal que pasa de 0 a 1 instantáneamente. Por dentro, son dos chapitas de metal chocando.

Cuando pulsas, esas chapas rebotan físicamente varias veces antes de quedarse pegadas (como una pelota de ping-pong cayendo al suelo).

  • Tiempo humano: Un rebote dura unos 10 o 20 milisegundos. Para ti es instantáneo.
  • Tiempo FPGA: Tu FPGA corre a 12 MHz (12 millones de ciclos por segundo). En 10 milisegundos, la FPGA “mira” el botón 120.000 veces.

Para la FPGA, ese “clic” único se ve como una metralleta de 0s y 1s.

Si usamos esa señal directamente como reloj o habilitador de un contador, contaremos cada pequeño rebote como una pulsación válida.

La estrategia: un filtro digital

Para solucionar esto, necesitamos implementar un filtro de tiempo (Debounce). La lógica es sencilla: “No me creeré que el botón ha cambiado de estado hasta que la señal se mantenga estable durante X tiempo”.

Un tiempo de espera de 10 a 20 ms es habitual, pero debes ajustarlo al pulsador y a la respuesta que necesite la aplicación.

Sincronización y metastabilidad

Antes de filtrar, hay una norma importante en FPGAs: nunca metas una señal externa asíncrona directamente a tu lógica.

El botón lo pulsa un humano, sin relación con el reloj de la FPGA. Si la señal cambia justo cuando el reloj está subiendo (violación de Setup/Hold time), podemos causar Metastabilidad (el Flip-Flop entra en un estado inestable que no es ni 0 ni 1).

Para reducir enormemente la probabilidad de que la metastabilidad se propague, pasamos la señal por una cadena de dos flip-flops sincronizadores. No elimina matemáticamente el riesgo, pero lo lleva a un nivel aceptable para la mayoría de diseños.

reg [1:0] sync_btn;
always @(posedge clk) begin
    // Shift register de 2 etapas
    sync_btn <= {sync_btn[0], boton_entrada_fisica};
end
wire boton_seguro = sync_btn[1]; // Usamos esta señal internamente
Copied!

Implementación del módulo debounce

Vamos a crear un módulo genérico. El contador se reinicia cuando la entrada vuelve al estado aceptado. Solo si la señal permanece de forma consecutiva en el nuevo estado hasta llegar al límite actualizamos la salida.

El algoritmo

  1. Comparamos la entrada actual (sincronizada) con nuestro estado guardado.
  2. Si son diferentes: Contamos cuánto tiempo lleva presente el posible estado nuevo.
  3. Si son iguales: El cambio no se ha confirmado o ya terminó, así que reiniciamos el contador.
  4. Si el contador está contando y llega al tope: Significa que la señal “nueva” ha sido estable durante todo el tiempo. ¡Es válida! Actualizamos el estado guardado.

Código Verilog

module debounce #(
    parameter CLK_FREQ = 12000000, // Frecuencia del reloj (12 MHz)
    parameter TIME_MS  = 20        // Tiempo de rebote (20 ms)
)(
    input wire clk,
    input wire rst,
    input wire btn_in,   // Entrada física (ruidosa)
    output reg btn_out   // Salida limpia y estable
);

    // 1. Calculamos hasta cuánto hay que contar
    // Ciclos = Tiempo * Frecuencia
    localparam MAX_COUNT = (CLK_FREQ / 1000) * TIME_MS;
    localparam COUNT_WIDTH = (MAX_COUNT <= 1) ? 1 : $clog2(MAX_COUNT);
    reg [COUNT_WIDTH-1:0] contador;
    
    // 2. Sincronización (Cadena de 2 FF)
    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. Máquina de filtrado
    always @(posedge clk) begin
        if (rst) begin
            contador <= 0;
            btn_out <= 1'b0;
        end else if (btn_sync_1 == btn_out) begin
            // Si la entrada es igual a lo que ya tenemos,
            // no hay nada que hacer. Reseteamos el contador.
            // (Estamos en estado estable)
            contador <= 0;
            
        end else begin
            // La entrada es diferente a la salida. ¿Es ruido o un cambio real?
            // Empezamos a contar tiempo.
            contador <= contador + 1;
            
            // Si conseguimos llegar al tope sin que la entrada cambie...
            if (contador == MAX_COUNT - 1) begin
                btn_out <= btn_sync_1; // ¡Aceptamos el cambio!
                contador <= 0;
            end
        end
    end

endmodule
Copied!

Detector de flancos (one-shot)

El módulo anterior nos da una salida btn_out limpia, pero que se mantiene en ‘1’ todo el tiempo que tienes el dedo puesto.

A menudo, en lógica digital, no queremos saber si el botón “está pulsado”, sino si el botón “acaba de ser pulsado”. Queremos un solo pulso de un ciclo de reloj, independientemente de si el usuario deja el dedo 1 segundo o 1 hora.

Esto se hace con un detector de flanco de subida, justo después del debounce.

reg btn_prev;
wire btn_pulsado; // Pulso de 1 ciclo (One-shot)

always @(posedge clk) begin
    btn_prev <= btn_out; // Guardamos el estado anterior
end

// Hubo flanco si: AHORA es 1 Y ANTES era 0
assign btn_pulsado = (btn_out == 1'b1) && (btn_prev == 1'b0);
Copied!

Es muy común empaquetar el Debounce y el Detector de Flanco en un solo módulo para tener una señal btn_down lista para usar en tus máquinas de estados.