martes, 10 de mayo de 2022

Máquinas de estado

 

https://cursos.mcielectronics.cl/2019/06/18/maquinas-de-estado/

Máquinas de estado: blink.ino nos enseña la función de reposo (sleep)

Daremos una mirada al esquema blink.ino de Arduino como una máquina de estado explícita. Posteriormente exploraremos las máquinas de estado donde se elaborará y pondrá en práctica un diagrama de reloj despertador.

Casi todos los esquemas de Arduino terminan usualmente con al menos una máquina de estado intencional y también un montón de máquinas de estado no intencionales. Aquí, introduciremos el concepto de máquinas de estado y las aplicaremos a una rutina blink “Hello World” en un microcontrolador. Luego de esto exploraremos algunas máquinas de estado más complicadas, tales como un reloj despertador.

Que es una máquina de estado

Un ‘estado’ es la condición de una cosa en un tiempo determinado. Algunos que pueden realizar tareas y que utilizan estados como su núcleo son máquinas de estado. También son conocidas como máquinas de estado finitas, lo que significa que sabemos todos los posibles estados de ella. La clave para la máquina de estado es el concepto del tiempo y la historia. El estado de la máquina es evaluada periódicamente. Cada vez que es evaluada, un nuevo estado es elegido (el que podría ser el mismo estado nuevamente) y el resultado es presentado.

Una máquina de estado genérica:

Maquinaestado1

Un diagrama de una máquina de estado genérica. El elemento de memoria  contiene el nuevo estado conocido como el estado variable. Cuando la máquina de estado cuenta con los servicios, el estado variable es actualizado con el valor de la próxima etapa. Acá lanueva etapa es una función de ambos; el estado actual y algunos inputs. La nube de la lógica es un sistema que decide cual será el próximo estado, o la próxima lógica de estado

Una máquina de estado simple: El contador.

Un clásico ejemplo de una máquina de estado es un contador. Por ejemplo, un ‘for loop’ o un circuito integrado lógico de 74×4040 trabajan como una máquina de estado. Cada vez que la máquina cambia de estado, ya sea por la línea del reloj o por el comportamiento de bucle, el estado de la memoria cambia a un nuevo estado igualando el estado anterior más uno. El set finito de estados que puede tener es la cantidad de números que puede contar.

Maquinaestado2

Un contador básico expresado como máquina de estado

Partes de la máquina de estado del contador:

  • El estado actual es el valor que ha sido contado.
  • El resultado es el estado actual.
  • La próxima etapa lógica es lo que sea el estado actual + 1.
  • No hay entradas. En un diagrama más avanzado, una entrada o una función de reseteo del contador será establecido como entrada.

La máquina de estado avanza a cualquier ritmo que sea atendida.

Máquinas de Estado Moore y Mealy

Los profesores de lógica digital aman preguntar en las pruebas sobre Moore vs. Mealy y es por eso que tenemos que mencionarlo. La distinción entre ellas muchas veces no hace sentido, mientras escribas en una maquina de estado en C; es más como una: distinción de “como hacer un hardware lógico”. Para nosotros la lección de Moore y Mealy es que existió gente que pensó sobre este tipo de cosas e inventó formas de notarlo. Mientras ellos se centraban en el hardware lógico, nosotros nos centraremos en el software c.

Máquina de estado Moore

Edward Moore escribió un ensayo en 1956 (Gedanken-experiments on Sequential Machines) y por lo tanto el estilo de la máquina lleva su nombre.  El dice que la salida depende solo del estado, y el próximo estado es dependiente del estado actual (o salida), y la entrada.

Maquinaestado3

Nuestro diagrama previo

Puedes notar que no importa cuál será el estado de la entrada, la salida solo depende el estado actual contenido dentro del elemento de la memoria.

Máquina de estado Mealy

George Mealy escribió un ensayo un año antes que Moore, titulado “A Method for Synthesizing Sequential Circuits”,  en el cual entra en profundidad acerca de crear máquinas de estado desde funciones matemáticas, y describe esas salidas de máquinas de estado en términos de sus entradas.

