Article RTOS-free Session 5 · 10 min de lecture

Scheduler Coopératif
Léger en C

Construire un ordonnanceur de tâches sans OS — avec SysTick, pointeurs de fonction, et exécution périodique garantie. Idéal pour les systèmes contraints sans RTOS.

Coopératif vs préemptif

Un scheduler préemptif (comme FreeRTOS) peut interrompre une tâche à tout moment pour en exécuter une autre. Un scheduler coopératif attend que chaque tâche rende la main volontairement avant de passer à la suivante.

✓ Coopératif (notre approche)
Simple à implémenter, pas de problème de concurrence, prédictible. Suffit pour la majorité des systèmes embarqués industriels.
Préemptif (FreeRTOS)
Réponse plus fine aux événements, mais complexité accrue : mutexes, sémaphores, stack par tâche. Phase 2 de notre formation.
⚠ règle fondamentale

En scheduler coopératif, aucune tâche ne doit bloquer. Pas de HAL_Delay(), pas de boucle d'attente active — toutes les attentes se font via des flags et des timers.

Le SysTick : cœur du scheduler

Le SysTick est un timer 24-bit intégré dans tout Cortex-M. Il génère une interruption à intervalle régulier — typiquement toutes les 1 ms. C'est notre source de temps globale.

C · systick.c
// Configurer SysTick à 1ms (SystemCoreClock = 168MHz pour STM32F4)
void SysTick_Init(void) {
    SysTick->LOAD = (SystemCoreClock / 1000U) - 1U; // 168000 - 1
    SysTick->VAL  = 0U;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk
                  | SysTick_CTRL_TICKINT_Msk
                  | SysTick_CTRL_ENABLE_Msk;
}

// Compteur global — incrémenté dans l'ISR
static volatile uint32_t s_tick_ms = 0;

void SysTick_Handler(void) {
    s_tick_ms++;
    Scheduler_Tick();  // notifier le scheduler
}

uint32_t GetTick(void) { return s_tick_ms; }

La struct Task

// Chaque tâche est décrite par cette structure
typedef struct Task_t
void (*handler)()
handler
pointeur vers la fonction tâche
uint32_t
period_ms
période d'exécution en ms
uint32_t
last_run_ms
timestamp de la dernière exécution
bool
enabled
tâche active ou suspendue

Le pointeur de fonction void (*handler)() permet d'enregistrer n'importe quelle fonction — c'est ce qui rend le scheduler générique.

Implémentation du scheduler

C · scheduler.c — implémentation complète
#include "scheduler.h"
#define MAX_TASKS  8U

typedef struct {
    void (*handler)(void);
    uint32_t period_ms;
    uint32_t last_run_ms;
    bool     enabled;
} Task_t;

static Task_t  s_tasks[MAX_TASKS];
static uint8_t s_task_count = 0;
static volatile bool s_tick_flag = false;

// Enregistrer une nouvelle tâche
bool Scheduler_AddTask(void (*handler)(void), uint32_t period_ms) {
    if (s_task_count >= MAX_TASKS || handler == NULL) return false;
    s_tasks[s_task_count].handler    = handler;
    s_tasks[s_task_count].period_ms  = period_ms;
    s_tasks[s_task_count].last_run_ms = GetTick();
    s_tasks[s_task_count].enabled    = true;
    s_task_count++;
    return true;
}

// Appelé depuis SysTick_Handler — rapide, juste un flag
void Scheduler_Tick(void) {
    s_tick_flag = true;
}

// Boucle principale — appeler en continu dans while(1)
void Scheduler_Run(void) {
    if (!s_tick_flag) return;   // pas encore 1ms
    s_tick_flag = false;

    uint32_t now = GetTick();

    for (uint8_t i = 0; i < s_task_count; i++) {
        if (!s_tasks[i].enabled) continue;

        if ((now - s_tasks[i].last_run_ms) >= s_tasks[i].period_ms) {
            s_tasks[i].last_run_ms = now;
            s_tasks[i].handler();   // exécuter la tâche
        }
    }
}

Utilisation — enregistrer des tâches

C · main.c — déclarer et lancer les tâches
// Les tâches — fonctions simples, jamais bloquantes
void Task_BlinkLed(void)  { GPIO_Toggle(&led); }
void Task_ReadButton(void){ Button_FSM_Update(); }
void Task_SendLog(void)   { Logger_Flush(); }

int main(void) {
    SystemClock_Config();
    SysTick_Init();
    GPIO_Init(&led);
    UART_Init(&uart);

    // Enregistrer les tâches avec leurs périodes
    Scheduler_AddTask(Task_BlinkLed,   500);  // toutes les 500ms
    Scheduler_AddTask(Task_ReadButton,  10);   // toutes les 10ms
    Scheduler_AddTask(Task_SendLog,     100);  // toutes les 100ms

    while (1) {
        Scheduler_Run();   // le scheduler décide qui tourne
    }
}

Chronogramme d'exécution

Visualisation des 3 tâches sur 100ms — chaque tâche s'exécute à sa propre cadence, de façon non-bloquante.

// Chronogramme — 100ms, tick = 10ms
0
10
20
30
40
50
60
70
80
90
LED
500ms
Button
10ms
Log
100ms
Task_BlinkLed (500ms)
Task_ReadButton (10ms)
Task_SendLog (100ms)

Points clés à retenir

Tick flag, pas de traitement dans ISR
L'ISR SysTick ne fait que lever un flag. Tout le traitement se passe dans Scheduler_Run(), hors interruption — plus simple et plus sûr.
Overflow du compteur
La soustraction now - last_run fonctionne même si uint32_t déborde (~49 jours à 1ms/tick) — arithmétique modulo garantie en C.
Pas de delay dans les tâches
Remplacer tout HAL_Delay(100) par une machine à états avec timestamp — la tâche retourne immédiatement et revient 100ms plus tard.
Passage vers FreeRTOS
Ce scheduler est la base mentale de FreeRTOS. Chaque Task_t deviendra une tâche RTOS ; period_ms deviendra vTaskDelay().
// phase suivante

Ce scheduler coopératif est la fondation. La Phase 2 de la formation migre vers FreeRTOS : préemption, queues inter-tâches, mutexes et gestion d'énergie avec modes sleep STM32.