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 : :doc:`tp02_priorites`, et l'anti-rebond du :doc:`../sans_os/tp01_gpio`. 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 : - ``osMessageQueuePut`` y **copie** un message ; si la file est pleine, la tâche attend (ou abandonne, selon le délai donné) ; - ``osMessageQueueGet`` en 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_communiquer`` comme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 et ``printf``), 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``) et ``ledTask`` (``StartLed``), et une file ``buttonQueue`` de **8** éléments de type ``uint8_t``. #. ``StartButton`` lit 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. #. ``StartLed`` attend un message avec ``osWaitForever``, et fait basculer la LED de même numéro. .. admonition:: Questions 1. Que fait ``ledTask`` entre deux appuis ? Combien de processeur consomme-t-elle ? 2. Ajoutez un ``osDelay(2000)`` dans ``StartLed`` après chaque bascule, et appuyez dix fois très vite. Que se passe-t-il ? Combien d'appuis sont perdus, et pourquoi ce nombre ? .. dropdown:: Solution de l'étape 1 :icon: code :color: secondary .. code-block:: c /* 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. .. dropdown:: Réponses aux questions 1 et 2 :icon: light-bulb :color: info **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âche ``sw1Task`` (``StartSw1``) en priorité **osPriorityAboveNormal**. #. Dans le rappel d'interruption, levez le sémaphore : .. Capture d'écran à insérer : CubeMX, NVIC : EXTI line[15:10] activée, priorité de préemption 5, « Uses FreeRTOS functions » cochée. Déposez _static/captures/rtos/tp03-nvic-exti.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp03-nvic-exti.png .. :align: center .. :width: 90% .. code-block:: c void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == SW1_Pin) { osSemaphoreRelease(sw1SemHandle); } } #. ``StartSw1`` attend 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. .. admonition:: Questions 3. Le sémaphore est créé avec une valeur initiale. Que se passe-t-il au démarrage s'il vaut 1 ? 4. Appuyez une fois sur SW1. Le compteur augmente-t-il parfois de plus d'une unité ? D'où viennent les appuis en trop ? 5. Proposez un anti-rebond **dans la tâche** (pas dans l'interruption), et appliquez-le. 6. Pourquoi ne pas faire basculer la LED et appeler ``printf`` directement dans ``HAL_GPIO_EXTI_Callback`` ? .. dropdown:: Solution de l'étape 2 :icon: code :color: secondary Le rappel d'interruption (``USER CODE BEGIN 0``, ou ``4``) : .. code-block:: c 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 : .. code-block:: c 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. .. dropdown:: Réponses aux questions 3 à 6 :icon: light-bulb :color: info **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**. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/rtos/tp03/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 (``osMessageQueuePut`` avec 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 ``osThreadFlagsSet`` et ``osThreadFlagsWait``, plus légers qu'un sémaphore. Après le TP ----------- Pour rendre la carte au robot, voir :ref:`etat-ratbot`.