TP 1 — Premières tâches ======================= Objectifs --------- - comprendre ce qu'apporte un système temps réel par rapport à la boucle infinie de la partie 1 ; - créer un projet FreeRTOS avec CubeMX, et plusieurs **tâches** ; - voir la différence entre une attente qui **bloque la tâche** (``osDelay``) et une attente qui **occupe le processeur** (``HAL_Delay``). Durée indicative : une séance de 3 h. Prérequis : la partie 1, au moins :doc:`../sans_os/tp01_gpio`. Ce qu'il faut savoir -------------------- Le problème de la boucle infinie ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Dans la partie 1, une seule boucle fait tout. Pour faire clignoter une LED toutes les 500 ms **et** une autre toutes les 130 ms, il a fallu remplacer ``HAL_Delay`` par des comparaisons avec ``HAL_GetTick()`` : chaque nouvelle activité complique la boucle, et une seule opération lente retarde toutes les autres. Les tâches ~~~~~~~~~~ **FreeRTOS** découpe le programme en **tâches** : chacune est une fonction avec sa propre boucle infinie, sa propre pile, et l'air de s'exécuter seule. Un **ordonnanceur** passe de l'une à l'autre, au rythme d'un **tic** d'horloge, ici 1 000 fois par seconde. Une tâche est toujours dans l'un de ces états : - **en cours** (*running*) : elle a le processeur ; - **prête** (*ready*) : elle pourrait tourner, et attend son tour ; - **bloquée** (*blocked*) : elle attend quelque chose (un délai, un message), et ne consomme rien. .. tikz:: Les trois états d'une tâche, et ce qui la fait passer de l'un à l'autre. \tikzset{st/.style={draw, circle, minimum size=22mm, align=center, font=\small, fill=blue!6}, tr/.style={-{Stealth}, thick}, lb/.style={font=\scriptsize, align=center}} \node[st, fill=green!12] (run) at (0,0) {en cours\\[-2pt]\scriptsize\itshape running}; \node[st] (rdy) at (-4.2,-3) {prête\\[-2pt]\scriptsize\itshape ready}; \node[st, fill=orange!12] (blk) at (4.2,-3) {bloquée\\[-2pt]\scriptsize\itshape blocked}; \draw[tr] (rdy) to[bend left=20] node[lb, above left]{l'ordonnanceur\\la choisit} (run); \draw[tr] (run) to[bend left=20] node[lb, below right, pos=0.4]{préemptée par plus\\prioritaire, ou fin\\de son tour (tic)} (rdy); \draw[tr] (run) to[bend left=20] node[lb, above right]{osDelay, attente d'un\\message, d'un sémaphore} (blk); \draw[tr] (blk) to[bend left=10] node[lb, below]{délai écoulé, message arrivé, sémaphore levé} (rdy); ``osDelay(500)`` **bloque** la tâche pendant 500 tics : le processeur passe aussitôt aux autres tâches. C'est toute la différence avec ``HAL_Delay``, qui attend en tournant en rond. Étape 1 — Le projet FreeRTOS ---------------------------- #. Créez un projet ``rtos_tp01_taches`` comme au TP 1 de la partie 1 (STM32H750VBTx, **SYS → Debug : Serial Wire**, horloge par défaut), avec **LED_1** à **LED_4** en sortie et **SW1** (PE10) en entrée sans tirage. #. Dans **Middleware and Software Packs → FREERTOS**, choisissez l'interface **CMSIS_V2**. #. **La base de temps de la HAL.** FreeRTOS utilise le minuteur *SysTick* pour son tic. La HAL ne doit plus s'en servir pour ``HAL_Delay`` : dans **System Core → SYS**, réglez **Timebase Source** sur **TIM6**. CubeMX affiche un avertissement tant que ce n'est pas fait. #. Dans l'onglet **Tasks and Queues** de FREERTOS, gardez ``defaultTask`` et ajoutez deux tâches : .. Capture d'écran à insérer : CubeMX, SYS : Timebase Source sur TIM6. Déposez _static/captures/rtos/tp01-timebase-tim6.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp01-timebase-tim6.png .. :align: center .. :width: 90% .. list-table:: :header-rows: 1 * - Nom - Fonction - Priorité - Pile (mots) * - ``led1Task`` - ``StartLed1`` - osPriorityNormal - 256 * - ``led2Task`` - ``StartLed2`` - osPriorityNormal - 256 #. Générez le code. Chaque tâche a maintenant une fonction, avec une boucle ``for (;;)`` et un ``osDelay(1)`` de départ. Selon la version de CubeMX, ces fonctions sont dans ``main.c`` ou dans ``freertos.c`` : cherchez ``StartLed1`` (**Ctrl-H**). .. Capture d'écran à insérer : CubeMX, FREERTOS, onglet Tasks and Queues avec defaultTask, led1Task et led2Task. Déposez _static/captures/rtos/tp01-tasks-and-queues.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp01-tasks-and-queues.png .. :align: center .. :width: 90% .. important:: Après ``osKernelStart()``, la boucle ``while (1)`` de ``main`` n'est **plus jamais exécutée** : c'est l'ordonnanceur qui a la main. Tout le travail se fait désormais dans les tâches. Étape 2 — Deux clignotements indépendants ----------------------------------------- #. Dans ``StartLed1``, faites clignoter LED_1 : bascule, puis ``osDelay(500)``. #. Dans ``StartLed2``, faites clignoter LED_2 : bascule, puis ``osDelay(130)``. #. Dans ``StartDefaultTask``, allumez LED_3 tant que SW1 est enfoncé, en lisant le bouton toutes les 20 ms. Chaque tâche est écrite comme si elle était seule, avec une attente simple : c'est le gain d'un système temps réel. .. admonition:: Questions 1. Comparez ce code avec la boucle de la partie 1 qui faisait la même chose avec ``HAL_GetTick()``. Lequel est le plus facile à modifier si l'on ajoute une troisième LED à un autre rythme ? 2. Combien de temps chaque tâche passe-t-elle réellement à travailler, sur une période de clignotement ? Que fait le processeur le reste du temps ? .. dropdown:: Solution de l'étape 2 :icon: code :color: secondary CubeMX a créé trois fonctions, une par tâche, chacune avec sa boucle ``for (;;)``. On les complète : .. code-block:: c void StartDefaultTask(void *argument) { (void)argument; for (;;) { HAL_GPIO_WritePin(LED_3_GPIO_Port, LED_3_Pin, HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin)); osDelay(20); /* lecture toutes les 20 ms */ } } void StartLed1(void *argument) { (void)argument; for (;;) { HAL_GPIO_TogglePin(LED_1_GPIO_Port, LED_1_Pin); osDelay(500); } } void StartLed2(void *argument) { (void)argument; for (;;) { HAL_GPIO_TogglePin(LED_2_GPIO_Port, LED_2_Pin); osDelay(130); } } **Ce que fait le système.** ``osKernelStart()``, à la fin de ``main``, démarre l'**ordonnanceur** : il choisit une tâche prête et lui donne le processeur. 1 000 fois par seconde, l'interruption **SysTick** (le *tic*) le réveille : il fait avancer le temps, réveille les tâches dont le délai est écoulé, et décide s'il faut changer de tâche. Changer de tâche, c'est sauvegarder les registres du processeur sur la pile de la tâche qui s'arrête, et restaurer ceux de la suivante (une interruption logicielle, **PendSV**, s'en charge). Chaque tâche a donc **sa propre pile** : c'est ce qui lui permet de reprendre exactement là où elle s'était arrêtée. **osDelay(500)** fait passer la tâche dans l'état **bloqué** jusqu'au 500\ :sup:`e` tic suivant. L'ordonnanceur ne la regarde plus pendant ce temps : elle ne coûte rien. **(void)argument** dit au compilateur que le paramètre est volontairement inutilisé : sans cela, il avertit. .. dropdown:: Réponses aux questions 1 et 2 :icon: light-bulb :color: info **1. Comparer avec la boucle de la partie 1.** Avec FreeRTOS, ajouter une LED à un autre rythme, c'est **ajouter une tâche** de quatre lignes, écrite comme si elle était seule, sans toucher aux autres. Avec la boucle et ``HAL_GetTick()``, il faut une variable de temps de plus, un test de plus, et vérifier que rien d'autre dans la boucle ne bloque. Le code de chaque activité y est **mélangé** à celui des autres : avec dix activités, la boucle devient illisible. FreeRTOS **sépare** les activités. **2. Le temps de travail.** Basculer une LED prend quelques dizaines de cycles, soit **moins d'une microseconde**, par période de 130 ou 500 ms. Les trois tâches travaillent donc environ un millionième du temps. Le reste du temps, elles sont toutes bloquées, et l'ordonnanceur exécute la **tâche d'inactivité** (*idle task*), créée automatiquement, de priorité minimale, qui ne fait que tourner en rond — ou, si on le lui demande, met le cœur en sommeil (instruction ``WFI``) jusqu'à la prochaine interruption pour économiser l'énergie. Le TP 9 mesure ce temps libre. Étape 3 — ``osDelay`` contre ``HAL_Delay`` ------------------------------------------ #. Dans ``StartLed2``, remplacez ``osDelay(130)`` par ``HAL_Delay(130)``. Les deux LED clignotent-elles toujours correctement ? #. Dans l'onglet **Tasks and Queues**, passez ``led2Task`` en priorité **osPriorityAboveNormal**, régénérez, et recommencez l'essai avec ``HAL_Delay``. Et avec ``osDelay`` ? .. admonition:: Questions 3. À priorité égale, pourquoi les deux LED clignotent-elles encore avec ``HAL_Delay`` ? (Indice : l'ordonnanceur partage le temps entre tâches de même priorité à chaque tic.) 4. Avec une priorité plus haute et ``HAL_Delay``, que deviennent LED_1 et LED_3 ? Pourquoi ce problème n'existe-t-il pas avec ``osDelay`` ? 5. En conclusion : dans une tâche, quelle fonction d'attente faut-il utiliser ? .. dropdown:: Réponses aux questions 3 à 5 :icon: light-bulb :color: info **3. À priorité égale, avec HAL_Delay.** Les deux LED clignotent encore, car FreeRTOS **partage le temps** (*time slicing*) entre les tâches prêtes de même priorité : à chaque tic, il passe à la suivante. ``led2Task``, qui tourne en rond dans ``HAL_Delay``, reste **prête** en permanence ; mais quand le délai de ``led1Task`` ou de ``defaultTask`` expire, celle-ci redevient prête, et au tic suivant (au plus 1 ms plus tard) l'ordonnanceur lui donne son tour. Elle bascule sa LED et se rebloque. Tout marche donc, mais ``led2Task`` **gaspille** le processeur : pendant ses 130 ms d'attente, elle consomme tout le temps que les autres ne prennent pas. **4. À priorité plus haute, avec HAL_Delay.** LED_1 et LED_3 **se figent**. La règle de FreeRTOS est stricte : la tâche prête la plus prioritaire s'exécute. Or ``led2Task``, prioritaire, n'est **jamais bloquée** : quand elle ne bascule pas sa LED, elle attend en tournant dans ``HAL_Delay``, donc elle reste prête. Les tâches de priorité normale ne reçoivent plus jamais le processeur : c'est la **famine** (TP 2). Avec ``osDelay``, ``led2Task`` se bloque 130 ms à chaque tour : les autres en profitent. À noter : ``HAL_Delay`` marche encore ici parce que son compteur avance grâce à l'interruption de **TIM6**, la base de temps de la HAL (étape 1). Les interruptions, elles, passent toujours avant les tâches. **5. La conclusion.** Dans une tâche, on attend **toujours** avec une fonction du système, qui **bloque** la tâche : ``osDelay``, ``osDelayUntil`` (TP 2), ou l'attente d'un message ou d'un sémaphore (TP 3). ``HAL_Delay`` n'a sa place qu'avant le démarrage de l'ordonnanceur. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/rtos/tp01/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. - **Quatre LED, quatre tâches** : chaque LED de façade a sa tâche et son rythme ; une seule fonction de tâche suffit, si l'on passe le numéro de la LED en argument (``argument`` de ``osThreadNew``). - **La pile** : réduisez la pile d'une tâche à 64 mots, ajoutez un grand tableau local, et observez. Cherchez ``configCHECK_FOR_STACK_OVERFLOW``. - **Mesurer la pile** : affichez au terminal (TP 5 de la partie 1) la marge de pile restante de chaque tâche, avec ``uxTaskGetStackHighWaterMark``. Après le TP ----------- Pour rendre la carte au robot, voir :ref:`etat-ratbot`.