Para diagramar la máquina Mealy, la salida está hecha para depender de ambos: el estado actual y la entrada. Aquí la nube de la lógica de la próxima etapa contiene la lógica de salida también:

Maquinaestado4

Una forma de dibujar la máquina Mealy

También puede ser dibujado separando la nube en la lógica del próximo estado o lógica de salida.

Maquinaestado5

Otra forma de dibujar una máquina Mealy

Diagramas de Estado Abstractos

Hasta ahora, hemos visto que las máquinas de estado tienen una memoria que almacena el estado actual, que debe provenir de algún estado, y que irá al siguiente estado. Aunque nuestro diagrama actual no muestra como se mueve a través de estados, el propósito debiese ser dibujar un mapa de movimiento entre los estados para que podamos escribir un código que emule nuestro diseño. Esto se llama diagrama de estado.

Este es un ejemplo de cómo se pueden diagramar los estados de una máquina de estado, usando ideas de Moore y Mealy.

Maquinaestado6

Un diagrama de estado abstracto

Partes del diagrama:

  • El círculo es el estado
  • El nombre coloquial del estado esta dado en la mitad superior del circulo
  • La salida del estado está dada en la mitad inferior del círculo. A veces esto es explicito, como “X=1,” y el estado se comporta como Moore, aunque a veces puede ser “SALIDA = ENTRADA” donde durante ese estado, hace la salida igual a lo que haya sido la entrada. En este sentido, la máquina es muy Mealy. Normalmente se engloban los dos para escribir máquinas de estado C porque realmente no importa la construcción C de la máquina.
  • La flecha es el recorrido de la transición de estado.
  • Las Xs, arriba de la línea, son las condiciones para que esa transición de estado ocurra. Si se dan las condiciones, ese será el recorrido de transición.
  • Las Ys, bajo la línea, son cosas que pasan durante la transición. Este concepto se desvía estrictamente del mundo lógico digital de Moore y Mealy y se origina de la naturaleza de ejecución consecutiva de C. Mientras utilizamos un programa, es fácil chequear las condiciones de transición y realizar algunas acciones de una sola vez mientras cambian los estados, entonces se deben poner las acciones bajo las condiciones.

Siempre ten en mente que estas son representaciones abstractas de códigos y son una herramienta para crear niveles más altos de operación. Agrega cuantos detalles necesites en el diagrama.

Considera un estado básico que se pueda mover hacia un nuevo estado.

Maquinaestado7

Estado simple con un set de transiciones de estado incompleto

¿Qué pasa si no se cumplen las condiciones de salida? El diagrama está incompleto y no se han  definido todas las posibles acciones del estado. Usualmente no sabríamos la solución completa del programa hasta que estén hechos los diagramas, y los vayamos completando. Con este diagrama, podemos asumir que si las condiciones no son concretadas, el estado volverá a sí mismo.

Maquinaestado8

Estado especificado completamente

Acá, se definen todas las posibles transiciones de estado.

Diagrama de estado del contador

Revisa el comportamiento del contador como se dibuja con un diagrama de estado.

Maquinaestado9

Diagrama de estado de un contador simple

Acá todos los estados posibles son representados con un círculo único. La acción del estado es agregar uno. Para determinar el próximo estado, es fácil ver que sólo tenemos una opción, que es regresar al estado en que estábamos.

El Sketch de blink.ino

blink_fsm.ino

Salgamos del mundo teórico al re-escribir el familiar sketch de blink.ino con el comportamiento y sintaxis de una máquina de estado, usando el caso de un interruptor. El ejemplo es iluminar el LED por 1 segundo, apagarlo 1 segundo y repetir el proceso. Acá hay dos estados, LED_ON y LED_OFF.

Maquinaestado10

Parpadeo básico de un LED

En cada estado, el valor del LED se muestra bajo la línea. Las fechas están cada una etiquetadas como VERDADERO porque no importa qué, nos moveremos al próximo estado de todas maneras. Esta máquina de estado no tiene ninguna entrada. Cada segundo, evaluaremos la máquina de estado. Si el estado es 1, la salida es 1 y se mueve al estado 0. Aquí está la implementación C.

Fuente archivo completo (github): blink_fsm.ino

