freertos-semaforos-binarios-sincronizacion

Semáforos binarios en FreeRTOS

  • 6 min

Un semáforo binario es un mecanismo de sincronización que solo puede estar disponible o tomado, como una señal simple entre tareas o entre una interrupción y una tarea.

Empezamos el Bloque 4: Sincronización y Recursos Compartidos. Esta es una de las partes más delicadas, porque es donde más errores aparecen en la programación de sistemas en tiempo real.

Hasta ahora, nuestras tareas funcionaban en paralelo pero cada una a lo suyo. El problema surge cuando dos tareas quieren acceder al mismo recurso a la vez (escribir en el mismo puerto Serie, modificar la misma variable global) o cuando una tarea debe esperar a que otra termine algo.

Si no controlamos esto, tenemos el caos: datos corruptos, bloqueos y comportamientos aleatorios. Para poner orden, FreeRTOS nos ofrece los Semáforos. Hoy empezamos por el más simple: el Semáforo Binario.

¿Qué es un semáforo binario?

Imagina un semáforo binario como una llave colgada en un tablero.

  • Solo hay una llave.
  • La llave puede estar en el tablero (Semáforo Disponible / Given).
  • O alguien puede tener la llave (Semáforo Tomado / Taken).

No guarda datos (no es una cola), solo guarda Estado: 0 (Vacío) o 1 (Lleno).

Operaciones básicas

  1. Take (Tomar): Una tarea intenta coger la llave.
  • Si la llave está ahí, la coge y continúa. El tablero queda vacío.
  • Si la llave no está (ya la tiene otro), la tarea se Bloquea (se duerme) esperando a que alguien devuelva la llave.
  1. Give (Dar): Una tarea (o interrupción) devuelve la llave al tablero.
  • Si había alguien esperando por ella, esa tarea se despierta inmediatamente y la coge.

Sincronización y exclusión mutua

Aunque técnicamente podemos usar un semáforo binario para proteger una variable (Exclusión Mutua), en FreeRTOS no es su uso recomendado (para eso existen los Mutex, que veremos luego).

El uso principal del Semáforo Binario es la Sincronización:

“Una tarea espera a que ocurra un evento generado por otra tarea o interrupción.”

Es decir, lo usamos como una bandera de señalización.

Creación y uso

Para usar semáforos, primero debemos incluir la librería (si no estamos en ESP32 nativo) y declarar el objeto SemaphoreHandle_t.

SemaphoreHandle_t miSemaforo;

void setup() {
    // 1. Crear el semáforo
    miSemaforo = xSemaphoreCreateBinary();
    
    // ¡OJO! Los semáforos binarios se crean "VACÍOS" (Taken).
    // Si haces un Take nada más crearlo, te bloquearás.
    // A veces interesa hacer un Give inicial:
    // xSemaphoreGive(miSemaforo); 
}
Copied!

Funciones de la API

  • xSemaphoreTake(handle, tiempo_espera): Intenta tomar el semáforo. Devuelve pdTRUE si lo consiguió.
  • xSemaphoreGive(handle): Libera el semáforo.

Caso práctico: procesamiento diferido de interrupciones

Este es uno de los patrones de diseño más importantes de este bloque.

Las interrupciones (ISRs) deben ser cortísimas. No puedes poner un Serial.print ni un cálculo complejo dentro de una interrupción porque bloqueas todo el sistema.

¿La solución?

  1. La Interrupción ocurre, hace lo mínimo (limpiar flags) y hace un Give al semáforo.
  2. Una Tarea normal está bloqueada haciendo Take al semáforo.
  3. Al recibir el Give, la tarea despierta y hace el trabajo pesado.

Esto se llama Deferring Interrupt Processing (Diferir el procesamiento).

// Definimos el semáforo
SemaphoreHandle_t semaforoBoton;

