Ir al contenido

Patrón Blink Sin delay()

Patrón Blink Sin delay()

Esta sección cubre el patrón de temporización no bloqueante usando millis(), una técnica fundamental para el desarrollo con ESP32. Al finalizar esta sección, podrás:

  • Reemplazar delay() con el patrón no bloqueante de millis()
  • Gestionar múltiples intervalos de temporización independientes simultáneamente
  • Entender por qué delay() es problemático en aplicaciones IoT
  • Implementar el patrón en la arquitectura del Sketch Básico
  • Arquitectura del Sketch Básico (01-11)
  • Comprensión de cómo funcionan setup() y loop()

La función delay() detiene toda la ejecución del código durante la duración especificada. Durante ese tiempo:

  • La conexión MQTT se cae: client.loop() no se llama, por lo que la conexión expira
  • Se pierden lecturas de sensores: Si una interrupción de sensor se dispara durante el delay, se ignora
  • No se detecta la desconexión Wi-Fi: El ESP32 no puede procesar eventos de Wi-Fi
  • Se pierden pulsaciones de botones: La entrada física no se consulta
  • El tiempo de respuesta se degrada: El dispositivo parece no responder

En resumen, delay() convierte un ESP32 de doble núcleo con capacidad multitarea en un dispositivo de una sola tarea.

La función millis() devuelve el número de milisegundos desde que el ESP32 se inició (se reinicia después de ~50 días). Comparando el millis() actual con un valor “previo” almacenado, podemos determinar cuándo corresponde realizar una acción — sin bloquear.

El Patrón:

unsigned long previousMillis = 0;
const unsigned long INTERVAL = 1000; // 1 segundo
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= INTERVAL) {
previousMillis = currentMillis;
// Realizar la acción temporizada
}
// El resto del código se ejecuta inmediatamente, sin bloquearse
}

millis() se reinicia a 0 después de aproximadamente 50 días. Sin embargo, la resta currentMillis - previousMillis funciona correctamente incluso después del desbordamiento porque el resultado es un unsigned long:

Escenario: previousMillis = 4,294,967,000 (cerca del desbordamiento)
currentMillis = 100 (después del desbordamiento)
currentMillis - previousMillis = 100 - 4,294,967,000 = 102 (¡correcto!)

Esto funciona debido al ajuste de la aritmética de enteros sin signo — no se necesita manejo especial.

Paso 1: Un Solo Temporizador — Parpadeo de LED

Sección titulada «Paso 1: Un Solo Temporizador — Parpadeo de LED»

El ejemplo clásico de “Blink Without Delay” para ESP32:

const int LED_PIN = 2; // LED integrado en la mayoría de los DevKits ESP32
// Variables de temporización
unsigned long previousBlinkMillis = 0;
const unsigned long BLINK_INTERVAL = 1000; // 1 segundo
int ledState = LOW;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
unsigned long currentMillis = millis();
// Verificación de parpadeo no bloqueante
if (currentMillis - previousBlinkMillis >= BLINK_INTERVAL) {
previousBlinkMillis = currentMillis;
// Alternar LED
ledState = (ledState == LOW) ? HIGH : LOW;
digitalWrite(LED_PIN, ledState);
}
// Otro código puede ir aquí — ¡se ejecuta inmediatamente!
// Por ejemplo: verificar MQTT, leer sensores, etc.
}

Paso 2: Múltiples Temporizadores Independientes

Sección titulada «Paso 2: Múltiples Temporizadores Independientes»

El verdadero poder de millis() es gestionar múltiples tareas concurrentes con diferentes intervalos:

