← Índice del curso

Robótica y Automatización · Tema 36

Máquinas de estados finitos: estados, eventos y transiciones

En el tema 35 la memoria de un proceso eran unos pocos bits enclavados. Cuando el proceso tiene varias etapas, modos y fallas, esos bits se enredan. Una máquina de estados ordena la historia: en cada instante el sistema está en un único estado, y cada evento lo mantiene ahí o lo lleva a otro. Es la herramienta central de la automatización secuencial.

01 · Punto de partida

Cuando los enclavamientos no alcanzan

La estación de bombeo del tema 35 tenía cuatro bits de memoria (habilitado, llenando, falla y un temporizador) y cinco líneas de lógica. Funciona porque el proceso es sencillo. Imaginá ahora un portón de garaje: se abre, se cierra, se puede detener a mitad, tiene una fotocélula, un límite de recorrido, un cierre automático y una falla. Cada comportamiento depende de lo que pasó antes: «el próximo pulso, ¿abre o cierra?» depende de qué estaba haciendo cuando se lo detuvo.

Con bits sueltos, esa lógica se vuelve una maraña de condiciones cruzadas, difícil de leer y de probar. Una máquina de estados finitos (FSM, finite state machine) propone otra forma de pensar: en lugar de recordar muchos hechos independientes, el sistema recuerda una sola cosa, en qué estado está. Todo lo demás se deduce del estado y del evento que llega.

Este tema da el modelo. El siguiente (tema 37) lo implementa sin bloqueos, con tablas y temporizadores con millis(); el 38 lo extiende con estados jerárquicos.

02 · El modelo

Qué es una máquina de estados

Una máquina de estados finitos se define con cinco elementos:

  • un conjunto finito de estados S;
  • un conjunto de eventos E que el sistema puede recibir;
  • una función de transición δ(estado, evento) → estado siguiente;
  • un estado inicial s0;
  • una función de salida λ que dice qué hacen los actuadores.

Es un modelo de «lo que el sistema sabe de su pasado»: dos historias distintas que conducen al mismo comportamiento futuro llevan al mismo estado. Un estado no es una variable cualquiera, es una situación distinguible: «cerrando», «detenido mientras abría», «en falla».

El ejemplo mínimo es una lámpara con un pulsador: dos estados (APAGADA, ENCENDIDA) y un evento (PULSO) que alterna entre ellos. Con un temporizador de apagado automático se agrega el evento T_OFF, que lleva de ENCENDIDA a APAGADA. Cuatro transiciones describen todo el comportamiento.

La palabra clave es finito: la cantidad de estados está acotada, lo que permite listar todas las situaciones, razonar sobre ellas e incluso verificarlas de forma exhaustiva. Un contador que sube sin límite no es un estado finito; se trata como una variable aparte (ver sección siguiente).

03 · Qué mueve a la máquina

Estados, eventos y condiciones

Un evento es algo que ocurre en un instante: se presionó el pulsador, se alcanzó el final de carrera, venció un temporizador, apareció un obstáculo. La máquina procesa un evento por vez y, según el estado actual, cambia de estado o lo ignora.

Es importante distinguirlo de una condición (o guarda): un valor que se mantiene, como «el nivel es alto» o «hay tensión». Una transición puede combinar ambos: «cuando llegue BOTON y la batería sea mayor al 20 %». Las entradas del programa son condiciones; para convertirlas en eventos hay que detectar los cambios:

  • Flancos. Un pulsador mantenido durante un segundo debe producir un evento BOTON, no cien. Se genera al pasar de 0 a 1 (con antirrebote).
  • Temporizadores. «Pasaron 10 s abierto» (T_AUTO) es un evento que genera un temporizador que arranca al entrar en un estado.
  • Sensores filtrados. Un final de carrera con histéresis o un filtro de varias lecturas (tema 32) produce FIN_A solo cuando el dato es confiable.

Los estados sirven para lo cualitativo; las variables para lo cuantitativo. Un portón con un contador de intentos fallidos no necesita un estado por cada valor del contador (sería una explosión de estados): mantiene una variable y la usa como guarda («si intentos < 3»). A esa combinación se la llama máquina de estados extendida.

04 · Dos maneras de mostrarla

Diagrama y tabla

El diagrama de estados dibuja cada estado como un nodo y cada transición como una flecha con su evento (y, si corresponde, su guarda y su acción: evento [guarda] / acción). Una flecha sin origen marca el estado inicial. Es ideal para entender la secuencia general y para explicar el diseño a otra persona.

