Módulo 7

Creación de Tareas, Colas y Semáforos

Herramientas de Sincronización en FreeRTOS

ESP32 FreeRTOS Sincronización UNAM

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.

Base del multitasking en FreeRTOS

Colas (Queues)

Estructuras FIFO que permiten el intercambio seguro de datos entre tareas, con capacidad de almacenamiento configurable.

Comunicación asíncrona entre tareas

Semáforos

Mecanismos de sincronización que controlan el acceso a recursos compartidos y coordinan la ejecución de tareas.

Sincronización y protección de recursos

Creación Avanzada de Tareas

Vamos más allá del multitasking básico implementando tareas con parámetros y manejo avanzado:

C++ - Tareas con Parámetros
// 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:

C++ - Sistema 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:

C++ - Semáforos Binarios y Mutex
// 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

1

Sistema Productor-Consumidor

Principiante 35 min

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

2

Mutex para Display Compartido

Intermedio 45 min

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

3

Sistema de Parking Inteligente

Avanzado 90 min

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ón
Priority Inversion

Tarea de baja prioridad bloquea a alta

Solución: Priority inheritance en mutex
Starvation

Tareas no obtienen tiempo de CPU

Solución: Balancear prioridades

Referencias y Documentación Especializada