freertos-semaforos-contadores-recursos-eventos

Semáforos contadores en FreeRTOS

  • 5 min

Un semáforo contador es un semáforo que puede almacenar varios permisos o eventos pendientes en lugar de limitarse a 0 o 1.

Hemos visto los Semáforos Binarios (una simple bandera: libre/ocupado) y los Mutex (una llave única con dueño). Ambos tienen algo en común: son binarios. Su valor interno solo puede ser 0 o 1.

La cosa cambia cuando no tenemos un solo recurso, sino varios.

Imagina un servidor web que limita el trabajo simultáneo a 3 clientes, un aparcamiento con 10 plazas o una interrupción que genera varios eventos antes de que la tarea pueda procesarlos.

Aquí el 0 y el 1 se nos quedan cortos. Necesitamos contar. Hoy vamos a añadir a nuestro arsenal los Semáforos Contadores (Counting Semaphores).

¿Qué es un semáforo contador?

Podemos visualizar un Semáforo Contador como una cesta con varias llaves (tokens) idénticas.

  • Si el semáforo tiene un valor de 3, significa que hay 3 llaves disponibles.
  • Cuando una tarea hace Take, coge una llave. Quedan 2.
  • Si el contador llega a 0, la cesta está vacía. La siguiente tarea que intente hacer Take se bloqueará hasta que alguien devuelva una llave (Give).

FreeRTOS utiliza estos semáforos para dos escenarios principales:

  1. Gestión de Recursos (Pool de recursos): Controlar el acceso a un número limitado de objetos idénticos.
  2. Conteo de Eventos: Registrar cuántas veces ha ocurrido algo, para procesarlo luego sin perder la cuenta.

Comparación de semáforos

Llegados a este punto, tenemos tres herramientas muy parecidas pero con usos distintos. Resumámoslo para no liarnos:

TipoRango ValorPropiedad (Owner)Uso PrincipalFunción Create
Binario0 - 1NoSincronización simple (1 evento)xSemaphoreCreateBinary
Mutex0 - 1Protección de recursos (Exclusión Mutua)xSemaphoreCreateMutex
Contador0 - NNoPool de recursos o Conteo de eventosxSemaphoreCreateCounting

La API xSemaphoreCreateCounting

La creación es ligeramente distinta a los anteriores, ya que necesitamos definir los límites.

SemaphoreHandle_t xSemContador;

void setup() {
    // Prototipo:
    // xSemaphoreCreateCounting( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount );

    // Ejemplo: Un parking de 5 plazas que empieza vacío
    xSemContador = xSemaphoreCreateCounting(5, 0); 
}
Copied!
  • uxMaxCount: El valor máximo que puede alcanzar (el tamaño de la cesta).
  • uxInitialCount: Con cuántas llaves nace el semáforo.

El parámetro uxInitialCount es la clave. Dependiendo de si lo ponemos a 0 o al máximo, cambia totalmente el comportamiento del semáforo (Eventos vs Recursos).

Gestionar un pool de recursos

Imagina que tenemos 3 conexiones disponibles. Si una cuarta tarea intenta usar una, debe esperar a que se libere.

En este caso:

  • Max Count: 3 (Total de recursos).
  • Initial Count: 3 (Al arrancar, todos los recursos están libres).

La lógica es:

  1. Tarea quiere recurso xSemaphoreTake. (Resta 1).
  2. Tarea usa recurso.
  3. Tarea termina xSemaphoreGive. (Suma 1).

Ejemplo práctico: el aparcamiento

Vamos a simular un aparcamiento de 3 plazas con 5 coches (tareas) intentando entrar.

SemaphoreHandle_t semParking;

