Ir al contenido principal
Todas las coleccionesRecursos técnicos
MQTT, Máquinas de Estados Finitos y Punteros en C/C++ (Parte II)
MQTT, Máquinas de Estados Finitos y Punteros en C/C++ (Parte II)

Este artículo es la segunda parte de la serie de implementación de FSM de Ubidots. Este artículo hará referencia a una nueva forma de crear FSM utilizando punteros en C.

David Sepúlveda avatar
Escrito por David Sepúlveda
Actualizado hace más de una semana

Esta es la segunda y última parte de nuestra implementación eficiente de la Máquina de Estados Finita, FSM. Puedes consultar la primera parte de la serie y las generalidades sobre FSM aquí.

INTRODUCCIÓN

Las Máquinas de Estados Finitas (FSM) son simplemente un cálculo matemático de una serie de causas y eventos. Una FSM se basa en estados y calcula una serie de eventos en función del estado de las entradas de la máquina, es decir, para el estado SENSOR_READ, si una lectura de sensor es mayor que un valor umbral, nuestra FSM activaría un relé (también conocido como un evento de control) o enviaría una alerta externa. Los estados son el ADN de la FSM que dictan el comportamiento interno o las interacciones con un entorno, como aceptar entradas o producir salidas que pueden o no causar que el sistema cambie o transicione su estado. Y, es nuestro trabajo como ingenieros de hardware elegir adecuadamente los estados de la FSM y activar eventos para obtener un comportamiento deseado que se ajuste a las necesidades de nuestro desarrollo o proyecto personalizado.

En la primera parte de esta serie de tutoriales sobre FSM, creamos una FSM utilizando la implementación clásica de switch-case. Ahora exploraremos y aprenderemos más para crear una FSM utilizando punteros de C/C++, lo que te permitirá desarrollar una aplicación más robusta con expectativas de mantenimiento de firmware más simples.

Nota:

El código utilizado en este tutorial fue demostrado en el día de Arduino en Bogotá por Jose Garcia, uno de los ingenieros de hardware de Ubidots, el 7 de abril de 2018. Puedes ver las partes completas del código y las notas del orador aquí.

Las Desventajas del Switch-Case:

En la primera parte de nuestro tutorial sobre FSM, analizamos el Switch-Case y cómo implementar una rutina simple. Ahora, ampliaremos esta idea introduciendo "Punteros" y cómo aplicarlos para simplificar tu rutina de FSM.

Una implementación de switch-case es muy similar a una rutina de if-else; nuestro firmware recorrerá cada caso, evaluará cada uno de ellos y verá si se alcanza la condición del caso desencadenante. Veamos un ejemplo de rutina a continuación:

switch(state) {    case 1:    /* hacer algo para el estado 1 */    state = 2;    break;  case 2:    /* hacer algo para el estado 2 */    state = 3;    break;  case 3:    /* hacer algo para el estado 3 */    state = 1;    break;  default:    /* hacer algo por defecto */    state = 1;}

Arriba encontrarás una FSM simple con tres estados; en el bucle infinito, el firmware irá al primer caso, verifica si la variable de estado es igual a uno, si es así, ejecuta su rutina; si no, procede al caso 2, verifica nuevamente el valor del estado; si el caso 2 no se satisface, la ejecución del código pasará al caso 3, y así sucesivamente hasta que se alcance el estado o se hayan agotado los casos.

Antes de entrar en el código, entendamos un poco más sobre algunas posibles desventajas de las implementaciones de switch-case o if-else para que podamos ver cómo mejorar nuestro desarrollo de firmware.

Supongamos que la variable de estado inicial es 3, nuestro firmware tendrá que hacer 3 validaciones de valor diferentes, esto puede no ser un problema para una FSM pequeña, pero imagina una máquina de producción industrial típica con cientos o miles de estados. La rutina necesitará hacer varias comprobaciones de valor innecesarias, lo que resultará en un uso ineficiente de los recursos. Esta ineficiencia se convierte en nuestra primera desventaja: el microcontrolador está limitado en recursos y se verá sobrecargado con rutinas de FSM ineficientes. En consecuencia, es nuestro deber como ingenieros inteligentes ahorrar tantos recursos de computación en el microcontrolador como sea posible.

