Control de Array de LEDs con 4 Pulsadores (Sin Función delay)

1. Descripción del Proyecto y Requisitos

El objetivo de este proyecto es controlar un array de 9 LEDs mediante 4 pulsadores, implementando una arquitectura de software totalmente no bloqueante, es decir, sin utilizar la función delay().

Al no usar delay(), el procesador puede responder instantáneamente a la pulsación de cualquier botón en cualquier milisegundo, sin congelar las animaciones ni perder lecturas.

Asignación de Pines

2. Comportamiento de los 4 Pulsadores

Pulsador Pin Arduino Modo Asociado Comportamiento de los LEDs
Pulsador 1 Pin 28 MODO_TODOS_ENCENDIDOS Se encienden todos los LEDs de forma fija.
Pulsador 2 Pin 26 MODO_TODOS_APAGADOS Se apagan todos los LEDs de forma fija.
Pulsador 3 Pin 24 MODO_CICLICO_SIMPLE Se enciende un LED y se apaga el anterior de manera cíclica (efecto desplazamiento o carrusel secuencial).
Pulsador 4 Pin 22 MODO_CICLICO_ACUMULADO Se enciende el primer LED, luego el siguiente sumándose al anterior hasta llegar al último (barra progresiva); en ese instante se apagan todos y el ciclo vuelve a comenzar.

3. Explicación Técnica del Funcionamiento

3.1. Eliminación de delay() mediante millis()

La función delay() detiene la ejecución del microcontrolador durante el tiempo indicado. Durante esa pausa, la CPU no puede leer los botones ni procesar eventos.

Para solucionarlo, se utiliza la función nativa millis(), que devuelve los milisegundos transcurridos desde que arrancó la placa. Calculamos el tiempo transcurrido:

if (tiempoActual - ultimoCambioPaso >= INTERVALO_ANIMACION)

Si la condición se cumple, se avanza un paso en la animación y se actualiza ultimoCambioPaso = tiempoActual. Si no se cumple, la función retorna de inmediato permitiendo que el bucle loop() continúe evaluando los botones miles de veces por segundo.

3.2. Antirrebote (Debounce) por Software sin delay

Los botones mecánicos generan rebotes parásitos (oscilaciones rápidas entre 0 y 1 durante los primeros 5-20 ms). Para evitar pulsaciones fantasma sin usar delay(), se define una estructura individual para cada pulsador:

struct Pulsador { int ultimoEstadoLectura; int estadoEstable; unsigned long ultimoTiempoRebote; };

Cuando la lectura física cambia, se reinicia el contador de rebote (ultimoTiempoRebote = millis()). Solo si el pin permanece estable durante más de 50 ms (TIEMPO_ANTIRREBOTE), se valida el cambio de estado y, al detectar el flanco de bajada (ESTADO_PRESIONADO == LOW), se conmuta el modo del sistema.

3.3. Máquina de Estados (enum Modo)

El comportamiento global se organiza como una máquina de estados con 4 modos posibles:

4. Código Arduino

// ==========================================
// DEFINICIÓN DE PINES
// ==========================================
const int leds[] = {13, 12, 11, 10, 9, 8, 7, 6, 5};
const int entradas[] = {28, 26, 24, 22};
const int numeroLeds = sizeof(leds) / sizeof(leds[0]);
const int numeroEntradas = sizeof(entradas) / sizeof(entradas[0]);

// Configuración de pulsadores (INPUT_PULLUP = LOW al presionar)
const int ESTADO_PRESIONADO = LOW;

// ==========================================
// TIEMPOS (en ms usando millis)
// ==========================================
const unsigned long INTERVALO_ANIMACION = 300; // Velocidad del ciclo de LEDs
const unsigned long TIEMPO_ANTIRREBOTE = 50;    // Filtro contra falsas pulsaciones

// ==========================================
// MODOS DE OPERACIÓN
// ==========================================
enum Modo {
  MODO_TODOS_ENCENDIDOS,
  MODO_TODOS_APAGADOS,
  MODO_CICLICO_SIMPLE,
  MODO_CICLICO_ACUMULADO
};

Modo modoActual = MODO_TODOS_APAGADOS;
unsigned long ultimoCambioPaso = 0;
int pasoAnimacion = 0;

// Estructura para el antirrebote individual de cada pulsador sin delay()
struct Pulsador {
  int ultimoEstadoLectura;
  int estadoEstable;
  unsigned long ultimoTiempoRebote;
};

Pulsador pulsadores[numeroEntradas];

// ==========================================
// FUNCIONES DE CONTROL DE LEDS
// ==========================================
void apagarTodosLosLeds() {
  for (int i = 0; i < numeroLeds; i++) {
    digitalWrite(leds[i], LOW);
  }
}

