freertos-semaforos-contadores-recursos-eventos

Counting Semaphores in FreeRTOS

  • 5 min

A counting semaphore is a semaphore that can store multiple permits or pending events instead of being limited to 0 or 1.

We’ve seen Binary Semaphores (a simple flag: free/busy) and Mutexes (a single key with an owner). Both have something in common: they are binary. Their internal value can only be 0 or 1.

Things change when we don’t have one single resource, but several.

Imagine a web server that limits concurrent work to 3 clients, a parking lot with 10 spaces, or an interrupt that generates several events before a task can process them.

Here, 0 and 1 are not enough. We need to count. Today we’ll add to our arsenal the Counting Semaphores.

What is a Counting Semaphore?

We can visualize a Counting Semaphore as a basket with several keys (tokens) that are identical.

  • If the semaphore has a value of 3, it means there are 3 keys available.
  • When a task performs a Take, it grabs a key. 2 remain.
  • If the counter reaches 0, the basket is empty. The next task that tries to Take will block until someone returns a key (Give).

FreeRTOS uses these semaphores for two main scenarios:

  1. Resource Management (Resource Pool): Controlling access to a limited number of identical objects.
  2. Event Counting: Recording how many times something has happened, to process it later without losing count.

Semaphore Comparison

At this point, we have three very similar tools but with different uses. Let’s summarize to avoid confusion:

TypeValue RangeOwnership (Owner)Main UseCreate function
Binary0 - 1NoSimple synchronization (1 event)xSemaphoreCreateBinary
Mutex0 - 1YesResource protection (Mutual Exclusion)xSemaphoreCreateMutex
Counter0 - NNoResource pool or Event countingxSemaphoreCreateCounting

The xSemaphoreCreateCounting API

Creation is slightly different from the previous ones, as we need to define the limits.

SemaphoreHandle_t xSemCounter;

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

    // Example: A parking lot with 5 spaces that starts empty
    xSemCounter = xSemaphoreCreateCounting(5, 0); 
}
Copied!
  • uxMaxCount: The maximum value it can reach (the size of the basket).
  • uxInitialCount: How many keys the semaphore starts with.

The uxInitialCount parameter is key. Depending on whether we set it to 0 or to the maximum, the semaphore’s behavior changes completely (Events vs Resources).

Managing a resource pool

Imagine we have 3 connections available. If a fourth task tries to use one, it must wait for one to be released.

In this case:

  • Max Count: 3 (Total resources).
  • Initial Count: 3 (At startup, all resources are free).

The logic is:

  1. Task wants resource xSemaphoreTake. (Subtracts 1).
  2. Task uses resource.
  3. Task finishes xSemaphoreGive. (Adds 1).

Practical example: the parking lot

Let’s simulate a parking lot with 3 spaces and 5 cars (tasks) trying to enter.

SemaphoreHandle_t semParking;

void CarTask(void *pvParameters) {
    int carId = (int)(intptr_t)pvParameters;

    for(;;) {
        Serial.printf("Car %d: Trying to enter...\n", carId);

        // Try to park. If full, wait.
        if(xSemaphoreTake(semParking, portMAX_DELAY) == pdTRUE) {
            
            // --- INSIDE THE PARKING LOT ---
            // Read how many free spaces remain (informational only)
            UBaseType_t freeSpaces = uxSemaphoreGetCount(semParking);
            Serial.printf(">>> Car %d: PARKED. (%d spaces remaining)\n", carId, freeSpaces);
            
            // Simulate parking time (random between 1 and 3 seconds)
            vTaskDelay(pdMS_TO_TICKS(1000 + random(2000)));

            Serial.printf("<<< Car %d: LEAVING.\n", carId);
            
            // Return the space
            xSemaphoreGive(semParking);
            
            // Drive around before trying again
            vTaskDelay(pdMS_TO_TICKS(5000));
        }
    }
}

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

    // Create semaphore: Max 3 spaces, Initially 3 (all free)
    semParking = xSemaphoreCreateCounting(3, 3);

    // Create 5 cars
    for(int i=0; i<5; i++) {
        char name[10];
        sprintf(name, "Car%d", i);
        // Pass the ID as a parameter (casting trick to void*)
        xTaskCreate(CarTask, name, 2048, (void*)(intptr_t)i, 1, NULL);
    }
}

void loop() {}
Copied!

Result: You will see 3 cars enter and the 4th and 5th wait until one of the first ones leaves. The system manages the waiting automatically.

Counting events in a burst

Remember the limit of the binary semaphore: if an interrupt occurs 3 times before the task can process it, only one event is recorded.

With a Counting Semaphore, we can record those bursts.

In this case:

  • Max Count: A high number (e.g., 10 or 100).
  • Initial Count: 0 (At startup, no event has occurred).

The logic is:

  1. Interrupt xSemaphoreGive. (Increments count: 0 -> 1 -> 2…).
  2. Deferred task xSemaphoreTake. (Processes one and decrements: 2 -> 1).
  3. The task loops processing while the semaphore has charge.

Example: Fast pulses

Imagine a sensor that sends very fast pulses.

SemaphoreHandle_t semEvents;

void IRAM_ATTR isrSensor() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // Each time it triggers, ADD 1 to the counter
    xSemaphoreGiveFromISR(semEvents, &xHigherPriorityTaskWoken);
    if(xHigherPriorityTaskWoken) portYIELD_FROM_ISR();
}

void ProcessingTask(void *param) {
    for(;;) {
        // Block until there is AT LEAST 1 event
        if(xSemaphoreTake(semEvents, portMAX_DELAY) == pdTRUE) {
            
            Serial.print("Processing event... ");
            // Simulate some processing time
            vTaskDelay(pdMS_TO_TICKS(100));
            
            // Check if more work has accumulated
            UBaseType_t pending = uxSemaphoreGetCount(semEvents);
            Serial.printf("Done. (Pending in queue: %d)\n", pending);
        }
    }
}

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

    // Semaphore for events: Starts at 0. Maximum 10 accumulated.
    semEvents = xSemaphoreCreateCounting(10, 0);

    if (semEvents == NULL) {
        Serial.println("Could not create semaphore");
        return;
    }

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

If you generate several rapid pulses, you will see the pending counter rise. The task processes them one by one until the semaphore is drained. If the counter reaches the maximum of 10, subsequent Give calls will fail, so you must size it appropriately and check the result when losing events is critical.