Para realizar la máquina de estado en C, debes crear una variable que sostiene el valor actual del estado, define las palabras para cada valor numérico en que puede estar el estado y escribe un estado del interruptor para evaluar el próximo estado.

Definir los estados:

//Define the states of the machine
 #define LED_OFF 0
 #define LED_ON 1

Crear el estado variable (global):

uint8_t fsm_state = LED_OFF;

Hasta ahora, el estado puede tener 1 o 2 opciones, entonces se selecciona el tipo de datos de tamaño más pequeño

La máquina de estado es construida en loop():

void loop(){  //state machine
  switch (fsm_state)  {
      case LED_OFF:  //Statements to execute every time LED_OFF is reached
      digitalWrite(13, LOW);
      fsm_state = LED_ON;
      break;
      case LED_ON:   //Statements to execute every time LED_ON is reached
      digitalWrite(13, HIGH);
      fsm_state = LED_OFF;
      break;
     default:
      break;
     }
 delay(1000);              // wait for a second
}

Cada vez que la función loop es ejecutada, se evalúa la máquina de estado. El estado variable (fsm_state) es global, entonces retiene el estado.

En este caso se puede notar que en cada segundo esperamos 1 segundo y evaluamos la máquina de estado. El código extra asociado al proceso de la máquina de esta causará que el ciclo de tiempo sea mucho mayor a un segundo y correrá un poco lento. Esto podría ser interrumpido con el fin de obtener mayor precisión.

blink_fsm_ms.ino

Maquinaestado11

No quiero tener que esperar un segundo completo. Durante ese tiempo podría estar realizando otras cosas! Preferiría que el proceso de la máquina de estado fuera a un intervalo más rápido. Como 1 microsegundo y se quede en un mismo estado 1000 veces con el fin  de crear retrasar.

Un programa básico de cambio de estado que funciona más rápido de lo que fue programado

Con este diseño, no dejaré el estado a menos que el msCounts alcance 1000. El loop se retrasa por 1 microsegundo en vez de 1000 microsegundos. Cuando la condición es verdadera para que una transición de estado ocurra, el estado del LED es escrita y el contador se resetea.

Fuente de archivo completo (github): blink_fsm_ms.ino

Como antes, los mismos estados y la variable del estado es utilizada. La máquina de estado se expande para proveer funcionalidad si y solo si la transición del estado va a ocurrir.

switch (fsm_state)
{
case LED_OFF:
//Statements to execute every time LED_OFF is reached
//Statements to execute when it's time to move to the next state
if(msCounts >= 1000)    {
digitalWrite(13, HIGH);  //Process the next state's function
fsm_state = LED_ON;
msCounts = 0;
}
 break;  case LED_ON:    //Statements to execute every time LED_ON is reached
//Statements to execute when it's time to move to the next state
 if(msCounts >= 1000)    {
digitalWrite(13, LOW);  //Process the next state's function
 fsm_state = LED_OFF;
msCounts = 0;    }
break;  default:
break;
}

Ahora, cada estado solo se mueve si la transición lógica de estado es verdadero usando un enunciado “IF”. Aquí es donde es obvio cuán fácil es agregar tareas de 1 tiempo a la acción de estados de transición. Ellos son solo agregados al enunciado “IF”, y serán ejecutados solo cuando el estado se mueve.

La máquina de estado funciona pero no como quisiéramos. Noten que el estado LED está en LOW durante el estado LED_ON y HIGH durante el estado LED_OFF. Es fácil ejecutar el código de una sola vez dejando un estado, pero no durante la entrada del estado.  Es contablemente intuitivo y puede ser realizado claramente al agregar dos estados más, cada uno sólo espera.

blink_fsm_final

Maquinaestado12

Un programa de cambio de estado que utilice un estado adicional de espera.

Acá los estados LED_ON y LED_OFF escriben al LED, limpian el contador y continúan adelante.

Nota lateral de sincronización y exactitud: El contador ha sido modificado a una cuenta de 999 para tener en cuenta el estado extra, pero no ayuda mucho. Llegamos más lejos mientas más rápido hacemos correr el reloj. Esto es debido a que el tiempo que toma evaluar la máquina de estado está comenzando a alcanzar el total de tiempo para ejecutar el loop() INCLUYENDO el retraso(1); enunciado.

