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 hacerTakese bloqueará hasta que alguien devuelva una llave (Give).
FreeRTOS utiliza estos semáforos para dos escenarios principales:
- Gestión de Recursos (Pool de recursos): Controlar el acceso a un número limitado de objetos idénticos.
- 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:
| Tipo | Rango Valor | Propiedad (Owner) | Uso Principal | Función Create |
|---|---|---|---|---|
| Binario | 0 - 1 | No | Sincronización simple (1 evento) | xSemaphoreCreateBinary |
| Mutex | 0 - 1 | Sí | Protección de recursos (Exclusión Mutua) | xSemaphoreCreateMutex |
| Contador | 0 - N | No | Pool de recursos o Conteo de eventos | xSemaphoreCreateCounting |
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);
}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:
- Tarea quiere recurso
xSemaphoreTake. (Resta 1). - Tarea usa recurso.
- 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() {}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:
- Interrupción
xSemaphoreGive. (Incrementa cuenta: 0 -> 1 -> 2…). - Tarea diferida
xSemaphoreTake. (Procesa uno y resta: 2 -> 1). - 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);
}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.