TP 4 — Partager une ressource ============================= Objectifs --------- - constater qu'une ressource utilisée par deux tâches à la fois produit des résultats faux ; - la protéger avec un **mutex** ; - provoquer une **inversion de priorité**, la mesurer, et voir comment le mutex la corrige. Durée indicative : une séance de 3 h. Prérequis : :doc:`tp03_communiquer`. Ce qu'il faut savoir -------------------- La section critique ~~~~~~~~~~~~~~~~~~~ Une liaison série, un bus I²C, une structure de données : si une tâche est interrompue au milieu de son utilisation, et qu'une autre s'en sert à son tour, le résultat est mélangé. Le passage où la ressource doit être utilisée d'un seul tenant s'appelle une **section critique**. Le mutex ~~~~~~~~ Un **mutex** (*mutual exclusion*) est un verrou : ``osMutexAcquire`` le prend, ou attend qu'il soit libre ; ``osMutexRelease`` le rend. Seule la tâche qui l'a pris peut le rendre. Il ressemble à un sémaphore binaire, avec une différence importante : l'**héritage de priorité**. Si une tâche prioritaire attend un mutex tenu par une tâche moins prioritaire, celle-ci prend **temporairement** la priorité de la première, le temps de finir sa section critique. L'inversion de priorité ~~~~~~~~~~~~~~~~~~~~~~~ Trois tâches : **haute**, **moyenne** et **basse**. Basse prend la ressource. Haute la demande et attend. Moyenne, qui n'a rien à voir avec la ressource, devient prête : plus prioritaire que basse, elle prend le processeur… et haute attend la fin de moyenne. Une tâche de priorité moyenne retarde la plus prioritaire : c'est l'**inversion de priorité**. .. tikz:: L'inversion de priorité : avec un sémaphore, mid retarde high ; le mutex l'empêche. \tikzset{lb/.style={font=\scriptsize}, res/.style={fill=red!35, draw}, run/.style={fill=blue!20, draw}, wait/.style={pattern=north east lines, pattern color=orange, draw=orange}} \def\s{0.009} \newcommand{\panel}[2]{% \node[lb, anchor=west, font=\small\bfseries] at (0,0.9) {#1}; \foreach \n/\y in {high/0, mid/-0.7, low/-1.4} \node[lb, anchor=east] at (-0.15,\y) {\n}; \foreach \t in {0,200,400,600,800,1000} { \draw[black!25] (\t*\s,0.4) -- (\t*\s,-1.8); \node[lb] at (\t*\s,-2.05) {\t}; } #2 } \begin{scope} \panel{avec un sémaphore binaire : high attend 990 ms}{ \draw[res] (0,-1.6) rectangle (10*\s,-1.2); \draw[wait] (10*\s,-0.2) rectangle (1000*\s,0.2); \draw[res] (10*\s,-1.6) rectangle (20*\s,-1.2); \draw[run] (20*\s,-0.9) rectangle (820*\s,-0.5); \draw[res] (820*\s,-1.6) rectangle (1000*\s,-1.2); \draw[run] (1000*\s,-0.2) rectangle (1015*\s,0.2); } \end{scope} \begin{scope}[yshift=-3.8cm] \panel{avec un mutex (héritage de priorité) : high attend 190 ms}{ \draw[res] (0,-1.6) rectangle (10*\s,-1.2); \draw[wait] (10*\s,-0.2) rectangle (200*\s,0.2); \draw[res, fill=red!60] (10*\s,-1.6) rectangle (200*\s,-1.2); \node[lb, anchor=west] at (205*\s,-1.4) {low hérite de la priorité de high}; \draw[run] (200*\s,-0.2) rectangle (215*\s,0.2); \draw[run] (215*\s,-0.9) rectangle (1015*\s,-0.5); } \end{scope} \begin{scope}[yshift=-6.6cm] \draw[res] (0,0) rectangle (0.4,0.3); \node[lb, anchor=west] at (0.45,0.15) {calcul, ressource tenue}; \draw[run] (3.8,0) rectangle (4.2,0.3); \node[lb, anchor=west] at (4.25,0.15) {calcul}; \draw[wait] (5.8,0) rectangle (6.2,0.3); \node[lb, anchor=west] at (6.25,0.15) {attend la ressource}; \node[lb, anchor=west] at (0,-0.4) {abscisse : temps en ms, à partir de la prise de la ressource par low}; \end{scope} Étape 1 — Deux tâches, une liaison série ---------------------------------------- #. Créez un projet ``rtos_tp04_partager`` comme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 et ``printf``, LED_1 à LED_4 en sortie). #. Créez deux tâches de même priorité, ``aTask`` et ``bTask``. Chacune, en boucle et sans attente, écrit une ligne au terminal : .. code-block:: text AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB puis attend 1 ms avec ``osDelay``. #. Observez le terminal. #. Dans l'onglet **Timers and Semaphores**, créez un mutex ``uartMutex``. Encadrez chaque ``printf`` par ``osMutexAcquire(uartMutexHandle, osWaitForever)`` et ``osMutexRelease(uartMutexHandle)``. .. Capture d'écran à insérer : le terminal sans mutex : les lignes de A et de B mélangées. Déposez _static/captures/rtos/tp04-lignes-melangees.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp04-lignes-melangees.png .. :align: center .. :width: 90% .. admonition:: Questions 1. Sans mutex, que voit-on au terminal ? Expliquez avec l'ordonnancement à priorité égale vu au TP 1. 2. Avec le mutex, les lignes sont-elles intactes ? Une tâche peut-elle encore être interrompue au milieu de son ``printf`` ? 3. Pourquoi faut-il garder la section critique **courte** ? .. dropdown:: Solution de l'étape 1 :icon: code :color: secondary .. code-block:: c void StartA(void *argument) { (void)argument; for (;;) { osMutexAcquire(uartMutexHandle, osWaitForever); /* entrée */ printf("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n"); osMutexRelease(uartMutexHandle); /* sortie */ osDelay(1); } } et ``StartB`` identique avec des B. **La paire acquire / release** encadre exactement la section critique : rien de plus. Chaque ``osMutexAcquire`` doit avoir son ``osMutexRelease`` sur **tous** les chemins : un ``return`` ou un ``continue`` oublié entre les deux laisse le mutex pris pour toujours, et toutes les tâches qui l'attendent se figent. .. dropdown:: Réponses aux questions 1 à 3 :icon: light-bulb :color: info **1. Sans mutex.** Des lignes **mélangées** : ``AAAAAABBBBBBBAAAA…``. Un ``printf`` de 42 caractères dure environ 3,6 ms à 115 200 bit/s, car il attend l'UART caractère par caractère. Les deux tâches ont la même priorité, et elles sont prêtes en même temps : l'ordonnanceur les fait **alterner à chaque tic** (1 ms). Chacune envoie donc quelques caractères, puis l'autre prend la main au milieu de sa ligne. La liaison série est une **ressource partagée**, utilisée par deux tâches sans coordination. **2. Avec le mutex.** Les lignes sont **intactes**. La tâche qui écrit peut toujours être interrompue au milieu de son ``printf`` — le mutex n'empêche pas l'ordonnancement ! — mais quand l'autre tâche prend la main et veut écrire, ``osMutexAcquire`` la **bloque** : elle rend aussitôt le processeur, et la première termine sa ligne. Le mutex ne rend pas le code ininterruptible ; il garantit qu'**une seule** tâche à la fois est dans la section critique. **3. Une section critique courte.** Pendant qu'une tâche tient le mutex, **toutes** les autres tâches qui en ont besoin attendent, **quelle que soit leur priorité**. Une section critique longue transforme une tâche urgente en tâche lente. On n'y met que l'accès à la ressource : préparer le texte **avant** (``snprintf`` dans un tampon local), puis ne protéger que l'envoi. Étape 2 — L'inversion de priorité --------------------------------- Pour simuler du **travail**, ``HAL_Delay`` ne convient plus : il mesure le temps écoulé, pas le temps passé à calculer, et une tâche préemptée pendant son ``HAL_Delay(200)`` le trouverait terminé à son retour. Cette fonction ne compte que les millisecondes où la tâche a effectivement tourné : .. code-block:: c static void busy_ms(uint32_t ms) { uint32_t last = HAL_GetTick(); while (ms > 0u) { uint32_t now = HAL_GetTick(); if (now != last) { last = now; ms--; } /* un tic vu = 1 ms de calcul */ } } Créez trois tâches et une ressource partagée, protégée d'abord par un **sémaphore binaire** ``resSem`` (état initial : disponible) : .. list-table:: :header-rows: 1 :widths: 20 25 55 * - Tâche - Priorité - Comportement, à chaque tour * - ``lowTask`` - osPriorityBelowNormal - prend la ressource, la garde le temps de **200 ms** de calcul (``busy_ms(200)``), la rend * - ``midTask`` - osPriorityNormal - démarre **20 ms** après lowTask, et calcule **800 ms** sans se bloquer (``busy_ms(800)``) * - ``highTask`` - osPriorityAboveNormal - démarre **10 ms** après lowTask : prend la ressource, la rend, et affiche le **temps d'attente** Les trois tâches sont périodiques, de période 1 000 ms, avec ``osDelayUntil`` (TP 2). Pour obtenir les décalages, ``midTask`` et ``highTask`` commencent par ``osDelay(20)`` et ``osDelay(10)`` avant leur boucle. Allumez une LED par tâche pendant qu'elle travaille : LED_1 pour basse, LED_2 pour moyenne, LED_3 pour haute qui attend. #. Mesurez le temps d'attente de ``highTask`` avec le sémaphore. #. Remplacez le sémaphore par un **mutex** ``resMutex`` et mesurez de nouveau. .. admonition:: Questions 4. Quel temps d'attente mesurez-vous avec le sémaphore ? Décomposez-le. 5. Et avec le mutex ? Quelle tâche a eu le processeur pendant que ``highTask`` attendait ? 6. Pourquoi un sémaphore binaire ne peut-il pas faire d'héritage de priorité ? (Indice : qui « possède » un sémaphore ?) .. dropdown:: Solution de l'étape 2 :icon: code :color: secondary Deux fonctions cachent le choix sémaphore / mutex : .. code-block:: c #define USE_MUTEX 1 /* 0 : sémaphore binaire ; 1 : mutex */ static void res_take(void) { #if USE_MUTEX osMutexAcquire(resMutexHandle, osWaitForever); #else osSemaphoreAcquire(resSemHandle, osWaitForever); #endif } static void res_give(void) { #if USE_MUTEX osMutexRelease(resMutexHandle); #else osSemaphoreRelease(resSemHandle); #endif } Les trois tâches : .. code-block:: c void StartLow(void *argument) /* osPriorityBelowNormal */ { uint32_t next = osKernelGetTickCount(); (void)argument; for (;;) { res_take(); HAL_GPIO_WritePin(LED_1_GPIO_Port, LED_1_Pin, GPIO_PIN_SET); busy_ms(200); /* 200 ms de calcul, ressource tenue */ HAL_GPIO_WritePin(LED_1_GPIO_Port, LED_1_Pin, GPIO_PIN_RESET); res_give(); next += 1000u; osDelayUntil(next); } } void StartMid(void *argument) /* osPriorityNormal */ { uint32_t next; (void)argument; osDelay(20); /* décalage de 20 ms */ next = osKernelGetTickCount(); for (;;) { HAL_GPIO_WritePin(LED_2_GPIO_Port, LED_2_Pin, GPIO_PIN_SET); busy_ms(800); /* n'utilise pas la ressource */ HAL_GPIO_WritePin(LED_2_GPIO_Port, LED_2_Pin, GPIO_PIN_RESET); next += 1000u; osDelayUntil(next); } } void StartHigh(void *argument) /* osPriorityAboveNormal */ { uint32_t next, t0; (void)argument; osDelay(10); /* décalage de 10 ms */ next = osKernelGetTickCount(); for (;;) { HAL_GPIO_WritePin(LED_3_GPIO_Port, LED_3_Pin, GPIO_PIN_SET); t0 = osKernelGetTickCount(); res_take(); /* combien de temps ici ? */ res_give(); HAL_GPIO_WritePin(LED_3_GPIO_Port, LED_3_Pin, GPIO_PIN_RESET); printf("high a attendu %lu ms\r\n", (unsigned long)(osKernelGetTickCount() - t0)); next += 1000u; osDelayUntil(next); } } Les LED rendent le scénario visible : LED_3 reste allumée tout le temps où ``highTask`` attend. .. dropdown:: Réponses aux questions 4 à 6 :icon: light-bulb :color: info **4. Avec le sémaphore : environ 990 ms.** Le déroulement, à partir de l'instant 0 où ``lowTask`` prend la ressource : .. code-block:: text 0 ms low prend la ressource, commence ses 200 ms de calcul 10 ms high se réveille, préempte low, demande la ressource : bloquée low reprend (10 ms de calcul faits) 20 ms mid se réveille, préempte low (mid > low), calcule 800 ms 820 ms mid se rebloque ; low reprend ses 180 ms restantes 1000 ms low rend la ressource ; high la prend aussitôt → high a attendu 1000 − 10 = 990 ms La tâche la plus prioritaire a attendu **800 ms de mid**, une tâche moins prioritaire qui ne se sert **même pas** de la ressource. C'est l'inversion de priorité : en pratique, ``mid`` est plus prioritaire que ``high``. Et le délai n'est pas borné : avec plusieurs tâches moyennes, il pourrait s'allonger sans limite. **5. Avec le mutex : environ 190 ms.** Quand ``high`` se bloque sur le mutex tenu par ``low``, FreeRTOS **élève temporairement la priorité de low** à celle de ``high`` : c'est l'**héritage de priorité**. À 20 ms, ``mid`` se réveille, mais ``low`` est maintenant plus prioritaire qu'elle : ``mid`` attend. ``low`` finit sa section critique à 200 ms, rend le mutex, **retrouve sa priorité d'origine** ; ``high`` prend le mutex et termine. ``mid`` ne s'exécute qu'ensuite. L'attente de ``high`` est bornée par la durée de la section critique : c'est exactement ce qu'on veut. (Le fameux exemple historique : en 1997, le robot *Mars Pathfinder* redémarrait sans cesse sur Mars à cause d'une inversion de priorité. Le correctif, envoyé depuis la Terre, a activé l'héritage de priorité sur un mutex.) **6. Pourquoi pas avec un sémaphore.** L'héritage suppose de savoir **à quelle tâche** prêter la priorité. Un mutex a un **propriétaire** : la tâche qui l'a pris, et seule elle peut le rendre. Un sémaphore n'en a pas : n'importe quelle tâche, ou une interruption, peut le libérer (c'est même son usage normal, TP 3). FreeRTOS ne sait donc pas qui « tient » un sémaphore. D'où la règle : **mutex pour protéger une ressource, sémaphore pour signaler un événement**. **Pourquoi busy_ms et pas HAL_Delay.** ``HAL_Delay`` compte le temps **écoulé**, pas le temps de calcul : préemptée pendant 800 ms au milieu de son ``HAL_Delay(200)``, ``lowTask`` le trouverait terminé à son retour, et la démonstration serait faussée. ``busy_ms`` ne compte que les millisecondes où la tâche a vraiment tourné : elle simule un calcul. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/rtos/tp04/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. - **L'étreinte fatale** (*deadlock*) : deux tâches, deux mutex, pris dans l'ordre inverse. Provoquez le blocage, puis corrigez-le en imposant le même ordre partout. - **Une fonction protégée** : écrivez ``log_printf(fmt, ...)`` qui prend le mutex, formate avec ``vsnprintf`` dans un tampon local et envoie la ligne. Ainsi aucune tâche ne peut oublier le mutex. - **Délai d'attente** : ``highTask`` abandonne si elle n'obtient pas la ressource en 100 ms, et allume LED_4. Avec le sémaphore, puis avec le mutex. Après le TP ----------- Pour rendre la carte au robot, voir :ref:`etat-ratbot`.