Fuente de archivo completo (github): blink_fsm_final.ino

Primero, los dos estados extra son agregados a la lista de #defines.

blink_fsm_final

//Define the states of the machine
#define LED_OFF 0
#define LED_ON_WAIT 1
#define LED_ON 2
#define LED_OFF_WAIT 3
El estado variable es el mismo entonces nos moveremos a la implementación de la máquina de estado actual.
//state machine
switch (fsm_state){
case LED_OFF:    //Statements to execute every time LED_OFF is reached
digitalWrite(13, LOW);
fsm_state = LED_ON_WAIT;
 break;
 case LED_ON_WAIT:    //Statements to execute every time LED_OFF_WAIT is reached
if(msCounts >= 1000)    {
 fsm_state = LED_ON;
msCounts = 0;
}
break;
case LED_ON:    //Statements to execute every time LED_ON is reached
digitalWrite(13, HIGH);
fsm_state = LED_OFF_WAIT;
break;
case LED_OFF_WAIT:    //Statements to execute every time LED_ON_WAIT is reached
if(msCounts >= 1000)    {
fsm_state = LED_OFF;
msCounts = 0;
}
break;
default:
break;
}

Vemos que los estados extra se han convertido en casos extras en la sentencia Switch. Los estados que siempre se mueven hacia delante de forma simple asignan el próximo estado al estado variable. El estado de retraso chequea las cuentas antes de asignar un nuevo estado, además retiene el estado en el que estaban.

Más notas sobre sincronización: Ejecutar el incrementador msCounts con una Rutina de Servicio de Interruptor de 1 microsegundo (ISR). Mientras tanto utilizar el loop en el FSM tan rápido como sea posible. Esto corregirá la sincronización. Debemos tener en cuenta que si el tiempo de ejecución del código entre las llamadas ISER (el tiempo de proceso de la máquina de estado) es más largo que el intervalo de la llamada ISER, el programa probablemente se bloqueará

Si ¿Y qué? ¡El LED estaba parpadeando para empezar!

Por favor considera las funciones de alarma de un reloj despertador. ¿Cómo se vería su modelo de comportamiento? El reloj despertador tiene varios estados que pueden existir dentro y tiene un par de entradas que pueden ser utilizadas para controlarlo.

Estados:

  • Mantener el tiempo agradablemente
  • Ejecución de alarma
  • Espera con impaciencia el final del ciclo de repetición de alarma por lo que puede que la alarma se active nuevamente

Entradas

  • Interruptor de brazo (Arm switch)
  • Botón de reposo (sleep)

Hay algunas más, como entradas para establecer hora y establecer alarma, pero este ejemplo está limitado a las operaciones de alarma solamente.

Descrito como máquina de estado:

Maquinaestado13

Las funciones de alarma del reloj despertador.

Si intentamos caminar mentalmente a través de la operación de la máquina de estado veremos que podemos obtener del ‘idle’ al sonar si el reloj despertador no está armado. También podemos volver a ‘idle’ al presionar el botón de reposo (sleep). Tenemos que desarmar la alarma, esto cumple nuestra definición interna de cómo actúa un reloj despertador, entonces proseguimos.

No existe una manera en que realmente demos seguimiento al tiempo, incluso si solo fuera para el experimento. En vez de darle seguimiento a los días, horas, etc., solo queremos darle seguimiento a los segundos. Entonces necesitaremos contar los segundos. La máquina de estado de parpadeo de LED ya hace eso! Solo lo cambiaremos para mantener el seguimiento y agregar a toda la máquina de estado dentro.

Maquinaestado14

Re-usando la máquina de estado de parpadeo para contar segundos.

Como un beneficio, se puede bloquear el parpadeo del LED a menos que la alarma se apague y usar eso como resultado de depuración.

Acá esta el archivo fuente completo (github): alarm_clock.ino

Conexiones de Hardware:

  • Conecta un botón de apertura normal entre el pin 7 y el GND de un Arduino. Esto servirá como “REPOSO” (sleep).
  • Conecta el switch SPST entre el pin 9 y la tierra. Esto servirá como “ARM” (brazo).

