TP 2 — Priorités et ordonnancement
Objectifs
comprendre la préemption : une tâche plus prioritaire prend la main dès qu’elle est prête ;
provoquer une famine (starvation), puis la corriger ;
écrire une tâche périodique exacte, avec
osDelayUntil;mesurer la pile réellement utilisée par une tâche.
Durée indicative : une séance de 3 h. Prérequis : TP 1 — Premières tâches, et la liaison série de TP 5 — La liaison série avec le PC pour afficher les mesures.
Ce qu’il faut savoir
La règle de l’ordonnanceur
FreeRTOS applique une règle simple : la tâche prête de plus haute priorité s’exécute. Si plusieurs tâches prêtes ont la même priorité, elles se partagent le processeur à chaque tic (time slicing).
Conséquence : une tâche de haute priorité qui ne se bloque jamais empêche toutes les tâches de priorité inférieure de s’exécuter. Elles ne sont pas plantées, elles sont affamées.
Délai relatif et délai absolu
osDelay(100) attend 100 tics à partir de maintenant. Si le travail de
la tâche dure lui-même 30 ms, la période réelle est de 130 ms, et elle varie
avec la durée du travail.
osDelayUntil(t) attend jusqu’à l”instant absolu t (en tics). En
ajoutant la période à t à chaque tour, la tâche se réveille toutes les
100 ms exactement, quelle que soit la durée du travail — tant qu’elle reste
plus courte que la période.

