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
- 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.
- 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);
}Funciones de la API
xSemaphoreTake(handle, tiempo_espera): Intenta tomar el semáforo. DevuelvepdTRUEsi 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?
- La Interrupción ocurre, hace lo mínimo (limpiar flags) y hace un
Giveal semáforo. - Una Tarea normal está bloqueada haciendo
Takeal semáforo. - 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() {}Análisis del código
- Reposo: Nadie toca el botón. La
TareaBotonestá bloqueada enxSemaphoreTakey no consume CPU. - Evento: Pulsas el botón. Salta la CPU a
isrBoton. - Señalización: La ISR da el semáforo (
Give) y termina en microsegundos. - Reacción: El Scheduler ve que el semáforo está disponible y despierta a
TareaBoton. - Procesamiento:
TareaBotonsale del bloqueo, imprime por pantalla y parpadea el LED. Al terminar el buclefor, vuelve alTakey 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?
- Desacoplamiento: Con Task Notification, la interrupción necesita conocer el
TaskHandlede 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. - 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
- Olvidar
xSemaphoreCreateBinary: Si intentas usar un semáforo sin crearlo (variable a NULL), el ESP32 fallará. - 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
Givese pierden (el semáforo ya estaba lleno).
- Solución: Usar un Semáforo Contador (lo veremos pronto) o una Cola.