FreeRTOS IPC Séances 13–14 · 12 min

FreeRTOS — Queues,
Mutex & Sémaphores

Les outils de communication inter-tâches (IPC) de FreeRTOS — pattern producteur/consommateur, protection de ressources partagées, et les pièges classiques du multithreading.

Vue d'ensemble des outils IPC

// Récapitulatif — outils de communication FreeRTOS
OutilUsageBloquant ?Données ?
xQueueSendEnvoyer une donnéeOptionnel (timeout)Oui — copie par valeur
xQueueReceiveRecevoir une donnéeOui (bloque si vide)Oui — copie vers buffer
xQueuePeekInspecter sans retirerOptionnelOui — non consommé
xSemaphoreTake/GiveSynchro ou mutexOui (bloque)Non
xTaskNotifySignal léger inter-tâchesNon (fire & forget)32 bits optionnels

Queue — Pattern Producteur / Consommateur

La queue est la primitive de communication de base. Elle transmet des données par valeur d'une tâche à une autre de manière thread-safe — pas de mutex nécessaire pour accéder à la queue elle-même.

// Producteur → Queue → Consommateur
vTaskProducer
prio 3 — haute

xQueueSend()
QUEUE[5]
cmd3
cmd2
cmd1
vTaskConsumer
prio 2 — normale

xQueueReceive()
Queue pleine → xQueueSend() bloque le producteur jusqu'à qu'une place se libère
Queue vide → xQueueReceive() bloque le consommateur jusqu'à qu'un élément arrive
C · producer_consumer.c
// Dans main — créer la queue avant les tâches
QueueHandle_t cmd_queue = xQueueCreate(5, sizeof(Command_t));

// Producteur — envoie des commandes
void vTaskProducer(void *pvParams) {
    AppContext_t *ctx = (AppContext_t *)pvParams;
    Command_t cmd;
    uint32_t seq = 0;

    for (;;) {
        cmd.id    = seq++;
        cmd.value = sensor_read();

        // Envoyer — bloquer 100ms max si queue pleine
        if (xQueueSend(ctx->cmd_queue, &cmd, pdMS_TO_TICKS(100)) != pdTRUE) {
            console_print(ctx, "[PROD] Queue pleine!\n");
        }
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

// Consommateur — traite les commandes
void vTaskConsumer(void *pvParams) {
    AppContext_t *ctx = (AppContext_t *)pvParams;
    Command_t cmd;

    for (;;) {
        // Bloquer indéfiniment jusqu'à réception
        if (xQueueReceive(ctx->cmd_queue, &cmd, portMAX_DELAY) == pdTRUE) {
            process_command(&cmd);
        }
    }
}

Mutex — Protéger une ressource partagée

Quand plusieurs tâches accèdent à une même ressource (stdout, bus I2C, buffer partagé), il faut garantir l'accès exclusif. Le mutex est un sémaphore binaire avec gestion de la priorité intégrée.

C · mutex_usage.c
// console_print — impression thread-safe via mutex
void console_print(AppContext_t *ctx, const char *msg) {
    // Prendre le mutex — bloquer si une autre tâche l'a
    if (xSemaphoreTake(ctx->mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
        printf("%s", msg);   // section critique — un seul thread à la fois
        xSemaphoreGive(ctx->mutex);  // libérer immédiatement
    }
    // Si timeout : on perd le message — logique de dégradation gracieuse
}
⚠ Priority Inversion

Si une tâche haute prio attend un mutex tenu par une tâche basse prio, et qu'une tâche moyenne prio préempte la tâche basse — la tâche haute est indirectement bloquée par la moyenne. Solution : utiliser xSemaphoreCreateMutex() (pas Binary) — FreeRTOS applique alors la Priority Inheritance automatiquement.

Sémaphore binaire — Synchronisation

Le sémaphore binaire sert à la synchronisation entre une ISR et une tâche. L'ISR donne le sémaphore (signal rapide), la tâche le prend (traitement lourd hors interruption).

C · watchdog_semaphore.c — pattern séance 12
// Watchdog logiciel via sémaphore binaire
SemaphoreHandle_t wdg_sem;

void vTaskWorker(void *pvParams) {
    for (;;) {
        do_work();
        xSemaphoreGive(wdg_sem);  // signaler "je suis vivant"
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void vTaskWatchdog(void *pvParams) {
    const TickType_t timeout = pdMS_TO_TICKS(1500); // 3x la période

    for (;;) {
        // Attendre le signal — timeout si worker bloqué
        if (xSemaphoreTake(wdg_sem, timeout) != pdTRUE) {
            handle_watchdog_timeout();  // reset, log, alarm...
        }
    }
}

xQueuePeek — Inspecter sans consommer

C · queue_peek.c — pattern séance 14
// Inspecter le prochain élément sans le retirer de la queue
// Utile pour décider si on veut le consommer ou laisser une autre tâche le faire
Command_t next_cmd;
if (xQueuePeek(ctx->cmd_queue, &next_cmd, 0) == pdTRUE) {
    if (next_cmd.priority == HIGH) {
        xQueueReceive(ctx->cmd_queue, &next_cmd, 0); // consommer
        handle_high_priority(&next_cmd);
    }
    // sinon laisser pour une autre tâche
}

// uxQueueMessagesWaiting — nombre d'éléments en attente
uint32_t pending = uxQueueMessagesWaiting(ctx->cmd_queue);
printf("Queue: %lu éléments en attente\n", pending);

Points clés à retenir

Queue = thread-safe par nature
Pas besoin de mutex pour accéder à une queue — FreeRTOS gère l'accès concurrent en interne. Le mutex n'est nécessaire que pour les ressources non-FreeRTOS (stdout, SPI bus...).
Mutex ≠ Sémaphore binaire
Utiliser xSemaphoreCreateMutex() pour protéger des ressources — il implémente la priority inheritance. Réserver CreateBinary() pour la synchronisation ISR/tâche.
Timeout = toujours
Ne jamais passer portMAX_DELAY sauf si on est certain que l'événement arrivera. Préférer un timeout explicite avec gestion de l'échec.
Deadlock
Tâche A tient mutex1, attend mutex2. Tâche B tient mutex2, attend mutex1. Solution : toujours acquérir les mutex dans le même ordre dans toutes les tâches.