
BermejaSat #5: Programando el reloj de vuelo — fases de misión sin delay()
Introducción
En la entrada anterior conseguimos que dos placas se hablaran de verdad por radio a 868 MHz, y ahí anunciamos que el siguiente paso natural era sumar el barómetro BMP280 a la trama de telemetría. Vamos a llegar a eso, pero antes toca una parada técnica que no tiene que ver con ningún sensor nuevo ni con ningún cable: tiene que ver con cómo le decimos al ESP32 que “espere” — y con un error tan común que casi todo el mundo lo comete en su primer proyecto con Arduino.
Esta entrada no necesita ni placa, ni pantalla, ni radio, ni sensor: solo el cable USB y el Monitor Serie. Es, a propósito, la entrada más “de programación pura” de toda la serie — porque el concepto que vamos a trabajar aquí es el que hará posible, más adelante, que el satélite pueda leer sensores y escuchar la radio al mismo tiempo, sin que una cosa bloquee a la otra.
🔬 El problema: delay() no es gratis
Hasta ahora, cada vez que hemos querido que el ESP32 “espere” un rato, hemos usado delay(2000) sin pensarlo mucho — funciona, y para un ejercicio aislado (el semáforo de la entrada #2, por ejemplo) no da ningún problema.
Pero delay() tiene una letra pequeña importante: mientras se está ejecutando, el procesador no hace absolutamente nada más. No lee sensores, no comprueba si ha llegado un paquete de radio, no responde a nada. Está literalmente parado, contando milisegundos con los brazos cruzados.
Eso es tolerable en un semáforo de juguete. Pero en el CanSat de verdad, el bucle principal va a tener que hacer varias cosas a la vez: leer el BMP280, comprobar si toca transmitir por LoRa, actualizar la pantalla… Si en medio de todo eso metemos un delay(2000), durante esos dos segundos el satélite se queda “sordo y ciego” a todo lo demás. En una fase crítica del vuelo, dos segundos pueden ser muchos datos perdidos.
La solución no es dejar de esperar — es aprender a esperar sin bloquear. Y para eso existe millis().
💡 millis(): un reloj que nunca se detiene
millis() es una función que, en cualquier momento, nos devuelve cuántos milisegundos han pasado desde que la placa se encendió. Es solo un número que crece y crece, sin parar nunca — no espera a nada ni bloquea nada, simplemente responde cuando se lo preguntamos.
La idea para sustituir un delay() es esta, en dos pasos:
- Cuando empieza algo, apuntamos “qué hora es ahora”:
unsigned long inicio = millis(); - En cada vuelta del bucle, preguntamos “¿cuánto ha pasado desde entonces?”:
millis() - inicio. Si ese número ya es lo bastante grande, es que ha llegado el momento de actuar.
Entre pregunta y pregunta, el bucle sigue libre para hacer cualquier otra cosa. Nadie se queda esperando con los brazos cruzados.
🚀 El ejercicio: fases de un vuelo de CanSat
Para practicarlo con algo que tenga sentido para el proyecto, en vez de un blink cualquiera, vamos a simular las fases reales de un vuelo de CanSat — la misma idea que dejamos apuntada en la entrada #2: rampa, lanzamiento, descenso, apertura del paracaídas y aterrizaje. Cada fase dura un tiempo, y cuando se cumple, pasamos automáticamente a la siguiente.
Paso 1: Guardar las fases en una lista
Lo primero es tener, en algún sitio, el nombre de cada fase y cuánto dura. En vez de escribir cinco variables sueltas, usamos un array (una lista ordenada):
const char* FASES[] = {"RAMPA", "LANZAMIENTO", "DESCENSO", "PARACAIDAS", "ATERRIZAJE"};
const int NUM_FASES = 5;
const unsigned long DURACION_MS[] = {5000, 3000, 4000, 4000, 0};
FASES[0] es "RAMPA", FASES[1] es "LANZAMIENTO", y así sucesivamente — la posición en la lista es lo que nos permite saber “en qué fase vamos” con un solo número. DURACION_MS[] guarda, en milisegundos, cuánto dura cada una (la última, ATERRIZAJE, vale 0 porque es el final: ahí nos quedamos).
Paso 2: Dos variables para llevar la cuenta
Necesitamos saber dos cosas en todo momento: en qué fase estamos, y cuándo empezó esa fase:
int faseActual = 0; // empezamos en la fase 0 = RAMPA
unsigned long tiempoInicioFase = 0; // "qué hora era" cuando empezó esta fase
faseActual es simplemente la posición dentro del array FASES[]. tiempoInicioFase es una fotografía de millis() tomada justo en el instante en que empezó la fase en la que estamos.
Paso 3: El setup() — arrancar el cronómetro
void setup() {
Serial.begin(115200);
delay(1000); // única excepción: una pausa corta solo para que el Monitor Serie conecte
Serial.println("=== BermejaSat: simulador de fases de vuelo ===");
tiempoInicioFase = millis();
anunciarFase();
}
El único delay() que sobrevive en todo el sketch es este de arranque, y es inofensivo porque ocurre una sola vez, antes de que empiece a importar la capacidad de reacción del programa. Después de esa pausa, apuntamos la hora de inicio y anunciamos que estamos en la primera fase.
Paso 4: El loop() — preguntar, no esperar
Aquí está el corazón del ejercicio:
void loop() {
if (faseActual == NUM_FASES - 1) {
return; // ya hemos aterrizado: misión terminada, no hacemos nada más
}
unsigned long ahora = millis();
unsigned long tiempoEnEstaFase = ahora - tiempoInicioFase;
if (tiempoEnEstaFase >= DURACION_MS[faseActual]) {
faseActual = faseActual + 1;
tiempoInicioFase = ahora;
anunciarFase();
}
}
Fijaos en que no hay ningún delay() aquí dentro. En cada vuelta del bucle —y el ESP32 da miles de vueltas por segundo— se hace la misma pregunta sencilla: “¿cuánto llevo en esta fase, y ya toca cambiar?”. Si la respuesta es sí, avanzamos a la siguiente fase de la lista y reiniciamos el cronómetro. Si es no, el bucle simplemente vuelve a empezar, libre, sin que nadie lo retenga.
Paso 5: Anunciar cada cambio de fase
void anunciarFase() {
unsigned long t = millis();
Serial.print("[T+");
Serial.print(t / 1000);
Serial.print("s] Fase: ");
Serial.println(FASES[faseActual]);
}
El "T+" es la notación real que se usa en el mundo aeroespacial para marcar el tiempo transcurrido desde el despegue (T+ significa “tiempo después del lanzamiento”; T- se usaría en la cuenta atrás previa). Aquí lo usamos igual: cada línea del Monitor Serie deja constancia de en qué segundo de la misión ocurrió cada cambio de fase.
💻 Código completo
const char* FASES[] = {"RAMPA", "LANZAMIENTO", "DESCENSO", "PARACAIDAS", "ATERRIZAJE"};
const int NUM_FASES = 5;
const unsigned long DURACION_MS[] = {5000, 3000, 4000, 4000, 0};
int faseActual = 0;
unsigned long tiempoInicioFase = 0;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== BermejaSat: simulador de fases de vuelo ===");
Serial.println();
tiempoInicioFase = millis();
anunciarFase();
}
void loop() {
if (faseActual == NUM_FASES - 1) {
return;
}
unsigned long ahora = millis();
unsigned long tiempoEnEstaFase = ahora - tiempoInicioFase;
if (tiempoEnEstaFase >= DURACION_MS[faseActual]) {
faseActual = faseActual + 1;
tiempoInicioFase = ahora;
anunciarFase();
}
}
void anunciarFase() {
unsigned long t = millis();
Serial.print("[T+");
Serial.print(t / 1000);
Serial.print("s] Fase: ");
Serial.println(FASES[faseActual]);
}
Conclusión pedagógica
Esta entrada no ha añadido ni un solo componente nuevo al CanSat, y aun así es una de las más importantes de la serie. El alumnado ha practicado tres ideas de programación que van a reaparecer constantemente a partir de ahora: guardar datos relacionados en una lista (array), usar un número que avanza solo (millis()) en vez de detener el programa a esperar, y construir una máquina de estados —algo prometido ya en la entrada #2— con una estructura tan simple como un if que compara dos tiempos.
Lo más importante es el cambio de mentalidad: en vez de escribir “espera aquí quieto 5 segundos”, ahora sabemos escribir “sigue haciendo tu vida, y de vez en cuando comprueba si ya han pasado 5 segundos”. Esa diferencia, que en este ejercicio es casi invisible, será la que permita que el satélite real lea el BMP280, revise la radio y actualice la pantalla sin que ninguna de esas tareas bloquee a las demás.
En la próxima entrada retomamos el hilo donde lo dejamos: integrar el barómetro de precisión BMP280 para sumar presión y altitud a la trama de telemetría — ya con esta base de temporización no bloqueante lista para usarse.
Etiqueta:arduino, BermejaSat, cansat, ESA, esp32, ies monterroso, millis, montesteam, programación