El código es muy similar a los ejemplos previos, pero tiene dos máquinas de estado construidas sobre él.

Primero, los estados y las variables de la máquina de estado del temporizador:

//Timer FSM numbers
uint16_t msCounts = 0;
uint16_t sCounts = 0;
#define LED_OFF 0
#define LED_ON_WAIT 1
#define LED_ON 2
#define LED_OFF_WAIT 3
uint8_t timer_fsm_state = LED_OFF;

Luego, los estados y las variables de la máquina de estado de alarma. La hora de la alarma se establece en 15 segundos del reinicio (de hecho cerca de 20 con error), y el ciclo de reposo se establece por 5 segundos.

//Alarm FSM numbers
#define ALARM_SECONDS 15
#define SNOOZE_SECONDS 5
uint8_t alarmActive = 0;
uint16_t nextAlarmTime = 65535;
#define ALARM_IDLE 0
#define ALARM_RINGING 1
#define ALARM_SNOOZING 2
uint8_t alarm_fsm_state = ALARM_IDLE;

Loop() evalúa ambas máquinas de estado cada vez que funciona.

//timer state machine

switch (timer_fsm_state)

{

case LED_OFF:    //Statements to execute every time LED_OFF is reached

digitalWrite(13, LOW);

timer_fsm_state = LED_ON_WAIT;

break;

case LED_ON_WAIT:    //Statements to execute every time LED_OFF_WAIT is reached

if(msCounts >= 1000)    {

timer_fsm_state = LED_ON;

msCounts = 0;    }

break;

case LED_ON:    //Statements to execute every time LED_ON is reached

if(alarmActive == 1)    {

digitalWrite(13, HIGH);

}

timer_fsm_state = LED_OFF_WAIT;

break;

case LED_OFF_WAIT:    //Statements to execute every time LED_ON_WAIT is reached

if(msCounts >= 1000)    {

timer_fsm_state = LED_OFF;

msCounts = 0;

if( sCounts < 0xFFFF )      {

sCounts++;

}

}

break;

default:

break;

}

uint8_t buttonValue = digitalRead(7);

uint8_t switchValue = digitalRead(9); //alarm state machine

switch (alarm_fsm_state){

case ALARM_IDLE:

if((sCounts >= nextAlarmTime)&&(switchValue == 0))    {      //Goto next state

alarm_fsm_state = ALARM_RINGING;

alarmActive = 1;    }

break;

case ALARM_RINGING:

if(buttonValue == 0)    {

nextAlarmTime = sCounts + SNOOZE_SECONDS;

alarm_fsm_state = ALARM_SNOOZING;

alarmActive = 0;    }

if(digitalRead(9) == 1)//If switch went off, goto idle instead

{

alarm_fsm_state = ALARM_IDLE;  // this overwrites the snooze button option

alarmActive = 0;    }

break;

case ALARM_SNOOZING:

if(sCounts >= nextAlarmTime)    {      //Goto next state

alarm_fsm_state = ALARM_RINGING;

alarmActive = 1;    }

if(switchValue == 0)    {      //Goto alarm idle

alarm_fsm_state = ALARM_IDLE;    }

break;

default:

break;

}

Para operar, cierra el  switch “ARM” (brazo) y carga el programa.

Conclusión

En nuestra experiencia, cualquier programador creador de códigos que puede diagramar un programa antes de escribirlo tendrá éxito con el programa. Se han escrito muchos programas que han terminado como un montón de “enunciados if” anidados porque no nos adelantamos a los hechos. Inevitablemente, se necesita agregar una cosa pequeña que corrompe enormemente el programa y somos forzados a re-evaluar las decisiones. El usar herramientas tales como diagramas de máquinas de estado, diagramas de flujo, diagramas de clase y tests de unidad que nos permiten escribir códigos más complejos pero mantenibles (mientras permanece relativamente cuerdo). La máquina de estado es solo otra idea para tener en la caja de herramientas y esperamos que les ayude en el futuro.

No hay comentarios:

Publicar un comentario