TP 9 — Charge du processeur =========================== Objectifs --------- - mesurer le **temps processeur** consommé par chaque tâche ; - en déduire la **charge** totale, à partir du temps passé dans la tâche d'inactivité ; - repérer la tâche qui coûte, et vérifier qu'une optimisation a payé. Durée indicative : une séance de 3 h. Prérequis : :doc:`tp04_partager` (la fonction ``busy_ms``) et :doc:`tp06_dma` (le compteur de cycles). Ce qu'il faut savoir -------------------- Le temps passé dans chaque tâche ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FreeRTOS peut tenir, pour chaque tâche, le **temps cumulé** pendant lequel elle a eu le processeur. Il lui faut pour cela une horloge **plus fine que son tic** : à chaque changement de tâche, il lit cette horloge et ajoute l'écart au compteur de la tâche qui s'arrête. On utilise ici le compteur de cycles ``DWT->CYCCNT`` : une unité par cycle, 64 millions par seconde. La tâche d'inactivité ~~~~~~~~~~~~~~~~~~~~~ Quand aucune tâche n'est prête, FreeRTOS exécute la **tâche d'inactivité** (*idle task*), de priorité minimale. Le temps qu'elle consomme est le temps **libre** du processeur. La charge vaut donc : .. code-block:: text charge = 100 % − part du temps passée dans la tâche d'inactivité Le firmware du robot publie ainsi sa charge, et la marge de pile de chaque tâche, dans ses diagnostics. Étape 1 — Activer les statistiques ---------------------------------- #. Créez un projet ``rtos_tp09_charge`` comme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 et ``printf``, LED_1 à LED_4, SW1 et SW2). #. Dans **FREERTOS → Config parameters**, activez **USE_TRACE_FACILITY**, **GENERATE_RUN_TIME_STATS** et **USE_STATS_FORMATTING_FUNCTIONS**. #. Générez le code. CubeMX ajoute deux fonctions vides, à compléter (cherchez ``configureTimerForRunTimeStats``) : .. Capture d'écran à insérer : CubeMX, FREERTOS → Config parameters : USE_TRACE_FACILITY, GENERATE_RUN_TIME_STATS et USE_STATS_FORMATTING_FUNCTIONS activés. Déposez _static/captures/rtos/tp09-config-stats.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp09-config-stats.png .. :align: center .. :width: 90% .. code-block:: c void configureTimerForRunTimeStats(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0u; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } unsigned long getRunTimeCounterValue(void) { return DWT->CYCCNT; } .. dropdown:: Solution de l'étape 1 :icon: code :color: secondary Les deux fonctions générées vides par CubeMX, complétées : .. code-block:: c void configureTimerForRunTimeStats(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; /* autorise le DWT */ DWT->CYCCNT = 0u; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; /* lance le compteur */ } unsigned long getRunTimeCounterValue(void) { return DWT->CYCCNT; } **Comment FreeRTOS s'en sert.** ``GENERATE_RUN_TIME_STATS`` définit, dans ``FreeRTOSConfig.h``, deux macros (``portCONFIGURE_TIMER_FOR_RUN_TIME_STATS`` et ``portGET_RUN_TIME_COUNTER_VALUE``) qui appellent ces fonctions. Au démarrage de l'ordonnanceur, il appelle la première. Puis, **à chaque changement de tâche**, il lit le compteur, et ajoute au total de la tâche qui s'arrête le temps écoulé depuis qu'elle avait pris la main. La mesure est exacte au cycle près, même pour une tâche qui ne tourne que quelques microsecondes entre deux tics. Étape 2 — Le tableau des tâches ------------------------------- Créez trois tâches en priorité normale : - ``blinkTask`` : fait clignoter LED_1 toutes les 100 ms ; - ``loadTask`` : toutes les 100 ms, calcule pendant ``load`` millisecondes avec ``busy_ms`` (TP 4). ``load`` vaut 10 au départ ; SW1 l'augmente de 10, SW2 le diminue de 10 (entre 0 et 90) ; - ``statsTask`` : toutes les 5 s, affiche le tableau des tâches : .. code-block:: c static char buf[512]; vTaskGetRunTimeStats(buf); printf("%s\r\n", buf); .. admonition:: Questions 1. Que contiennent les colonnes du tableau ? Retrouvez-vous les tâches de CubeMX, la tâche ``IDLE`` et la tâche des timers ``Tmr Svc`` ? 2. Quelle part du temps ``loadTask`` consomme-t-elle avec ``load`` à 10 ? à 50 ? Est-ce le résultat attendu ? 3. Pourquoi ``buf`` est-il déclaré ``static`` ? Que se passerait-il avec un tableau local de 512 octets et une pile de 256 mots ? .. dropdown:: Solution de l'étape 2 :icon: code :color: secondary .. code-block:: c static volatile uint32_t g_load_ms = 10u; void StartLoad(void *argument) { uint32_t next = osKernelGetTickCount(); uint8_t prev1 = 0u, prev2 = 0u; (void)argument; for (;;) { uint8_t sw1 = (HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin) == GPIO_PIN_SET) ? 1u : 0u; uint8_t sw2 = (HAL_GPIO_ReadPin(SW2_GPIO_Port, SW2_Pin) == GPIO_PIN_SET) ? 1u : 0u; if (sw1 && !prev1 && (g_load_ms < 90u)) { g_load_ms += 10u; } if (sw2 && !prev2 && (g_load_ms > 0u)) { g_load_ms -= 10u; } prev1 = sw1; prev2 = sw2; busy_ms(g_load_ms); /* calcul : load ms sur 100 */ next += 100u; osDelayUntil(next); } } void StartStats(void *argument) { static char buf[512]; (void)argument; for (;;) { osDelay(5000); vTaskGetRunTimeStats(buf); printf("%s\r\n", buf); } } Les boutons sont lus toutes les 100 ms, au rythme de la tâche : c'est assez lent pour ignorer les rebonds, sans autre précaution. .. dropdown:: Réponses aux questions 1 à 3 :icon: light-bulb :color: info **1. Les colonnes.** Pour chaque tâche : son **nom**, son **temps cumulé** (en unités du compteur, ici des cycles à 64 MHz), et sa **part** du temps total depuis le démarrage, en pourcentage. On y trouve les tâches créées dans CubeMX (``defaultTask`` si elle est gardée), **IDLE** (la tâche d'inactivité, créée par FreeRTOS) et **Tmr Svc** (la tâche des timers, créée si ``configUSE_TIMERS`` vaut 1, ce qui est le cas par défaut). Les interruptions n'apparaissent pas : leur temps est compté à la tâche qu'elles ont interrompue. **2. La part de loadTask.** Environ **10 %** avec ``load`` à 10, **50 %** avec ``load`` à 50 : ``load`` millisecondes de calcul toutes les 100 ms. Mais le tableau donne des parts **depuis le démarrage** : après avoir changé ``load``, la part n'évolue que lentement vers la nouvelle valeur, d'autant plus lentement que la carte tourne depuis longtemps. C'est l'objet de l'étape 3. **3. buf en static.** ``vTaskGetRunTimeStats`` écrit le tableau dans le tampon : environ 40 octets par tâche. Un tableau **local** de 512 octets serait sur la **pile** de la tâche ; avec une pile de 256 mots (1 Kio), il en prendrait la moitié, et ``printf`` (plusieurs centaines d'octets, TP 2) ferait le reste : **débordement**. ``static`` met le tampon en mémoire globale, une fois pour toutes. Règle générale sur microcontrôleur : un gros tableau ne va **jamais** sur la pile. Étape 3 — La charge sur une fenêtre ----------------------------------- Le tableau donne des parts **depuis le démarrage** : un pic récent y est noyé. Calculons la charge sur les **5 dernières secondes** : #. Dans ``statsTask``, mémorisez à chaque tour le temps total (``getRunTimeCounterValue()``) et le temps de la tâche d'inactivité (``ulTaskGetIdleRunTimeCounter()``). #. La charge sur la fenêtre est ``100 − 100 × Δidle / Δtotal``, avec les écarts depuis le tour précédent. Affichez-la, et allumez LED_4 au-delà de 80 %. .. Capture d'écran à insérer : le terminal : le tableau de vTaskGetRunTimeStats et la charge sur 5 s. Déposez _static/captures/rtos/tp09-tableau-taches.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp09-tableau-taches.png .. :align: center .. :width: 90% .. admonition:: Questions 4. Faites varier ``load`` avec les boutons : la charge suit-elle ? 5. ``DWT->CYCCNT`` est un compteur de 32 bits à 64 MHz. Au bout de combien de temps revient-il à zéro ? Le calcul par écarts de l'étape 3 reste-t-il juste à ce moment ? Et le tableau de l'étape 2 ? 6. ``statsTask`` elle-même consomme du temps : combien ? Comment le voir ? .. dropdown:: Solution de l'étape 3 :icon: code :color: secondary Dans ``StartStats``, avant la boucle : .. code-block:: c uint32_t prev_total = getRunTimeCounterValue(); uint32_t prev_idle = ulTaskGetIdleRunTimeCounter(); et à chaque tour : .. code-block:: c uint32_t total = getRunTimeCounterValue(); uint32_t idle = ulTaskGetIdleRunTimeCounter(); uint32_t d_total = total - prev_total; /* juste même au passage par 0 */ uint32_t d_idle = idle - prev_idle; uint32_t load_pct; prev_total = total; prev_idle = idle; load_pct = (d_total > 0u) ? 100u - (uint32_t)(((uint64_t)d_idle * 100u) / d_total) : 0u; printf("consigne %lu ms / 100 ms -> charge %lu %%\r\n", (unsigned long)g_load_ms, (unsigned long)load_pct); HAL_GPIO_WritePin(LED_4_GPIO_Port, LED_4_Pin, (load_pct > 80u) ? GPIO_PIN_SET : GPIO_PIN_RESET); **Le calcul sur 64 bits.** ``d_idle × 100`` peut dépasser 2\ :sup:`32` : sur 5 s, ``d_idle`` atteint 320 millions de cycles, et ×100 donne 32 milliards. Le transtypage en ``uint64_t`` évite le débordement. **%%** dans ``printf`` affiche un signe pour cent. .. dropdown:: Réponses aux questions 4 à 6 :icon: light-bulb :color: info **4. La charge suit-elle ?** Oui, en une fenêtre de 5 s : elle vaut ``load`` pour cent, **plus** quelques pourcents. Ce surplus est le travail des autres tâches (le clignotement, ``statsTask`` elle-même), et surtout le coût du **système** : les interruptions du tic, les changements de tâche. **5. Le rebouclage du compteur.** 2\ :sup:`32` / 64 000 000 = **67 s**. - Le calcul **par écarts** (étape 3) reste juste : la soustraction de deux entiers non signés se fait modulo 2\ :sup:`32`, et donne l'écart réel tant qu'il est inférieur à 67 s. La fenêtre fait 5 s : aucun problème. - Le **tableau cumulé** (étape 2) devient **faux** après 67 s : le total et les compteurs des tâches rebouclent chacun à des moments différents, et les pourcentages n'ont plus de sens. Pour une mesure longue, il faut un compteur plus **lent** (un timer à 10 ou 100 kHz, qui reboucle au bout de 5 jours ou de 12 heures), ou de **64 bits** (``configRUN_TIME_COUNTER_TYPE``). **6. Le coût de statsTask.** Sa propre ligne du tableau le donne : on la lit comme les autres. Il est presque entièrement dû à ``printf`` : le tableau fait quelques centaines de caractères, envoyés en mode bloquant à 87 µs chacun, soit **plusieurs dizaines de millisecondes** toutes les 5 s — de l'ordre de 1 % du processeur, pour mesurer… le processeur. C'est l'**effet de la mesure sur ce qu'on mesure**, à garder en tête ; l'émission par DMA (TP 6, et dernier exercice) le réduit presque à rien. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/rtos/tp09/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'écran** : affichez la charge et la marge de pile de chaque tâche sur l'écran OLED (TP 8 de la partie 1), rafraîchi toutes les secondes. - **Dormir quand on n'a rien à faire** : dans le crochet de la tâche d'inactivité (``vApplicationIdleHook``, à activer dans CubeMX), exécutez ``__WFI()`` pour arrêter le cœur jusqu'à la prochaine interruption. La mesure de charge change-t-elle ? Qu'est-ce qui change, physiquement ? - **Retrouver le coût de printf** : mesurez le temps processeur de ``statsTask`` avec ``printf`` bloquant, puis avec l'émission par DMA du TP 6. Après le TP ----------- Pour rendre la carte au robot, voir :ref:`etat-ratbot`.