FreeRTOS [1], l’environnement exécutif de Richard Barry plébiscité par Amazon Web Services (AWS), fonctionne sur une plateforme matérielle ou son émulateur munis de seulement 2,5 KB de RAM. La mise en œuvre de FreeRTOS dans aussi peu de ressources fournit une opportunité de plonger dans les détails de l’implémentation de ses fonctions.
Le microcontrôleur Atmega32U4 fait partie des petites architectures 8-bits très faciles à appréhender, compte tenu de ses faibles ressources et de la taille réduite de la documentation technique limitée à quelque 400 pages [2]. Pour son malheur, il s’agit aussi d’une plateforme communément supportée par la bibliothèque Arduino avec toutes les bêtises qui traînent sur le Web autour de cette approche supposée simple de la programmation. Malgré les ressources très réduites des 2,5 KB de RAM, nous nous interrogeons sur la capacité à exécuter l’environnement exécutif FreeRTOS et ainsi aborder les techniques classiques de développement de systèmes embarqués exploitant une multitude de tâches – et donc l’accès concurrent aux ressources protégé par mutex et sémaphores – voire le partage d’informations entre tâches par les queues.
1. FreeRTOS
Nous avions déjà abordé FreeRTOS sur STM32 dans [3] et [4] en argumentant que l’environnement exécutif donne l’impression au développeur de travailler sur un système d’exploitation avec plusieurs tâches exécutées apparemment simultanément, sous la supervision d’un ordonnanceur (scheduler), mais sans autoriser l’allocation dynamique de ressources par le chargement d’un exécutable ou d’une bibliothèque, le binaire exécuté par le système embarqué étant « cross-compilé » de façon statique sur la plateforme de travail hôte – généralement un PC compatible Intel – vers la cible, ici un microcontrôleur 8-bits. Ce faisant, plusieurs développeurs peuvent collaborer sur un même projet en séparant les tâches et en partageant le fruit de leurs calculs, non par une API dont ils décident entre eux, mais par les mécanismes fournis par l’environnement exécutif, par exemple les queues de données [4]. Finalement, seul l’ordonnanceur peut garantir l’atomicité d’une opération, c’est-à-dire de ne pas changer de contexte de tâche au cours d’une opération complexe telle que lire le statut d’un drapeau, modifier ce statut et stocker le résultat : c’est donc à lui de coordonner les requêtes d’accès aux ressources communes que sont les périphériques matériels ou la mémoire, et ce grâce à des drapeaux garantissant un accès mutuellement exclusif nommés mutex.
Parmi la pléthore d’environnements exécutifs qui ne cessent de se multiplier, FreeRTOS se limite à seulement 6 fichiers portables en C donc facilement « cross-compilables », et un fichier nommé port.c qui contient les implémentations spécifiques à une architecture et un compilateur donné. Dans l’arborescence actuelle de FreeRTOS à https://github.com/FreeRTOS/FreeRTOS-Kernel, les 6 fichiers portables sont event_groups.c, list.c, queue.c, stream_buffer.c, tasks.c et timers.c tandis que la partie non portable se trouve dans... portable ! On trouvera dans la variable SRCS du Makefile de https://github.com/jmfriedt/tp_freertos/blob/master/1basic/Makefile.avr32u4 l’ensemble des fichiers nécessaires à la compilation, à savoir ceux, portables, fournis par FreeRTOS, le gestionnaire d’allocation de mémoire issu de portable/MemMang (voir plus bas ce choix), l’implémentation locale de quelques fonctions d’initialisation, de communication et d’abstraction du matériel pour rendre le code FreeRTOS portable (ainsi au lieu de manipuler les registres des GPIO – ports d’entrée-sortie généralistes – pour changer l’état des LED, nous appelons une fonction générique Led_Hi et Led_Lo dont la bibliothèque supportant un microcontrôleur donné fournit l’implémentation), et les fonctions spécifiques au microcontrôleur ciblé avec port.c. Une inspiration pour notre objectif d’appréhender l’Atmega32U4 pourrait être portable/GCC/ATMega323, mais les détails entre les deux microcontrôleurs sont trop éloignés pour fonctionner immédiatement. En partant de https://github.com/feilipu/miniAVRfreeRTOS qui a extrait une version donnée de FreeRTOS et a ajouté les fonctions spécifiques pour l’Atmega32U4, nous avons séparé la partie non portable en vue de l’insérer dans l’arborescence officielle de FreeRTOS, et ainsi toujours bénéficier des dernières évolutions sans nous arrêter à une version donnée, qui deviendra inévitablement obsolète. Il semble ainsi peu judicieux, comme le fait https://github.com/feilipu/miniAVRfreeRTOS, d’inclure la partie portable de FreeRTOS dans une archive sur GitHub qui ne peut ainsi que devenir rapidement obsolète vis-à-vis des évolutions du projet officiel, tel que nous le constatons déjà ici avec l’utilisation de FreeRTOS 10.5.0 d’il y a 7 mois (version du 16 septembre 2022).
2. FreeRTOS sur Atmega32U4
2,5 KB de mémoire ou très exactement (page 18, section 5 de [2]) 2560 octets de RAM, ce n’est vraiment pas beaucoup, tout juste ce que proposait le vénérable ZX81/TS1000 en 1982. Nous avons cependant gagné en performance en quittant le Basic pour un compilateur C performant et une utilisation efficace des pointeurs au lieu de PEEK et POKE. Par ailleurs, FreeRTOS ne gaspille pas de ressources avec des pilotes et des interfaces standardisées pour accéder au matériel, dont le contrôle passe toujours par la lecture de la datasheet et l’écriture dans les registres adéquats. Ainsi, avant toute velléité d’aborder FreeRTOS, il faudra s’assurer d’avoir des bases solides de bibliothèques permettant d’accéder aux périphériques de communication : nous avons ainsi proposé https://github.com/jmfriedt/tp_freertos/blob/master/common/usart_atmega.c le minimum vital pour initialiser le port de communication asynchrone compatible RS232 dans Usart1_Init(), l’initialisation des GPIO auxquels sont connectées les LED et une paire de fonctions pour abstraire le matériel et rendre les codes sources FreeRTOS portables entre les diverses architectures abordées par le dépôt https://github.com/jmfriedt/tp_freertos (STM32 avec la bibliothèque de ST Microelectronics ou libopencm3, Stellaris, maintenant Atmel AVR), et quelques fonctions de communication pour afficher un caractère uart_putc(char) et donc une chaîne de caractères uart_puts(char *). Nous voici donc équipés pour initialiser et communiquer avec le matériel.
En termes d’arborescence des codes sources, nous avons fait le choix de partir des codes sources de FreeRTOS disponibles à https://www.freertos.org/a00104.html (version 202212.01 à la date de rédaction), et de copier (cp -r) le contenu de Atmega32U4 de https://github.com/jmfriedt/tp_freertos dans le répertoire FreeRTOS/Source/portable/GCC/ de l’environnement exécutif pour ajouter le support de ce microcontrôleur. Finalement, dans le premier exemple le plus simple 1basic, les règles de compilation Makefile.avr32u4 font appel au contenu du répertoire FREERTOS_PATH supposé pointer sur le sommet de l’arborescence de FreeRTOS, les diverses règles se contentant de compiler les 6 fichiers portables en C de FreeRTOS, le fichier spécifique à l’architecture cible, et la bibliothèque de fonctions de communication que nous avons proposée.
L’exemple le plus simple de FreeRTOS consiste en :
qui se résume à la déclaration de deux tâches vLeds1() et vLeds2() faisant clignoter une LED sur un GPIO avec une période différente pour chaque tâche – noter le préfixe v de chaque procédure indiquant qu’elle ne renvoie rien (void) dans la convention de nommage de FreeRTOS – et vUart() qui affiche un message sur le port de communication asynchrone. Chaque tâche contient des attentes selon deux mécanismes complémentaires que sont vTaskDelayUntil() ou vTaskDelay() afin de laisser la liberté à l’ordonnanceur d’orchestrer les diverses tâches selon les règles de préemption en vigueur – option configUSE_PREEMPTION ou configUSE_TIME_SLICING dans FreeRTOSConfig.h pour choisir le mode de commutation entre tâches. On retiendra que l’implémentation de FreeRTOS sur Atmega32U4 utilise l’oscillateur du chien de garde (watchdog) pour cadencer ses opérations et que cet oscillateur est documenté comme inexact, étant simplement formé d’un grossier circuit RC au lieu d’être asservi sur un résonateur à quartz, et qu’il ne faut donc pas s’attendre à une exactitude meilleure qu’une dizaine de % sur les intervalles de temps requis. Ces attentes pour retirer la tâche de la queue d’exécution de l’ordonnanceur facilitent la prévision du cadencement des opérations au lieu de s’appuyer sur les priorités puisqu’ici, nous explicitons à l’ordonnanceur qu’une tâche a achevé ses opérations et peut laisser le temps aux autres fonctions de s’exécuter.
La fonction principale main() commence par initialiser les périphériques (Usart1_Init() et Led_Init() pour le port de communication et les GPIO en sortie), puis enregistre chaque tâche auprès de l’ordonnanceur avant de l’exécuter. La pile de la fonction main() est détruite au lancement de l’ordonnanceur par vTaskStartScheduler() et il faudra prendre soin de stocker toute variable passée en argument aux tâches sur le tas en les préfixant du qualificatif const. Nous avons cependant dû bricoler un peu pour définir des constantes en chargeant de façon conditionnelle FreeRTOSVariant.h qui romprait sinon avec la portabilité du code FreeRTOS entre diverses architectures. Le test sur la nature du microcontrôleur cible est inspiré des tests définissant le chargement de constantes dans /usr/lib/avr/include/avr/io.h qui teste la constante produite par l’argument -mmcu= en argument du compilateur avr-gcc (paquet gcc-avr sous Debian/GNU Linux et son implémentation des fonctions classiques du C dans le paquet avr-libc qui fournit ces fichiers d’en-tête) pour choisir la liste des ports actifs.
La compilation de ce programme par make -f Makefile.avr32u4, puis son transfert en mémoire non volatile du microcontrôleur par avrdude -c avr109 -b57600 -D -p atmega32u4 -P /dev/ttyACM0 -e -U flash:w:output/main.elf se conclut par son exécution telle qu’illustrée en Fig. 1.
3. Émulation de l’Atmega32U4 : simavr
Nous avions exprimé notre admiration [5] pour l’excellent émulateur de microcontrôleurs Atmel qu’est simavr et en particulier sa capacité à émuler les périphériques qui entourent le cœur du processeur. Nous allons donc vérifier le bon fonctionnement de FreeRTOS sur Atmega32U4 dans cet environnement de simulation, ne serait-ce que pour que le lecteur ne possédant pas le matériel puisse poursuivre la démonstration par une mise en pratique. Afin de ne pas nous épuiser avec une représentation graphique des LED qui nous détournerait de l’objectif initial de cette présentation, nous activons le mode de déverminage de simavr téléchargé depuis https://github.com/buserror/simavr en modifiant la ligne 25 de https://github.com/buserror/simavr/blob/master/simavr/sim/avr_ioport.c par :
Par défaut, la macro D ne fait rien et donc n’affiche pas les messages de déverminage. Une fois la modification faite, la compilation s’obtient sans trop de problèmes par make... sauf pour les applications graphiques qui échouent peut-être (toujours elles !) si nous n’ajoutons pas -lGL oublié par pkg-config --libs glu. Avec cette version mise à jour de simavr, l’émulation du microcontrôleur par :
se traduit par l’alternance de messages communiqués sur port série, et le changement d’état des LED indiqué par le changement d’état du registre contrôlant le port GPIO correspondant, tout fonctionne donc bien pour cet exemple basique (Fig. 2).
4. Dépassement de pile
FreeRTOS gère la mémoire de façon un peu originale, car alloue sa propre zone mémoire de la taille définie par la constante configTOTAL_HEAP_SIZE dans laquelle il pioche l’emplacement des piles allouées à chaque tâche (oui, c’est bien cela... FreeRTOS pioche des emplacements de piles dans une zone mémoire qualifiée de tas !). Une fois cela compris, nous allouons dans src/FreeRTOSConfig.h une taille inférieure à la taille de la RAM de l’Atmega32U4 – nous avons pour notre part :
et chaque initialisation de tâche de la forme :
peut prendre un peu de mémoire, ici 64 octets (4e argument d’après https://www.freertos.org/a00125.html), pour y stocker ses variables locales. Un problème classique en développement de systèmes embarqués est le dépassement de capacité de la pile, quand le système s’est vu allouer trop peu de mémoire pour stocker ses variables et finit soit par écrire en dehors de la RAM, soit dans la zone mémoire allouée à une autre tâche. FreeRTOS fournit un mécanisme intéressant pour détecter ce type de problème et en informer le développeur, à défaut d’y remédier. Ainsi, d’après https://www.freertos.org/Stacks-and-stack-overflow-checking.html, activer l’option configCHECK_FOR_STACK_OVERFLOW dans src/FreeRTOSConfig.h en lui assignant la valeur 1 ou 2 permet d’appeler la procédure vApplicationStackOverflowHook() en cas de détection de dépassement ou de corruption de la pile d’une tâche. Ici encore, ce mécanisme fonctionne fort bien sur Atmega32U4 : si nous modifions le programme d’affichage sur le port série en ajoutant l’allocation d’un tableau c de 256 octets et son assignation par la fonction sprintf() tel que :
et ajoutons un gestionnaire appelé en cas de détection de dépassement de pile :
alors l’exécution du programme se traduit par :
qui aidera le développeur à identifier une cause de dysfonctionnement du programme, voire éventuellement d’agir en conséquence lorsque le système embarqué passerait sinon dans un état instable lors de la corruption du contenu de la pile. Ici, le problème vient de ce que la création de la tâche n’avait alloué que 64 octets sur la pile de vPrintUart() alors que nous allouons 256 octets au tableau c. Il faut donc augmenter la taille de la pile allouée à vPrintUart() lors de l’appel à xTaskCreate() en gardant un peu de marge par rapport aux 256 octets prévus pour laisser de la place aux variables implicitement allouées par FreeRTOS pour gérer la tâche. Cet exemple met par ailleurs en évidence l’énorme consommation de ressources de stdio puisque c’est bien l’utilisation de sprintf() qui induit le dépassement de capacité de la pile de la fonction affichant sur le port série. Il suffit de lire https://github.com/avrdudes/avr-libc/blob/main/libc/stdio/vfprintf.c pour se convaincre du grand nombre de tableaux et variables intermédiaires créés par cette fonction quand souvent une procédure ciselée pour n’afficher qu’un type de variable (char, short ou long) sera plus efficace et moins gourmande en ressources que la fonction générique.
5. Affichage de la liste des tâches : le malheur du malloc()
Finalement, FreeRTOS propose une fonctionnalité sympathique d’affichage de la liste des tâches selon le nom fourni en 2e argument de xTaskCreate() et leur statut au sein de l’ordonnanceur ainsi que l’occupation de leur pile respective. Nous désirons donc tester cette fonctionnalité sur Atmega32U4 afin d’afficher une liste de tâches qui n’est pas sans rappeler la sortie de ps aux sous GNU/Linux. Pour ce faire (https://www.freertos.org/a00021.html#vTaskList), nous activons les options configUSE_TRACE_FACILITY et configUSE_STATS_FORMATTING_FUNCTIONS en les définissant à 1 dans src/FreeRTOSConfig.h et faisons appel à vTaskList(char *) qui remplit un tableau de caractères du message à afficher, par exemple au moyen de uart_puts(char *). Après compilation, nous exécutons sur émulateur ou sur microcontrôleur, pour constater l’échec : rien ne s’affiche, et pourtant les LED clignotent. L’émulateur simavr indique par ailleurs qu’un unique caractère est transmis par le port série, mais en aucun cas la liste des tâches et leurs attributs.
Puisque les autres fonctions ont opéré comme prévu, le problème vient de l’implémentation de vTaskList() que nous trouvons dans https://github.com/FreeRTOS/FreeRTOS-Kernel/blob/main/tasks.c#L4433 avec :
et là, nous constatons avec désespoir que FreeRTOS exploite une allocation dynamique de mémoire. Nous savons bien [6] que l’allocation dynamique de mémoire est interdite sur système embarqué : avec une latence non déterministe, une structure de données mémorisant les blocs mémoire alloués en vue de les libérer [7], voire une inefficacité intrinsèque à la conception du code statique qui manipule le plus souvent des structures de données de taille connue à la compilation, le couple malloc/free apprécié sur système d’exploitation allouant dynamiquement des ressources n’a pas lieu d’être sur système embarqué, au point d’en être proscrit par la norme MISRA [8]. Mais si FreeRTOS fait le choix d’utiliser l’allocation dynamique de mémoire, essayons au moins de comprendre pourquoi elle échoue, et de la réparer.
Il est connu que FreeRTOS propose plusieurs modèles de gestion du tas : https://www.freertos.org/a00111.html explique la différence entre les diverses implémentations de
FreeRTOS/FreeRTOS-Kernel/tree/main/portable/MemMang (un répertoire dans portable qui est pourtant générique !) et en particulier heap_3.c qui s’appuie sur malloc/free de la bibliothèque implémentant les fonctions C (avr-libc dans notre cas) contre heap_2.c qui implémente sa propre gestion du pointeur de tas et d’allocation/libération de ressources. Par défaut, https://github.com/feilipu/miniAVRfreeRTOS a fait le choix de heap_3, et bien entendu, l’implémentation de malloc dans avr-libc est cassée (ou tout au moins nécessite une initialisation qui n’est pas faite par défaut, voir ci-dessous). Remplacer heap_3.c par heap_2.c dans le Makefile résout en effet le problème, et l’affichage de la liste de tâches devient fonctionnel, sous émulateur comme sur plateforme matérielle (Fig. 3).
La solution n’est pas complètement satisfaisante : changer de modèle de gestion de la mémoire pour arriver à ses fins est pour le moins une méthode de programmation scabreuse. Suite à réclamation auprès de l’auteur du dépôt GitHub original dont nous nous inspirons, nous apprenons [9] que le pointeur de tas n’est pas correctement initialisé par avr-libc pour répondre à son modèle de gestion de la mémoire [10]. Ainsi, [9] préconise d’initialiser le pointeur de tas par __malloc_heap_end = (char *)(RAMEND - __malloc_margin) avant toute utilisation de l’allocation dynamique de mémoire, les constantes étant définies dans stdlib.h qu’il faut inclure au préalable. En effet, après cette modification, l’allocation dynamique de mémoire par malloc fourni par avr-libc appelé par heap_3.c devient fonctionnelle.
Conclusion
Nous avons exploré la compatibilité de FreeRTOS avec une cible munie d’une très faible quantité de mémoire – 2,5 KB – pour constater que le système est fonctionnel avec l’utilisation d’une centaine d’octets par tâche, laissant présager la capacité à exécuter quelques dizaines de tâches en plus de l’ordonnanceur et sa tâche de veille idle. Ainsi, le développeur pourra à moindres frais, voire sans frais avec un émulateur, se familiariser avec les préceptes du développement sous FreeRTOS avant d’aborder des cibles plus ambitieuses et plus complexes à maîtriser. Nous constatons néanmoins qu’une compréhension détaillée du fonctionnement du compilateur avr-gcc et surtout de l’éditeur de liens avr-ld et de la gestion et l’allocation de mémoire lors de l’exécution du programme reste nécessaire lors de l’ajout de l’abstraction de l’environnement exécutif pour identifier et pallier les causes potentielles de dysfonctionnement.
L’ensemble des codes sources discutés dans cet article est disponible à https://github.com/jmfriedt/tp_freertos/ et en particulier les sous-répertoires 1basic pour l’applicatif et Atmega32U4 pour le support de l’Atmega32U4 dans FreeRTOS.
Références
[1] A. Filiz, FreeRTOS introduction, FOSDEM 2015 à
https://archive.fosdem.org/2015/schedule/event/freertos/
et R. Barry, FreeRTOS on RISC-V, FOSEM 2019 à
https://archive.fosdem.org/2019/schedule/event/riscvfreertos/
[2] Documentation technique de l’Atmega32U4 à
https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7766-8-bit-AVR-ATmega16U4-32U4_Datasheet.pdf
[3] Q. Macé, J.-M Friedt, « FreeRTOS : application à la réalisation d’un analyseur de réseau numérique sur STM32 », GNU/Linux Magazine France 207 (sept. 2017) : https://connect.ed-diamond.com/GNU-Linux-Magazine/glmf-207/freertos-application-a-la-realisation-d-un-analyseur-de-reseau-numerique-sur-stm32
[4] J.-M Friedt, « Intercorrélation par transformée de Fourier rapide sur microcontrôleur sous FreeRTOS, et les pointeurs de pointeurs », Hackable 43 (juill.-août. 2022) : https://connect.ed-diamond.com/hackable/hk-043/intercorrelation-par-transformee-de-fourier-rapide-sur-microcontroleur-sous-freertos-et-les-pointeurs-de-pointeurs
[5] J.-M Friedt, « Émulation d’un circuit comportant un processeur Atmel avec simavr », Hackable 34 (juill.-sept. 2020) : https://connect.ed-diamond.com/Hackable/hk-034/emulation-d-un-circuit-comportant-un-processeur-atmel-avec-simavr
[6] Al Williams, Embedded Memory Allocation, Dr. Dobb’s Journal (13 octobre 2014) reproduit à
https://rosetta.vn/short/2017/05/12/embedded-memory-allocation-dr-dobbs/
[7] Memory Allocation Hooks à
https://www.gnu.org/software/libc/manual/html_node/Hooks-for-Malloc.html
[8] Norme de programmation de la Motor Industry Software Reliability Association à
https://www.misra.org.uk/app/uploads/2021/06/MISRA-C-2012-AMD2.pdf qui indique clairement “AMD2.78: The identifiers calloc, malloc, realloc, aligned_alloc and free shall not be used and no macro with one of these names shall be expanded.”
[9] https://github.com/feilipu/miniAVRfreeRTOS/issues/7
[10] Memory Areas and Using malloc() à https://www.nongnu.org/avr-libc/user-manual/malloc.html