01 · Punto de partida
El problema de las banderas
Imaginá el control de un portón corredizo con un único pulsador. Una pulsación abre si está cerrado, detiene si está en movimiento, cierra si está abierto. Un fin de carrera avisa que llegó arriba o abajo, y una fotocélula detecta un obstáculo: si hay algo mientras cierra, el portón debe volver a abrir. Una primera implementación, tal como sale al principio, se ve así:
static bool abriendo, cerrando, abierto, cerrado, detenido; /* ¿qué está pasando? */
void al_pulsar_boton(void) {
if (abierto && !cerrando) { abierto = false; cerrando = true; motor_cerrar(); }
else if (cerrado && !abriendo) { cerrado = false; abriendo = true; motor_abrir(); }
else if (abriendo || cerrando) { abriendo = cerrando = false; detenido = true; motor_parar(); }
else if (detenido && !abierto) { detenido = false; cerrando = true; motor_cerrar(); }
/* ...¿y si el botón llega justo cuando termina de abrir? ¿y con el obstáculo a la vez? */
}
El código funciona en las pruebas fáciles y se rompe en las difíciles. Con cinco banderas hay 32 combinaciones posibles, pero solo cinco tienen sentido (abriendo y cerrando a la vez no existe en el mundo); las otras 27 son «imposibles» solo mientras cada if esté perfecto. Cada evento nuevo obliga a revisar todas las ramas, y nadie puede responder con seguridad «¿qué hace el portón si el obstáculo aparece mientras está detenido?» sin leer todo el código.
Una máquina de estados finitos (FSM, Finite State Machine) resuelve el problema cambiando la pregunta. En lugar de una colección de banderas, el sistema tiene una sola variable, el estado actual, que puede valer uno de un conjunto finito de valores; y el comportamiento se describe respondiendo, para cada estado y para cada evento posible, qué pasa. Es una tabla, y una tabla se puede revisar, completar y probar entera.
Este tema es de modelado: pensar con lápiz y papel antes de escribir una sola línea de control. Es la parte que más rinde, porque un error de modelado cuesta una hora de dibujo; el mismo error descubierto con el portón en marcha cuesta un portón.
02 · El vocabulario
Los elementos de una máquina de estados
Una máquina de estados tiene un puñado de piezas, y conviene usar los nombres exactos porque son los que usan los libros, las herramientas y los colegas:
| Elemento | Qué es | En el portón |
|---|---|---|
| Estado | Una situación estable del sistema que resume lo que «recuerda» del pasado. En todo momento hay exactamente uno activo | CERRADO, ABRIENDO, ABIERTO, CERRANDO, DETENIDO |
| Estado inicial | El estado en que arranca la máquina, elegido con cuidado (suele ser el más seguro) | CERRADO (o DETENIDO si no se sabe dónde está la hoja) |
| Evento | Algo que ocurre en un instante y a lo que la máquina debe reaccionar: es discreto, no persiste | BOTON, FC_ABIERTO, FC_CERRADO, OBSTACULO, TIMEOUT |
| Transición | El cambio de un estado a otro cuando ocurre un evento: (estado, evento) → estado nuevo | (CERRANDO, OBSTACULO) → ABRIENDO |
| Acción | Algo que el sistema hace: encender una salida, enviar un mensaje, arrancar un temporizador | motor_abrir(), motor_parar() |
| Guarda | Una condición booleana que debe cumplirse para que la transición ocurra | [puerta_con_energia], [intentos < 3] |
| Acciones de entrada y de salida | Lo que se ejecuta siempre al entrar a un estado, o al salir de él, sin importar por qué transición | Entrada a ABIERTO: arrancar el temporizador de autocierre. Salida: cancelarlo |
Cuatro ideas que separan un buen modelo de uno mediocre:
- Un estado representa lo que el sistema necesita recordar, no lo que está haciendo.
ABRIENDOes un estado porque el sistema debe recordar que el motor está subiendo (y que el próximo fin de carrera relevante es el de arriba); «el motor gira» es una consecuencia, no el estado. - Un evento es un hecho puntual; un estado dura. «Se apretó el botón» es un evento; «el botón está apretado» es un nivel. Confundirlos es la fuente número uno de errores (apartado 5).
- La misma pulsación significa cosas distintas según el estado. Ésa es la esencia del modelo: el botón abre, detiene o cierra según dónde se esté. En el código con banderas esa dependencia está escondida en condiciones; en la máquina está a la vista.
- Un estado puede recibir un evento y no hacer nada: ignorarlo. Siempre que sea una decisión y no un olvido (apartado 6).
03 · Dos maneras de escribirla
Diagramas y tablas de transiciones
Una máquina de estados se escribe de dos formas equivalentes, y cada una responde mejor a una pregunta. El diagrama de estados muestra el recorrido: los estados son cajas, las transiciones son flechas rotuladas con evento [guarda] / acción, y un punto negro marca el estado inicial. Se lee de un vistazo la «historia» del sistema:
La tabla de transiciones es la misma máquina vista por filas y columnas: una fila por estado, una columna por evento, y en cada celda el estado al que se va (más la acción). Es menos intuitiva de leer, pero tiene una virtud que el diagrama no tiene: obliga a mirar todas las celdas, incluidas las vacías, y las vacías son donde viven los errores de diseño.
| Estado \ Evento | BOTON | FC_ABIERTO | FC_CERRADO | OBSTACULO | TIMEOUT |
|---|---|---|---|---|---|
| CERRADO | ABRIENDO / abrir | · | · | · | · |
| ABRIENDO | DETENIDO / parar | ABIERTO / parar | · | · | · |
| ABIERTO | CERRANDO / cerrar | · | · | · | CERRANDO / cerrar |
| CERRANDO | DETENIDO / parar | · | CERRADO / parar | ABRIENDO / abrir | · |
| DETENIDO | CERRANDO / cerrar | · | · | · | · |
Se usan los dos juntos: el diagrama para pensar y conversar con el cliente o el equipo; la tabla para verificar que no falta nada y para programar, porque se traduce casi literalmente a código (tema 25). Cuando los dos no coinciden, el defecto está en uno de los dos, y descubrirlo antes de programar es el objetivo del modelado. Del cuadro se desprende además una cuenta útil: 5 estados por 5 eventos son 25 celdas, y el portón define 9 transiciones; hay 16 celdas que ignoran el evento, y cada una merece una mirada.
04 · Las salidas
Moore y Mealy: dónde van las salidas
Una máquina de estados no solo cambia de estado: hace cosas. La teoría distingue dos formas de asociar las salidas, y en firmware se usan las dos:
| Modelo | La salida depende de... | Ejemplo en el portón | Ventajas y costos |
|---|---|---|---|
| Moore | Solo del estado actual | La luz de cortesía está encendida en todos los estados salvo CERRADO; el motor «sube» en ABRIENDO | Salidas estables y fáciles de verificar (el estado dice todo); pueden requerir más estados |
| Mealy | Del estado y del evento (la transición) | Al pasar de CERRANDO a ABRIENDO por un obstáculo, se invierte el motor en ese mismo instante | Menos estados y reacción inmediata; la salida puede depender de un evento fugaz, y hay que cuidar que sea la correcta |
En código real se mezclan, y la regla práctica es: las salidas que representan una condición sostenida van en el estado (con acciones de entrada y de salida: «mientras esté ABIERTO, la luz está encendida»), y las que son una orden puntual van en la transición («arrancar el motor hacia arriba»). Las acciones de entrada son especialmente útiles porque evitan repetir lo mismo en cada transición que llega a un estado: si tres caminos distintos llevan a ABIERTO, el temporizador de autocierre se arranca en un solo lugar, la entrada, y no hay manera de olvidarlo en uno de los tres.
Conviene que las acciones sean breves y no bloqueantes: cambiar una salida, encolar un mensaje, armar un temporizador. Una acción que espera o hace un cálculo largo dentro de una transición congela a la máquina y a todo lo que comparta su tarea (temas 14 y 19). Lo largo se modela como un estado más: el portón no «espera a que llegue arriba» dentro de una acción, sino que está en ABRIENDO hasta que llega el evento FC_ABIERTO.
05 · La frontera con el mundo físico
De las señales a los eventos
Las máquinas de estados reciben eventos, pero el hardware entrega señales: niveles en pines, contadores que avanzan, bytes que llegan. Convertir unas en otros es un trabajo propio, y donde más se equivoca la gente:
- Nivel contra flanco. «El botón está apretado» es un nivel, que dura mientras el dedo esté sobre él; «se apretó el botón» es un evento, que ocurre una vez. Si la máquina recibiera el nivel como evento, un dedo que tarda 300 ms generaría miles de eventos a la velocidad del bucle, y el portón abriría, detendría, cerraría y abriría de nuevo. Se detecta el flanco: comparar el valor actual con el anterior.
- Rebote. Un contacto mecánico rebota unos milisegundos (tema 14). Un flanco sin filtrar son varios eventos. Se filtra antes de que la señal sea un evento: la máquina ve «un botón» limpio, no el ruido del contacto.
- El tiempo como evento. «Pasaron 10 segundos en
ABIERTO» es un evento, producido por un temporizador (tema 17) que se arma en la acción de entrada del estado y se cancela en la de salida. La máquina no «cuenta»: recibeTIMEOUT. - Mensajes y comandos que llegan por una comunicación (un comando por UART, una orden remota) son eventos con datos; el parser del tema 23 los produce.
- Eventos simultáneos. Si en el mismo instante hay un obstáculo y un pulso del botón, hay que decidir un orden. Se procesa un evento por vez, empezando por el más crítico para la seguridad: el obstáculo antes que el botón.
typedef enum { EV_NINGUNO, EV_OBSTACULO, EV_FC_CERRADO, EV_FC_ABIERTO, EV_TIMEOUT, EV_BOTON } Evento;
Evento siguiente_evento(void) {
static bool boton_antes;
bool boton = boton_estable(); /* ya sin rebote (tema 14) */
bool flanco = boton && !boton_antes; /* solo el instante en que pasa de suelto a apretado */
boton_antes = boton;
if (obstaculo_detectado()) return EV_OBSTACULO; /* la seguridad tiene prioridad sobre el usuario */
if (fin_de_carrera_cerrado()) return EV_FC_CERRADO;
if (fin_de_carrera_abierto()) return EV_FC_ABIERTO;
if (temporizador_vencio()) return EV_TIMEOUT;
if (flanco) return EV_BOTON;
return EV_NINGUNO; /* no pasó nada: la máquina no hace nada */
}
Hay una trampa sutil en ese código: fin_de_carrera_cerrado() es un nivel (el portón sigue contra el tope mientras esté cerrado), y devolverlo en cada pasada produciría un evento FC_CERRADO permanente. La máquina del portón lo ignora en todos los estados salvo CERRANDO, y por eso no se nota; pero un diseño descuidado multiplicaría eventos sin sentido. Lo correcto es generar el evento en el flanco del fin de carrera, igual que con el botón.
Por último, la máquina procesa los eventos de a uno y hasta terminar (run to completion): cada evento ejecuta su transición completa, con sus acciones, antes de atender el siguiente. Con eso se evita que dos transiciones se pisen, y es el fundamento de la programación orientada a eventos del tema 26, donde los eventos esperan en una cola.
06 · Verificar sobre el papel
Comprobar el modelo antes de programarlo
La gran ventaja de una máquina de estados es que se puede analizar sin ejecutarla, porque es una estructura finita. Estas son las comprobaciones que conviene hacer a cualquier modelo, y que el programa de Python de este tema automatiza:
| Propiedad | Pregunta | Qué hacer si falla |
|---|---|---|
| Completitud | ¿Para cada par (estado, evento) hay una decisión explícita? | Recorrer las celdas vacías una por una y escribir al lado «ignorado porque...» o agregar la transición |
| Determinismo | ¿Cada par produce exactamente una transición? (Dos guardas que se solapan dan dos) | Hacer las guardas mutuamente excluyentes |
| Alcanzabilidad | ¿Se puede llegar a todos los estados desde el inicial? | Un estado inalcanzable es código muerto o una transición olvidada |
| Ausencia de trampas | ¿Desde todo estado se puede volver al estado normal? | Agregar una salida (un reinicio, un tiempo máximo), salvo estados de falla deliberados |
| Seguridad | ¿Hay combinaciones prohibidas de salidas (abrir y cerrar a la vez)? | Verificar que nunca están activas en el mismo estado |
| Tiempo | ¿Todo estado «de espera» tiene un tiempo máximo o una salida si el evento esperado nunca llega? | Agregar eventos TIMEOUT hacia un estado seguro |
La completitud es la más rentable. De las 16 celdas que el portón ignora, no todas son iguales. Las hay legítimas: BOTON mientras ya está CERRADO y se está esperando el fin de carrera no tiene un significado distinto. Pero otras son sospechosas: ¿qué significa recibir FC_CERRADO estando ABIERTO? Físicamente es imposible, de modo que si ocurre, un sensor está fallando o está mal cableado. Ignorarlo en silencio esconde un defecto; una respuesta razonable es agregar un estado FALLO (motor apagado, aviso) al que llevan los eventos imposibles. Esa decisión no aparece nunca si no se miran las celdas vacías.
Y lo mismo con obstáculo mientras abre: ¿se detiene, se ignora? El modelo del apartado 3 lo ignora (el obstáculo es un peligro al cerrar, no al abrir), pero es una decisión de producto que debería estar escrita y firmada, no una omisión. La variante del laboratorio que ignora el obstáculo al cerrar muestra el costado peligroso del mismo mecanismo: un evento ignorado por descuido en la celda equivocada produce un portón que baja sobre un objeto.
También sirve para planificar las pruebas. Cada celda es un caso de prueba: 25 celdas, 25 casos. El análisis del programa encuentra una secuencia de eventos que las recorre todas (44 eventos para el portón) usando caminos mínimos entre estados, y esa secuencia se convierte en una prueba automática que se ejecuta en la computadora y en el equipo. Es una cobertura que con banderas sueltas es imposible de definir.
07 · Lo que sale mal
Errores de modelado
| Error | Cómo se ve | Corrección |
|---|---|---|
| Estados que son banderas disfrazadas | Una variable estado más cuatro booleanas que modifican lo que hace cada estado | Cada combinación con significado es un estado propio; si son demasiadas, dividir en dos máquinas |
| Explosión de estados | Dos comportamientos independientes (el motor y la luz, con 3 y 4 estados cada uno) se mezclan en una sola máquina de 12 estados | Dos máquinas pequeñas que se hablan por eventos, o una máquina jerárquica (tema 26) |
| Eventos de nivel | La máquina recibe la misma «señal» en cada pasada del bucle | Generar el evento solo en el flanco |
| Estados que esperan sin salida | La máquina queda en ESPERANDO_RESPUESTA para siempre si la respuesta no llega | Un TIMEOUT hacia un estado de reintento o de falla |
| Celdas vacías por descuido | «No se me ocurrió que eso pudiera pasar» | Revisar la tabla completa y justificar cada celda ignorada |
| Acciones largas o bloqueantes | Una transición que hace esperar_ms(500) | Convertir la espera en un estado y un evento TIMEOUT |
| Estados con nombre de acción | Estados como ENVIAR y RECIBIR que no se distinguen por lo que el sistema recuerda | Preguntarse qué diferencia observable hay entre estar en uno y en otro |
Una cuenta ayuda a entender por qué la separación importa. Dos máquinas independientes de 4 y 5 estados, si se mezclan en una sola, necesitan 4 × 5 = 20 estados combinados; si se mantienen separadas, son 4 + 5 = 9. Con tres comportamientos de 4 estados cada uno, 64 contra 12. La separación en máquinas independientes que se comunican con eventos es la herramienta principal contra la explosión.
También conviene saber cuándo no usar una máquina de estados. Si el comportamiento es un cálculo (un filtro, un controlador PI) no hay «estados» en el sentido que importa: es una función. Si es un procesamiento de datos con estructura (interpretar un protocolo complejo) puede ser mejor un analizador sintáctico. La máquina es la herramienta para comportamiento reactivo que depende de la historia.
08 · Analogía con programación
Analizar una máquina en Python
El programa representa la máquina del portón como un diccionario (estado, evento) → (estado nuevo, acción), que es exactamente la tabla del apartado 3. Con esa representación hace tres cosas: ejecuta escenarios y muestra cada transición; analiza el modelo (completitud, alcanzabilidad, trampas) y construye una secuencia de eventos que prueba todas las celdas; y cuenta cuántas combinaciones de banderas habría que gobernar si se implementara el mismo comportamiento sin máquina de estados.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
from collections import deque
from itertools import product
ESTADOS = ["CERRADO", "ABRIENDO", "ABIERTO", "CERRANDO", "DETENIDO"]
EVENTOS = ["BOTON", "FC_ABIERTO", "FC_CERRADO", "OBSTACULO", "TIMEOUT"]
# (estado, evento) -> (estado nuevo, acción). Lo que no está en la tabla es un evento que ese estado ignora.
TABLA = {
("CERRADO", "BOTON"): ("ABRIENDO", "motor_abrir"),
("ABRIENDO", "FC_ABIERTO"): ("ABIERTO", "motor_parar"),
("ABRIENDO", "BOTON"): ("DETENIDO", "motor_parar"),
("ABIERTO", "BOTON"): ("CERRANDO", "motor_cerrar"),
("ABIERTO", "TIMEOUT"): ("CERRANDO", "motor_cerrar"),
("CERRANDO", "FC_CERRADO"): ("CERRADO", "motor_parar"),
("CERRANDO", "BOTON"): ("DETENIDO", "motor_parar"),
("CERRANDO", "OBSTACULO"): ("ABRIENDO", "motor_abrir"),
("DETENIDO", "BOTON"): ("CERRANDO", "motor_cerrar"),
}
def procesar(estado, evento):
"""Un paso de la máquina: devuelve (estado nuevo, acción). Si el estado ignora el evento, no cambia nada."""
return TABLA.get((estado, evento), (estado, None))
def correr(titulo, eventos, inicial="CERRADO"):
print(titulo)
estado = inicial
for ev in eventos:
nuevo, accion = procesar(estado, ev)
if accion:
print(f" {estado:9s} --{ev:10s}--> {nuevo:9s} [{accion}]")
else:
print(f" {estado:9s} --{ev:10s}--> (ignorado)")
estado = nuevo
print(f" estado final: {estado}\n")
print("Parte 1: la máquina del portón en acción")
correr(" Apertura y cierre normales", ["BOTON", "FC_ABIERTO", "BOTON", "FC_CERRADO"])
correr(" Obstáculo mientras cierra", ["BOTON", "FC_ABIERTO", "BOTON", "OBSTACULO", "FC_ABIERTO"])
correr(" Botón a mitad de camino, y autocierre", ["BOTON", "BOTON", "BOTON", "FC_CERRADO", "BOTON", "FC_ABIERTO", "TIMEOUT", "OBSTACULO"])
# --- Parte 2: analizar el modelo antes de programarlo -------------------------------------------
celdas = list(product(ESTADOS, EVENTOS))
definidas = [c for c in celdas if c in TABLA]
print("Parte 2: análisis de la tabla")
print(f" celdas (estado × evento): {len(celdas)}; con transición: {len(definidas)}; ignoradas: {len(celdas) - len(definidas)}")
def alcanzables(origen):
visto, cola = {origen}, deque([origen])
while cola:
s = cola.popleft()
for (e, ev), (n, _) in TABLA.items():
if e == s and n not in visto:
visto.add(n); cola.append(n)
return visto
print(f" alcanzables desde CERRADO: {sorted(alcanzables('CERRADO'))}")
sin_retorno = [s for s in ESTADOS if "CERRADO" not in alcanzables(s)]
print(f" estados desde los que no se puede volver a CERRADO: {sin_retorno or 'ninguno'}")
sin_salida = [s for s in ESTADOS if not any(e == s and n != s for (e, _), (n, _) in TABLA.items())]
print(f" estados sin ninguna salida (trampas): {sin_salida or 'ninguno'}")
print(" celdas ignoradas por estado:")
for s in ESTADOS:
ign = [ev for ev in EVENTOS if (s, ev) not in TABLA]
print(f" {s:9s}: {', '.join(ign)}")
# secuencia de prueba que visita las 25 celdas (cada par estado/evento al menos una vez)
def camino(origen, destino):
previo, cola = {origen: None}, deque([origen])
while cola:
s = cola.popleft()
if s == destino: break
for ev in EVENTOS:
n = procesar(s, ev)[0]
if n not in previo:
previo[n] = (s, ev); cola.append(n)
ruta, s = [], destino
while previo[s] is not None:
p, ev = previo[s]; ruta.append(ev); s = p
return ruta[::-1]
actual, prueba = "CERRADO", []
for estado, evento in celdas:
for ev in camino(actual, estado): prueba.append(ev); actual = procesar(actual, ev)[0]
prueba.append(evento); actual = procesar(actual, evento)[0]
print(f" una secuencia que prueba las {len(celdas)} celdas tiene {len(prueba)} eventos")
# --- Parte 3: el mismo comportamiento con banderas sueltas ------------------------------------
banderas = ["abriendo", "cerrando", "abierto", "cerrado", "detenido"]
combinaciones = list(product([False, True], repeat=len(banderas)))
legales = [c for c in combinaciones if sum(c) == 1]
print()
print(f"Parte 3: con {len(banderas)} banderas booleanas hay {len(combinaciones)} combinaciones; solo {len(legales)} tienen sentido")
print(f" las otras {len(combinaciones) - len(legales)} (por ejemplo 'abriendo y cerrando a la vez') deben ser imposibles a fuerza de cuidado")
for n in (5, 8, 10, 12):
print(f" {n:2d} banderas -> {2 ** n:5d} combinaciones posibles")
Resultado, parte 1: en el escenario normal, el portón pasa por CERRADO → ABRIENDO → ABIERTO → CERRANDO → CERRADO. Con un obstáculo mientras cierra, CERRANDO --OBSTACULO--> ABRIENDO invierte el motor y termina en ABIERTO. En el tercer escenario, el botón a mitad de camino lleva a DETENIDO y otro botón lo manda a CERRANDO; el último OBSTACULO hace volver a ABRIENDO. Parte 2: la tabla tiene 25 celdas, 9 con transición y 16 que ignoran el evento; los cinco estados son alcanzables desde CERRADO, no hay estados desde los que no se pueda volver y no hay trampas. El análisis lista las celdas ignoradas de cada estado: DETENIDO ignora cuatro eventos y CERRANDO solo dos, y es ahí donde hay que decidir si cada una es legítima o un olvido. Una secuencia de prueba que recorre las 25 celdas tiene 44 eventos. Parte 3: con 5 banderas booleanas hay 32 combinaciones, de las cuales solo 5 tienen sentido; 12 banderas serían 4096.
Probá quitar una transición de la tabla (por ejemplo, CERRANDO + OBSTACULO) y ejecutá de nuevo el escenario del obstáculo: el programa no avisará nada, porque desde el punto de vista de la máquina, ignorar es válido. Lo que sí hace es mostrarte la celda vacía. Esa es la razón de revisar las celdas, no de confiar en que el código compile.
09 · Exploración interactiva
Un portón automático
El laboratorio ejecuta la máquina del portón. Disparás eventos con los botones y ves cómo cambia el estado, qué hacen las salidas (el motor y la luz son de tipo Moore, dependen del estado) y qué registra la bitácora. La tabla de transiciones destaca la fila del estado actual. Dos opciones modifican el diseño para mostrar el efecto de las decisiones: el autocierre (que el evento TIMEOUT cierre solo desde ABIERTO) y la regla del obstáculo.
Portón con cinco estados y cinco eventos
| Estado \ Evento | BOTON | FC_ABIERTO | FC_CERRADO | OBSTACULO | TIMEOUT |
|---|
Bitácora (últimas transiciones)
—
Disparar un evento mueve la máquina; los que el estado ignora quedan registrados.
Recorrido guiado
- Elegí Apertura y cierre normales y presioná «Ejecutar»:
CERRADO → ABRIENDO → ABIERTO → CERRANDO → CERRADO. Observá la luz: se enciende al salir deCERRADOy se apaga al volver. El motor sube, para, baja y para: sus valores dependen solo del estado (Moore). - Ejecutá Obstáculo mientras cierra: en
CERRANDO, el eventoOBSTACULOhace pasar aABRIENDOy el portón termina enABIERTO. Cero eventos ignorados: el diseño tiene una respuesta para cada cosa que pasó. - Cambiá «Obstáculo mientras cierra» a Se ignora y repetí el escenario: el obstáculo no produce ninguna transición, el estado sigue siendo
CERRANDOy el motor sigue bajando. Un aviso de peligro aparece en pantalla. Es el mismo diseño con una celda vacía por descuido, y es la clase de defecto que la revisión de completitud encuentra antes de grabar el equipo. - Ejecutá Botón a mitad de camino (vuelta a la opción original): el botón durante
ABRIENDOlleva aDETENIDO, otro botón aCERRANDO, y recién el fin de carrera cierra. Un mismo evento,BOTON, produce acciones distintas según el estado. - Probá Autocierre por tiempo y después ponelo en «No»: sin autocierre,
TIMEOUTenABIERTOqueda ignorado y el portón se queda abierto para siempre. No es un error del modelo, es una decisión de producto, y la tabla la muestra. - Ejecutá Eventos que no deberían ocurrir:
FC_CERRADOestandoCERRADOoFC_ABIERTOcon el portón cerrando son físicamente imposibles. La tabla los ignora y el laboratorio te lo señala. Un diseño más cuidadoso los llevaría a un estadoFALLO.
Desafío: ¿cómo cambiarías la tabla para que el botón durante CERRANDO reabra en vez de detener?
Habría que modificar una sola celda: (CERRANDO, BOTON) pasaría de DETENIDO / parar a ABRIENDO / abrir. Ése es el valor del modelo: el cambio de comportamiento es una línea de la tabla, y se ve a simple vista qué otras celdas se ven afectadas (DETENIDO deja de recibir un evento desde CERRANDO pero sigue siendo alcanzable desde ABRIENDO). Con el código de banderas del apartado 1 habría que auditar todas las condiciones que mencionan cerrando.
10 · Método de trabajo
Método para modelar
- Escribí en prosa qué hace el dispositivo, desde el punto de vista del usuario y del mundo físico: «con el portón cerrado, un pulso abre». Cada frase es una pista de un estado o de una transición.
- Listá los eventos. Todo lo que puede ocurrir y a lo que el sistema debe reaccionar: acciones del usuario, señales de sensores, tiempos, mensajes. Para cada uno, decidí si es un nivel o un flanco, y cómo se detecta.
- Encontrá los estados preguntando: «¿qué necesita recordar el sistema para saber cómo reaccionar al próximo evento?». Dos situaciones que reaccionan distinto a algún evento son estados distintos; dos que reaccionan igual a todo, el mismo.
- Elegí el estado inicial más seguro y definí qué pasa al arrancar (el estado real puede no ser el supuesto).
- Dibujá el diagrama con las transiciones obvias, y recién entonces armá la tabla completa. Cada celda vacía recibe una decisión escrita: ignorar (¿por qué?), transición a
FALLOo error. - Asigná las acciones: condiciones sostenidas como acciones de entrada y de salida del estado; órdenes puntuales en la transición. Todas breves y sin espera.
- Agregá los tiempos máximos: todo estado que espera algo del mundo externo necesita un
TIMEOUTo una salida. - Verificá las propiedades: completitud, determinismo, alcanzabilidad, ausencia de trampas, combinaciones prohibidas de salidas. Idealmente con un programa que lo haga por vos.
- Derivá las pruebas de la tabla: una por celda, con la secuencia que las recorre. Es lo que se ejecutará contra el código.
- Revisá el tamaño. Si hay más de unos diez estados, buscá comportamientos independientes que se puedan separar o jerarquías que simplifiquen (tema 26).
11 · Comprensión
Confusiones frecuentes
«Una máquina de estados es un diagrama de flujo»
Un diagrama de flujo describe una secuencia de pasos que se ejecutan una vez; una máquina de estados describe un sistema que espera eventos y reacciona según lo que recuerda. El diagrama de flujo termina; la máquina vive mientras el equipo esté encendido.
«Cada if del programa es un estado»
Un estado es lo que el sistema recuerda entre un evento y el siguiente. Un if sobre una entrada del momento no agrega memoria. Los estados se encuentran preguntando qué información hace falta conservar, no contando condiciones.
«Los estados deben ser lo que el sistema está haciendo»
A veces coincide, pero la definición correcta es lo que debe recordar. Dos momentos en que el sistema hace lo mismo (el motor parado) pueden ser estados distintos (CERRADO, ABIERTO y DETENIDO), porque reaccionan distinto al próximo botón.
«Si la celda está vacía, el evento se ignora, y eso es seguro»
Ignorar es una decisión como cualquier otra, y a veces la peor: puede esconder un fallo de sensor o dejar al sistema indefenso ante un peligro (el obstáculo al cerrar). Cada celda vacía debe tener una razón escrita.
«Más estados significan un modelo más preciso»
Significan un modelo más grande y más difícil de verificar. Un buen modelo tiene los estados mínimos necesarios; dos máquinas pequeñas e independientes suelen ser mejores que una gigante.
«Moore y Mealy son teoría de circuitos digitales; en firmware no importan»
Importan en la práctica: decidir si una salida es una condición sostenida (va en el estado) o una orden puntual (va en la transición) cambia dónde se escribe el código y cuánto cuesta olvidarse de algo.
«Una máquina de estados es solo para protocolos»
Es la herramienta general para cualquier comportamiento reactivo que depende de la historia: interfaces de usuario, cerraduras, cargadores de batería, lavarropas, motores, protocolos. Casi todo dispositivo con un botón la necesita.
12 · Práctica guiada
Predecí antes de comprobar
1. Modelar una cerradura de código
Una cerradura abre con el código de 3 dígitos «4-2-7». Teclas posibles: dígitos del 0 al 9. Tres errores seguidos la bloquean 30 segundos. Definí los estados y los eventos, y escribí las transiciones principales.
Ver solución
Estados: ESPERA_1 (nada ingresado), ESPERA_2 (el primer dígito fue 4), ESPERA_3 (los dos primeros fueron 4 y 2), ABIERTA (por un tiempo fijo) y BLOQUEADA (30 s). Eventos: DIGITO(n), TIMEOUT. Transiciones: en ESPERA_1, DIGITO(4) → ESPERA_2; cualquier otro dígito → error. En ESPERA_2, DIGITO(2) → ESPERA_3; otro → error. En ESPERA_3, DIGITO(7) → ABIERTA; otro → error. Un «error» vuelve a ESPERA_1 e incrementa un contador de fallos; al tercero → BLOQUEADA. ABIERTA y BLOQUEADA salen por TIMEOUT hacia ESPERA_1 (con el contador a cero). El contador es una variable de datos (una «extensión» de la máquina) que se usa en una guarda [fallos < 3]; sin ella harían falta dos estados más de intentos.
2. Contar celdas
Una máquina tiene 6 estados y 4 eventos, y el diseño define 11 transiciones. ¿Cuántas celdas ignoran un evento? ¿Qué hacés con ellas?
Ver solución
6 × 4 = 24 celdas; 24 − 11 = 13 celdas sin transición. Se revisan una por una y se les escribe una razón («ignorado porque no tiene sentido en este estado») o se convierten en transiciones, por ejemplo a un estado de falla cuando el evento es físicamente imposible. En una revisión de diseño, la lista de las 13 celdas con su justificación es el documento más útil que se puede pedir.
3. Moore o Mealy
Clasificá: (a) «el LED está encendido mientras el estado es ABIERTO»; (b) «al recibir un obstáculo cerrando, se emite un pitido corto»; (c) «el motor sube mientras el estado es ABRIENDO».
Ver solución
(a) Moore: depende solo del estado; conviene implementarla como acciones de entrada y de salida de ABIERTO. (b) Mealy: depende de la transición (CERRANDO con OBSTACULO); es una acción puntual, ligada al evento. (c) Moore: el motor sube en todo momento en que el estado es ABRIENDO; se activa al entrar y se apaga al salir.
4. ¿Evento o estado?
Clasificá como evento o como estado (nivel): (a) «el botón está apretado»; (b) «se apretó el botón»; (c) «pasaron 5 segundos»; (d) «el portón está abierto»; (e) «llegó el comando UMBRAL 25».
Ver solución
(a) Nivel (dura mientras se mantiene); (b) evento (el flanco); (c) evento (lo produce un temporizador); (d) estado (una situación que persiste, un valor de la máquina); (e) evento con datos. Un error de diseño típico es tratar (a) como un evento que se repite en cada pasada del bucle.
5. Explosión de estados
Un cargador tiene un comportamiento de carga con 4 estados (reposo, carga rápida, carga lenta, mantenimiento) y, de forma independiente, un indicador luminoso con 3 modos (fijo, parpadeo lento, parpadeo rápido). ¿Cuántos estados tendría una sola máquina que lo combine? ¿Y con dos máquinas?
Ver solución
Una sola: 4 × 3 = 12 estados, la mayoría combinaciones sin interés. Dos máquinas separadas: 4 + 3 = 7 estados, donde la de carga envía eventos (CARGA_RAPIDA, CARGA_LENTA...) a la del indicador, que decide cómo parpadear. Es el principio de separar comportamientos independientes; con tres comportamientos de 4 estados, 64 contra 12.
6. Agregar un estado de falla
Querés que el portón vaya a un estado FALLO (motor apagado) cuando un fin de carrera se activa en un estado en el que es físicamente imposible. ¿Qué transiciones agregás y cómo se sale de FALLO?
Ver solución
Se agregan las celdas hoy vacías que son imposibles: FC_CERRADO en ABRIENDO y en ABIERTO; FC_ABIERTO en CERRANDO y en CERRADO. Cuatro transiciones nuevas hacia FALLO con la acción motor_parar (más una señal de aviso). El estado nuevo agrega una fila a la tabla: FALLO ignora todos los eventos salvo un REINICIO (por ejemplo, el botón mantenido 5 s) que lo lleva a DETENIDO, un estado seguro desde el que el próximo botón cierra. Así se evita una trampa permanente y la máquina pasa a tener 6 estados y 13 transiciones.
13 · Síntesis
Ideas para recordar
- Una máquina de estados finitos reemplaza un grupo de banderas por una sola variable, el estado, y describe el comportamiento con una tabla de (estado, evento) → (estado nuevo, acción). La tabla se revisa, se completa y se prueba entera.
- Un estado es lo que el sistema recuerda, no lo que hace. Un evento es un hecho puntual (el flanco, el tiempo vencido, el mensaje); un nivel es un estado. El mismo evento significa cosas distintas según el estado.
- Se escribe con un diagrama (para pensar y conversar) y con una tabla (para verificar y programar). La tabla obliga a mirar todas las celdas, incluidas las vacías, que es donde viven los errores.
- Las salidas que son una condición sostenida van en el estado (Moore, con acciones de entrada y salida); las órdenes puntuales van en la transición (Mealy). Las acciones son breves y no bloqueantes: lo largo se modela como un estado.
- Las señales se convierten en eventos antes de entrar a la máquina: detectar el flanco, filtrar el rebote, producir
TIMEOUTcon un temporizador, priorizar los eventos de seguridad. La máquina procesa un evento por vez, hasta terminar. - El modelo se verifica sin ejecutarlo: completitud (cada celda decidida), determinismo, alcanzabilidad, ausencia de trampas, combinaciones prohibidas de salidas y tiempos máximos en toda espera. Cada celda es un caso de prueba.
- Los errores típicos son banderas disfrazadas de estados, eventos de nivel, celdas vacías por descuido, esperas sin salida y mezclar comportamientos independientes (explosión de estados: 4 × 5 = 20 contra 4 + 5 = 9).
- Una máquina de estados es la herramienta para el comportamiento reactivo que depende de la historia; para un cálculo, una función; para un protocolo con estructura compleja, a veces un analizador.
¿Listo para el tema 25?
Antes de avanzar, modelá un dispositivo que tengas a mano (una lámpara con atenuador de tres niveles, una puerta de ascensor, un cargador): listá los eventos, encontrá los estados, dibujá el diagrama, completá la tabla justificando cada celda vacía y marcá qué salidas van en los estados. Si cada celda tiene una decisión escrita, el modelo está listo para programarse.
En el próximo tema vemos cómo implementar una máquina de estados en C: el switch/case, las tablas de transiciones con punteros a función, las acciones de entrada y de salida, y las buenas prácticas para que el código se parezca a la tabla que acabás de dibujar.