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 : :doc:`tp01_taches`, et la liaison série de :doc:`../sans_os/tp05_uart` 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. .. tikz:: Délai relatif et délai absolu : avec osDelayUntil, la période ne dépend plus du travail. \tikzset{w/.style={draw, fill=orange!35, minimum height=5mm}, lb/.style={font=\scriptsize}} \def\s{0.03} % 1 ms = 0,03 cm \foreach \t in {0,100,200,300,400} { \draw[black!30] (\t*\s,0.6) -- (\t*\s,-2.4); \node[lb] at (\t*\s,-2.65) {\t\ ms}; } \node[lb, anchor=east] at (-0.2,0) {osDelay(100)}; \foreach \k in {0,1,2} { \draw[w] (130*\k*\s,-0.25) rectangle ++(30*\s,0.5); \draw[{Stealth}-{Stealth}, black!60] (130*\k*\s+30*\s,0.4) -- node[above, lb]{100} ++(100*\s,0); } \draw[w] (390*\s,-0.25) rectangle ++(30*\s,0.5); \node[lb, anchor=west] at (13.2,0) {période : 130 ms}; \node[lb, anchor=east] at (-0.2,-1.6) {osDelayUntil}; \foreach \k in {0,1,2,3,4} { \draw[w] (100*\k*\s,-1.85) rectangle ++(30*\s,0.5); } \foreach \k in {0,1,2,3} \draw[{Stealth}-{Stealth}, black!60] (100*\k*\s+30*\s,-1.2) -- node[above, lb]{70} ++(70*\s,0); \node[lb, anchor=west] at (13.2,-1.6) {période : 100 ms}; \draw[w] (0,-3.3) rectangle ++(0.5,0.4); \node[lb, anchor=west] at (0.6,-3.1) {travail (30 ms)}; \node[lb, anchor=west] at (3.2,-3.1) {flèches : temps d'attente demandé ou calculé}; Étape 1 — Le projet ------------------- #. Créez un projet ``rtos_tp02_priorites`` comme au TP 1 : FREERTOS en CMSIS_V2, **Timebase Source** sur **TIM6**, LED_1 à LED_4 en sortie. #. Ajoutez **UART5** et ``printf`` comme au TP 5 de la partie 1. Si CubeMX propose d'activer ``USE_NEWLIB_REENTRANT``, acceptez : ``printf`` sera 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 avec ``HAL_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 ``workTask`` en **osPriorityAboveNormal** et relancez. #. Remettez-la en priorité normale, et passez cette fois ``blinkTask`` en **osPriorityAboveNormal**. #. Enfin, gardez ``workTask`` au-dessus, mais remplacez son ``HAL_Delay(50)`` par ``osDelay(50)``. .. admonition:: Questions 1. Au deuxième essai, LED_1 s'arrête. La tâche ``blinkTask`` est-elle plantée ? Dans quel état est-elle ? 2. 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 ? 3. Énoncez la règle : à quelle condition une tâche de haute priorité laisse-t-elle vivre les autres ? .. dropdown:: Solution de l'étape 2 :icon: code :color: secondary .. code-block:: c 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.) .. dropdown:: Réponses aux questions 1 à 3 :icon: light-bulb :color: info **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 : .. code-block:: c uint32_t next = osKernelGetTickCount(); for (;;) { next += 100u; /* ... travail ... */ osDelayUntil(next); } .. admonition:: Questions 4. Quelle période mesurez-vous avec ``osDelay(100)`` ? avec ``osDelayUntil`` ? 5. Que se passe-t-il avec ``osDelayUntil`` si le travail dure 150 ms ? 6. Pour échantillonner un capteur à 100 Hz, laquelle des deux attentes faut-il utiliser ? .. dropdown:: Solution de l'étape 3 :icon: code :color: secondary .. code-block:: c 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. .. dropdown:: Réponses aux questions 4 à 6 :icon: light-bulb :color: info **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 : .. code-block:: c 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 ``StartWork`` un tableau local de 300 entiers, remplissez-le, et relevez la marge de ``workTask``. .. Capture d'écran à insérer : le terminal avec les marges de pile ; ou, en débogage, la vue FreeRTOS Task List de CubeIDE. Déposez _static/captures/rtos/tp02-marges-pile.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp02-marges-pile.png .. :align: center .. :width: 90% .. admonition:: Questions 7. Quelle tâche utilise le plus sa pile ? Pourquoi celle qui appelle ``printf`` ? 8. De combien la marge de ``workTask`` a-t-elle baissé avec le tableau ? Est-ce le résultat attendu ? .. dropdown:: Solution de l'étape 4 :icon: code :color: secondary Dans ``StartBlink``, tous les 8 tours (2 s) : .. code-block:: c 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 : .. code-block:: c 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. .. dropdown:: Réponses aux questions 7 et 8 :icon: light-bulb :color: info **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. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/rtos/tp02/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``, avec ``osDelay`` puis avec ``osDelayUntil``. - **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 fonction ``vApplicationStackOverflowHook`` pour allumer LED_4 quand elle se déclenche. - **Priorités dynamiques** : un appui sur SW1 fait passer ``workTask`` au-dessus des autres (``osThreadSetPriority``), un second appui la remet à sa place. Après le TP ----------- Pour rendre la carte au robot, voir :ref:`etat-ratbot`.