Ejercicio 04 — Toggle

Dificultad: amarillo · Módulo 02 (Entradas digitales)

Enunciado

Toggle. Cada pulsación completa cambia el estado del LED (enciende/apaga). El LED queda fijo entre pulsaciones (interruptor con botón momentáneo).

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 reto del módulo, y el ejercicio que junta todo lo anterior. Un pulsador es un botón momentáneo: solo está cerrado mientras lo aprietas. Lo que se pide aquí es convertirlo en un interruptor: cada pulsación completa cambia el estado del LED, y el LED se queda así hasta la pulsación siguiente. Es el comportamiento de casi cualquier botón de encendido que hayas tocado en tu vida.

El montaje vuelve a ser el completo: botón entre el pin 2 y GND, LED en el pin 8 con su resistencia de 220 Ω hacia GND.

Lo interesante es que el programa es el del ejercicio 03 con dos líneas cambiadas. Mismo antirrebote de 50 ms, misma detección de flanco de bajada; lo único distinto es qué se hace en ese instante. Allí se incrementaba un contador, aquí se invierte un estado. Que dos comportamientos tan distintos compartan el 90 % del código es la lección de fondo del módulo: el esqueleto “filtrar, detectar el flanco, actuar” es una herramienta general, no una receta de un ejercicio concreto.

La variable nueva es esta:

bool ledEncendido = false;

Un bool guarda solo dos valores, true o false, que es exactamente lo que necesitamos para “el LED debería estar encendido o no”. Podría haber sido un int con 0 y 1, pero bool dice lo que significa.

Y otra vez lo mismo, porque es el punto donde más gente tropieza: está declarada fuera del loop(). Tiene que estarlo. El programa no puede preguntarle al pin si el LED está encendido, porque lo que necesita saber no es el presente sino la decisión que tomó en la pulsación anterior, y esa decisión solo existe si alguien la guarda. Si declararas bool ledEncendido = false; dentro del loop(), se reiniciaría a false miles de veces por segundo y el LED no se encendería nunca. La memoria del programa vive en las variables que sobreviven a la vuelta.

En setup() hay un detalle que no estaba en los ejercicios anteriores:

pinMode(PIN_BOTON, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, LOW);

Ese digitalWrite(PIN_LED, LOW) inicial apaga el LED al arrancar. Es lo que hace que el mundo físico y la variable coincidan: ledEncendido empieza valiendo false y el LED empieza apagado. Si no lo pusieras, el pin arrancaría en un estado que no controlas y podrías tener el LED encendido con la variable diciendo false; a partir de ahí, la primera pulsación lo apagaría en vez de encenderlo y todo iría invertido para siempre. Mantener sincronizados el estado guardado y el estado real del hardware es una disciplina que se paga sola.

El loop() reutiliza el esqueleto conocido y remata así:

if (estadoEstable == LOW) {        // flanco de bajada = nueva pulsación
    ledEncendido = !ledEncendido;  // invierte el estado
    digitalWrite(PIN_LED, ledEncendido ? HIGH : LOW);
}

El operador ! es la negación lógica: convierte true en false y false en true. La línea ledEncendido = !ledEncendido; se lee “el nuevo valor es lo contrario del que había”, y es la forma canónica de escribir un conmutador. Merece la pena mirarla con calma porque tiene la misma variable a los dos lados del =: primero se calcula !ledEncendido con el valor viejo, y luego el resultado se guarda encima. Ese patrón, actualizar una variable a partir de sí misma, aparece en todas partes.

La línea siguiente traduce esa decisión al mundo físico: si ledEncendido es true el pin va a HIGH, si no a LOW. Y aquí está la diferencia esencial con el ejercicio 01, que conviene decir en voz alta: allí el LED seguía al botón instante a instante, aquí el LED sigue a una variable que el botón solo modifica en momentos puntuales. Entre pulsaciones el botón está suelto y al programa le da igual: el LED se queda como lo dejó la última decisión. Eso es tener estado.

Y aquí es donde el antirrebote se cobra el trabajo del ejercicio 02. Imagina este mismo programa sin filtrar: una sola pulsación produciría cuatro o seis rebotes, cada uno detectado como una pulsación nueva, y el LED conmutaría cuatro o seis veces en cinco milisegundos. Como no lo ves, el resultado aparente sería que el LED “a veces cambia y a veces no”, según si el número de rebotes salió par o impar. Es el fallo más frustrante de todos porque parece aleatorio, y buscar la causa en la lógica del programa no lleva a ninguna parte: la lógica es correcta, lo que está sucio es la señal. Si quieres verlo, comenta el filtro y prueba; es de esos errores que hay que sufrir una vez.