Délai relatif et délai absolu : avec osDelayUntil, la période ne dépend plus du travail.
Étape 1 — Le projet
Créez un projet
rtos_tp02_prioritescomme au TP 1 : FREERTOS en CMSIS_V2, Timebase Source sur TIM6, LED_1 à LED_4 en sortie.Ajoutez UART5 et
printfcomme au TP 5 de la partie 1. Si CubeMX propose d’activerUSE_NEWLIB_REENTRANT, acceptez :printfsera appelé depuis plusieurs tâches, et la bibliothèque C doit le savoir.Créez trois tâches, toutes en osPriorityNormal, pile 512 mots :
blinkTask(StartBlink) : fait clignoter LED_1 toutes les 250 ms ;workTask(StartWork) : simule un calcul qui ne se bloque jamais — une boucle qui fait clignoter LED_2 en attendant avecHAL_Delay(50);periodTask(StartPeriod) : on l’écrira à l’étape 3.
Étape 2 — La famine
Lancez le programme : à priorité égale, LED_1 et LED_2 clignotent toutes les deux.
Passez
workTasken osPriorityAboveNormal et relancez.Remettez-la en priorité normale, et passez cette fois
blinkTasken osPriorityAboveNormal.Enfin, gardez
workTaskau-dessus, mais remplacez sonHAL_Delay(50)parosDelay(50).
Questions
Au deuxième essai, LED_1 s’arrête. La tâche
blinkTaskest-elle plantée ? Dans quel état est-elle ?Au troisième essai, les deux LED clignotent. Pourquoi la tâche la plus prioritaire ne gêne-t-elle pas l’autre, cette fois ?
Énoncez la règle : à quelle condition une tâche de haute priorité laisse-t-elle vivre les autres ?
Solution de l’étape 2
void StartBlink(void *argument)
{
(void)argument;
for (;;)
{
HAL_GPIO_TogglePin(LED_1_GPIO_Port, LED_1_Pin);
osDelay(250);
}
}
void StartWork(void *argument)
{
(void)argument;
for (;;)
{
HAL_GPIO_TogglePin(LED_2_GPIO_Port, LED_2_Pin);
HAL_Delay(50); /* le « calcul » : ne se bloque jamais.
Dernier essai : osDelay(50) */
}
}
Les priorités se changent dans l’onglet Tasks and Queues de CubeMX ; il
faut régénérer le code à chaque fois. (On peut aussi les changer en cours
de route avec osThreadSetPriority, c’est un exercice.)
Réponses aux questions 1 à 3
1. La tâche blinkTask est-elle plantée ? Non. Elle est dans l’état
prête (ready) : son délai de 250 ms est écoulé, elle attend le
processeur. Mais workTask, plus prioritaire, est elle aussi toujours
prête (elle attend en tournant dans HAL_Delay, sans jamais se
bloquer), et l’ordonnanceur donne toujours le processeur à la tâche prête
la plus prioritaire. blinkTask est affamée. Dans le débogueur
(fenêtre FreeRTOS Task List de CubeIDE), on la voit dans l’état
Ready, jamais Running.
2. Le troisième essai. blinkTask est maintenant la plus prioritaire,
mais elle passe presque tout son temps bloquée dans osDelay(250) : à
chaque tic, l’ordonnanceur constate qu’elle n’est pas prête, et donne le
processeur à workTask. Quand son délai expire, elle préempte
workTask immédiatement (elle prend la main au tic, même si workTask
était au milieu de son calcul), bascule sa LED en une microseconde, et se
rebloque. workTask reprend là où elle était.
3. La règle. Une tâche de haute priorité ne laisse vivre les tâches moins prioritaires que si elle se bloque : attente d’un délai, d’un message, d’un sémaphore. Une tâche prioritaire doit donc être courte : elle réagit vite, travaille peu, et rend la main. Le travail long et continu va dans les tâches de faible priorité. C’est la base de la conception d’un système temps réel : les priorités se choisissent selon l”urgence, pas selon l’importance.
Étape 3 — Une tâche périodique
Dans StartPeriod, à chaque tour :
faites basculer LED_3 ;
simulez un travail de 30 ms avec
HAL_Delay(30);attendez avec
osDelay(100).
Comptez les tours pendant 10 s (avec osKernelGetTickCount()), puis
affichez la période moyenne au terminal.
Remplacez ensuite l’attente par une attente absolue :
uint32_t next = osKernelGetTickCount();
for (;;)
{
next += 100u;
/* ... travail ... */
osDelayUntil(next);
}
Questions
Quelle période mesurez-vous avec
osDelay(100)? avecosDelayUntil?Que se passe-t-il avec
osDelayUntilsi le travail dure 150 ms ?Pour échantillonner un capteur à 100 Hz, laquelle des deux attentes faut-il utiliser ?
Solution de l’étape 3
void StartPeriod(void *argument)
{
uint32_t start = osKernelGetTickCount();
uint32_t turns = 0u;
uint32_t next = start;
(void)argument;
for (;;)
{
HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin);
HAL_Delay(30); /* le travail : 30 ms */
turns++;
if (osKernelGetTickCount() - start >= 10000u) /* toutes les 10 s */
{
printf("periode moyenne : %lu ms\r\n",
(unsigned long)((osKernelGetTickCount() - start) / turns));
start = osKernelGetTickCount();
turns = 0u;
next = start;
}
/* version 1 : osDelay(100); délai relatif */
next += 100u; /* version 2 : délai absolu */
osDelayUntil(next);
}
}
osKernelGetTickCount() donne le nombre de tics depuis le démarrage de l’ordonnanceur : des millisecondes ici, puisque le tic est à 1 kHz.
next += 100 : on calcule l’instant du prochain réveil à partir de
l’instant prévu du réveil précédent, pas de l’instant présent. Le
travail et les retards éventuels ne s’accumulent pas : la tâche reste
calée sur une grille de 100 ms, comme une horloge. On réinitialise next
après le printf (qui prend quelques millisecondes) pour repartir sur
une grille propre.
Réponses aux questions 4 à 6
4. Les périodes mesurées.
avec
osDelay(100): environ 130 ms. Chaque tour dure le travail (30 ms) plus l’attente (100 ms), comptée à partir de la fin du travail ; s’y ajoute le temps où d’autres tâches ont pris le processeur. La période dépend du travail : elle change si le travail change.avec
osDelayUntil: 100 ms, exactement en moyenne. L’attente s’ajuste toute seule : 70 ms si le travail dure 30 ms, 40 ms s’il en dure 60.
5. Si le travail dure 150 ms. L’instant visé (next) est déjà passé
quand la tâche demande à attendre. osDelayUntil ne peut pas attendre
dans le passé : elle rend la main aussitôt (en renvoyant une erreur,
osErrorParameter dans cette version de CMSIS-RTOS2). La tâche tourne
alors en continu, toujours en retard, et next prend de plus en plus de
retard sur l’heure réelle. La période n’est plus tenue : c’est un
dépassement d’échéance. Un système temps réel sérieux le détecte
(en comparant next à l’heure courante) et le signale.
6. Pour échantillonner à 100 Hz : osDelayUntil. Un filtre, une
intégration (le cap du gyroscope, TP 10), une dérivée supposent un pas de
temps constant ; avec osDelay, il varie avec la charge du
processeur, et le résultat se dégrade sans qu’on comprenne pourquoi.
Étape 4 — La pile
Chaque tâche a sa propre pile, réservée à sa création : 512 mots, soit 2 Kio. Trop petite, elle déborde sur la mémoire voisine ; trop grande, elle gaspille la RAM.
Toutes les 2 s, dans
StartBlink, affichez la marge de pile de chaque tâche, c’est-à-dire le plus petit nombre de mots restés libres depuis le démarrage :printf("blink %lu work %lu period %lu mots libres\r\n", (unsigned long)uxTaskGetStackHighWaterMark(blinkTaskHandle), (unsigned long)uxTaskGetStackHighWaterMark(workTaskHandle), (unsigned long)uxTaskGetStackHighWaterMark(periodTaskHandle));
La fonction est déclarée dans
task.h(#include "task.h").Ajoutez dans
StartWorkun tableau local de 300 entiers, remplissez-le, et relevez la marge deworkTask.
Questions
Quelle tâche utilise le plus sa pile ? Pourquoi celle qui appelle
printf?De combien la marge de
workTaska-t-elle baissé avec le tableau ? Est-ce le résultat attendu ?
Solution de l’étape 4
Dans StartBlink, tous les 8 tours (2 s) :
if (++n % 8u == 0u)
{
printf("blink %lu work %lu period %lu mots libres\r\n",
(unsigned long)uxTaskGetStackHighWaterMark(blinkTaskHandle),
(unsigned long)uxTaskGetStackHighWaterMark(workTaskHandle),
(unsigned long)uxTaskGetStackHighWaterMark(periodTaskHandle));
}
et dans StartWork, le tableau :
volatile uint32_t tab[300];
for (uint32_t i = 0u; i < 300u; i++) { tab[i] = i; }
Comment FreeRTOS mesure la pile. À la création d’une tâche, il remplit
toute sa pile d’un motif connu (l’octet 0xA5). La pile se remplit ensuite
depuis le haut ; uxTaskGetStackHighWaterMark compte combien de mots, en
bas, portent encore le motif : jamais touchés depuis le démarrage. C’est
la marge minimale — la « laisse de haute mer » (high water mark).
Elle ne remonte jamais : elle garde le souvenir du pire moment.
Pourquoi volatile sur le tableau. Le compilateur voit que le tableau est
rempli mais jamais lu : il a le droit de supprimer la boucle, et même le
tableau. volatile l’oblige à faire chaque écriture, donc à réserver le
tableau sur la pile.
Réponses aux questions 7 et 8
7. La tâche qui utilise le plus sa pile est blinkTask, celle qui
appelle printf. printf est une fonction lourde : elle analyse le
format, convertit les nombres en texte dans des tampons locaux, et appelle
plusieurs niveaux de fonctions de la bibliothèque C (_vfprintf_r,
_write…), chacun avec ses variables locales. Plusieurs centaines
d’octets de pile, contre quelques dizaines pour une tâche qui ne fait que
basculer une LED. Toute tâche qui appelle printf doit avoir une pile
généreuse.
8. L’effet du tableau. La marge de workTask baisse d’environ 300
mots, c’est-à-dire 300 × 4 = 1 200 octets : le tableau tient 300 entiers
de 32 bits. FreeRTOS compte la pile en mots de 32 bits, pas en octets :
les 512 de CubeMX font 2 Kio. Avec une pile de 128 mots, le tableau ne
tiendrait pas : il déborderait sur la mémoire voisine (une autre pile,
une structure du système), sans erreur immédiate — d’où la vérification de
l’exercice supplémentaire.
Tout le code de la correction
taches.c
— compilé sans avertissement contre la HAL et le FreeRTOS 10.6.2 du
firmware RatBot.
Le fichier contient les fonctions des tâches, qui remplacent les
squelettes générés par CubeMX (dans main.c ou freertos.c selon la
version), et les extraits à placer dans les blocs USER CODE signalés en
commentaire. La configuration CubeMX attendue est rappelée en tête du
fichier, et les réponses aux questions à la fin.
Exercices supplémentaires
Ces exercices n’ont pas de correction.
Gigue : mesurez, sur 1 000 tours, l’écart maximal entre la période voulue et la période réelle de
periodTask, avecosDelaypuis avecosDelayUntil.Débordement : ramenez la pile de
workTaskà 128 mots en gardant le tableau. Le projet CubeMX active une vérification (configCHECK_FOR_STACK_OVERFLOW) : écrivez la fonctionvApplicationStackOverflowHookpour allumer LED_4 quand elle se déclenche.Priorités dynamiques : un appui sur SW1 fait passer
workTaskau-dessus des autres (osThreadSetPriority), un second appui la remet à sa place.
Après le TP
Pour rendre la carte au robot, voir Remettre la carte en état RatBot.