FreeRTOS Phase 2 Séances 11–12 · 10 min

FreeRTOS — Tâches
& Scheduler Préemptif

Du scheduler coopératif au RTOS — comment FreeRTOS gère plusieurs tâches indépendantes, les priorités, et la préemption automatique sur simulateur Windows.

Coopératif vs Préemptif

Le scheduler coopératif vu en Phase 1 est simple mais limité : une tâche qui prend du temps retarde toutes les autres. FreeRTOS est préemptif — il interrompt une tâche à tout moment pour en exécuter une de priorité plus haute.

Coopératif (Phase 1)
Chaque tâche cède la main volontairement. Simple, prédictible, mais une tâche lente bloque toutes les autres.
Préemptif (FreeRTOS)
Le scheduler interrompt à chaque tick. Une tâche haute priorité prend le CPU immédiatement, sans attendre. Temps-réel garanti.

États d'une tâche FreeRTOS

// Task state machine — 4 états possibles
READY
prête à tourner
→ scheduler →
RUNNING
sur le CPU
→ vTaskDelay() →
BLOCKED
attend un event
SUSPENDED
vTaskSuspend()
← état explicite, jamais schedulé
RUNNING → READY : préemption (tâche de plus haute prio arrive)
BLOCKED → READY : délai expiré, queue reçue, sémaphore donné
RUNNING → BLOCKED : vTaskDelay(), xQueueReceive() bloquant

Créer une tâche : xTaskCreate

C · freertos_tasks.c
// Prototype d'une fonction tâche — toujours void, toujours boucle infinie
void vTaskAcquisition(void *pvParams) {
    AppContext_t *ctx = (AppContext_t *)pvParams;  // récupérer le contexte

    for (;;) {
        // Travail de la tâche
        console_print(ctx, "[ACQ] lecture capteur\n");

        vTaskDelay(pdMS_TO_TICKS(500));  // dormir 500ms — libère le CPU
    }
    // Ne jamais retourner — si la tâche se termine, appeler vTaskDelete(NULL)
}

int main(void) {
    AppContext_t ctx = { .mutex = xSemaphoreCreateMutex() };

    // Créer 3 tâches avec des priorités différentes
    xTaskCreate(
        vTaskAcquisition,   // fonction
        "Acquisition",      // nom (debug)
        1024,               // stack en words (pas en octets !)
        &ctx,               // pvParams → passé à la tâche
        3,                  // priorité (plus haut = plus urgent)
        NULL                // handle optionnel
    );

    xTaskCreate(vTaskDisplay, "Display", 1024, &ctx, 2, NULL);
    xTaskCreate(vTaskLogger,  "Logger",  1024, &ctx, 1, NULL);

    vTaskStartScheduler();  // démarre FreeRTOS — ne retourne jamais
    for (;;);
}

Priorités et préemption

// Règles de scheduling FreeRTOS
PrioritéTâcheComportementTypiquement
Haute (3+)Acquisition, ISR handlerPréempte toutes les autres immédiatementTemps-réel strict
Moyenne (2)Traitement, affichageTourne quand haute prio est bloquéeLogique applicative
Basse (1)Logging, monitoringTourne seulement si tout le reste est bloquéTâches de fond
⚠ Deux tâches de même priorité → round-robin (time slicing à chaque tick)
⚠ Une tâche haute prio qui ne bloque jamais → toutes les autres sont affamées (starvation)

Le pattern contexte (AppContext)

Plutôt que des variables globales, toutes les ressources partagées (queue, mutex, flags) sont regroupées dans une struct AppContext passée à chaque tâche via pvParams.

C · app_context.h
typedef struct {
    QueueHandle_t     cmd_queue;    // file de commandes inter-tâches
    SemaphoreHandle_t mutex;        // protection stdout partagé
    volatile bool     shutdown_req; // signal d'arrêt propre
} AppContext_t;

// Dans chaque tâche :
void vMyTask(void *pvParams) {
    AppContext_t *ctx = (AppContext_t *)pvParams;
    // Accès via ctx->mutex, ctx->cmd_queue, etc.
    // Zéro variable globale dans le code applicatif
}
// avantages pattern contexte

Zéro globale applicative · console_print protège stdout via le mutex du contexte · La queue est partagée proprement · On pourrait instancier deux AppContext indépendants sans toucher au code des tâches.

Stack sizing et uxTaskGetStackHighWaterMark

Chaque tâche a sa propre pile allouée statiquement. Trop petite → stack overflow silencieux sur STM32. FreeRTOS fournit un outil de diagnostic :

C · stack_monitor.c
// Dans une tâche de monitoring (basse priorité)
void vTaskMonitor(void *pvParams) {
    for (;;) {
        UBaseType_t watermark = uxTaskGetStackHighWaterMark(NULL);
        printf("[MON] Stack libre min: %lu words\n", watermark);
        // Si watermark < 20 → stack trop petite, risque d'overflow
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}
⚠ stack en words, pas en octets

Le 4ème argument de xTaskCreate est en words (4 octets sur ARM Cortex-M). Passer 128 donne 512 octets de pile — souvent insuffisant. Commencer à 256 et ajuster selon le watermark.

Points clés à retenir

vTaskDelay obligatoire
Toute tâche doit appeler vTaskDelay() ou attendre sur une queue/sémaphore — sinon elle monopolise le CPU et affame les tâches de priorité inférieure.
Ne jamais retourner
Une fonction tâche ne doit jamais retourner. Si la tâche a terminé son travail, appeler vTaskDelete(NULL) pour se supprimer proprement.
pvParams = contexte
Passer toutes les ressources partagées via pvParams plutôt que des globales — code plus testable, plus modulaire, et thread-safe par construction.
configASSERT en debug
Toujours vérifier les valeurs de retour : xTaskCreate retourne pdPASS ou errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY. Ne jamais ignorer silencieusement.