Cuando lo tengas funcionando, un par de extensiones que valen la pena. La primera: hacer que el LED conmute al soltar en vez de al pulsar, cambiando LOW por HIGH en ese último if. Notarás que se siente distinto al usarlo, y por eso los sistemas reales eligen conscientemente uno u otro. La segunda: añadir un Serial.println(ledEncendido) dentro del bloque para ver en el Monitor Serie que cada clic físico produce exactamente un cambio, y ninguno más.

Con esto ya tienes las tres piezas de cualquier interfaz de botón seria: leer sin flotar, filtrar el rebote y reaccionar al flanco guardando estado. A partir del módulo 03, cuando delay te empiece a estorbar de verdad, verás que la forma de pensar que has usado aquí, la de “en cada vuelta miro en qué estado estoy y decido”, es exactamente la que resuelve el problema.

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 04 — PRÁCTICA (rellena los TODO)
 * Enunciado: Toggle. Cada pulsación completa cambia el estado del LED (enciende/apaga).
 *            El LED queda fijo entre pulsaciones (interruptor con botón momentáneo).
 * Dificultad: amarillo
 * 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/ej04) / arduino-cli (ver README)
 *
 * Idea: combinamos antirrebote + flanco (como ej03) pero, en vez de contar, invertimos el
 * estado guardado del LED en cada flanco de bajada.
 */
const int PIN_BOTON = 2;
const int PIN_LED   = 8;
const unsigned long ANTIRREBOTE = 50;
 
int estadoEstable   = HIGH;
int lecturaAnterior = HIGH;
unsigned long tUltimoCambio = 0;
bool ledEncendido = false;
 
void setup() {
    // TODO: configura PIN_BOTON como INPUT_PULLUP y PIN_LED como OUTPUT
    // TODO: apaga el LED al arrancar -> digitalWrite(PIN_LED, LOW)
}
 
void loop() {
    // TODO: lee el pin y aplica el antirrebote con millis() (igual que ej02/ej03)
 
    // TODO: en el flanco de bajada (estadoEstable pasa a LOW):
    //         invierte ledEncendido -> ledEncendido = !ledEncendido
    //         escribe el LED -> digitalWrite(PIN_LED, ledEncendido ? HIGH : LOW)
}

Solución — cópiala y ejecútala

/*
 * Curso de Arduino — Módulo 02: Entradas digitales
 * Ejercicio 04 — MODELO (resuelto)
 * Enunciado: Toggle. Cada pulsación completa cambia el estado del LED (enciende/apaga).
 *            El LED queda fijo entre pulsaciones (interruptor con botón momentáneo).
 * Dificultad: amarillo
 * 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/ej04) / arduino-cli (ver README)
 *
 * Idea: combinamos antirrebote + flanco (como ej03) pero, en vez de contar, invertimos el
 * estado guardado del LED en cada flanco de bajada.
 */
const int PIN_BOTON = 2;
const int PIN_LED   = 8;
const unsigned long ANTIRREBOTE = 50;
 
int estadoEstable   = HIGH;
int lecturaAnterior = HIGH;
unsigned long tUltimoCambio = 0;
bool ledEncendido = false;
 
void setup() {
    pinMode(PIN_BOTON, INPUT_PULLUP);
    pinMode(PIN_LED, OUTPUT);
    digitalWrite(PIN_LED, LOW);
}
 
void loop() {
    int lectura = digitalRead(PIN_BOTON);
 
    if (lectura != lecturaAnterior) {
        tUltimoCambio = millis();
        lecturaAnterior = lectura;
    }
 
    if ((millis() - tUltimoCambio) > ANTIRREBOTE) {
        if (lectura != estadoEstable) {
            estadoEstable = lectura;
            if (estadoEstable == LOW) {        // flanco de bajada = nueva pulsación
                ledEncendido = !ledEncendido;  // invierte el estado
                digitalWrite(PIN_LED, ledEncendido ? HIGH : LOW);
            }
        }
    }
}

Cómo ejecutar la simulación

Sin instalar nada, en el navegador

  1. Abre wokwi.com y crea un proyecto nuevo de la placa que pide el ejercicio.
  2. Pega la solución (o tu intento) en la pestaña sketch.ino.
  3. Abre la pestaña diagram.json, borra lo que haya y pega el circuito de aquí abajo.
  4. 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/ej04/. Con la extensión Wokwi instalada y la licencia activada (lo cuenta el onboarding del curso):

make sim EJ=02-entradas-digitales/ej04

y después, en VS Code, Ctrl+Shift+PWokwi: 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.

Conexiones