const int LED_PIN = 2;
const int STATUS_LED_PIN = 15;
// Temporizador 1: Parpadeo de LED (rápido)
unsigned long previousBlinkMillis = 0;
const unsigned long BLINK_INTERVAL = 500; // 0.5 segundos
// Temporizador 2: LED de estado (lento)
unsigned long previousStatusMillis = 0;
const unsigned long STATUS_INTERVAL = 3000; // 3 segundos
// Temporizador 3: Impresión serial
unsigned long previousPrintMillis = 0;
const unsigned long PRINT_INTERVAL = 10000; // 10 segundos
int ledState = LOW;
int statusLedState = LOW;
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
pinMode(STATUS_LED_PIN, OUTPUT);
}
void loop() {
unsigned long currentMillis = millis();
// Temporizador 1: Parpadeo rápido de LED (500ms)
if (currentMillis - previousBlinkMillis >= BLINK_INTERVAL) {
previousBlinkMillis = currentMillis;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
// Temporizador 2: LED de estado lento (3000ms)
if (currentMillis - previousStatusMillis >= STATUS_INTERVAL) {
previousStatusMillis = currentMillis;
statusLedState = !statusLedState;
digitalWrite(STATUS_LED_PIN, statusLedState);
}
// Temporizador 3: Latido serial (10000ms)
if (currentMillis - previousPrintMillis >= PRINT_INTERVAL) {
previousPrintMillis = currentMillis;
Serial.print("Heartbeat - Uptime: ");
Serial.print(currentMillis / 1000);
Serial.println(" seconds");
}
}

En el Sketch Básico (01-11), el patrón no bloqueante se usa tanto para el parpadeo del LED como para la publicación periódica de datos:

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
// Configuración MQTT y Wi-Fi (abreviada — ver 01-11 para el código completo)
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
// Intervalo de publicación
unsigned long previousPublishMillis = 0;
const unsigned long PUBLISH_INTERVAL = 5000;
// Latido del LED
unsigned long previousHeartbeatMillis = 0;
const unsigned long HEARTBEAT_INTERVAL = 1000;
// Verificación de keep-alive de MQTT
unsigned long previousMqttMillis = 0;
const unsigned long MQTT_CHECK_INTERVAL = 100;
int ledState = LOW;
void setup() {
Serial.begin(115200);
pinMode(2, OUTPUT);
// Conectar Wi-Fi y MQTT (ver 01-11)
connectToWiFi();
connectToMQTT();
}
void loop() {
unsigned long currentMillis = millis();
// 1. Mantenimiento MQTT (cada 100ms — muy frecuente)
if (currentMillis - previousMqttMillis >= MQTT_CHECK_INTERVAL) {
previousMqttMillis = currentMillis;
if (!mqttClient.connected()) {
connectToMQTT();
}
mqttClient.loop();
}
// 2. Latido del LED (cada 1000ms)
if (currentMillis - previousHeartbeatMillis >= HEARTBEAT_INTERVAL) {
previousHeartbeatMillis = currentMillis;
ledState = !ledState;
digitalWrite(2, ledState);
}
// 3. Publicar datos del sensor (cada 5000ms)
if (currentMillis - previousPublishMillis >= PUBLISH_INTERVAL) {
previousPublishMillis = currentMillis;
publishSensorData();
}
}

Paso 4: Patrón Avanzado — Múltiples Temporizadores con Struct

Sección titulada «Paso 4: Patrón Avanzado — Múltiples Temporizadores con Struct»

Para proyectos complejos, organiza los temporizadores usando una estructura:

struct Timer {
unsigned long previousMillis;
unsigned long interval;
bool enabled;
Timer(unsigned long interval)
: previousMillis(0), interval(interval), enabled(true) {}
bool isReady() {
if (!enabled) return false;
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
return true;
}
return false;
}
void setInterval(unsigned long newInterval) {
interval = newInterval;
}
void reset() {
previousMillis = millis();
}
void disable() { enabled = false; }
void enable() { enabled = true; }
};
// Uso
Timer blinkTimer(500);
Timer sensorTimer(5000);
Timer publishTimer(30000);
void loop() {
if (blinkTimer.isReady()) {
// Alternar LED
}
if (sensorTimer.isReady()) {
// Leer sensor
}
if (publishTimer.isReady()) {
// Publicar datos
}
}
  • El LED parpadea al intervalo esperado sin delay()
  • Múltiples temporizadores se ejecutan de forma independiente a ritmos diferentes
  • La conexión MQTT permanece activa (sin desconexiones por tiempo de espera)
  • La salida serial confirma la precisión de la temporización durante períodos prolongados
  • El enfoque de temporizador basado en struct funciona para proyectos más complejos

El LED parpadea más rápido o más lento de lo esperado

Sección titulada «El LED parpadea más rápido o más lento de lo esperado»

Causas:

  • previousMillis no se actualiza después de la acción
  • Tipo de dato incorrecto (usar int en lugar de unsigned long)

Soluciones:

  1. Asegúrate de que previousMillis = currentMillis; esté dentro del bloque if
  2. Usa siempre unsigned long para almacenar valores de millis() (un int se desbordará en ~32 segundos)
  3. Verifica que el valor del intervalo sea correcto (1000 = 1 segundo, 500 = 0.5 segundos)

La acción se dispara múltiples veces rápidamente

Sección titulada «La acción se dispara múltiples veces rápidamente»

Causa: currentMillis - previousMillis >= interval se evalúa como verdadero varias veces antes de que la siguiente iteración del loop lo restablezca.

Soluciones:

  1. Actualiza previousMillis inmediatamente dentro del bloque if (antes del código de la acción)
  2. El patrón previousMillis = currentMillis; al inicio del bloque previene esto

El temporizador parece detenerse después de algunas horas

Sección titulada «El temporizador parece detenerse después de algunas horas»

Causa: Si bien el desbordamiento de millis() se maneja con aritmética sin signo, las marcas de tiempo almacenadas en otros formatos pueden no manejarse.

Solución: El patrón estándar currentMillis - previousMillis >= interval es seguro. Nunca conviertas millis() a signed long o int.

  • Nunca uses delay() en el bucle principal: Reserva delay() solo para setup() (p. ej., esperar el Serial o el calentamiento de sensores)
  • Usa siempre unsigned long: El tipo de dato para almacenar valores de millis() debe ser unsigned long
  • Usa nombres de variables descriptivos: previousBlinkMillis es más claro que prev o lastTime
  • Usa const para los intervalos: const unsigned long INTERVAL = 1000; previene modificaciones accidentales
  • Verificación periódica de MQTT cada 50-100ms: Esto asegura la estabilidad de la conexión sin desperdiciar CPU
  • Considera usar el struct Timer para proyectos con más de 5 temporizadores: Reduce la duplicación de código
  1. delay() bloquea toda la ejecución del código y debe evitarse en aplicaciones IoT
  2. El patrón millis() usa resta de enteros sin signo para crear temporizadores no bloqueantes
  3. Múltiples temporizadores independientes pueden ejecutarse concurrentemente, cada uno con intervalos diferentes
  4. El patrón se integra perfectamente con la arquitectura del Sketch Básico
  5. Una abstracción de temporizador basada en struct simplifica la gestión de muchos temporizadores concurrentes
  6. El desbordamiento de millis() después de ~50 días se maneja correctamente con aritmética de enteros sin signo

¿Desarrollas un producto IoT comercial?

Ofrecemos servicios ODM de ESP32, del diseño a la fabricación. Del prototipo a la producción: el equipo detrás de este tutorial puede construirlo contigo.

Hablemos →