La tabla de transiciones lista, para cada estado y cada evento, el estado siguiente. Es ideal para comprobar que no falta nada, porque obliga a decidir qué ocurre en cada combinación. Para el portón (siete estados, siete eventos):

Tabla de transiciones del portón: estado actual (filas) y evento (columnas). «·» significa que el evento se ignora.
EstadoBOTONFIN_AFIN_COBSTT_AUTOT_MAXRESETSalidas (abrir, cerrar, luz, alarma)
CERRADOABRIENDO······0, 0, 0, 0
ABRIENDOPARADO_AABIERTO···FALLA·1, 0, 1, 0
ABIERTOCERRANDO···CERRANDO··0, 0, 1, 0
CERRANDOPARADO_C·CERRADOABRIENDO·FALLA·0, 1, 1, 0
PARADO_ACERRANDO······0, 0, 1, 0
PARADO_CABRIENDO······0, 0, 1, 0
FALLA······PARADO_C0, 0, 0, 1

La tabla tiene 49 celdas; solo 13 son transiciones, y las otras 36 se ignoran. Eso no es un descuido: es una decisión que hay que tomar a conciencia en cada celda. Que el portón ignore un pulsador cuando está en FALLA es una decisión (solo RESET sale de allí); que ignore el obstáculo mientras cierra sería un grave defecto. Ignorar es una respuesta válida; olvidarse, no.

Un diagrama y su tabla describen exactamente lo mismo, y conviene tener ambos: el primero para pensar, el segundo para verificar y para codificar. El laboratorio 1 dibuja el diagrama del portón y permite recorrerlo.

05 · Qué hacen los actuadores

Salidas: Moore y Mealy

Hay dos formas de asociar salidas a la máquina:

Dos modelos de salida.
ModeloLas salidas dependen de…Ejemplo en el portón
MooreSolo del estado actualEn ABRIENDO el motor de apertura está encendido y la luz también
MealyDel estado y del evento (la salida «cuelga» de la transición)Un aviso sonoro corto en el instante en que se pulsa BOTON durante el cierre

Para actuadores que permanecen (motores, luces, válvulas) conviene Moore: las salidas son una función del estado, hay una sola tabla que verificar y un cambio de estado siempre cambia las salidas de la misma forma. Es el que usamos aquí (las salidas son el último renglón de la tabla). Para acciones puntuales (un pulso, un registro, un aviso) las salidas de Mealy, o las acciones de entrada («al entrar a FALLA, registrar la hora»), son más naturales.

Una regla práctica: en cada estado, todo lo que está encendido lo decide el estado; las acciones puntuales se asocian a la transición o a la entrada. Y en el estado inicial, y en todo estado de falla, los actuadores peligrosos están apagados (tema 35).

06 · El método

Cómo se diseña una

  1. Describir el comportamiento en lenguaje natural y listar las situaciones: «cerrado», «abriéndose», «abierto», «cerrándose», «detenido a mitad», «con falla».
  2. Decidir qué se distingue y qué no. Dos situaciones son estados distintos si el sistema debe responder distinto al mismo evento. Si el sistema se comporta igual, es un solo estado.
  3. Listar los eventos: las entradas relevantes convertidas en sucesos, incluidos los temporizadores.
  4. Completar la tabla: para cada estado y evento, decidir el destino o el «ignorar».
  5. Asignar salidas a cada estado.
  6. Fijar el estado inicial (el seguro) y revisar qué pasa tras un corte de energía.
  7. Prever lo inesperado: eventos imposibles (el final de carrera «abierto» y el «cerrado» a la vez), tiempos excedidos, entradas inconsistentes. Deben llevar a un estado seguro, no ignorarse en silencio.

En el portón, el punto sutil es el segundo. Cuando se pulsa BOTON en medio del recorrido, el portón se detiene. Y cuando se vuelve a pulsar, ¿abre o cierra? Lo razonable es que invierta el sentido anterior. Eso significa que la máquina debe recordar en qué sentido iba, y la única forma de hacerlo es tener dos estados de parada: PARADO_A (detenido mientras abría; el próximo pulso cierra) y PARADO_C (detenido mientras cerraba; el próximo pulso abre). Con un solo estado PARADO la máquina olvidó el sentido y ya no puede alternar. La memoria está en los estados, y se paga con estados.