Ahora, imagina nuevamente una FSM con miles de estados, si eres un nuevo desarrollador y necesitas implementar un cambio en uno de esos estados, tendrás que buscar en miles de líneas dentro de tu rutina principal loop(), esta rutina generalmente puede incluir muchas otras cosas no relacionadas con la máquina en sí, por lo que se vuelve un poco difícil comenzar un depurado si centras toda la lógica de la FSM dentro del loop(). Aquí viene el segundo problema, con las rutinas de switch-case que puedes crear, puedes tener FSM innecesariamente complejas que son demasiado largas para depurar errores.

Finalmente, un código con un montón de if-else o miles de switch-case no es elegante ni legible para la mayoría de los programadores embebidos, siendo esta la desventaja final.

Punteros de C/C++

Ahora veamos cómo podemos implementar una FSM concisa utilizando punteros de C/C++. Un puntero, como su nombre indica, 'apunta' a algún lugar dentro del microcontrolador. En C/C++, un "puntero" apunta a una dirección de memoria y está destinado a recuperar información de dicha dirección de memoria. Un puntero es útil para obtener el valor almacenado de una variable durante el tiempo de ejecución sin conocer específicamente la dirección de memoria de la variable en sí. Usar punteros correctamente será un gran beneficio para la estructura de tu rutina y la simplicidad para mantener o editar la programación en el futuro.

Ejemplo de Código de Puntero:

int a = 1462;int myAddressPointer = &a;int myAddressValue = *myAddressPointer;

Analicemos qué sucede en el código anterior. La variable myAddressPointer apunta a la dirección de memoria de la variable a (1462), mientras que la variable myAddressValue recupera el valor de la dirección de memoria apuntada por myAddressPointer. En consecuencia, se puede esperar obtener un valor de 874 de myAddressPointer y de 1462 para myAddressValue. ¿Por qué es esto útil para nuestro caso de uso? Porque en memoria no solo almacenamos valores, también almacenamos el comportamiento de funciones y métodos. Por ejemplo, el espacio de memoria 874 está almacenando el valor 1462, pero esta dirección de almacenamiento también puede gestionar funciones para calcular una intensidad de corriente en kA que se almacena. Usando punteros, ganamos acceso a esta funcionalidad adicional y usabilidad de las direcciones de memoria sin necesidad de declarar una declaración de función en otra parte del código. Un puntero de función típico se puede implementar como a continuación:

void (*funcPtr) (void);

¿Puedes imaginar cómo usar esta herramienta en nuestra FSM? Simplemente puedes crear un puntero dinámico que 'apunte' a las diferentes funciones o estados de nuestra FSM en lugar de a una variable. Si tenemos una sola variable que almacena un puntero que cambia dinámicamente a dónde 'apunta', cambiaríamos los estados de la FSM en función de las condiciones de entrada, siendo este el verdadero valor de los punteros para nuestro caso de uso.

Tablas de búsqueda

Revisemos otro concepto importante, las tablas de búsqueda o LUT. Las LUT son una forma ordenada de almacenar datos, básicamente es un tipo de estructura de datos que almacena valores predefinidos. Serán útiles para nosotros para almacenar datos dentro de los valores de nuestra FSM.

La principal ventaja de la LUT es que si están declaradas de manera estática; sus valores se pueden acceder a través de la dirección de memoria, lo que es una forma de acceso a valores muy efectiva en C/C++. A continuación, puedes encontrar una declaración típica para una LUT para una FSM

