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 : TP 4 — Partager une ressource
(la fonction busy_ms) et TP 6 — Le 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 :
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_chargecomme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 etprintf, 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) :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; }
Solution de l’étape 1
Les deux fonctions générées vides par CubeMX, complétées :
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 pendantloadmillisecondes avecbusy_ms(TP 4).loadvaut 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 :static char buf[512]; vTaskGetRunTimeStats(buf); printf("%s\r\n", buf);
Questions
Que contiennent les colonnes du tableau ? Retrouvez-vous les tâches de CubeMX, la tâche
IDLEet la tâche des timersTmr Svc?Quelle part du temps
loadTaskconsomme-t-elle avecloadà 10 ? à 50 ? Est-ce le résultat attendu ?Pourquoi
bufest-il déclaréstatic? Que se passerait-il avec un tableau local de 512 octets et une pile de 256 mots ?
Solution de l’étape 2
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.
Réponses aux questions 1 à 3
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 %.
Questions
Faites varier
loadavec les boutons : la charge suit-elle ?DWT->CYCCNTest 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 ?statsTaskelle-même consomme du temps : combien ? Comment le voir ?
Solution de l’étape 3
Dans StartStats, avant la boucle :
uint32_t prev_total = getRunTimeCounterValue();
uint32_t prev_idle = ulTaskGetIdleRunTimeCounter();
et à chaque tour :
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 232 :
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.
Réponses aux questions 4 à 6
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. 232 / 64 000 000 = 67 s.
Le calcul par écarts (étape 3) reste juste : la soustraction de deux entiers non signés se fait modulo 232, 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.
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.
À 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
statsTaskavecprintfbloquant, puis avec l’émission par DMA du TP 6.
Après le TP
Pour rendre la carte au robot, voir Remettre la carte en état RatBot.