Las demás decisiones también salen del método: la inversión de seguridad ante OBST mientras se cierra (nunca mientras se abre, donde no hace falta), el límite de tiempo T_MAX en ambos movimientos (si el final de carrera falla, el motor no marcha para siempre) y el RESET como única salida de FALLA, que lleva a PARADO_C porque la posición se desconoce y lo más prudente es que el siguiente pulso abra.

07 · Antes de construirla

Verificar el diseño

La gran ventaja de un modelo finito es que se puede analizar de forma exhaustiva, sin hardware. Algunas verificaciones simples:

  • Alcanzabilidad. Desde el estado inicial, ¿se puede llegar a cada estado? Un estado inalcanzable es código muerto o una conexión que falta.
  • Ausencia de callejones. Desde cada estado alcanzable, ¿hay un camino de vuelta al estado de reposo? Un estado desde el que no se vuelve es una trampa.
  • Propiedades de seguridad. «Abrir y cerrar nunca están activos a la vez» (se comprueba mirando la tabla de salidas); «todo movimiento tiene un límite de tiempo»; «cerrando, un obstáculo provoca la inversión».
  • Completitud y determinismo. Cada combinación de estado y evento tiene una respuesta (un destino o ignorar), y solo una. La tabla lo garantiza por construcción.
  • Escenarios. Secuencias de eventos con el resultado esperado: el recorrido normal, la detención y reanudación, la falla y su recuperación.

El laboratorio 2 aplica estas verificaciones automáticamente a cinco variantes del diseño, cuatro de ellas con un defecto deliberado: sin límite de tiempo, sin reacción al obstáculo, con un solo estado de parada y con un estado huérfano. Los defectos aparecen sin necesidad de simular el portón.

Una advertencia: se verifica el modelo. Si el modelo está mal (un sensor que en la realidad rebota, un evento que nunca llega) el diseño correcto en el papel falla en la máquina. Por eso se prueba también con el hardware, y se mide.

08 · De la tabla al código

Llevarla al código

La forma más simple de programar una FSM en Arduino es con un tipo enumerado para los estados, otro para los eventos y un switch sobre el estado actual. El consejo central es separar la lógica de las acciones:

  • siguiente(estado, evento) es una función pura: recibe un estado y un evento, devuelve el estado siguiente, y no lee pines, no usa el reloj, no escribe a nada. Eso la hace trivial de probar.
  • salidas(estado) es otra función pura que da los valores de los actuadores.
  • Una capa externa se ocupa de lo impuro: convertir entradas y tiempo en eventos, llamar a siguiente() y aplicar las salidas a los pines.

Esa separación permite ejecutar la máquina con una lista de eventos escrita a mano (como hace el sketch de este tema), compararla con la tabla de diseño y detectar errores antes de conectar nada.

Dos reglas que se desarrollan en el tema 37: no bloquear (los tiempos se miden con millis() y generan eventos; nunca delay() dentro de un estado) y un evento a la vez, procesado por completo antes del siguiente. Y una buena costumbre: registrar cada transición (estado anterior, evento, estado nuevo, hora). Con ese registro, una falla en la máquina se reconstruye leyendo.

09 · Código para la placa

El portón en Arduino

El sketch implementa siguiente() y salidas() para el portón, ejecuta una secuencia guionada de quince eventos y verifica de forma exhaustiva las 49 combinaciones de estado y evento. No depende de ningún pin: es la lógica pura.

// Máquina de estados de un portón automático. La lógica se separa en dos funciones puras:
//   siguiente(estado, evento) -> estado     (las transiciones)
//   salidas(estado)           -> Salidas    (máquina de Moore: las salidas dependen solo del estado)
// Al no leer pines ni usar el tiempo, se pueden probar sin hardware con una lista de eventos.
enum class Estado : byte { CERRADO, ABRIENDO, ABIERTO, CERRANDO, PARADO_A, PARADO_C, FALLA, CUANTOS };
enum class Evento : byte { BOTON, FIN_A, FIN_C, OBST, T_AUTO, T_MAX, RESET, CUANTOS };

const char *NOMBRE_ESTADO[] = {"CERRADO", "ABRIENDO", "ABIERTO", "CERRANDO", "PARADO_A", "PARADO_C", "FALLA"};
const char *NOMBRE_EVENTO[] = {"BOTON", "FIN_A", "FIN_C", "OBST", "T_AUTO", "T_MAX", "RESET"};