void TareaCoche(void *pvParameters) {
    int idCoche = (int)(intptr_t)pvParameters;

    for(;;) {
        Serial.printf("Coche %d: Intentando entrar...\n", idCoche);

        // Intentamos aparcar. Si está lleno, esperamos.
        if(xSemaphoreTake(semParking, portMAX_DELAY) == pdTRUE) {
            
            // --- DENTRO DEL PARKING ---
            // Leemos cuántas plazas quedan libres (solo informativo)
            UBaseType_t plazasLibres = uxSemaphoreGetCount(semParking);
            Serial.printf(">>> Coche %d: APARCADO. (Quedan %d plazas)\n", idCoche, plazasLibres);
            
            // Simulamos tiempo de estancia (aleatorio entre 1 y 3 seg)
            vTaskDelay(pdMS_TO_TICKS(1000 + random(2000)));

            Serial.printf("<<< Coche %d: SALIENDO.\n", idCoche);
            
            // Devolvemos la plaza
            xSemaphoreGive(semParking);
            
            // Nos vamos a dar una vuelta antes de volver a intentar
            vTaskDelay(pdMS_TO_TICKS(5000));
        }
    }
}

void setup() {
    Serial.begin(115200);

    // Creamos semáforo: Máximo 3 plazas, Inicialmente 3 (todas libres)
    semParking = xSemaphoreCreateCounting(3, 3);

    // Creamos 5 coches
    for(int i=0; i<5; i++) {
        char nombre[10];
        sprintf(nombre, "Coche%d", i);
        // Pasamos el ID como parámetro (truco de casting a void*)
        xTaskCreate(TareaCoche, nombre, 2048, (void*)(intptr_t)i, 1, NULL);
    }
}

void loop() {}
Copied!

Resultado: Verás que entran 3 coches y el 4.º y 5.º se quedan esperando hasta que uno de los primeros sale. El sistema gestiona la espera automáticamente.

Contar eventos en ráfaga

Recuerda el límite del semáforo binario: si una interrupción ocurre 3 veces antes de que la tarea pueda procesarla, solo queda registrado un evento.

Con un Semáforo Contador, podemos registrar esas ráfagas.

En este caso:

  • Max Count: Un número alto (ej. 10 o 100).
  • Initial Count: 0 (Al arrancar, no ha ocurrido ningún evento).

La lógica es:

  1. Interrupción xSemaphoreGive. (Incrementa cuenta: 0 -> 1 -> 2…).
  2. Tarea diferida xSemaphoreTake. (Procesa uno y resta: 2 -> 1).
  3. La tarea hace un bucle procesando mientras el semáforo tenga carga.

Ejemplo: pulsos rápidos

Imagina un sensor que envía pulsos muy rápidos.

SemaphoreHandle_t semEventos;

void IRAM_ATTR isrSensor() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // Cada vez que salta, SUMAMOS 1 al contador
    xSemaphoreGiveFromISR(semEventos, &xHigherPriorityTaskWoken);
    if(xHigherPriorityTaskWoken) portYIELD_FROM_ISR();
}

void TareaProcesadora(void *param) {
    for(;;) {
        // Bloqueamos hasta que haya AL MENOS 1 evento
        if(xSemaphoreTake(semEventos, portMAX_DELAY) == pdTRUE) {
            
            Serial.print("Procesando evento... ");
            // Simulamos que tardamos un poco en procesar
            vTaskDelay(pdMS_TO_TICKS(100));
            
            // Miramos si se nos han acumulado más trabajos
            UBaseType_t pendientes = uxSemaphoreGetCount(semEventos);
            Serial.printf("Hecho. (Pendientes en cola: %d)\n", pendientes);
        }
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(0, INPUT_PULLUP);

    // Semáforo para eventos: Empieza en 0. Máximo 10 acumulados.
    semEventos = xSemaphoreCreateCounting(10, 0);

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

    xTaskCreate(TareaProcesadora, "Proc", 2048, NULL, 1, NULL);
    attachInterrupt(digitalPinToInterrupt(0), isrSensor, FALLING);
}
Copied!

Si generas varios pulsos seguidos, verás que el contador de pendientes sube. La tarea los procesa uno a uno hasta vaciar el semáforo. Si el contador alcanza el máximo de 10, los siguientes Give fallarán, por lo que debes dimensionarlo y comprobar el resultado cuando perder eventos sea crítico.