Una máquina de estados en RTOS es una forma de organizar el comportamiento de una tarea según el estado actual del sistema y los eventos que recibe.
Las máquinas de estados ya son útiles en programación secuencial. Con FreeRTOS lo son todavía más, porque nos ayudan a evitar tareas llenas de if dispersos, flags globales y condiciones que nadie recuerda quién puso ahí.
Por qué usar estados
Muchos dispositivos no “hacen una cosa”, sino que pasan por fases:
- Arrancando.
- Conectando al WiFi.
- Esperando configuración.
- Funcionando.
- En error.
- Reiniciando.
Si intentamos representar todo eso con variables sueltas, el código se vuelve confuso enseguida. Una máquina de estados obliga a responder dos preguntas muy sanas:
- ¿En qué estado estoy?
- ¿Qué eventos pueden cambiarme de estado?
Eso reduce bastante el caos. Y en sistemas concurrentes, reducir caos siempre es una buena inversión.
FSM dentro de una tarea
La forma más simple es implementar la máquina de estados dentro de una única tarea.
enum EstadoSistema {
ESTADO_CONECTANDO_WIFI,
ESTADO_SINCRONIZANDO_HORA,
ESTADO_FUNCIONANDO,
ESTADO_ERROR
};
void TareaSistema(void *pvParameters) {
EstadoSistema estado = ESTADO_CONECTANDO_WIFI;
for(;;) {
switch (estado) {
case ESTADO_CONECTANDO_WIFI:
if (wifiConectado()) {
estado = ESTADO_SINCRONIZANDO_HORA;
}
break;
case ESTADO_SINCRONIZANDO_HORA:
if (horaSincronizada()) {
estado = ESTADO_FUNCIONANDO;
}
break;
case ESTADO_FUNCIONANDO:
ejecutarLogicaPrincipal();
break;
case ESTADO_ERROR:
gestionarError();
break;
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}Este enfoque es fácil de entender. Una tarea tiene el control del estado y nadie más lo modifica directamente.
Siempre que puedas, haz que una sola tarea sea propietaria del estado. El resto le envía eventos, pero no modifica la variable estado directamente.
FSM basada en eventos
Una mejora habitual es que la tarea no esté preguntando constantemente, sino esperando eventos por una cola.
enum EventoSistema {
EVENTO_WIFI_OK,
EVENTO_WIFI_ERROR,
EVENTO_HORA_OK,
EVENTO_BOTON_RESET
};
QueueHandle_t colaEventos;
void TareaEstados(void *pvParameters) {
EstadoSistema estado = ESTADO_CONECTANDO_WIFI;
EventoSistema evento;
for(;;) {
if (xQueueReceive(colaEventos, &evento, portMAX_DELAY) == pdTRUE) {
switch (estado) {
case ESTADO_CONECTANDO_WIFI:
if (evento == EVENTO_WIFI_OK) {
estado = ESTADO_SINCRONIZANDO_HORA;
}
else if (evento == EVENTO_WIFI_ERROR) {
estado = ESTADO_ERROR;
}
break;
case ESTADO_SINCRONIZANDO_HORA:
if (evento == EVENTO_HORA_OK) {
estado = ESTADO_FUNCIONANDO;
}
break;
default:
break;
}
}
}
}Aquí la tarea se bloquea hasta que pasa algo. No gasta CPU revisando flags y el flujo queda bastante claro.
Estados repartidos entre tareas
En sistemas más grandes, podemos tener varias tareas con su propia máquina de estados:
- Una tarea para conectividad.
- Una tarea para sensores.
- Una tarea para control.
- Una tarea para interfaz de usuario.
Cada tarea gestiona su pequeño mundo y se comunica con las demás mediante colas, notificaciones o Event Groups.
Esto funciona bien, pero hay que evitar que cada tarea mantenga una versión distinta de “la verdad”. Si una tarea cree que el sistema está conectado y otra cree que no, ya tenemos fiesta.
Una estrategia limpia es tener:
- Estados locales dentro de cada tarea.
- Un estado global mínimo gestionado por una tarea coordinadora.
- Eventos explícitos para anunciar cambios importantes.
Event Groups para estados globales
Los Event Groups encajan muy bien cuando el estado se puede representar con banderas.
#define BIT_WIFI_OK (1 << 0)
#define BIT_HORA_OK (1 << 1)
#define BIT_MQTT_OK (1 << 2)
EventGroupHandle_t eventosSistema;
void TareaPrincipal(void *pvParameters) {
xEventGroupWaitBits(
eventosSistema,
BIT_WIFI_OK | BIT_HORA_OK | BIT_MQTT_OK,
pdFALSE,
pdTRUE,
portMAX_DELAY
);
ejecutarSistemaListo();
vTaskDelete(NULL);
}En este ejemplo, la tarea principal espera hasta que WiFi, hora y MQTT estén listos. Es una forma compacta de expresar “no sigas hasta que todo esto sea cierto”.
Errores comunes
Hay varios tropiezos típicos al mezclar FreeRTOS y máquinas de estados:
- Modificar el estado desde varias tareas sin protección.
- Usar flags globales sin un protocolo de sincronización.
volatilepor sí solo no evita carreras ni hace atómicas las operaciones. - Meter delays largos dentro de un estado crítico.
- No definir qué pasa ante errores o timeouts.
- Crear demasiadas tareas cuando una FSM sencilla bastaba.
Una máquina de estados no arregla un diseño confuso por sí sola. Ayuda muchísimo, pero hay que definir estados, eventos y transiciones con intención.