Patrón Blink Sin delay()
Patrón Blink Sin delay()
Descripción General
Sección titulada «Descripción General»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 demillis() - 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
Requisitos Previos
Sección titulada «Requisitos Previos»- Arquitectura del Sketch Básico (01-11)
- Comprensión de cómo funcionan
setup()yloop()
Conceptos Clave
Sección titulada «Conceptos Clave»El Problema con delay()
Sección titulada «El Problema con delay()»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 Solución millis()
Sección titulada «La Solución millis()»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}Desbordamiento (Rollover) de millis()
Sección titulada «Desbordamiento (Rollover) de millis()»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.
Pasos de Implementación
Sección titulada «Pasos de Implementación»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ónunsigned 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 serialunsigned 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"); }}Paso 3: Integración con el Sketch Básico
Sección titulada «Paso 3: Integración con el Sketch Básico»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ónunsigned long previousPublishMillis = 0;const unsigned long PUBLISH_INTERVAL = 5000;
// Latido del LEDunsigned long previousHeartbeatMillis = 0;const unsigned long HEARTBEAT_INTERVAL = 1000;
// Verificación de keep-alive de MQTTunsigned 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; }};
// UsoTimer 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 }}Verificación
Sección titulada «Verificación»- 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
Solución de Problemas
Sección titulada «Solución de Problemas»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:
previousMillisno se actualiza después de la acción- Tipo de dato incorrecto (usar
inten lugar deunsigned long)
Soluciones:
- Asegúrate de que
previousMillis = currentMillis;esté dentro del bloqueif - Usa siempre
unsigned longpara almacenar valores de millis() (unintse desbordará en ~32 segundos) - 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:
- Actualiza
previousMillisinmediatamente dentro del bloqueif(antes del código de la acción) - 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.
Buenas Prácticas
Sección titulada «Buenas Prácticas»- Nunca uses
delay()en el bucle principal: Reservadelay()solo parasetup()(p. ej., esperar el Serial o el calentamiento de sensores) - Usa siempre
unsigned long: El tipo de dato para almacenar valores demillis()debe serunsigned long - Usa nombres de variables descriptivos:
previousBlinkMillises más claro queprevolastTime - Usa
constpara 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
Resumen
Sección titulada «Resumen»delay()bloquea toda la ejecución del código y debe evitarse en aplicaciones IoT- El patrón
millis()usa resta de enteros sin signo para crear temporizadores no bloqueantes - Múltiples temporizadores independientes pueden ejecutarse concurrentemente, cada uno con intervalos diferentes
- El patrón se integra perfectamente con la arquitectura del Sketch Básico
- Una abstracción de temporizador basada en struct simplifica la gestión de muchos temporizadores concurrentes
- El desbordamiento de
millis()después de ~50 días se maneja correctamente con aritmética de enteros sin signo
Referencias
Sección titulada «Referencias»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 →