¿Por qué un botón con Arduino registra cinco pulsaciones en lugar de una?

Los pulsadores no hacen un contacto limpio: el metal rebotará durante milisegundos y registrará varios contactos. Aprende cómo funciona y a corregirlo sin delay().
Cuando un Arduino registra cinco o seis pulsaciones tras un único toque físico en el botón, el microcontrolador no está roto ni el código está embrujado. El botón está sufriendo un rebote mecánico. Un pulsador táctil contiene dos finas láminas metálicas flexibles que chocan entre sí al presionarlo. A escala humana, el clic parece instantáneo. A la escala del microcontrolador —donde un ATmega328P estándar a 16 MHz ejecuta aproximadamente dieciséis instrucciones cada microsegundo—, las láminas de contacto colisionan, rebotan, vuelven a hacer contacto y se estabilizan a lo largo de un intervalo de dos a veinte milisegundos. Para el Arduino, ese traqueteo mecánico equivale a cinco pulsaciones distintas.
Cómo es realmente el rebote mecánico de un interruptor
En un circuito estándar de placa de pruebas (breadboard) que utilice una resistencia pulldown externa o la resistencia interna INPUT_PULLUP, el pin del microcontrolador vigila una transición de tensión entre 0 V y 5 V. Si el código comprueba el pin dentro de un bucle rápido void loop() mediante una condición simple:
if (digitalRead(buttonPin) == LOW) {
pressCount++;
}se producen dos problemas distintos de manera simultánea:
- Detección por nivel frente a detección por flanco: El código contabiliza una pulsación cada vez que el bucle se ejecuta mientras el botón permanece presionado. Si el bucle tarda 200 microsegundos en completarse, mantener el botón pulsado durante medio segundo registrará 2500 pulsaciones.
- Rebote de contacto (bounce): Incluso si se suelta el pulsador al instante, la tensión oscila entre niveles altos y bajos varias veces a lo largo de 5 a 15 milisegundos mientras las láminas metálicas se estabilizan. El procesador es lo bastante rápido como para captar cada rebote individual como una transición válida.
El reflejo habitual de quien empieza es recurrir a delay(). Funciona en una prueba rudimentaria, pero en cualquier proyecto real crea problemas mucho más graves de los que resuelve.
Por qué delay(50) arruina por completo el rendimiento de tu robot
Si se le indica al Arduino que espere cincuenta milisegundos inmediatamente después de detectar una pulsación —delay(50);—, el conmutador termina de rebotar antes de que el microcontrolador vuelva a comprobar el pin. El contador incrementa entonces de forma limpia de uno en uno.
Sin embargo, delay() es una función bloqueante. Detiene por completo el procesamiento de instrucciones. Mientras el chip permanece en esa pausa de 50 ms, no puede actualizar la modulación por ancho de pulsos (PWM) de los motores, no puede sondear el pin de eco de un sensor ultrasónico, no puede leer el pulso de un codificador y no puede decodificar los comandos serie entrantes de un ESP32 o un módulo Bluetooth. En un contador estático, un retardo bloqueante apenas se nota. En un robot móvil o en una barrera de acceso automatizada, una congelación de 50 ms cada vez que se activa un final de carrera provoca pérdidas de marcas de posición, tirones en los motores y una navegación errática.
La eliminación de rebotes por hardware (debouncing por hardware) —añadiendo un condensador cerámico de 100 nF entre los contactos del pulsador o utilizando un filtro paso bajo RC con un disparador Schmitt— limpia la señal antes de que alcance el microcontrolador. Es una solución elegante a nivel eléctrico, pero requiere componentes adicionales en el taller, añade desorden en la placa de pruebas o exige soldaduras, e incrementa los costes de material en un aula de 30 alumnos. En prácticamente cualquier entorno educativo, el seguimiento de estado por software es la respuesta adecuada.
La solución limpia: detección de flancos y millis()
Resolver esto limpiamente en el código requiere dos pasos lógicos independientes: detectar cuándo cambia el estado (detección de flanco) y comprobar que ha transcurrido suficiente tiempo desde la última transición antes de aceptar una nueva (temporización de antirrebote o debounce).
Esto requiere tres variables:
- La lectura actual del pin.
- La lectura anterior del pin procedente de la iteración previa del bucle.
- Una marca de tiempo unsigned long que registre cuándo cambió el estado por última vez.
Una implementación sólida y no bloqueante tiene este aspecto:
const int buttonPin = 2;
int buttonState = HIGH;
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 40; // milisegundos
int counter = 0;
void setup() {
pinMode(buttonPin, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
int reading = digitalRead(buttonPin);
// Comprueba si el estado del pin cambió por rebote o por pulsación física
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
// Solo evalúa el estado si se ha mantenido estable más tiempo que el margen fijado
if ((millis() - lastDebounceTime) > debounceDelay) {
// Si el estado verificado ha cambiado
if (reading != buttonState) {
buttonState = reading;
// Se activa solo al presionar (flanco de bajada con INPUT_PULLUP)
if (buttonState == LOW) {
counter++;
Serial.print("Pulsaciones verificadas: ");
Serial.println(counter);
}
}
}
lastButtonState = reading;
// El resto de loop() se ejecuta sin interrupciones ni retardos
}En esta arquitectura, millis() actúa como un reloj de pared no bloqueante. El código inspecciona continuamente el pin, reinicia una marca de tiempo cada vez que se produce una oscilación y solo acepta un cambio de estado una vez que el pin permanece en reposo durante 40 milisegundos consecutivos. El procesador queda 100 % libre entre comprobaciones para realizar cálculos de control PID o vigilar paradas de emergencia.
Cómo explicar esto en la pizarra en diez minutos
Al alumnado le cuesta asimilar el concepto de antirrebote cuando se le presenta como un muro de estructuras condicionales. Para enseñarlo con éxito, separa la mecánica de la sintaxis mediante una analogía sencilla en el aula:
- La puerta que se cierra de golpe: Dibuja una puerta de madera pesada cerrándose de golpe. Al chocar contra el marco no se queda inmóvil al instante; retiembla en el pestillo durante una fracción de segundo. Explica que un pulsador está hecho de resortes de latón o acero que hacen exactamente lo mismo.
- El vigilante de seguridad ultrarrápido: Pregunta a la clase: «Si un guardia cuenta a todas las personas que cruzan una puerta, pero parpadea una vez cada cinco minutos, ¿qué ocurre?». Se le escapará gente. «¿Y si el guardia puede mirar la puerta dieciséis mil veces por segundo?». Contará cada vibración del pestillo como si fuera una nueva persona entrando. Eso es lo que hace un Arduino ejecutando
digitalRead(). - Estado frente a flanco: Escribe las palabras «ESTÁ» y «PASÓ A ESTAR» en la pizarra. Muéstrales que su primer código preguntaba: «¿Está el botón pulsado?» (verdadero durante 200 000 ciclos). El nuevo código debe preguntar: «¿Acaba de pasar a estar pulsado?».
- La regla de los 40 ms: Explica que en lugar de cerrar los ojos (
delay), el vigilante mira su reloj de pulsera (millis). Si la puerta tiembla, reiniciamos un temporizador de 40 milisegundos. No anotamos una visita hasta que la puerta haya permanecido totalmente quieta durante cuarenta tics.
Si estás equipando un laboratorio escolar o introduciendo la computación física en las aulas de Senior Phase (fase sénior), hay paquetes curriculares estructurados y kits de componentes verificados disponibles en la tienda de Sheen Robotics, diseñados específicamente para evitar componentes de placa de pruebas poco fiables que complican la resolución de problemas mecánicos más de la cuenta.
Una vez que los estudiantes comprenden que los interruptores son objetos mecánicos imperfectos que interactúan con ordenadores ultrarrápidos, el antirrebote deja de parecer un fallo de software misterioso y se convierte en su primera lección real de ingeniería de sistemas embebidos.



