Ejercicio 02 — Antirrebote (debounce) por software con millis()
Dificultad: verde · Módulo 02 (Entradas digitales)
Enunciado
Antirrebote (debounce) por software con millis(): aceptar el cambio de estado del botón solo si se mantuvo estable un tiempo mínimo. Enciende el LED al pulsar.
Montaje (Arduino UNO)
Botón entre pin 2 y GND; LED en pin 8 con resistencia 220 ohm a GND
Cómo se resuelve
El resultado visible de este ejercicio es idéntico al del anterior: el LED del pin 8 se enciende mientras mantienes pulsado el botón del pin 2. El montaje tampoco cambia. Lo que cambia es todo lo de dentro, y por una razón que todavía no se ve pero que va a hacer falta enseguida: el botón rebota.
Un pulsador son dos láminas metálicas que se tocan, y al tocarse botan como una pelota contra el suelo. Durante unos pocos milisegundos el contacto se abre y se cierra varias veces antes de asentarse. Tu dedo hace un clic; el pin ve una ráfaga de HIGH y LOW. En el ejercicio 01 daba igual porque el LED seguía el nivel del pin y un parpadeo de 5 ms no se ve. Aquí montamos el filtro que lo elimina, aún sin necesitarlo, para tenerlo listo cuando en el ejercicio 03 haya que contar.
La idea del antirrebote cabe en una frase: no te creas un cambio hasta que lleve un rato quieto. Ese rato es la constante del programa:
const unsigned long ANTIRREBOTE = 50; // ms que la señal debe mantenerse estable50 ms está elegido con criterio: es mucho más que la duración típica de un rebote (del orden de 1 a 10 ms) y mucho menos que lo que tarda un humano en pulsar dos veces a propósito, que son unos 150 ms como poco. Todo lo que dure menos de 50 ms es ruido mecánico; todo lo que dure más es intención.
Lo importante es cómo se espera ese rato. La tentación es escribir delay(50), y funciona, pero congela la placa entera durante esos 50 ms: no puede leer otro botón, ni atender un sensor, ni mantener un parpadeo. Por eso el programa usa millis(), que no espera a nada: simplemente devuelve cuántos milisegundos han pasado desde que arrancó la placa. En vez de “para 50 ms”, el programa apunta cuándo ocurrió el último cambio y en cada vuelta comprueba si ya han pasado 50 ms desde entonces.
Para eso hacen falta tres variables declaradas fuera del loop():
int lecturaAnterior = HIGH; // última lectura cruda del pin
int estadoEstable = HIGH; // estado ya filtrado (sin rebotes)
unsigned long tUltimoCambio = 0; // momento del último cambio crudoQue estén fuera no es cosmética: son la memoria del programa. Cada vuelta del loop() empieza de cero salvo por lo que esté guardado en globales, y un filtro que compara el presente con el pasado necesita, por definición, recordar el pasado. Si las declararas dentro del loop() se reinicializarían miles de veces por segundo a HIGH y a 0, y el filtro no filtraría nada.
Los dos primeros valen HIGH al arrancar porque ese es el estado en reposo con INPUT_PULLUP: si el programa empezara creyendo que el botón está pulsado, detectaría un cambio falso en la primera vuelta. Y tUltimoCambio es unsigned long, no int, porque millis() devuelve un contador que crece sin parar: en un int de 16 bits se desbordaría a los 32 segundos y el antirrebote se volvería loco. Con unsigned long tardarías casi 50 días, y además la resta sigue dando bien incluso cuando el contador da la vuelta, siempre que las dos variables sean del mismo tipo sin signo.
El loop() tiene tres bloques. Primero, la foto del pin:
int lectura = digitalRead(PIN_BOTON);Esta es la lectura cruda, con rebotes y todo. No hay que creérsela todavía.
Segundo, el cronómetro:
if (lectura != lecturaAnterior) {
tUltimoCambio = millis();
lecturaAnterior = lectura;
}Si la lectura de este instante difiere de la de la vuelta anterior, algo se ha movido: se anota el momento y se actualiza la referencia. Durante una ráfaga de rebotes esto se ejecuta varias veces seguidas, y cada vez el cronómetro vuelve a cero. Ahí está la clave de todo el mecanismo: mientras haya rebotes, el cronómetro nunca llega a arrancar de verdad.
Tercero, el filtro:
if ((millis() - tUltimoCambio) > ANTIRREBOTE) {
if (lectura != estadoEstable) {
estadoEstable = lectura;
}
}La condición de fuera dice “la lectura lleva más de 50 ms sin cambiar”, y solo se cumple cuando el contacto ya se ha asentado. La de dentro dice “y además es distinta de lo que yo creía”, que es lo que evita reasignar lo mismo una y otra vez. Cuando ambas se cumplen, el programa por fin se cree la lectura y la copia a estadoEstable. Ese estadoEstable es el botón “limpio”, el que puedes usar sin miedo.
La resta millis() - tUltimoCambio merece un segundo de atención. No es “esperar”, es “mirar el reloj y calcular cuánto ha pasado”. El loop() sigue dando vueltas a toda velocidad mientras tanto; simplemente, la mayoría de esas vueltas no hacen nada porque la condición todavía es falsa. Ese cambio de “haz esto, espera, haz lo otro” a “en cada vuelta mira si ya toca” es la forma de pensar que vas a usar en todo lo que venga después.
Y al final, la salida:
digitalWrite(PIN_LED, estadoEstable == LOW ? HIGH : LOW);El operador ternario condición ? valorSiCierto : valorSiFalso es la forma corta del if/else del ejercicio 01, cuando lo único que cambia es un valor. Se lee: si estadoEstable es LOW (o sea, pulsado, con la lógica invertida de la pull-up) escribe HIGH en el pin del LED; si no, escribe LOW. Ojo a que los dos LOW de la línea significan cosas distintas: el primero es el nivel del botón y el segundo el del LED. Es exactamente la clase de línea que confunde al leerla rápido.
Prueba a poner ANTIRREBOTE a 500 y verás que el LED va con un retardo perceptible y que si pulsas rápido se pierden pulsaciones. Ponlo a 1 y vuelves prácticamente al ejercicio 01. Entre esos dos extremos, 50 es un valor cómodo para casi cualquier pulsador.
Para practicar — cópialo y complétalo
Pega este esqueleto y completa los TODO. Es la mejor forma de aprender: inténtalo antes de mirar la solución.
/*
* Curso de Arduino — Módulo 02: Entradas digitales
* Ejercicio 02 — PRÁCTICA (rellena los TODO)
* Enunciado: Antirrebote (debounce) por software con millis(): aceptar el cambio de estado
* del botón solo si se mantuvo estable un tiempo mínimo. Enciende el LED al pulsar.
* Dificultad: verde
* Placa: Arduino UNO Montaje: botón entre pin 2 y GND; LED en pin 8 con resistencia 220 ohm a GND
* Correr: Arduino IDE / Wokwi (wokwi/ej02) / arduino-cli (ver README)
*
* Idea: el contacto mecánico "rebota" unos ms al pulsar. Ignoramos lecturas hasta que la
* señal lleva ANTIRREBOTE ms estable. Usamos millis() (no delay) para no congelar el programa.
*/
const int PIN_BOTON = 2;
const int PIN_LED = 8;
const unsigned long ANTIRREBOTE = 50; // ms que la señal debe mantenerse estable
int lecturaAnterior = HIGH; // última lectura cruda del pin
int estadoEstable = HIGH; // estado ya filtrado (sin rebotes)
unsigned long tUltimoCambio = 0; // momento del último cambio crudo
void setup() {
// TODO: configura PIN_BOTON como INPUT_PULLUP y PIN_LED como OUTPUT
}
void loop() {
// TODO: lee el pin -> int lectura = digitalRead(PIN_BOTON)
// TODO: si lectura != lecturaAnterior -> guarda tUltimoCambio = millis() y lecturaAnterior = lectura
// TODO: si (millis() - tUltimoCambio) > ANTIRREBOTE y lectura != estadoEstable -> estadoEstable = lectura
// TODO: enciende el LED si estadoEstable == LOW (pulsado), apágalo si no
}Solución — cópiala y ejecútala
/*
* Curso de Arduino — Módulo 02: Entradas digitales
* Ejercicio 02 — MODELO (resuelto)
* Enunciado: Antirrebote (debounce) por software con millis(): aceptar el cambio de estado
* del botón solo si se mantuvo estable un tiempo mínimo. Enciende el LED al pulsar.
* Dificultad: verde
* Placa: Arduino UNO Montaje: botón entre pin 2 y GND; LED en pin 8 con resistencia 220 ohm a GND
* Correr: Arduino IDE / Wokwi (wokwi/ej02) / arduino-cli (ver README)
*
* Idea: el contacto mecánico "rebota" unos ms al pulsar. Ignoramos lecturas hasta que la
* señal lleva ANTIRREBOTE ms estable. Usamos millis() (no delay) para no congelar el programa.
*/
const int PIN_BOTON = 2;
const int PIN_LED = 8;
const unsigned long ANTIRREBOTE = 50; // ms que la señal debe mantenerse estable
int lecturaAnterior = HIGH; // última lectura cruda del pin
int estadoEstable = HIGH; // estado ya filtrado (sin rebotes)
unsigned long tUltimoCambio = 0; // momento del último cambio crudo
void setup() {
pinMode(PIN_BOTON, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);
}
void loop() {
int lectura = digitalRead(PIN_BOTON);
// Si la lectura cruda cambió, reiniciamos el cronómetro del antirrebote.
if (lectura != lecturaAnterior) {
tUltimoCambio = millis();
lecturaAnterior = lectura;
}
// Si la lectura lleva estable más de ANTIRREBOTE ms, la damos por buena.
if ((millis() - tUltimoCambio) > ANTIRREBOTE) {
if (lectura != estadoEstable) {
estadoEstable = lectura;
}
}
// estadoEstable LOW = pulsado (lógica invertida por INPUT_PULLUP)
digitalWrite(PIN_LED, estadoEstable == LOW ? HIGH : LOW);
}Cómo ejecutar la simulación
Sin instalar nada, en el navegador
- Abre wokwi.com y crea un proyecto nuevo de la placa que pide el ejercicio.
- Pega la solución (o tu intento) en la pestaña
sketch.ino. - Abre la pestaña
diagram.json, borra lo que haya y pega el circuito de aquí abajo. - Dale al play. Verás el circuito funcionando: LEDs, pantallas y monitor serie.
Circuito del ejercicio (diagram.json)
{
"version": 1,
"author": "ESIBot",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-arduino-uno", "id": "uno", "top": 0, "left": 0, "attrs": {} },
{ "type": "wokwi-pushbutton", "id": "btn", "top": -100, "left": 200, "attrs": { "color": "green" } },
{ "type": "wokwi-led", "id": "led", "top": 100, "left": 200, "attrs": { "color": "red" } },
{ "type": "wokwi-resistor", "id": "r1", "top": 130, "left": 120, "attrs": { "value": "220" } }
],
"connections": [
[ "uno:2", "btn:1.l", "green", [] ],
[ "btn:2.l", "uno:GND.1", "black", [] ],
[ "uno:8", "r1:1", "blue", [] ],
[ "r1:2", "led:A", "blue", [] ],
[ "led:C", "uno:GND.2", "black", [] ]
],
"dependencies": {}
}En VS Code, con el repo del curso
El paquete de simulación de este ejercicio está en practica/02-entradas-digitales/wokwi/ej02/. Con la extensión Wokwi instalada y la licencia activada (lo cuenta el onboarding del curso):
make sim EJ=02-entradas-digitales/ej02y después, en VS Code, Ctrl+Shift+P → Wokwi: Start Simulator. La extensión no compila: simula un firmware ya compilado, y de compilarlo se encarga ese make.
Cómo usarlo
Con placa: abre el .ino en el Arduino IDE, elige placa y puerto y pulsa Subir. Sin placa: simulalo en Wokwi con cualquiera de las dos vias de arriba. Antes de mirar la solucion, intenta completar tu el esqueleto: es la mejor forma de aprender.