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.
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.
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 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.
// 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; }
Le pointeur de fonction void (*handler)() permet d'enregistrer n'importe quelle fonction — c'est ce qui rend le scheduler générique.
#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 } } }
// 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 } }
Visualisation des 3 tâches sur 100ms — chaque tâche s'exécute à sa propre cadence, de façon non-bloquante.
Scheduler_Run(), hors interruption — plus simple et plus sûr.now - last_run fonctionne même si uint32_t déborde (~49 jours à 1ms/tick) — arithmétique modulo garantie en C.HAL_Delay(100) par une machine à états avec timestamp — la tâche retourne immédiatement et revient 100ms plus tard.Task_t deviendra une tâche RTOS ; period_ms deviendra vTaskDelay().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.