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 TP 1 — Allumer une LED, lire un bouton.

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.

Figure made with TikZ

Les trois états d’une tâche, et ce qui la fait passer de l’un à l’autre.

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

  1. 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.

  2. Dans Middleware and Software Packs → FREERTOS, choisissez l’interface CMSIS_V2.

  3. 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.

  4. Dans l’onglet Tasks and Queues de FREERTOS, gardez defaultTask et ajoutez deux tâches :

    Nom

    Fonction

    Priorité

    Pile (mots)

    led1Task

    StartLed1

    osPriorityNormal

    256

    led2Task

    StartLed2

    osPriorityNormal

    256

  5. 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).

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

  1. Dans StartLed1, faites clignoter LED_1 : bascule, puis osDelay(500).

  2. Dans StartLed2, faites clignoter LED_2 : bascule, puis osDelay(130).

  3. 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.

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 ?

Solution de l’étape 2

CubeMX a créé trois fonctions, une par tâche, chacune avec sa boucle for (;;). On les complète :

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 500e 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.

Réponses aux questions 1 et 2

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

  1. Dans StartLed2, remplacez osDelay(130) par HAL_Delay(130). Les deux LED clignotent-elles toujours correctement ?

  2. 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 ?

Questions

  1. À 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.)

  2. 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 ?

  3. En conclusion : dans une tâche, quelle fonction d’attente faut-il utiliser ?

Réponses aux questions 3 à 5

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.

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.

  • 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 Remettre la carte en état RatBot.