void encenderTodosLosLeds() {
  for (int i = 0; i < numeroLeds; i++) {
    digitalWrite(leds[i], HIGH);
  }
}

// Configura y arranca el modo inmediatamente
void seleccionarModo(Modo nuevoModo) {
  modoActual = nuevoModo;
  pasoAnimacion = 0;
  ultimoCambioPaso = millis();

  switch (modoActual) {
    case MODO_TODOS_ENCENDIDOS:
      encenderTodosLosLeds();
      break;
    case MODO_TODOS_APAGADOS:
      apagarTodosLosLeds();
      break;
    case MODO_CICLICO_SIMPLE:
      apagarTodosLosLeds();
      if (numeroLeds > 0) {
        digitalWrite(leds[0], HIGH);
      }
      break;
    case MODO_CICLICO_ACUMULADO:
      apagarTodosLosLeds();
      if (numeroLeds > 0) {
        digitalWrite(leds[0], HIGH);
      }
      break;
  }
}

// Actualiza las secuencias cíclicas según millis()
void actualizarAnimaciones() {
  unsigned long tiempoActual = millis();
  if (tiempoActual - ultimoCambioPaso < INTERVALO_ANIMACION) {
    return; // Aún no ha pasado el intervalo
  }
  ultimoCambioPaso = tiempoActual;

  if (modoActual == MODO_CICLICO_SIMPLE) {
    // 3 => Se enciende un led y se apaga el anterior (cíclico)
    pasoAnimacion = (pasoAnimacion + 1) % numeroLeds;
    for (int i = 0; i < numeroLeds; i++) {
      digitalWrite(leds[i], (i == pasoAnimacion) ? HIGH : LOW);
    }
  } 
  else if (modoActual == MODO_CICLICO_ACUMULADO) {
    // 4 => Se enciende un led y luego el siguiente, así hasta el último,
    //      entonces se apagan todos (cíclico)
    pasoAnimacion++;
    
    if (pasoAnimacion < numeroLeds) {
      for (int i = 0; i < numeroLeds; i++) {
        digitalWrite(leds[i], (i <= pasoAnimacion) ? HIGH : LOW);
      }
    } else {
      // Llegó al final: se apagan todos los LEDs y reinicia
      apagarTodosLosLeds();
      pasoAnimacion = -1; // En el próximo tick pasará a 0
    }
  }
}

// Lectura con antirrebote independiente para cada pulsador
void leerPulsadores() {
  unsigned long tiempoActual = millis();
  for (int i = 0; i < numeroEntradas; i++) {
    int lectura = digitalRead(entradas[i]);
    if (lectura != pulsadores[i].ultimoEstadoLectura) {
      pulsadores[i].ultimoTiempoRebote = tiempoActual;
      pulsadores[i].ultimoEstadoLectura = lectura;
    }
    if ((tiempoActual - pulsadores[i].ultimoTiempoRebote) > TIEMPO_ANTIRREBOTE) {
      if (lectura != pulsadores[i].estadoEstable) {
        pulsadores[i].estadoEstable = lectura;
        // Detecta el momento en que se pulsa el botón
        if (pulsadores[i].estadoEstable == ESTADO_PRESIONADO) {
          switch (i) {
            case 0: // Pulsador 1 (Pin 28)
              seleccionarModo(MODO_TODOS_ENCENDIDOS);
              break;
            case 1: // Pulsador 2 (Pin 26)
              seleccionarModo(MODO_TODOS_APAGADOS);
              break;
            case 2: // Pulsador 3 (Pin 24)
              seleccionarModo(MODO_CICLICO_SIMPLE);
              break;
            case 3: // Pulsador 4 (Pin 22)
              seleccionarModo(MODO_CICLICO_ACUMULADO);
              break;
          }
        }
      }
    }
  }
}

// ==========================================
// SETUP Y LOOP
// ==========================================
void setup() {
  for (int i = 0; i < numeroLeds; i++) {
    pinMode(leds[i], OUTPUT);
    digitalWrite(leds[i], LOW);
  }
  for (int i = 0; i < numeroEntradas; i++) {
    pinMode(entradas[i], INPUT_PULLUP);
    pulsadores[i].ultimoEstadoLectura = HIGH;
    pulsadores[i].estadoEstable = HIGH;
    pulsadores[i].ultimoTiempoRebote = 0;
  }
  seleccionarModo(MODO_TODOS_APAGADOS);
}

void loop() {
  leerPulsadores();        // Lee los 4 pulsadores al instante
  actualizarAnimaciones(); // Gestiona los tiempos con millis() sin bloquear
}