Introducción a las Herramientas de FreeRTOS
En los sistemas de tiempo real, la coordinación entre tareas es fundamental. FreeRTOS proporciona tres herramientas esenciales: tareas, colas y semáforos, que permiten crear aplicaciones robustas y eficientes en el ESP32.
¿Por qué son importantes estas herramientas?
- Comunicación entre procesos: Intercambio seguro de datos
- Sincronización: Coordinación temporal de tareas
- Protección de recursos: Acceso exclusivo a recursos compartidos
- Respuesta determinística: Comportamiento predecible en tiempo real
Los Tres Pilares de la Comunicación
Tareas (Tasks)
Unidades independientes de ejecución con su propio contexto, pila y estado. Permiten la ejecución concurrente de múltiples funciones.
Colas (Queues)
Estructuras FIFO que permiten el intercambio seguro de datos entre tareas, con capacidad de almacenamiento configurable.
Semáforos
Mecanismos de sincronización que controlan el acceso a recursos compartidos y coordinan la ejecución de tareas.
Creación Avanzada de Tareas
Vamos más allá del multitasking básico implementando tareas con parámetros y manejo avanzado:
// Estructura para pasar parámetros a las tareas
struct TaskParameters {
int ledPin;
int blinkInterval;
String taskName;
};
// Handles para control de tareas
TaskHandle_t ledTaskHandle1 = NULL;
TaskHandle_t ledTaskHandle2 = NULL;
// Tarea parametrizada para control de LED
void ledBlinkTask(void *pvParameters) {
// Convertir parámetros
TaskParameters *params = (TaskParameters*)pvParameters;
// Configurar pin
pinMode(params->ledPin, OUTPUT);
// Mostrar información de inicio
Serial.printf("Iniciando %s en pin %d, intervalo %dms\n",
params->taskName.c_str(),
params->ledPin,
params->blinkInterval);
for (;;) {
digitalWrite(params->ledPin, HIGH);
Serial.printf("%s: LED ON (Núcleo %d)\n",
params->taskName.c_str(),
xPortGetCoreID());
vTaskDelay(pdMS_TO_TICKS(params->blinkInterval));
digitalWrite(params->ledPin, LOW);
Serial.printf("%s: LED OFF\n", params->taskName.c_str());
vTaskDelay(pdMS_TO_TICKS(params->blinkInterval));
}
}
// Tarea de monitoreo de sistema
void systemMonitorTask(void *pvParameters) {
for (;;) {
// Información del sistema
Serial.println("\n=== Monitor del Sistema ===");
Serial.printf("Heap libre: %d bytes\n", ESP.getFreeHeap());
Serial.printf("Tiempo de ejecución: %lu ms\n", millis());
// Información de las tareas
if (ledTaskHandle1 != NULL) {
UBaseType_t stackFree1 = uxTaskGetStackHighWaterMark(ledTaskHandle1);
Serial.printf("Stack libre Tarea 1: %d bytes\n", stackFree1 * sizeof(StackType_t));
}
if (ledTaskHandle2 != NULL) {
UBaseType_t stackFree2 = uxTaskGetStackHighWaterMark(ledTaskHandle2);
Serial.printf("Stack libre Tarea 2: %d bytes\n", stackFree2 * sizeof(StackType_t));
}
Serial.println("========================\n");
vTaskDelay(pdMS_TO_TICKS(5000)); // Monitoreo cada 5 segundos
}
}
void setup() {
Serial.begin(115200);
delay(1000);
// Parámetros para las tareas
static TaskParameters params1 = {2, 500, "LED_Rápido"};
static TaskParameters params2 = {4, 1000, "LED_Lento"};
Serial.println("=== Sistema de Tareas Avanzado ===");
// Crear tareas con parámetros
xTaskCreatePinnedToCore(
ledBlinkTask,
"LED_Task_1",
2048,
¶ms1, // Pasar parámetros
2, // Prioridad alta
&ledTaskHandle1,
0 // Núcleo 0
);
xTaskCreatePinnedToCore(
ledBlinkTask,
"LED_Task_2",
2048,
¶ms2, // Pasar parámetros
2, // Prioridad alta
&ledTaskHandle2,
1 // Núcleo 1
);
// Tarea de monitoreo
xTaskCreatePinnedToCore(
systemMonitorTask,
"Monitor_Task",
3072, // Stack más grande para Serial
NULL,
1, // Prioridad menor
NULL,
1 // Núcleo 1
);
Serial.println("Todas las tareas creadas exitosamente");
}
void loop() {
// Loop principal vacío - todo en tareas
vTaskDelete(NULL); // Eliminar tarea loop
}
Comunicación con Colas
Las colas permiten comunicación segura entre tareas. Implementemos un sistema de sensores con colas:
// Estructura para datos del sensor
struct SensorData {
int sensorID;
float value;
unsigned long timestamp;
String unit;
};
// Cola para datos de sensores
QueueHandle_t sensorQueue;
const int QUEUE_SIZE = 10;
// Tarea productora - Lee sensores
void sensorReaderTask(void *pvParameters) {
SensorData data;
for (;;) {
// Simular lectura de diferentes sensores
for (int i = 0; i < 3; i++) {
data.sensorID = i + 1;
data.timestamp = millis();
switch (i) {
case 0: // Sensor de temperatura
data.value = 20.0 + (random(0, 100) / 10.0);
data.unit = "°C";
break;
case 1: // Sensor de humedad
data.value = 40.0 + (random(0, 400) / 10.0);
data.unit = "%";
break;
case 2: // Sensor de luz
data.value = random(0, 1024);
data.unit = "lux";
break;
}
// Enviar datos a la cola
if (xQueueSend(sensorQueue, &data, pdMS_TO_TICKS(100)) == pdPASS) {
Serial.printf("Sensor %d enviado: %.2f %s\n",
data.sensorID, data.value, data.unit.c_str());
} else {
Serial.println("Error: Cola llena!");
}
vTaskDelay(pdMS_TO_TICKS(500));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// Tarea consumidora - Procesa datos
void dataProcessorTask(void *pvParameters) {
SensorData receivedData;
for (;;) {
// Recibir datos de la cola
if (xQueueReceive(sensorQueue, &receivedData, pdMS_TO_TICKS(1000)) == pdPASS) {
Serial.printf("Procesando sensor %d: %.2f %s (t=%lu)\n",
receivedData.sensorID,
receivedData.value,
receivedData.unit.c_str(),
receivedData.timestamp);
// Simular procesamiento
if (receivedData.sensorID == 1 && receivedData.value > 30.0) {
Serial.println("⚠️ ALERTA: Temperatura alta!");
}
if (receivedData.sensorID == 2 && receivedData.value > 80.0) {
Serial.println("⚠️ ALERTA: Humedad alta!");
}
// Simular tiempo de procesamiento
vTaskDelay(pdMS_TO_TICKS(200));
} else {
Serial.println("Timeout: No hay datos en cola");
}
}
}
// Tarea de estadísticas
void statsTask(void *pvParameters) {
for (;;) {
UBaseType_t itemsInQueue = uxQueueMessagesWaiting(sensorQueue);
UBaseType_t freeSpaces = uxQueueSpacesAvailable(sensorQueue);
Serial.printf("📊 Cola: %d elementos, %d espacios libres\n",
itemsInQueue, freeSpaces);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== Sistema de Colas para Sensores ===");
// Crear la cola
sensorQueue = xQueueCreate(QUEUE_SIZE, sizeof(SensorData));
if (sensorQueue == NULL) {
Serial.println("Error: No se pudo crear la cola");
return;
}
// Crear tareas
xTaskCreatePinnedToCore(sensorReaderTask, "SensorReader", 3072, NULL, 3, NULL, 0);
xTaskCreatePinnedToCore(dataProcessorTask, "DataProcessor", 3072, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(statsTask, "Stats", 2048, NULL, 1, NULL, 1);
Serial.println("Sistema iniciado - monitoreo activo");
}
void loop() {
vTaskDelete(NULL);
}
Sincronización con Semáforos
Los semáforos permiten sincronizar tareas y proteger recursos compartidos. Implementemos diferentes tipos de semáforos:
// Semáforos para sincronización
SemaphoreHandle_t binarySemaphore; // Sincronización entre tareas
SemaphoreHandle_t mutexSemaphore; // Protección de recurso compartido
SemaphoreHandle_t countingSemaphore; // Control de recursos múltiples
// Recurso compartido protegido
volatile int sharedCounter = 0;
volatile bool dataReady = false;
// Tarea productora que genera datos
void dataProducerTask(void *pvParameters) {
for (;;) {
// Simular recopilación de datos
Serial.println("Productor: Recopilando datos...");
vTaskDelay(pdMS_TO_TICKS(2000));
// Proteger recurso compartido con mutex
if (xSemaphoreTake(mutexSemaphore, pdMS_TO_TICKS(1000)) == pdPASS) {
sharedCounter++;
dataReady = true;
Serial.printf("Productor: Datos listos #%d\n", sharedCounter);
xSemaphoreGive(mutexSemaphore);
// Señalizar que hay datos disponibles
xSemaphoreGive(binarySemaphore);
// Incrementar contador de recursos disponibles
xSemaphoreGive(countingSemaphore);
} else {
Serial.println("Productor: No se pudo acceder al recurso");
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// Tarea consumidora que espera datos
void dataConsumerTask(void *pvParameters) {
for (;;) {
Serial.println("Consumidor: Esperando datos...");
// Esperar señal de que hay datos
if (xSemaphoreTake(binarySemaphore, pdMS_TO_TICKS(5000)) == pdPASS) {
// Acceder al recurso compartido con mutex
if (xSemaphoreTake(mutexSemaphore, pdMS_TO_TICKS(1000)) == pdPASS) {
if (dataReady) {
Serial.printf("Consumidor: Procesando datos #%d\n", sharedCounter);
dataReady = false;
// Simular procesamiento
vTaskDelay(pdMS_TO_TICKS(500));
Serial.println("Consumidor: Datos procesados exitosamente");
}
xSemaphoreGive(mutexSemaphore);
}
} else {
Serial.println("Consumidor: Timeout - no hay datos");
}
}
}
// Tarea que controla recursos limitados
void resourceManagerTask(void *pvParameters) {
for (;;) {
// Intentar obtener un recurso del pool
if (xSemaphoreTake(countingSemaphore, pdMS_TO_TICKS(100)) == pdPASS) {
Serial.println("Manager: Recurso obtenido del pool");
// Usar el recurso por un tiempo
vTaskDelay(pdMS_TO_TICKS(1500));
Serial.println("Manager: Recurso liberado");
// El recurso se consume, no se devuelve
} else {
Serial.println("Manager: No hay recursos disponibles");
}
vTaskDelay(pdMS_TO_TICKS(800));
}
}
// Tarea de monitoreo de semáforos
void semaphoreMonitorTask(void *pvParameters) {
for (;;) {
UBaseType_t countingValue = uxSemaphoreGetCount(countingSemaphore);
Serial.println("\n=== Estado de Semáforos ===");
Serial.printf("Recursos disponibles: %d\n", countingValue);
Serial.printf("Contador global: %d\n", sharedCounter);
Serial.printf("Datos listos: %s\n", dataReady ? "Sí" : "No");
Serial.println("============================\n");
vTaskDelay(pdMS_TO_TICKS(4000));
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== Sistema de Semáforos Avanzado ===");
// Crear semáforos
binarySemaphore = xSemaphoreCreateBinary();
mutexSemaphore = xSemaphoreCreateMutex();
countingSemaphore = xSemaphoreCreateCounting(5, 0); // Max 5 recursos, empezar con 0
if (binarySemaphore == NULL || mutexSemaphore == NULL || countingSemaphore == NULL) {
Serial.println("Error: No se pudieron crear los semáforos");
return;
}
// Crear tareas
xTaskCreatePinnedToCore(dataProducerTask, "Producer", 2048, NULL, 3, NULL, 0);
xTaskCreatePinnedToCore(dataConsumerTask, "Consumer", 2048, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(resourceManagerTask, "ResourceMgr", 2048, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(semaphoreMonitorTask, "Monitor", 2048, NULL, 1, NULL, 1);
Serial.println("Sistema de semáforos iniciado");
}
void loop() {
vTaskDelete(NULL);
}
Ejercicios Prácticos
Objetivo: Implementar un sistema donde una tarea genera datos y otra los consume usando colas.
Materiales:
- ESP32 DevKit
- Potenciómetro (sensor)
- LED para visualización
Conceptos: xQueueCreate, xQueueSend, xQueueReceive
Objetivo: Proteger el acceso a una pantalla OLED usando mutex entre múltiples tareas.
Materiales: ESP32, Display OLED I2C, sensores varios
Conceptos: xSemaphoreCreateMutex, critical sections
Objetivo: Implementar un sistema completo que use tareas, colas y semáforos para controlar espacios de estacionamiento.
Materiales: ESP32, sensores ultrasónicos, LEDs, display
Funciones: Detección, conteo, visualización, alertas
Proyecto: Sistema de Monitoreo Industrial
Monitoreo Multi-Sensor con Alertas
Desarrolla un sistema industrial completo que integre todos los conceptos aprendidos:
Tareas del Sistema:
- Monitor de temperatura crítica
- Control de velocidad motores
- Sistema de alertas
- Logging de eventos
Herramientas FreeRTOS:
- 5 tareas concurrentes
- 3 colas de datos
- Mutex y semáforos
- Temporizadores por software
Arquitectura del Sistema
Hardware Requerido:
- ESP32 DevKit 1
- Sensores de temperatura 3
- Display OLED 1
- Buzzer/Alarma 1
Especificaciones:
- Frecuencia muestreo 1 Hz
- Buffer de datos 50 elementos
- Respuesta alertas < 100ms
- Núcleos utilizados 2
Mejores Prácticas y Optimización
✅ Mejores Prácticas
Gestión de Memoria
- Usar stack size apropiado
- Monitorear con uxTaskGetStackHighWaterMark()
- Liberar recursos no utilizados
Comunicación Eficiente
- Usar timeouts apropiados
- Dimensionar colas correctamente
- Preferir notificaciones para señales simples
⚠️ Problemas Comunes
Deadlocks
Evitar esperas circulares entre mutex
Solución: Orden consistente de adquisiciónPriority Inversion
Tarea de baja prioridad bloquea a alta
Solución: Priority inheritance en mutexStarvation
Tareas no obtienen tiempo de CPU
Solución: Balancear prioridades