// --- Interrupción (ISR) ---
// IRAM_ATTR permite colocar la ISR en RAM cuando el diseño lo requiere.
void IRAM_ATTR isrBoton() {
    // Variable necesaria para FreeRTOS en ISRs
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // Damos el semáforo desde la interrupción
    // Usamos la versión "FromISR"
    xSemaphoreGiveFromISR(semaforoBoton, &xHigherPriorityTaskWoken);
    
    // Si al dar el semáforo hemos despertado a una tarea importante,
    // forzamos un cambio de contexto YA, para que se ejecute al salir de la ISR.
    if(xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

// --- Tarea Manejadora ---
void TareaBoton(void *pvParameters) {
    for(;;) {
        // La tarea vive bloqueada aquí. No consume CPU.
        // Espera eternamente (portMAX_DELAY) a que el semáforo esté libre.
        if(xSemaphoreTake(semaforoBoton, portMAX_DELAY) == pdTRUE) {
            
            // Si pasamos aquí, es que alguien pulsó el botón.
            // Ahora podemos hacer cosas lentas sin miedo.
            Serial.println("¡Botón pulsado! Procesando evento complejo...");
            
            // Simulamos trabajo pesado
            for(int i=0; i<10; i++) { 
                digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
                vTaskDelay(pdMS_TO_TICKS(100));
            }
        }
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(LED_BUILTIN, OUTPUT);
    pinMode(0, INPUT_PULLUP); // Botón BOOT

    // 1. Creamos el semáforo
    semaforoBoton = xSemaphoreCreateBinary();

    if (semaforoBoton == NULL) {
        Serial.println("No se pudo crear el semáforo");
        return;
    }

    // 2. Creamos la tarea que procesará el evento
    // Le damos prioridad alta para que responda rápido tras la interrupción
    xTaskCreate(TareaBoton, "ManejaBoton", 2048, NULL, 2, NULL);

    // 3. Configuramos la interrupción
    attachInterrupt(digitalPinToInterrupt(0), isrBoton, FALLING);
}

void loop() {}
Copied!

Análisis del código

  1. Reposo: Nadie toca el botón. La TareaBoton está bloqueada en xSemaphoreTake y no consume CPU.
  2. Evento: Pulsas el botón. Salta la CPU a isrBoton.
  3. Señalización: La ISR da el semáforo (Give) y termina en microsegundos.
  4. Reacción: El Scheduler ve que el semáforo está disponible y despierta a TareaBoton.
  5. Procesamiento: TareaBoton sale del bloqueo, imprime por pantalla y parpadea el LED. Al terminar el bucle for, vuelve al Take y se duerme de nuevo.

Fíjate en portYIELD_FROM_ISR. Si la ISR ha desbloqueado una tarea más prioritaria, permite que el cambio de contexto ocurra al salir de la interrupción. Sin esa petición, la tarea esperaría hasta la siguiente oportunidad de planificación.

Semáforo binario y task notification

En el artículo anterior vimos que podíamos hacer esto mismo con vTaskNotifyGiveFromISR. ¿Por qué usar un Semáforo entonces?

  1. Desacoplamiento: Con Task Notification, la interrupción necesita conocer el TaskHandle de la tarea específica. Con un Semáforo, la interrupción solo le da la llave al objeto semáforo. No le importa quién lo recoja. Podríamos cambiar la tarea consumidora por otra y la ISR ni se enteraría.
  2. Múltiples Consumidores: (Raro en binarios, pero posible). Varias tareas podrían estar esperando el mismo semáforo; la primera que lo pille gana.

Problemas comunes

  1. Olvidar xSemaphoreCreateBinary: Si intentas usar un semáforo sin crearlo (variable a NULL), el ESP32 fallará.
  2. Perder eventos: Un semáforo binario solo guarda 1 o 0. Si la interrupción salta 5 veces rapidísimo antes de que la tarea pueda procesar el primero, los otros 4 Give se pierden (el semáforo ya estaba lleno).
  • Solución: Usar un Semáforo Contador (lo veremos pronto) o una Cola.