Estado siguiente(Estado e, Evento ev) {
  switch (e) {
    case Estado::CERRADO:
      if (ev == Evento::BOTON) return Estado::ABRIENDO;
      break;
    case Estado::ABRIENDO:
      if (ev == Evento::FIN_A) return Estado::ABIERTO;
      if (ev == Evento::BOTON) return Estado::PARADO_A;
      if (ev == Evento::T_MAX) return Estado::FALLA;           // límite de tiempo si el final de carrera no responde
      break;
    case Estado::ABIERTO:
      if (ev == Evento::BOTON || ev == Evento::T_AUTO) return Estado::CERRANDO;
      break;
    case Estado::CERRANDO:
      if (ev == Evento::FIN_C) return Estado::CERRADO;
      if (ev == Evento::OBST) return Estado::ABRIENDO;         // inversión de seguridad
      if (ev == Evento::BOTON) return Estado::PARADO_C;
      if (ev == Evento::T_MAX) return Estado::FALLA;
      break;
    case Estado::PARADO_A:                                     // se detuvo abriendo: el próximo pulso cierra
      if (ev == Evento::BOTON) return Estado::CERRANDO;
      break;
    case Estado::PARADO_C:                                     // se detuvo cerrando: el próximo pulso abre
      if (ev == Evento::BOTON) return Estado::ABRIENDO;
      break;
    case Estado::FALLA:
      if (ev == Evento::RESET) return Estado::PARADO_C;        // posición desconocida: el próximo pulso abre
      break;
    default:
      break;
  }
  return e;                                                    // cualquier otra combinación se ignora, y está dicho
}

struct Salidas { bool abrir, cerrar, luz, alarma; };

Salidas salidas(Estado e) {
  switch (e) {
    case Estado::ABRIENDO: return {true,  false, true,  false};
    case Estado::CERRANDO: return {false, true,  true,  false};
    case Estado::ABIERTO:
    case Estado::PARADO_A:
    case Estado::PARADO_C: return {false, false, true,  false};
    case Estado::FALLA:    return {false, false, false, true};
    default:               return {false, false, false, false};
  }
}

void imprimirAncho(const char *texto, byte ancho) {
  Serial.print(texto);
  for (byte i = strlen(texto); i < ancho; i++) Serial.print(' ');
}

void setup() {
  Serial.begin(115200);
  Serial.println("Secuencia de eventos desde CERRADO:");
  const Evento guion[] = {Evento::BOTON, Evento::FIN_A, Evento::T_AUTO, Evento::OBST, Evento::BOTON, Evento::BOTON,
                          Evento::BOTON, Evento::BOTON, Evento::T_MAX, Evento::BOTON, Evento::RESET, Evento::BOTON,
                          Evento::FIN_A, Evento::BOTON, Evento::FIN_C};
  Estado e = Estado::CERRADO;
  for (byte i = 0; i < sizeof(guion) / sizeof(guion[0]); i++) {
    const Estado nuevo = siguiente(e, guion[i]);
    const Salidas s = salidas(nuevo);
    Serial.print("  "); imprimirAncho(NOMBRE_EVENTO[(byte)guion[i]], 8); Serial.print(": ");
    imprimirAncho(NOMBRE_ESTADO[(byte)e], 10); Serial.print("-> "); imprimirAncho(NOMBRE_ESTADO[(byte)nuevo], 10);
    Serial.print("abrir="); Serial.print(s.abrir); Serial.print(" cerrar="); Serial.print(s.cerrar);
    Serial.print(" luz="); Serial.print(s.luz); Serial.print(" alarma="); Serial.print(s.alarma);
    if (nuevo == e) Serial.print("  (ignorado)");
    Serial.println();
    e = nuevo;
  }

  // Verificación exhaustiva: cuántas de las 49 combinaciones cambian de estado, y que nunca se active abrir y cerrar juntos.
  byte definidas = 0;
  bool exclusivas = true;
  for (byte s = 0; s < (byte)Estado::CUANTOS; s++) {
    if (salidas((Estado)s).abrir && salidas((Estado)s).cerrar) exclusivas = false;
    for (byte v = 0; v < (byte)Evento::CUANTOS; v++) if (siguiente((Estado)s, (Evento)v) != (Estado)s) definidas++;
  }
  Serial.print("Transiciones definidas: "); Serial.print(definidas); Serial.print(" de ");
  Serial.print((byte)Estado::CUANTOS * (byte)Evento::CUANTOS); Serial.println(" celdas");
  Serial.println(exclusivas ? "Abrir y cerrar nunca coinciden" : "ERROR: abrir y cerrar coinciden en algun estado");
}

