01 · Punto de partida
Del modelo al programa
En el tema 36 la máquina de estados era un modelo: funciones puras que, dado un estado y un evento, devuelven el estado siguiente. Pero un equipo real no recibe eventos «ya hechos». Tiene que generarlos leyendo pulsadores y sensores, y tiene que medir el tiempo: «pasaron 3 s en amarillo», «el portón lleva 20 s moviéndose». Y todo eso, mientras sigue pendiente de lo demás.
La forma ingenua de esperar es delay(). Es sencilla y funciona en un ejemplo de laboratorio; en cuanto el programa debe hacer dos cosas a la vez, se vuelve un obstáculo. Este tema explica por qué y qué hacer en su lugar.
Es la misma idea del ciclo de scan de un PLC (tema 35) y de los temas 16 y 19 sobre movimiento sin bloqueo: leer, decidir, actuar, y volver enseguida.
02 · Por qué no esperar
El costo de esperar
Considerá este programa, que hace parpadear un LED y quiere reaccionar a un pulsador:
void loop() {
if (digitalRead(PIN_BOTON) == LOW) accionar(); // se mira UNA vez por vuelta
digitalWrite(PIN_LED, HIGH); delay(500);
digitalWrite(PIN_LED, LOW); delay(500);
}
Cada vuelta dura un segundo, y durante 999 ms el programa duerme: no ve el pulsador, no lee sensores, no atiende el puerto serie. Las consecuencias:
- Latencia. Una pulsación que llega justo después de la lectura espera hasta un segundo para ser atendida.
- Pulsaciones perdidas. Si el pulsador se aprieta y se suelta (100 ms) entre dos lecturas, el programa nunca se entera. No hay error: simplemente no pasa nada.
- Imposibilidad de hacer dos cosas a la vez. Un segundo LED con otro ritmo, un motor que debe avanzar, un temporizador de seguridad: nada de eso puede correr durante un
delay(). - Seguridad. Una parada de emergencia leída por software puede tardar en ser atendida exactamente lo que dura la espera.
En general, la latencia máxima de un programa con esperas bloqueantes es la suma de los bloqueos de una vuelta. El laboratorio 1 la calcula: con dos delay(500) y un pulsador que se mantiene 100 ms, una pulsación a los 300 ms se pierde, y una a los 950 ms se atiende con 50 ms de retraso. El mismo programa sin bloqueo detecta ambas en menos de un milisegundo.
Para una máquina de estados, el efecto es peor: un delay(3000) dentro del estado AMARILLO congela la máquina durante tres segundos, y cualquier evento que llegue en ese lapso queda sin atender (la fotocélula, el botón de emergencia).
03 · El esqueleto
La estructura del loop
Un loop() sin bloqueos tiene siempre la misma forma, que reconoce el ciclo de scan:
- Leer las entradas y convertirlas en eventos (flancos, filtros, tiempos vencidos).
- Actualizar la máquina de estados: con cada evento, decidir la transición.
- Aplicar las salidas según el estado nuevo.
- Volver al principio, sin pausas.
Cada paso es rápido: unas decenas de microsegundos. La vuelta completa dura bastante menos que un milisegundo, de modo que el programa «mira» cada entrada miles de veces por segundo. Las funciones no deben contener esperas ni bucles largos: si una operación necesita tiempo, se divide en pasos que se avanzan una vez por vuelta (como el planificador del brazo en el tema 24).
Una condición de diseño que ayuda: un solo punto de salida. Solo el paso 3 escribe a los pines, de modo que el estado de las salidas siempre es una función de la máquina. Si cualquier parte del código puede escribir a un actuador, cuesta razonar sobre él.
04 · Medir sin esperar
El tiempo con millis()
La función millis() devuelve los milisegundos desde el arranque, como un entero sin signo de 32 bits. En lugar de «dormir tres segundos» se anota cuándo empezó el estado y se pregunta, en cada vuelta, si ya pasó el tiempo:
unsigned long tEntrada; // cuándo entramos al estado actual
void entrar(Estado nuevo) { estado = nuevo; tEntrada = millis(); }
if (millis() - tEntrada >= T_AMARILLO) entrar(Estado::ROJO);
Tres reglas que evitan los errores clásicos:
- Restar siempre el instante de inicio:
ahora − inicio ≥ duración. Nunca guardar un «instante de vencimiento» absoluto para compararlo conmillis(). - Usar
unsigned longpara todos los instantes y duraciones. Unintde 16 bits se desborda a los 32 s. - Comparar con
≥, no con==. Si el programa se retrasa (una operación larga, una escritura serie), el instante exacto puede pasar sin ser visto, y la condición de igualdad nunca se cumple.
El contador de 32 bits se desborda y vuelve a cero cada 49,7 días. La resta de enteros sin signo lo maneja bien por una propiedad aritmética: es una resta módulo 232. Con el inicio en 4 294 967 290 y el contador ya vuelto a 10, la diferencia es 16 ms, que es lo correcto; una resta con signo daría un número enorme y negativo. Por eso el patrón «ahora − inicio» es seguro y los instantes absolutos no.
Para tareas periódicas (leer un sensor cada 20 ms) hay dos variantes:
if (ahora - ultimo >= PERIODO) { ultimo = ahora; tarea(); } // el período real crece con cada retraso
if (ahora - ultimo >= PERIODO) { ultimo += PERIODO; tarea(); } // el período medio es exacto (con retrasos, se recupera)
La primera acumula el retraso de cada ejecución (deriva); la segunda mantiene el ritmo medio. Si interesa medir la duración real entre dos ejecuciones, se calcula con ahora − ultimo antes de actualizar, como hicimos con Δt en los temas 16 y 18.
Dentro de una máquina de estados, el tiempo se expresa como tiempoEnEstado(): lo transcurrido desde que se entró. Un estado con duración (AMARILLO: 3 s) simplemente espera esa condición. Los temporizadores del tema 35 (TON, TOF) son esta misma idea.
05 · De la entrada al evento
Generar eventos desde las entradas
Un pulsador mantenido durante medio segundo no debe producir cientos de eventos BOTON: debe producir uno. Y un contacto mecánico rebota (varias transiciones en unos milisegundos, tema 7). Se resuelve con dos técnicas:
- Antirrebote por tiempo. Se acepta un cambio solo cuando la lectura se mantuvo estable un lapso mínimo (por ejemplo, 20 ms). Se hace con
millis(), sin esperas. - Detección de flanco. El evento se genera una sola vez, al pasar de «suelto» a «apretado» (flanco de subida), no mientras esté apretado.
const unsigned long REBOTE_MS = 20;
bool estable = false, previa = false;
unsigned long tCambio = 0;
bool hayFlancoBoton() { // true una sola vez por pulsación
const bool lectura = digitalRead(PIN_BOTON) == LOW;
if (lectura != previa) { previa = lectura; tCambio = millis(); } // cambió: empezar a medir
if (millis() - tCambio >= REBOTE_MS && lectura != estable) { // estable el tiempo suficiente
estable = lectura;
return estable; // solo el flanco de subida
}
return false;
}
Si pueden llegar varios eventos en la misma vuelta (un botón y un final de carrera al mismo tiempo), la máquina procesa uno por vez. Se puede guardar los pendientes en una cola circular pequeña (unos 8 elementos) y vaciar una por vuelta. Los eventos urgentes (emergencia) se atienden primero. Una cola que se llena indica que la máquina es demasiado lenta o que llegan más eventos de los que puede procesar: se debe detectar y registrar, no ignorar.
Los pedidos que llegan en un momento en que la máquina no los puede atender son una decisión de diseño: se ignoran, se guardan o se rechazan. En el semáforo de este tema, el pedido de cruce solo se registra con los autos en verde; uno que llega durante el ciclo de peatones se descarta (el siguiente peatón volverá a pulsar).
06 · Código
Tres formas de escribir los estados
| Forma | Cómo es | Ventajas | Límites |
|---|---|---|---|
switch sobre el estado | Un caso por estado; en cada uno, los if de sus transiciones | Directo, fácil de leer, sin estructuras auxiliares | La lógica queda repartida en el código; crece mal con muchos estados |
| Tabla de transiciones | Un arreglo de filas (estado, condición, destino, acción) recorrido por un motor genérico | Los datos separados del motor; se ve toda la máquina junta; se verifica con herramientas | Más abstracta; las condiciones se vuelven funciones |
| Punteros a función por estado | Cada estado es una función que devuelve el siguiente | Cada estado se escribe aislado, con entrada y salida propias | Más difícil de depurar; no se ve la máquina entera |
Para máquinas de hasta unos diez estados, el switch es suficiente y claro. Cuando la máquina crece o cambia seguido, la tabla paga su costo: agregar un estado es agregar filas, y el motor no se toca. Una fila típica:
struct Transicion {
Estado desde;
bool (*condicion)(); // función sin argumentos que devuelve true si la transición corresponde
Estado hacia;
void (*accion)(); // opcional: algo que hacer al transitar (puede ser nullptr)
};
const Transicion TABLA[] = {
{Estado::AMARILLO, []() { return tiempoEnEstado() >= T_AMARILLO; }, Estado::ROJO, nullptr},
// ...
};
// El motor: recorre las filas; la primera cuyo "desde" coincide con el estado y cuya condición se cumple, gana.
Las dos versiones deben dar exactamente el mismo comportamiento; el código de Python de este tema lo comprueba ejecutando ambas sobre la misma secuencia. En cualquier forma, conviene:
- Acciones de entrada en una única función
entrar(nuevo): cambia el estado, anota el instante, y hace lo que cada estado necesite al empezar (por ejemplo, borrar el pedido pendiente). - Salidas en un solo lugar (máquina de Moore, tema 36):
aplicarSalidas()decide las luces por el estado. - Registro de cada transición con la hora. Es la herramienta de diagnóstico más barata.
07 · Lo que vuelve a bloquear
Trampas frecuentes
- Un
delay()escondido. En una biblioteca, en una función de utilidad o dentro de un caso delswitch. Un solodelay(100)en un camino frecuente arruina la latencia. - Funciones bloqueantes de lectura.
pulseIn()(hasta su tiempo de espera),Serial.readString()(espera el tiempo de espera del puerto), lecturas I2C lentas. Se usan con tiempos de espera cortos o se reemplazan por versiones sin bloqueo (tema 32). - Imprimir demasiado.
Serial.printbloquea cuando el búfer de salida se llena. A 9600 baudios, 50 caracteres tardan unos 50 ms. Imprimir en cada vuelta degrada todo; se imprime en cada transición o cada cierto tiempo. - Bucles
whileque esperan una condición física («esperar que se cierre el final de carrera»). Se convierten en un estado y un evento. - Cálculos largos. Operaciones con muchos
floatoStringpueden tardar milisegundos; se dividen o se evitan. - Variables compartidas con interrupciones sin
volatileni protección (tema 9).
Una prueba sencilla: contar cuántas vueltas de loop() se hacen por segundo, o medir la duración de la vuelta más larga. Si supera la latencia que tolerás, hay algo que bloquea.
08 · Código para la placa
Un semáforo de cruce en Arduino
El ejemplo es un semáforo con pulsador para peatones. Estados: VERDE (autos), AMARILLO (3 s), ROJO (autos detenidos, peatones cruzan 8 s), PARPADEO (la luz de peatones parpadea 4 s) y DESPEJE (todo en rojo 1 s) antes de volver a VERDE. Un peatón puede pedir cruzar durante el verde, pero los autos tienen derecho a un mínimo de 10 s de verde: el pedido se recuerda y se atiende cuando se cumplen los 10 s.
// Semáforo con pulsador de peatones, sin delay(): una máquina de estados con tiempos medidos con millis().
// SIMULAR = 1: reloj virtual de 1 ms y pulsaciones guionadas (no necesita hardware; imprime las transiciones).
// SIMULAR = 0: pines y millis() reales; ver las conexiones más abajo.
#define SIMULAR 1
const byte PIN_AUTO_R = 2, PIN_AUTO_A = 3, PIN_AUTO_V = 4, PIN_PEA_R = 5, PIN_PEA_V = 6, PIN_BOTON = 7; // LEDs con resistencia; botón a GND
const unsigned long T_VERDE_MIN = 10000, T_AMARILLO = 3000, T_ROJO = 8000, T_PARPADEO = 4000, T_DESPEJE = 1000;
enum class Estado : byte { VERDE, AMARILLO, ROJO, PARPADEO, DESPEJE };
const char *NOMBRE[] = {"VERDE", "AMARILLO", "ROJO", "PARPADEO", "DESPEJE"};
Estado estado = Estado::VERDE;
unsigned long tEntrada = 0;
bool solicitud = false, botonPrevio = false;
// ---- Reloj y entradas: lo único que cambia entre la prueba y el equipo real ----
#if SIMULAR
unsigned long reloj = 0;
unsigned long ahora() { return reloj; }
bool botonPresionado() { return (reloj >= 4000 && reloj < 4100) || (reloj >= 40000 && reloj < 40100); }
#else
unsigned long ahora() { return millis(); }
bool botonPresionado() { return digitalRead(PIN_BOTON) == LOW; } // en un equipo real, agregar antirrebote por tiempo
#endif
// Tiempo transcurrido en el estado actual. La resta de enteros sin signo da el resultado correcto
// aunque millis() haya dado la vuelta (a los 49,7 días): nunca guardar "vencimientos" absolutos.
unsigned long tiempoEnEstado() { return ahora() - tEntrada; }
void entrar(Estado nuevo) {
Serial.print("t="); Serial.print(ahora()); Serial.print(" ms: ");
Serial.print(NOMBRE[(byte)estado]); Serial.print(" -> "); Serial.println(NOMBRE[(byte)nuevo]);
estado = nuevo;
tEntrada = ahora(); // acción de entrada: arrancar el temporizador del estado
if (nuevo == Estado::AMARILLO) solicitud = false; // el pedido ya fue atendido
}
void leerEntradas() {
const bool boton = botonPresionado();
if (boton && !botonPrevio && estado == Estado::VERDE) solicitud = true; // flanco de subida: se recuerda el pedido
botonPrevio = boton;
}
void actualizar() { // las transiciones: ninguna espera, solo comprueba
switch (estado) {
case Estado::VERDE: if (solicitud && tiempoEnEstado() >= T_VERDE_MIN) entrar(Estado::AMARILLO); break;
case Estado::AMARILLO: if (tiempoEnEstado() >= T_AMARILLO) entrar(Estado::ROJO); break;
case Estado::ROJO: if (tiempoEnEstado() >= T_ROJO) entrar(Estado::PARPADEO); break;
case Estado::PARPADEO: if (tiempoEnEstado() >= T_PARPADEO) entrar(Estado::DESPEJE); break;
case Estado::DESPEJE: if (tiempoEnEstado() >= T_DESPEJE) entrar(Estado::VERDE); break;
}
}
void aplicarSalidas() { // máquina de Moore: las luces dependen del estado (y del tiempo en él)
bool ar = false, aa = false, av = false, pr = false, pv = false;
switch (estado) {
case Estado::VERDE: av = true; pr = true; break;
case Estado::AMARILLO: aa = true; pr = true; break;
case Estado::ROJO: ar = true; pv = true; break;
case Estado::PARPADEO: ar = true; pv = (tiempoEnEstado() / 250) % 2 == 0; break; // parpadeo a 2 Hz sin delay()
case Estado::DESPEJE: ar = true; pr = true; break;
}
#if !SIMULAR
digitalWrite(PIN_AUTO_R, ar); digitalWrite(PIN_AUTO_A, aa); digitalWrite(PIN_AUTO_V, av);
digitalWrite(PIN_PEA_R, pr); digitalWrite(PIN_PEA_V, pv);
#endif
}
void ciclo() { leerEntradas(); actualizar(); aplicarSalidas(); }
void setup() {
Serial.begin(115200);
#if SIMULAR
Serial.println("Simulacion de 60 s (pedidos de cruce a los 4000 y 40000 ms):");
for (reloj = 0; reloj <= 60000; reloj++) ciclo();
#else
const byte salidas[] = {PIN_AUTO_R, PIN_AUTO_A, PIN_AUTO_V, PIN_PEA_R, PIN_PEA_V};
for (byte i = 0; i < 5; i++) pinMode(salidas[i], OUTPUT);
pinMode(PIN_BOTON, INPUT_PULLUP);
tEntrada = millis();
#endif
}
void loop() {
#if !SIMULAR
ciclo(); // sin delay(): el ciclo se repite miles de veces por segundo
#endif
}
El sketch tiene dos modos. Con SIMULAR = 1 (el valor por defecto) usa un reloj virtual y pulsaciones guionadas a los 4000 ms y 40000 ms, y escribe las transiciones. Con SIMULAR = 0 usa millis() y los pines reales (cinco LED con resistencia y un pulsador a GND). Lo único que cambia entre ambos modos son ahora() y botonPresionado(): el resto es idéntico.
La salida de la simulación es: t=10000 ms: VERDE -> AMARILLO (el pedido de los 4000 ms espera el mínimo de 10 s), t=13000 ms: AMARILLO -> ROJO, t=21000 ms: ROJO -> PARPADEO, t=25000 ms: PARPADEO -> DESPEJE, t=26000 ms: DESPEJE -> VERDE. Luego el pedido de los 40000 ms llega con el verde ya cumplido y el ciclo arranca de inmediato: t=40000 ms: VERDE -> AMARILLO, y así hasta t=56000 ms: DESPEJE -> VERDE.
Observá en el código: tiempoEnEstado() con resta sin signo; entrar() como única función que cambia el estado, anota el instante y limpia el pedido; el flanco del pulsador en leerEntradas(); el parpadeo a 2 Hz como función del tiempo en el estado, sin ningún temporizador aparte; y el ciclo leerEntradas → actualizar → aplicarSalidas sin esperas. En un equipo real agregá el antirrebote por tiempo y, si las luces gobiernan una instalación real, una secuencia de seguridad independiente del programa (por ejemplo, un monitor de conflictos que impida dos verdes cruzados).
09 · Exploración interactiva
Laboratorios de latencia y de cruce
1. Lo que pierde un programa con delay()
Un loop() que enciende un LED, espera, lo apaga y espera, leyendo el pulsador una sola vez por vuelta. El eje es el tiempo; las marcas naranja son las lecturas del pulsador; la barra roja, la pulsación. El programa sin bloqueo lee el pulsador cada milisegundo.
Elegí los tiempos.
Con los valores iniciales, la pulsación cae entre dos lecturas y se pierde. Moverla a 950 ms la hace coincidir con la lectura de los 1000 ms (50 ms de retraso). Con delay(100) las lecturas son cada 200 ms: una pulsación de 100 ms a los 150 ms se detecta con 50 ms de retraso, y a los 250 ms se pierde. Para que una pulsación de duración d no se pierda nunca hace falta leer al menos cada d, es decir, un loop() mucho más rápido que cualquier delay.
2. El semáforo de cruce en tiempo real
El mismo programa del sketch, corriendo con un reloj virtual que podés acelerar. Pulsá «Pedir cruce»: si los autos llevan menos de 10 s en verde, el pedido se guarda y se atiende al cumplirse el mínimo; si ya cumplieron, el ciclo arranca enseguida. Los pedidos fuera del verde se descartan, como en el sketch.
Probá pedir el cruce a los 4 s: queda guardado y el amarillo empieza a los 10 s. Pedilo después de los 10 s de verde: el amarillo arranca al instante. Pedilo durante el rojo de autos: se descarta. Mirá el parpadeo de la luz verde de peatones en el último tramo: sale de dividir el tiempo en el estado por 250 ms. Cambiá la velocidad a 20×: la lógica no cambia, porque depende del reloj y no del número de vueltas.
10 · Código ejecutable
Comparar implementaciones en Python
La misma máquina escrita con switch (una cadena de condiciones) y con tabla, ejecutadas sobre el mismo guion de 60 s, la aritmética sin signo del desbordamiento y el cálculo de la latencia de un programa con delay().
Probalo en el navegador. Python se descarga con Pyodide la primera vez y requiere conexión. El código es editable y no controla ningún equipo.
T_VERDE_MIN, T_AMARILLO, T_ROJO, T_PARPADEO, T_DESPEJE = 10000, 3000, 8000, 4000, 1000
MASCARA = 2 ** 32 # millis() es un entero sin signo de 32 bits
def transcurrido(ahora, inicio):
return (ahora - inicio) % MASCARA # resta sin signo: correcta aunque el contador haya dado la vuelta
class CruceSwitch:
"""Versión con switch: la lógica de transición se escribe dentro de cada caso."""
def __init__(self):
self.estado, self.t_entrada, self.solicitud, self.registro, self.previo = "VERDE", 0, False, [], False
def entrar(self, nuevo, ahora):
self.registro.append((ahora, self.estado, nuevo))
self.estado, self.t_entrada = nuevo, ahora
if nuevo == "AMARILLO":
self.solicitud = False
def ciclo(self, ahora, boton):
if boton and not self.previo and self.estado == "VERDE": # flanco de subida: se recuerda el pedido
self.solicitud = True
self.previo = boton
t = transcurrido(ahora, self.t_entrada)
if self.estado == "VERDE" and self.solicitud and t >= T_VERDE_MIN:
self.entrar("AMARILLO", ahora)
elif self.estado == "AMARILLO" and t >= T_AMARILLO:
self.entrar("ROJO", ahora)
elif self.estado == "ROJO" and t >= T_ROJO:
self.entrar("PARPADEO", ahora)
elif self.estado == "PARPADEO" and t >= T_PARPADEO:
self.entrar("DESPEJE", ahora)
elif self.estado == "DESPEJE" and t >= T_DESPEJE:
self.entrar("VERDE", ahora)
class CruceTabla(CruceSwitch):
"""Versión con tabla: cada fila es (estado, condición, destino); el motor es siempre el mismo."""
def __init__(self):
super().__init__()
self.tabla = [
("VERDE", lambda s, t: s.solicitud and t >= T_VERDE_MIN, "AMARILLO"),
("AMARILLO", lambda s, t: t >= T_AMARILLO, "ROJO"),
("ROJO", lambda s, t: t >= T_ROJO, "PARPADEO"),
("PARPADEO", lambda s, t: t >= T_PARPADEO, "DESPEJE"),
("DESPEJE", lambda s, t: t >= T_DESPEJE, "VERDE"),
]
def ciclo(self, ahora, boton):
if boton and not self.previo and self.estado == "VERDE":
self.solicitud = True
self.previo = boton
t = transcurrido(ahora, self.t_entrada)
for desde, condicion, hacia in self.tabla:
if desde == self.estado and condicion(self, t):
self.entrar(hacia, ahora)
break
def pulsador(ahora):
return 4000 <= ahora < 4100 or 40000 <= ahora < 40100
def ejecutar(sistema, duracion=60000):
for ahora in range(duracion + 1):
sistema.ciclo(ahora, pulsador(ahora))
return sistema.registro
registro = ejecutar(CruceSwitch())
print("Transiciones (pedidos de cruce a los 4000 y 40000 ms):")
for t, a, b in registro:
print(f" t={t} ms: {a} -> {b}")
print("La version con tabla da el mismo resultado:", registro == ejecutar(CruceTabla()))
# --- El desbordamiento de millis() ---
inicio, ahora = 4294967290, 10
print(f"Desbordamiento: inicio={inicio}, ahora={ahora} -> transcurrido (sin signo) = {transcurrido(ahora, inicio)} ms; "
f"la resta con signo daria {ahora - inicio}")
print(f"Un contador de 32 bits en milisegundos da la vuelta a los {MASCARA / 1000 / 86400:.1f} dias")
# --- Latencia de un programa con delay() ---
def latencia_bloqueante(demora, inicio_pulso, duracion):
lecturas = range(0, 20000, 2 * demora) # el pulsador se lee una vez por vuelta de loop()
for l in lecturas:
if inicio_pulso <= l < inicio_pulso + duracion:
return l - inicio_pulso
return None
for demora, p, d in ((500, 300, 100), (500, 950, 100), (100, 150, 100), (100, 250, 100)):
r = latencia_bloqueante(demora, p, d)
print(f"delay({demora}) dos veces por vuelta, pulso de {d} ms en t={p}: " + ("pulso perdido" if r is None else f"se detecta con {r} ms de retraso"))Las diez transiciones ocurren a los 10000, 13000, 21000, 25000, 26000, 40000, 43000, 51000, 55000 y 56000 ms, y las dos implementaciones dan el mismo registro. Con el contador a punto de desbordarse (inicio 4 294 967 290, ahora 10) el tiempo transcurrido es 16 ms y la resta con signo daría −4 294 967 280. Con dos delay(500), la pulsación de 100 ms a los 300 ms se pierde y la de los 950 ms se detecta con 50 ms de retraso.
Cambiá los instantes de las pulsaciones y comprobá el resultado a mano. Agregá un estado PRE_AMARILLO de 2 s y observá qué hay que tocar en cada versión: en la de switch, una rama nueva y una modificada; en la de tabla, una fila nueva y una modificada. Probá inicio = 4294967000 y ahora = 500 para ver el desbordamiento.
11 · Método y diagnóstico
Pruebas y diagnóstico
- Buscá en todo el programa y en las bibliotecas cualquier
delay(),pulseIn()owhileque espere. - Medí cuántas vueltas de
loop()se hacen por segundo y cuánto dura la más larga. - Probá la máquina con el reloj simulado: comprobá cada transición contra la tabla y cada tiempo contra su duración.
- Probá el desbordamiento: iniciá el contador cerca del máximo (o simulalo) y verificá que los tiempos siguen correctos.
- Probá pulsaciones muy breves, muy largas y repetidas (rebote): una pulsación debe producir un evento.
- Provocá eventos en momentos inoportunos (un pulso durante un cambio de estado) y comprobá que no se pierden ni se duplican.
- Dejalo funcionando horas y revisá que no se desvíe ni acumule errores.
«Es solo un delay(10), no afecta a nada»
Si está en el camino de cada vuelta, la latencia pasa a ser de al menos 10 ms, y se suma con todos los demás. Además, es un mal hábito: el siguiente delay será de 100.
«Con millis() ya no hay problemas de tiempo»
Hay errores sutiles: comparar con ==, usar int, guardar vencimientos absolutos o dejar derivar el período. Los tres aparecen solo en algún momento.
«El desbordamiento a los 49 días nunca me va a pasar»
Un equipo industrial puede estar encendido meses. Con la resta sin signo el problema desaparece sin costo; con un error, el equipo falla una vez al mes.
«La tabla es siempre mejor que el switch»
Para pocos estados el switch es más claro. La tabla convence cuando la máquina crece, cambia seguido o debe verificarse con herramientas.
«Imprimo el estado en cada vuelta para ver qué pasa»
Eso bloquea el programa y falsea la prueba. Imprimí al cambiar de estado, o cada cierto tiempo.
«Si el pulsador rebota, filtro con un delay(50) después de leerlo»
Congela el programa durante el filtrado. El antirrebote se hace midiendo con millis() cuánto tiempo estable lleva la lectura.
12 · Práctica guiada
Predecí antes de comprobar
1. Pulsación perdida
Un loop() tiene dos delay(300) y lee el pulsador al principio de cada vuelta (t = 0, 600, 1200…). Una pulsación de 150 ms empieza en t = 100. ¿Se detecta?
Ver solución
La ventana de la pulsación es [100, 250) ms y no contiene ninguna lectura (0 y 600). Se pierde. Para detectarla con seguridad habría que leer al menos cada 150 ms.
2. Desbordamiento
El estado se entró cuando millis() valía 4 294 967 000 y ahora vale 500. ¿Cuánto tiempo pasó?
Ver solución
Hasta el desbordamiento faltaban 296 ms (4 294 967 296 − 4 294 967 000) y después pasaron 500: 796 ms. La resta sin signo 500 − 4 294 967 000 módulo 232 da exactamente 796.
3. Igual contra mayor o igual
¿Por qué el código compara tiempoEnEstado() >= T_AMARILLO y no == T_AMARILLO?
Ver solución
Si una vuelta de loop() se retrasa (una escritura serie larga), el instante exacto de los 3000 ms puede pasar sin ser observado y la igualdad nunca se cumpliría: la máquina quedaría clavada en AMARILLO. Con ≥ se cumple en la primera vuelta posterior.
4. Deriva del período
Una tarea de 100 ms se comprueba con ultimo = ahora y cada comprobación llega 3 ms tarde. ¿Cuánto se atrasa tras 100 ejecuciones? ¿Y con ultimo += 100?
Ver solución
Con ultimo = ahora cada período dura 103 ms: tras 100 ejecuciones, unos 300 ms de atraso. Con ultimo += 100 el ritmo medio se conserva: no se acumula atraso (cada ejecución se retrasa 3 ms respecto de su instante ideal, pero no se suman).
5. Parpadeo
La luz de peatones usa (tiempoEnEstado()/250) % 2 == 0. ¿Está encendida a los 700 ms y a los 800 ms? ¿A qué frecuencia parpadea?
Ver solución
A los 700 ms: 700/250 = 2 (división entera), 2 % 2 = 0: encendida. A los 800 ms: 3 % 2 = 1: apagada. Cada 250 ms cambia: período de 500 ms, 2 Hz.
6. Un pedido en mal momento
Un peatón pulsa durante el AMARILLO. En el sketch se descarta. ¿Qué cambiarías para no perderlo y qué efecto tendría?
Ver solución
Aceptar el pedido en cualquier estado salvo el ciclo de peatones y marcarlo como «pendiente para el próximo verde». Tendría que borrarse al entrar en ROJO (no al entrar en AMARILLO), para no atender un pedido ya cumplido; pero un pedido guardado en el último instante podría provocar un ciclo innecesario. Es una decisión de diseño con costos en ambas direcciones.
7. Agregar un estado
Se agrega un estado PRE_AMARILLO de 2 s (luz verde intermitente de autos) antes del AMARILLO. ¿Qué hay que tocar con cada implementación?
Ver solución
Con switch: un caso nuevo (PRE_AMARILLO → AMARILLO a los 2 s), cambiar el destino en VERDE y agregar el caso a aplicarSalidas(). Con tabla: una fila nueva, un destino modificado y la misma línea de salidas. El resto del programa, igual.
13 · Síntesis
Ideas para recordar
delay()congela el programa: introduce latencia, pierde pulsaciones breves y impide hacer dos cosas a la vez.- Un
loop()sin bloqueos lee entradas, genera eventos, actualiza la máquina y aplica salidas, sin esperas y con un solo punto de escritura a los actuadores. - Los tiempos se miden con
millis():ahora − inicio ≥ duración, conunsigned longy comparando con≥. - La resta sin signo sobrevive al desbordamiento de los 49,7 días; los vencimientos absolutos, no.
- Un pulsador se convierte en un evento con antirrebote por tiempo y detección de flanco; los eventos concurrentes se encolan.
switcho tabla de transiciones: ambas dan el mismo comportamiento; la tabla escala mejor y separa datos de motor.- Las acciones de entrada van en una función única, las salidas en un solo lugar y cada transición se registra.
- Cuidado con los bloqueos escondidos:
pulseIn, impresiones largas,whilede espera y cálculos pesados.
¿Listo para el tema 38?
Antes de avanzar, revisá un programa tuyo y contá los delay(). Reescribí uno como estado con temporizador, y medí cuántas vueltas por segundo hace tu loop() antes y después.
En el próximo tema juntamos varias máquinas y modos en una sola estructura: máquinas de estados jerárquicas y comportamientos, con subestados, modos de operación y manejo de fallos.