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 : TP 3 — Communiquer entre tâches.
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é.

L’inversion de priorité : avec un sémaphore, mid retarde high ; le mutex l’empêche.
Étape 1 — Deux tâches, une liaison série
Créez un projet
rtos_tp04_partagercomme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 etprintf, LED_1 à LED_4 en sortie).Créez deux tâches de même priorité,
aTasketbTask. Chacune, en boucle et sans attente, écrit une ligne au terminal :AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
puis attend 1 ms avec
osDelay.Observez le terminal.
Dans l’onglet Timers and Semaphores, créez un mutex
uartMutex. Encadrez chaqueprintfparosMutexAcquire(uartMutexHandle, osWaitForever)etosMutexRelease(uartMutexHandle).
Questions
Sans mutex, que voit-on au terminal ? Expliquez avec l’ordonnancement à priorité égale vu au TP 1.
Avec le mutex, les lignes sont-elles intactes ? Une tâche peut-elle encore être interrompue au milieu de son
printf?Pourquoi faut-il garder la section critique courte ?
Solution de l’étape 1
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.
Réponses aux questions 1 à 3
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é :
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) :
Tâche |
Priorité |
Comportement, à chaque tour |
|---|---|---|
|
osPriorityBelowNormal |
prend la ressource, la garde le temps de 200 ms de calcul
( |
|
osPriorityNormal |
démarre 20 ms après lowTask, et calcule 800 ms sans se
bloquer ( |
|
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
highTaskavec le sémaphore.Remplacez le sémaphore par un mutex
resMutexet mesurez de nouveau.
Questions
Quel temps d’attente mesurez-vous avec le sémaphore ? Décomposez-le.
Et avec le mutex ? Quelle tâche a eu le processeur pendant que
highTaskattendait ?Pourquoi un sémaphore binaire ne peut-il pas faire d’héritage de priorité ? (Indice : qui « possède » un sémaphore ?)
Solution de l’étape 2
Deux fonctions cachent le choix sémaphore / mutex :
#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 :
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.
Réponses aux questions 4 à 6
4. Avec le sémaphore : environ 990 ms. Le déroulement, à partir de
l’instant 0 où lowTask prend la ressource :
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.
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.
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 avecvsnprintfdans un tampon local et envoie la ligne. Ainsi aucune tâche ne peut oublier le mutex.Délai d’attente :
highTaskabandonne 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 Remettre la carte en état RatBot.