void loop() {}

La salida es, línea por línea, la secuencia de abajo (el sketch muestra el estado anterior, el nuevo y las salidas): BOTON lleva de CERRADO a ABRIENDO; FIN_A, a ABIERTO; T_AUTO, a CERRANDO; OBST, de vuelta a ABRIENDO (inversión de seguridad); BOTON, a PARADO_A; BOTON, a CERRANDO; BOTON, a PARADO_C; BOTON, a ABRIENDO; T_MAX, a FALLA (con la alarma encendida); BOTON en FALLA se ignora; RESET lleva a PARADO_C; BOTON, a ABRIENDO; FIN_A, a ABIERTO; BOTON, a CERRANDO; FIN_C, a CERRADO. Al final informa Transiciones definidas: 13 de 49 celdas y que abrir y cerrar nunca coinciden.

Para usarla en un portón real, agregá la capa impura: leer el pulsador con antirrebote y generar BOTON en el flanco, leer los finales de carrera y la fotocélula, medir con millis() el tiempo de recorrido (T_MAX) y la espera abierto (T_AUTO), y aplicar salidas() a los relés del motor. Probalo primero con una carga de prueba, y recordá que la parada de emergencia y la protección del motor deben ser cableadas e independientes del programa (tema 35).

10 · Exploración interactiva

Laboratorios del portón

1. Recorrer la máquina

El diagrama muestra el estado actual en lima. Disparás eventos con los botones; los que tienen una transición definida desde el estado actual aparecen resaltados. La última transición se marca en el diagrama. Los eventos sin transición se ignoran, y el registro lo dice.

Diagrama de estados del portónSiete estados conectados por flechas etiquetadas con eventos: cerrado, abriendo, abierto, cerrando, parado mientras abría, parado mientras cerraba y falla. El estado actual está resaltado.
Estado actual—
Salidas (Moore)—