void (*const state_table [MAX_STATES][MAX_EVENTS]) (void) ={ action_s1_e1, action_s1_e2 }, /* procedimientos para el estado{ action_s2_e1, action_s2_e2 }, /* procedimientos para el estado{ action_s3_e1, action_s3_e2 } /* procedimientos para el estado};

Ha sido mucho concepto para digerir, pero son necesarios para implementar nuestra nueva FSM eficiente, codifiquémosla para que puedas ver lo fácil que este tipo de FSM puede crecer con el tiempo.

Nota: El código completo de la FSM se puede encontrar aquí, por simplicidad lo hemos dividido en 5 partes para facilitar su explicación.

Codificación

Crearemos una FSM simple para implementar una rutina de parpadeo de LED, con esto puedes adaptar el ejemplo a tus propias necesidades. Tendremos 2 estados: ledOn y ledOff para la FSM y el LED se apagará y encenderá cada segundo. Comencemos.

***********************************   CONFIGURACIÓN DE LA MÁQUINA DE ESTADOS ***********************************//*   Estados válidos de la máquina de estados*/typedef enum {  LED_ON,  LED_OFF,  NUM_STATES} StateType;/*   Estructura de la tabla de la máquina de estados*/typedef struct {  StateType State;  // Crear el puntero de función  void (*function)(void);} StateMachineType;

La primera parte para implementar nuestra LUT es crear nuestros estados, convenientemente usamos el método enum() para asignar un valor de 0 y 1 a nuestros estados. El número máximo de estados también se asigna con un valor de 2 que tiene sentido con nuestra arquitectura de FSM. Este typedef se etiquetará como StateType para que podamos instanciarlo más adelante en nuestro código.

A continuación, creamos una estructura para almacenar nuestros estados. Observa que también declaramos un puntero etiquetado como function, este será nuestro puntero de memoria dinámica para llamar en ejecución a los diferentes estados de la FSM.

/*    Declaración del estado inicial de la SM y funciones*/StateType SmState = LED_ON;void Sm_LED_ON();void Sm_LED_OFF();/*   Tabla de búsqueda con estados y funciones a ejecutar*/StateMachineType StateMachine[] ={  {LED_ON, Sm_LED_ON},  {LED_OFF, Sm_LED_OFF}};

Aquí creamos una instancia de nuestros estados con el estado inicial LED_ON, y declaramos nuestros dos estados y finalmente creamos nuestra LUT. Observa cómo están relacionadas tanto la declaración del estado como el comportamiento en la LUT para que podamos acceder a sus valores fácilmente a través de sus índices, es decir, para acceder al método sm_LED_ON() deberíamos codificar algo como StateMachineInstance[0];.

/*   Rutinas de funciones de estado personalizadas*/void Sm_LED_ON() {  // Código de función personalizada  digitalWrite(LED_BUILTIN, HIGH);  delay(1000);  // Pasar al siguiente estado  SmState = LED_OFF;}void Sm_LED_OFF() {  // Código de función personalizada  digitalWrite(LED_BUILTIN, LOW);  delay(1000);  // Pasar al siguiente estado  SmState = LED_ON;}

En el código anterior, se implementa la lógica de nuestros métodos e incluye nada especial además de la actualización del número de estado al final de cada función.

/*   Rutina de cambio de estado de la función principal*/void Sm_Run(void) {  // Asegúrate de que el estado actual sea válido  if (SmState < NUM_STATES) {    (*StateMachine[SmState].function) ();  }  else {    // Código de excepción de error    Serial.println("[ERROR] Estado no válido");  }}

La función Sm_Run() es el corazón de nuestra FSM. Observa que usamos un puntero, *, para extraer la posición de memoria de la función de nuestra LUT, ya que durante el tiempo de ejecución accederemos dinámicamente a una posición de memoria en la LUT. La Sm_Run() siempre ejecutará múltiples instrucciones, es decir, eventos de FSM, ya almacenados en una dirección de memoria del microcontrolador.

/***********************************   FUNCIONES PRINCIPALES DE ARDUINO ***********************************/void setup() {  // pon tu código de configuración aquí, para ejecutar una vez:  pinMode(LED_BUILTIN, OUTPUT);}void loop() {  // pon tu código principal aquí, para ejecutar repetidamente:  Sm_Run();}

Nuestras funciones principales de Arduino son ahora realmente simples, el bucle infinito siempre se ejecuta con la rutina de cambio de estado previamente definida. Esta función será la que maneje el evento para activar y también para actualizar el estado actual de la FSM.

Conclusiones

En esta parte dos de las Máquinas de Estados Finitas y Punteros de C/C++, revisamos las principales desventajas de las rutinas de FSM con switch-case e identificamos los punteros como una opción adecuada y deseable para ahorrar memoria y aumentar la funcionalidad del microcontrolador.

En resumen, aquí hay algunas de las ventajas y desventajas de usar punteros en tu rutina de Máquina de Estados Finita:

Ventajas:

  • Si necesitas agregar más estados, simplemente tienes que declarar el nuevo método de transición y actualizar la tabla de búsqueda; la función principal será la misma.

  • No tienes que realizar cada declaración if-else, el puntero permite que el firmware 'vaya' al conjunto deseado de instrucciones en la memoria del microcontrolador.

  • Esta es una forma concisa y profesional de implementar FSM en C.

Desventajas:

  • Necesitas más memoria estática para almacenar la tabla de búsqueda que almacena los eventos de la FSM.

¿Ha quedado contestada tu pregunta?