TP 3 — Communiquer entre tâches
Objectifs
faire passer une information d’une tâche à une autre par une file de messages (queue), sans variable globale partagée ;
réveiller une tâche depuis une interruption avec un sémaphore ;
savoir quelles fonctions FreeRTOS une interruption a le droit d’appeler.
Durée indicative : une séance de 3 h. Prérequis : TP 2 — Priorités et ordonnancement, et l’anti-rebond du TP 1 — Allumer une LED, lire un bouton.
Ce qu’il faut savoir
Pourquoi pas une variable globale ?
Une tâche qui attend une information d’une autre peut surveiller une variable globale… en la relisant sans cesse : elle consomme du processeur pour rien, et rate les changements trop rapides. Les outils de FreeRTOS bloquent la tâche qui attend, et la réveillent quand l’information arrive.
La file de messages
Une file est un tampon de taille fixe, géré par le système :
osMessageQueuePuty copie un message ; si la file est pleine, la tâche attend (ou abandonne, selon le délai donné) ;osMessageQueueGeten retire le plus ancien ; si la file est vide, la tâche se bloque jusqu’à l’arrivée d’un message.
Les messages sont copiés : l’émetteur peut réutiliser sa variable aussitôt.
Le sémaphore
Un sémaphore binaire est un drapeau : osSemaphoreRelease le lève,
osSemaphoreAcquire attend qu’il soit levé et le rabaisse. C’est l’outil
classique pour qu’une interruption, qui doit être très courte, confie le
travail à une tâche.
Important
Une interruption ne peut appeler que les fonctions FreeRTOS prévues
pour cela, et jamais une fonction qui attend. Avec l’interface
CMSIS_V2, osSemaphoreRelease et osMessageQueuePut (délai 0) sont
permises. Il faut aussi que la priorité de l’interruption soit
numériquement supérieure ou égale à 5 (donc moins urgente) : c’est
la limite configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY.
Étape 1 — Bouton → file → LED
Créez un projet
rtos_tp03_communiquercomme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 etprintf), avec LED_1 à LED_4 en sortie et SW1 à SW4 en entrée sans tirage.Dans l’onglet Tasks and Queues, créez deux tâches en priorité normale,
buttonTask(StartButton) etledTask(StartLed), et une filebuttonQueuede 8 éléments de typeuint8_t.StartButtonlit les quatre boutons toutes les 10 ms. À chaque appui (front montant, anti-rebond compris), elle envoie le numéro du bouton (0 à 3) dans la file.StartLedattend un message avecosWaitForever, et fait basculer la LED de même numéro.
Questions
Que fait
ledTaskentre deux appuis ? Combien de processeur consomme-t-elle ?Ajoutez un
osDelay(2000)dansStartLedaprès chaque bascule, et appuyez dix fois très vite. Que se passe-t-il ? Combien d’appuis sont perdus, et pourquoi ce nombre ?
Solution de l’étape 1
/* Scrutation toutes les 10 ms ; anti-rebond : 3 lectures identiques. */
void StartButton(void *argument)
{
static GPIO_TypeDef *const port[4] = { SW1_GPIO_Port, SW2_GPIO_Port,
SW3_GPIO_Port, SW4_GPIO_Port };
static const uint16_t pin[4] = { SW1_Pin, SW2_Pin, SW3_Pin, SW4_Pin };
uint8_t stable[4] = { 0u, 0u, 0u, 0u };
uint8_t count[4] = { 0u, 0u, 0u, 0u };
(void)argument;
for (;;)
{
for (uint8_t b = 0u; b < 4u; b++)
{
uint8_t now = (HAL_GPIO_ReadPin(port[b], pin[b]) == GPIO_PIN_SET) ? 1u : 0u;
if (now == stable[b]) { count[b] = 0u; continue; } /* rien de neuf */
if (++count[b] < 3u) { continue; } /* pas encore sûr */
stable[b] = now; /* changement confirmé */
count[b] = 0u;
if (now) /* un appui */
{
osMessageQueuePut(buttonQueueHandle, &b, 0u, 0u);
}
}
osDelay(10);
}
}
void StartLed(void *argument)
{
static GPIO_TypeDef *const port[4] = { LED_1_GPIO_Port, LED_2_GPIO_Port,
LED_3_GPIO_Port, LED_4_GPIO_Port };
static const uint16_t pin[4] = { LED_1_Pin, LED_2_Pin, LED_3_Pin, LED_4_Pin };
uint8_t b;
(void)argument;
for (;;)
{
if ((osMessageQueueGet(buttonQueueHandle, &b, NULL, osWaitForever) == osOK)
&& (b < 4u))
{
HAL_GPIO_TogglePin(port[b], pin[b]);
}
}
}
Le message est copié. osMessageQueuePut recopie l’octet b dans la
file : la tâche des boutons peut modifier b aussitôt après. La file
appartient au système, qui la protège : deux tâches peuvent y écrire en
même temps sans précaution.
Les délais. osMessageQueuePut(…, 0u) : si la file est pleine, on
n’attend pas, l’appui est perdu. Attendre bloquerait la tâche des boutons,
qui ne verrait plus les autres boutons. osMessageQueueGet(…,
osWaitForever) : la tâche des LED attend aussi longtemps qu’il faut.
La vérification b < 4 : une tâche qui reçoit des messages ne fait pas confiance à leur contenu. Ici, elle évite d’indexer hors des tableaux si un jour quelqu’un envoie autre chose.
Réponses aux questions 1 et 2
1. ledTask entre deux appuis. Elle est bloquée dans
osMessageQueueGet : l’ordonnanceur l’a retirée de la liste des tâches
prêtes, et l’a inscrite dans la liste d’attente de la file. Elle ne
consomme rien. Quand un message arrive, osMessageQueuePut la remet
elle-même dans la liste des tâches prêtes : elle se réveille aussitôt,
sans scruter quoi que ce soit. C’est tout l’intérêt d’une file sur une
variable globale relue en boucle.
2. Dix appuis rapides avec un osDelay(2000). Les LED basculent l’une
après l’autre, une toutes les 2 s, bien après les appuis : les messages
attendent dans la file. La file contient 8 éléments ; ledTask en a
déjà retiré un, qu’elle traite. Elle absorbe donc 9 appuis ; au-delà,
osMessageQueuePut échoue (délai 0) et les appuis sont perdus. Ici,
dix appuis : un perdu. Une file absorbe les rafales, mais si le
consommateur est durablement plus lent que le producteur, elle déborde
toujours : sa taille se choisit d’après la plus longue rafale à absorber.
Étape 2 — Une interruption réveille une tâche
Dans CubeMX, configurez SW1 (PE10) en GPIO_EXTI10, mode External Interrupt Mode with Rising edge trigger detection, sans tirage.
Dans System Core → NVIC, activez EXTI line[15:10] interrupts, et donnez-lui une priorité de préemption de 5. Cochez la colonne Uses FreeRTOS functions si votre version de CubeMX l’affiche.
Dans l’onglet Timers and Semaphores de FREERTOS, créez un sémaphore binaire
sw1Sem, et une tâchesw1Task(StartSw1) en priorité osPriorityAboveNormal.Dans le rappel d’interruption, levez le sémaphore :
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == SW1_Pin) { osSemaphoreRelease(sw1SemHandle); } }
StartSw1attend le sémaphore, fait basculer LED_4, compte les appuis et affiche le compteur au terminal.
Retirez SW1 de la lecture par scrutation de StartButton : il est désormais
géré par interruption.
Questions
Le sémaphore est créé avec une valeur initiale. Que se passe-t-il au démarrage s’il vaut 1 ?
Appuyez une fois sur SW1. Le compteur augmente-t-il parfois de plus d’une unité ? D’où viennent les appuis en trop ?
Proposez un anti-rebond dans la tâche (pas dans l’interruption), et appliquez-le.
Pourquoi ne pas faire basculer la LED et appeler
printfdirectement dansHAL_GPIO_EXTI_Callback?
Solution de l’étape 2
Le rappel d’interruption (USER CODE BEGIN 0, ou 4) :
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == SW1_Pin)
{
osSemaphoreRelease(sw1SemHandle);
}
}
et la tâche, avec l’anti-rebond de la question 5 :
void StartSw1(void *argument)
{
uint32_t presses = 0u;
(void)argument;
for (;;)
{
osSemaphoreAcquire(sw1SemHandle, osWaitForever); /* attend un front */
osDelay(50); /* rebonds finis */
(void)osSemaphoreAcquire(sw1SemHandle, 0u); /* oublie ceux levés entre-temps */
if (HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin) != GPIO_PIN_SET) { continue; }
HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin);
presses++;
printf("SW1 : %lu appuis\r\n", (unsigned long)presses);
}
}
Dans StartButton, la boucle commence à b = 1 : SW1 n’est plus scruté.
Le chemin de l’interruption. Un front montant sur PE10 lève le drapeau
de la ligne 10 de l”EXTI (le contrôleur d’interruptions externes). Les
lignes 10 à 15 partagent une seule interruption, EXTI15_10_IRQHandler
(dans stm32h7xx_it.c), qui appelle HAL_GPIO_EXTI_IRQHandler, qui
efface le drapeau et appelle notre rappel avec le numéro de broche. D’où
le test GPIO_Pin == SW1_Pin : le même rappel sert à toutes les lignes.
Pourquoi la priorité 5. FreeRTOS protège ses structures en masquant
les interruptions de priorité numérique 5 à 15 pendant ses sections
critiques (configMAX_SYSCALL_INTERRUPT_PRIORITY). Une interruption
plus urgente (0 à 4) n’est jamais masquée : excellente pour réagir vite,
mais elle pourrait interrompre FreeRTOS au milieu d’une mise à jour de ses
listes, et osSemaphoreRelease y trouverait un état incohérent. Une
telle interruption ne doit appeler aucune fonction de FreeRTOS. Avec
le contrôle de configuration actif (configASSERT), FreeRTOS arrête le
programme si la règle est violée — sinon, c’est un plantage rare et
inexplicable.
osSemaphoreRelease dans une interruption. En CMSIS-RTOS2, la même
fonction sert aux tâches et aux interruptions : elle détecte qu’elle est en
interruption et appelle la version FromISR de FreeRTOS, qui ne bloque
jamais. Si la tâche réveillée est plus prioritaire que celle qui a été
interrompue, le changement de tâche se fait dès la sortie de
l’interruption : sw1Task réagit en quelques microsecondes.
Réponses aux questions 3 à 6
3. La valeur initiale. S’il vaut 1, le sémaphore est déjà levé au
démarrage : sw1Task le prend aussitôt, croit à un appui, bascule LED_4 et
affiche « 1 appui » sans qu’on ait rien touché. Selon la version, CubeMX
crée un sémaphore binaire disponible (1) par défaut : il faut choisir
l’état initial Depleted (0), ou le vider au démarrage de la tâche. Un
sémaphore qui signale un événement doit partir à 0 ; un sémaphore qui
garde une ressource part à 1 (la ressource est libre).
4. Plusieurs appuis comptés. Oui, souvent. Le contact rebondit (partie 1, TP 1) : un appui produit plusieurs fronts montants en quelques millisecondes, donc plusieurs interruptions. Le sémaphore étant binaire, les fronts qui arrivent pendant que la tâche n’a pas encore pris le sémaphore se fondent en un seul (on lève un drapeau déjà levé). Mais ceux qui arrivent après qu’elle l’a pris le relèvent : la tâche repasse une seconde fois.
5. Un anti-rebond dans la tâche : voir la solution ci-dessus. Après le
premier front, la tâche attend 50 ms (les rebonds sont finis), vide
le sémaphore levé par les rebonds (un osSemaphoreAcquire à délai nul,
qui réussit ou échoue sans attendre), puis vérifie que le bouton est
bien enfoncé (un parasite bref sur la ligne serait écarté). L’interruption,
elle, reste réduite au minimum.
6. Pourquoi pas tout dans l’interruption. Une interruption doit être
très courte : pendant qu’elle s’exécute, les tâches sont arrêtées, et
les interruptions de priorité égale ou inférieure attendent. printf sur
la liaison série dure plusieurs millisecondes, et attend l’UART en
tournant. De plus, printf n’est pas fait pour être appelé en
interruption : il peut utiliser des verrous de la bibliothèque C, qu’une
tâche interrompue tient peut-être déjà — blocage assuré. La règle : dans
l’interruption, constater et signaler ; dans une tâche, agir.
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.
Messages structurés : la file transporte une structure
{numéro du bouton, instant de l'appui}; la tâche réceptrice affiche le temps écoulé entre deux appuis.Durée d’appui : l’interruption se déclenche sur les deux fronts, et place l’instant dans une file (
osMessageQueuePutavec un délai 0) ; une tâche calcule et affiche la durée de chaque appui.Sémaphore à compteur : remplacez le sémaphore binaire par un sémaphore à compteur (
osSemaphoreNew(10, 0, NULL)) et comparez le nombre d’appuis comptés quand la tâche est lente.Drapeaux de tâche : refaites l’étape 2 avec
osThreadFlagsSetetosThreadFlagsWait, plus légers qu’un sémaphore.
Après le TP
Pour rendre la carte au robot, voir Remettre la carte en état RatBot.