Probá BOTON.

    Una secuencia para probar: BOTON, FIN_A, T_AUTO (cierre automático), OBST (el portón invierte y vuelve a abrir), BOTON (se detiene), BOTON (cierra), BOTON (se detiene otra vez), BOTON (abre: alterna), T_MAX (falla: alarma encendida), BOTON (se ignora), RESET (sale de la falla a PARADO_C), BOTON (abre). Probá también eventos que no corresponden, como FIN_C mientras abre: se ignoran.

    2. Verificar variantes del diseño

    Elegí una versión del diseño. El laboratorio dibuja su diagrama (en rojo, los estados inalcanzables) y ejecuta las verificaciones automáticas de la sección 7.

    Diagrama de la variante elegidaDiagrama de estados de la versión elegida, con los estados inalcanzables en rojo.
    Estados alcanzables—
    Transiciones—

    Problemas detectados

      Elegí una versión.

      La versión completa pasa todas las verificaciones. «Sin T_MAX» pierde el estado FALLA (ya no se alcanza) y deja los dos movimientos sin límite de tiempo. «Sin obstáculo» tiene un diagrama casi idéntico, pero CERRANDO ya no reacciona a OBST: el defecto está en una flecha que falta. «Un solo PARADO» tiene un estado menos y un comportamiento peor. «Huérfano» tiene un estado al que no llega ninguna flecha.

      11 · Código ejecutable

      Analizar la máquina en Python

      La máquina como un diccionario de transiciones, la ejecución de la secuencia de eventos y las verificaciones automáticas (alcanzabilidad, retorno al reposo, límites de tiempo, reacción al obstáculo, salidas exclusivas) sobre el diseño y sus variantes.

      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.

      ESTADOS = ["CERRADO", "ABRIENDO", "ABIERTO", "CERRANDO", "PARADO_A", "PARADO_C", "FALLA"]
      EVENTOS = ["BOTON", "FIN_A", "FIN_C", "OBST", "T_AUTO", "T_MAX", "RESET"]
      
      TRANSICIONES = {                      # estado -> {evento: estado siguiente}; lo que no figura se ignora
          "CERRADO":  {"BOTON": "ABRIENDO"},
          "ABRIENDO": {"FIN_A": "ABIERTO", "BOTON": "PARADO_A", "T_MAX": "FALLA"},
          "ABIERTO":  {"BOTON": "CERRANDO", "T_AUTO": "CERRANDO"},
          "CERRANDO": {"FIN_C": "CERRADO", "OBST": "ABRIENDO", "BOTON": "PARADO_C", "T_MAX": "FALLA"},
          "PARADO_A": {"BOTON": "CERRANDO"},
          "PARADO_C": {"BOTON": "ABRIENDO"},
          "FALLA":    {"RESET": "PARADO_C"},
      }
      SALIDAS = {                           # máquina de Moore: dependen solo del estado (abrir, cerrar, luz, alarma)
          "CERRADO": (0, 0, 0, 0), "ABRIENDO": (1, 0, 1, 0), "ABIERTO": (0, 0, 1, 0), "CERRANDO": (0, 1, 1, 0),
          "PARADO_A": (0, 0, 1, 0), "PARADO_C": (0, 0, 1, 0), "FALLA": (0, 0, 0, 1),
      }
      
      def siguiente(trans, estado, evento):
          return trans.get(estado, {}).get(evento, estado)
      
      def alcanzables(trans, inicio="CERRADO"):
          vistos, cola = {inicio}, [inicio]
          while cola:
              e = cola.pop(0)
              for destino in trans.get(e, {}).values():
                  if destino not in vistos:
                      vistos.add(destino); cola.append(destino)
          return vistos
      
      def hay_camino(trans, origen, destino):
          return destino in alcanzables(trans, origen)
      
      def variante(nombre):
          t = {e: dict(d) for e, d in TRANSICIONES.items()}
          if nombre == "sin_timeout":
              for e in ("ABRIENDO", "CERRANDO"):
                  del t[e]["T_MAX"]
          elif nombre == "sin_obstaculo":
              del t["CERRANDO"]["OBST"]
          elif nombre == "un_parado":
              t["ABRIENDO"]["BOTON"] = "PARADO"; t["CERRANDO"]["BOTON"] = "PARADO"
              t["PARADO"] = {"BOTON": "CERRANDO"}
              del t["PARADO_A"]; del t["PARADO_C"]
              t["FALLA"]["RESET"] = "PARADO"
          elif nombre == "huerfano":
              t["MANTENIMIENTO"] = {"RESET": "CERRADO"}
          return t
      
      def analizar(trans, salidas=SALIDAS):
          problemas = []
          estados = set(trans)
          for destinos in trans.values():
              estados |= set(destinos.values())
          inalcanzables = sorted(estados - alcanzables(trans))
          if inalcanzables:
              problemas.append(f"estados inalcanzables: {', '.join(inalcanzables)}")
          sin_salida = sorted(e for e in alcanzables(trans) if not hay_camino(trans, e, "CERRADO"))
          if sin_salida:
              problemas.append(f"estados desde los que no se vuelve a CERRADO: {', '.join(sin_salida)}")
          for e in ("ABRIENDO", "CERRANDO"):
              if e in trans and "T_MAX" not in trans[e]:
                  problemas.append(f"{e} no tiene limite de tiempo: puede quedar en marcha si falla el final de carrera")
          if "CERRANDO" in trans and "OBST" not in trans["CERRANDO"]:
              problemas.append("CERRANDO ignora el obstaculo: el porton podria cerrar sobre una persona")
          if "PARADO" in trans:
              problemas.append("un solo estado PARADO: no recuerda en que sentido iba, el siguiente pulso no puede alternar")
          simultaneas = [e for e, s in salidas.items() if s[0] and s[1]]
          if simultaneas:
              problemas.append(f"salidas abrir y cerrar a la vez en: {', '.join(simultaneas)}")
          return problemas
      
      # --- Una secuencia de eventos ---
      print("Secuencia de eventos desde CERRADO:")
      estado = "CERRADO"
      for ev in ["BOTON", "FIN_A", "T_AUTO", "OBST", "BOTON", "BOTON", "BOTON", "BOTON", "T_MAX", "BOTON", "RESET", "BOTON", "FIN_A", "BOTON", "FIN_C"]:
          nuevo = siguiente(TRANSICIONES, estado, ev)
          marca = "" if nuevo != estado else "  (ignorado)"
          s = SALIDAS[nuevo]
          print(f"  {ev:7s}: {estado:9s} -> {nuevo:9s} abrir={s[0]} cerrar={s[1]} luz={s[2]} alarma={s[3]}{marca}")
          estado = nuevo
      
      # --- Tamaño de la tabla ---
      definidas = sum(len(d) for d in TRANSICIONES.values())
      total = len(ESTADOS) * len(EVENTOS)
      print(f"Transiciones definidas: {definidas} de {total} celdas; el resto ({total - definidas}) se ignora de forma explicita")
      
      # --- Analisis de variantes del diseno ---
      for nombre in ("completo", "sin_timeout", "sin_obstaculo", "un_parado", "huerfano"):
          t = TRANSICIONES if nombre == "completo" else variante(nombre)
          problemas = analizar(t)
          print(f"{nombre}: {'sin problemas detectados' if not problemas else str(len(problemas)) + ' problema(s)'}")
          for p in problemas:
              print(f"   - {p}")

      La secuencia termina en CERRADO tras quince eventos, con un evento ignorado (BOTON en FALLA). La tabla tiene 13 transiciones y 36 celdas ignoradas de 49. La versión completa no tiene problemas; sin T_MAX hay tres (FALLA inalcanzable y dos movimientos sin límite); sin OBST, uno; con un solo PARADO, uno; con el estado huérfano, uno (MANTENIMIENTO inalcanzable).

      Probá agregar tu propia verificación, por ejemplo «ningún estado de movimiento permite la salida de la alarma», o añadir un estado PRE_CIERRE (luz intermitente 3 s antes del cierre automático) y comprobar que sigue sin problemas. Escribir la verificación de lo que sale mal es una forma muy buena de entender el diseño.

      12 · Método y diagnóstico

      Pruebas y diagnóstico

      1. Escribí el diagrama y la tabla completa antes de programar; revisá cada celda ignorada.
      2. Probá siguiente() con la lista de escenarios, sin hardware: el recorrido normal, las interrupciones, la falla y la recuperación.
      3. Recorré exhaustivamente todas las combinaciones de estado y evento y comprobá que ninguna lleva a un estado inesperado.
      4. Verificá las propiedades de seguridad sobre la tabla de salidas.
      5. Con el hardware, simulá cada evento por separado (apretá el pulsador, accioná el final de carrera, tapá la fotocélula) y compará con el diagrama.
      6. Provocá las fallas: desconectá un final de carrera, bloqueá el motor, cortá la energía a mitad de recorrido y verificá el estado de arranque.
      7. Registrá las transiciones con la hora; ante un comportamiento raro, el registro dice por dónde pasó.
      «La máquina tiene un estado por cada combinación de variables»

      Eso lleva a una explosión de estados. Los estados son para lo cualitativo; las cantidades (contadores, tiempos, medidas) van en variables con guardas.

      «Los eventos que no figuran en la tabla no importan»

      Cada celda es una decisión. Algunos eventos imposibles deberían llevar a FALLA en vez de ignorarse, porque revelan un sensor roto.

      «El portón guarda el último sentido en una variable y usa un solo PARADO»

      Es una alternativa válida (máquina extendida), pero mueve la memoria a una variable que hay que mantener y verificar aparte. Con dos estados, la memoria es visible en el diagrama.

      «Un delay() dentro de un estado es lo más simple»

      Congela la máquina: durante la espera no atiende pulsadores ni la fotocélula. El tiempo debe ser un evento (T_AUTO), no una pausa.

      «Mientras el pulsador esté apretado, llegan eventos BOTON»

      Hay que generar el evento en el flanco de subida, con antirrebote. Con un evento por scan, el portón alternaría abrir y cerrar cada ciclo.

      «Moore o Mealy da lo mismo»

      Describen el mismo comportamiento, pero Moore facilita verificar las salidas (una por estado) y evita glitches. Mealy es útil para acciones puntuales.

      13 · Práctica guiada

      Predecí antes de comprobar

      1. Una lámpara con temporizador

      Una lámpara tiene un pulsador y un apagado automático a los 60 s. Definí los estados, los eventos y la tabla de transiciones.

      Ver solución

      Estados: APAGADA, ENCENDIDA. Eventos: PULSO, T_OFF. APAGADA + PULSO → ENCENDIDA; ENCENDIDA + PULSO → APAGADA; ENCENDIDA + T_OFF → APAGADA; los demás se ignoran (T_OFF en APAGADA). El temporizador arranca al entrar en ENCENDIDA.

      2. Seguir una secuencia

      Desde CERRADO llegan cuatro eventos BOTON seguidos. ¿Por qué estados pasa?

      Ver solución

      ABRIENDO, PARADO_A, CERRANDO, PARADO_C. El portón alterna entre moverse y detenerse, y cada vez que se detiene, el próximo pulso invierte el sentido.

      3. Eventos ignorados

      ¿Qué pasa con OBST en ABIERTO? ¿Y con FIN_C mientras se abre? ¿Cuál de las dos ignorancias es sospechosa?

      Ver solución

      OBST en ABIERTO se ignora: no hay movimiento. Es razonable. FIN_C mientras se abre significa que el final de carrera «cerrado» se activó con el portón abriéndose: es inconsistente y revela un sensor defectuoso; convendría llevar a FALLA en vez de ignorar.

      4. Moore o Mealy

      Se quiere un «bip» breve cada vez que se pulsa el botón. ¿Es una salida de Moore o de Mealy? ¿Cómo se obtendría con una máquina de Moore pura?

      Ver solución

      Es de Mealy: depende del evento y ocurre en la transición. Con Moore puro habría que introducir un estado transitorio «BIP» de duración fija entre los estados, que complicaría el diagrama; por eso se admite una acción asociada a la transición.

      5. Una extensión

      Se agrega un aviso de 3 s con luz intermitente antes del cierre automático. ¿Qué estado y qué eventos se agregan y cuántas transiciones tiene la máquina nueva?

      Ver solución

      Estado PRE_CIERRE y evento T_PRE (vence el aviso). ABIERTO + T_AUTO → PRE_CIERRE (cambia el destino); PRE_CIERRE + T_PRE → CERRANDO; PRE_CIERRE + BOTON → CERRANDO; PRE_CIERRE + OBST → ABIERTO (cancela). Las transiciones pasan de 13 a 16 y la tabla, a 8 × 8 = 64 celdas.

      6. Verificar

      Se borra la transición T_MAX de ABRIENDO y de CERRANDO. ¿Qué verificación automática lo detecta y qué estado deja de ser alcanzable?

      Ver solución

      La alcanzabilidad: FALLA ya no se alcanza desde CERRADO, porque era la única vía hacia ella. Y la verificación de límite de tiempo: ABRIENDO y CERRANDO podrían quedar en marcha indefinidamente si falla un final de carrera.

      7. Por qué dos estados de parada

      Explicá en pocas líneas por qué PARADO_A y PARADO_C no pueden fundirse en un solo estado.

      Ver solución

      Ante el mismo evento (BOTON) deben responder distinto: uno cierra y el otro abre. Dos situaciones que exigen respuestas diferentes son estados diferentes; con uno solo se perdería la información del sentido y el portón no podría alternar.

      14 · Síntesis

      Ideas para recordar

      • Una máquina de estados finitos recuerda una sola cosa: en qué estado está. Se define con estados, eventos, una función de transición, un estado inicial y las salidas.
      • Un estado es una situación distinguible: dos situaciones que piden respuestas distintas al mismo evento son estados distintos.
      • Los eventos son sucesos puntuales (flancos, temporizadores, sensores filtrados); las condiciones son valores sostenidos que sirven de guardas; las cantidades van en variables.
      • El diagrama sirve para pensar y la tabla para verificar: cada celda es una decisión, y ignorar un evento debe ser una elección consciente.
      • Moore: las salidas dependen del estado; Mealy: del estado y del evento. Para actuadores permanentes conviene Moore.
      • Se diseña partiendo de las situaciones, completando la tabla, fijando un estado inicial seguro y previendo lo inesperado.
      • Un modelo finito se verifica sin hardware: alcanzabilidad, retorno al reposo, límites de tiempo, reacción a los obstáculos y salidas exclusivas.
      • En el código, la transición y las salidas son funciones puras; lo impuro (pines, tiempo) queda afuera.

      ¿Listo para el tema 37?

      Antes de avanzar, tomá un proceso con varias etapas (un lavarropas, un ascensor de tres pisos) y dibujá su diagrama y su tabla de transiciones. Marcá cada celda ignorada y justificá por qué. Anotá qué tiempos medirías y qué eventos generarían.

      En el próximo tema llevamos la máquina a un programa que no se bloquea: máquinas de estados sin bloqueos, con switch, tablas de transición y temporizadores con millis().