Programmer un microcontrôleur
Comme n’importe quel ordinateur, un microcontrôleur doit être programmé. Pour cela il faut choisir un langage, une (ou des) bibliothèque(s) et une IDE (acronyme anglais pour « Integrated Development Environment »).
Les langages
On peut décliner les langages de programmation en deux catégories : les langages de bas niveau et les langages de haut niveau qui peuvent être soit compilés soit interprétés. Passons ces termes en revue…
Les langages de bas niveau
Un microprocesseur interprète des instructions codées en binaire. C’est ce que l’on appelle le langage machine. Un programme rédigé en langage machine apparaît donc comme une très longue série de “0” et de “1” sans espaces ni sauts de lignes (du genre “00010000011100011…”). Il sera illisible pour quiconque ne connaît pas parfaitement le modèle de microprocesseur concerné.
On imagine bien qu’un tel programme est à la fois très délicat à écrire et à maintenir, la moindre modification ou correction impliquant beaucoup d’attention pour ne pas y introduire des erreurs. Créer en langage machine un programme complexe qui fonctionne correctement est un véritable défi ! Qui plus est, un programme en langage machine ne fonctionnera que sur le modèle précis de microprocesseur pour lequel il est écrit puisqu’il est construit avec les adresses de ses registres, mémoires et instructions. Pour cette raison, le langage machine est dit de bas niveau.
Une première amélioration vint avec le langage d’assemblage. C’est toujours un langage de bas niveau car il est équivalent au langage machine mais il est plus facilement lisible par l’homme. Par exemple, le listing d’une fraction de programme écrite en langage d’assemblage pour un microprocesseur ARM Cortex M3 ressemble à ceci :
Start:
mov r0,#0 @ mise zero de r0
mov r2,#10 @ charger la valeur 10 dans r2
Loop:
add r0,r0,r2,LSL #1 @ r0=r0+2*r2
subs r2,r2,#1 @ r2--
bne Loop
b Start
Source : Cours de M. Tarik Graba, Telecom Paris Tech, année 2018/2019.
Ce listing sera ensuite lu ligne par ligne par un programme appelé assembleur qui va remplacer directement chaque mot clef (MOV, R, R0…) par son écriture binaire pour finalement produire un fichier objet en langage machine. Celui-ci ne contient pas encore le programme qui sera exécuté sur le microcontrôleur, qui sera autrement appelé le firmware. Une dernière étape (que nous n’expliquerons pas pour rester synthétiques), l’édition de liens, est requise pour obtenir le firmware à partir d’un ou plusieurs fichiers objets.
La figure qui suit illustre ces étapes :
Les langages de haut niveau
Le langage d’assemblage génère des programmes compacts et performants ; il était la seule option d’abstraction possible à l’époque où les fréquences de fonctionnement des microprocesseurs étaient tout juste suffisantes pour les applications et où l’espace mémoire réduit était contraignant.
Mais par la suite, en suivant la Loi de Moore, l’espace mémoire et la vélocité des microcontrôleurs ont progressé de façon spectaculaire et la nécessité de les programmer avec des langages dits “de haut niveau”, qui répondent entre autres aux problématiques suivantes, s’est imposée :
-
Leur syntaxe est abstraite, elle ne fait plus de références explicites aux adresses de registres, mémoires et instructions d’un microprocesseur en particulier. Un programme bien écrit dans un langage de haut niveau pourra donc fonctionner sur un grand nombre de microprocesseurs sans pratiquement aucune modification de son code source. On dit qu’il est portable car on pourra aisément le modifier pour qu’il s’exécute sur un autre microprocesseur de la même famille, ce qui représentera des économies substantielles en temps de développement ;
-
Leur syntaxe structurée, inspirée des démonstrations mathématiques, permet d’exprimer des algorithmes presque intuitivement à l’aide de boucles, de tests, de fonctions, etc. Les mots clés sont en général tirés de l’anglais (if, else, select…). Tout ceci permet d’écrire des programmes beaucoup plus complexes en y passant moins de temps qu’avec le langage d’assemblage.
La programmation des microcontrôleurs est généralement réalisée avec le langage C (ou avec son extension ultérieure le langage C++). L’invention de C date de l’année 1972 dans les Laboratoires Bell par Dennis Ritchie et Kenneth Thompson ; il est particulièrement efficace pour manipuler la mémoire des microprocesseurs. Il appartient à la famille des langages compilés.
Les langages compilés
Les listings qui suivent sont ceux d’un même programme très simple (et dénué d’intérêt : il calcule 1 à partir de 256 !) écrit en langage C pour un microcontrôleur STM32F103 et l’une de ses possibles traductions, partielle, en langage d’assemblage :
Pour passer du langage C au langage d’assemblage, un nouvel outil logiciel est nécessaire, un traducteur appelé compilateur. Pour récapituler, la création d’un firmware pour un microcontrôleur passe par les trois étapes suivantes :
- Ecriture par le programmeur du listing – on parle de code source – dans un langage de haut niveau, relativement facile à apprendre, comprendre et manipuler par celui-ci ;
- Traduction par un programme appelé compilateur du listing en langage de haut niveau en langage d’assemblage. Ensuite assemblage et production d’un fichier objet. Ces deux étapes constituent la compilation ;
- On devra finalement construire le firmware à partir du fichier objet (en général il y en a plusieurs) au moyen de l’éditeur de liens, exactement comme avec le code source en langage d’assemblage.
La figure qui suit illustre ces étapes :
Les langages interprétés
Les langages compilés présentent plusieurs inconvénients, mais essentiellement le fait que l’étape de compilation nécessite du temps. Or, le développement d’un programme un peu complexe est un processus itératif qui nécessite des milliers de corrections, de compilations et de tests. Le délai de compilation pèse donc lourdement sur celui-ci. Essentiellement pour cette raison ont été développés des langages interprétés. Les étapes pour l’exécution dans un microcontrôleur d’un programme écrit dans un langage interprété sont les suivantes :
- Un firmware appelé interpréteur est installé et exécuté sur le microcontrôleur cible dès son démarrage ;
- Ensuite, on écrit le listing de notre programme dans un fichier qui est également recopié dans la mémoire du microcontrôleur ;
- Dernière étape, l’interpréteur va lire les instructions du listing, les traduire en langage machine une par une et les envoyer au microprocesseur.
La figure qui suit illustre ces étapes :
Par exemple, pour un programme en (Micro)Python, on a le code source suivant :
# Objet du script :
# Exemple faisant clignoter la LED bleue de la NUCLEO-WB55 à une fréquence donnée.
import pyb # pour les accès aux périphériques (GPIO, LED, etc.)
from time import sleep # pour faire des pauses système (entre autres)
# Initialisation de la LED bleue
led_bleue = pyb.LED(1) # sérigraphiée LED1 sur le PCB
delai = 0.5 # Temps d'attente avant de changer l'état de la LED
# La boucle va se répéter dix fois (pour i de 0 à 9)
for i in range(10):
# Affiche l'index de l'itération sur le port série de l'USB User
print("Itération %d : "%i)
led_bleue.on() # Allume la LED
print("LED bleue allumée")
sleep(delai) # Attends delai secondes
led_bleue.off() # Eteint la LED
print("LED bleue éteinte")
sleep(delai) # Attends delai secondes
Celui-ci doit-être ensuite transféré dans la mémoire flash du microcontrôleur afin que son firmware interpréteur puisse l’exécuter.
Pour paraphraser cette source, alors qu’un compilateur traduit une bonne fois pour toute un code source en un firmware indépendant exécutable, l’interpréteur est nécessaire à chaque lancement du programme, pour traduire au fur et à mesure son code source en code machine. Les langages interprétés sont pénalisés par cette traduction à la volée et restent significativement moins performants que leurs alternatives compilées. En contrepartie ils facilitent grandement le développement des programmes, et leurs codes source sont bien souvent plus épurés et plus lisibles que ceux des langages compilés.
Pour ces raisons les langages interprétés sont désormais dominants sur nos ordinateurs et smartphones qui disposent de beaucoup de mémoire et de microprocesseurs rapides, mais ils ne sont encore que très peu utilisés sur les microcontrôleurs aux architectures plus contraintes pour lesquelles la perte de performances liée à leur usage reste sensible. Leur adoption a cependant commencé avec MicroPython, à la fois un portage du langage Python et une bibliothèque (voir plus loin) qui fait l’objet d’une section de notre site. On peut supposer qu’ils s’imposeront pour un certain nombre d’applications dans un avenir proche puisque la puissance des microcontrôleurs ne cesse d’augmenter.
Pour finir, nous devons préciser que la plupart des langages “modernes” qui ne sont pas compilés (par exemple C#, Java, Javascript, Perl, Python…) ne sont pas non plus rigoureusement des langages interprétés tels que nous les avons définis, ils sont en fait d’abord compilés au sein du microcontrôleur en un “bytecode” qui sera ensuite interprété par un firmware appelé machine virtuelle dans un souci d’optimisation de leur portabilité.
Les bibliothèques pour les langages de haut niveau
Que le langage de haut niveau que vous utilisez soit compilé ou interprété, vous aurez forcément recours à une ou plusieurs bibliothèques logicielles. Une bibliothèque est un ensemble de programmes déjà écrits (et parfois précompilés en fichiers “objets”), regroupés par thématiques et répondant à des cas d’usage récurrents.
Supposons par exemple que vous souhaitiez programmer les entrées-sorties d’un microcontrôleur STM32F103 en langage C ou C++. Au moins ces trois options s’offrent à vous :
-
Vous pouvez le faire sans aucune bibliothèque. Dans ce cas votre code source fera appel directement aux adresses des registres des circuits d’entrée-sortie, que vous devrez écrire vous-même dans le listing après les avoir cherchées dans la documentation technique du STM32F103. C’est un exercice fastidieux et une source d’erreurs ; votre code sera difficile à écrire, peu lisible et il perdra sa portabilité.
-
Vous pouvez le faire avec la bibliothèque Hardware Abstraction Layer (HAL) de STMicroelectronics. Entre autres commodités, HAL fournit un fichier de correspondance entre les noms de registres et leurs adresses pour tous les microcontrôleurs de la famille STM32. Avec HAL vous nommez les registres par leurs noms (ARR, CCR, ODR …) dans votre listing. C’est ensuite le compilateur qui vérifie le modèle précis de microcontrôleur que vous programmez et qui se charge de substituer les noms des registres par leurs adresses dans votre listing.
-
Vous pouvez le faire avec la bibliothèque STM32duino, qui étend la bibliothèque Arduino aux puces STM32. Avec STM32duino la gestion des entrées-sorties est considérablement simplifiée par des fonctions écrites en langage C++. Elles permettent de paramétrer les broches de votre microcontrôleur en quelques lignes de code plutôt que des dizaines avec la bibliothèque HAL. Ceci rend le code particulièrement lisible, facile et rapide à écrire, mais interdit certaines applications du fait que STM32duino masque les fonctions avancées des entrées-sorties selon sa logique simplificatrice.
Il faut idéalement choisir le langage puis les bibliothèques qui vous permettront de réaliser votre application avec le moins d’efforts.
Les environnements de développement intégrés (IDE)
L’écriture d’un programme est une tâche méticuleuse qui nécessite beaucoup de pratique et de temps. Ceci rend indispensable l’utilisation d’outils logiciels pour faciliter :
-
La rédaction du code source et la vérification de sa syntaxe. Cette étape nécessite un éditeur de texte qui peut être aussi minimaliste que le bloc note de Windows ou aussi abouti que celui des IDE Eclipse ou Visual Studio Code qui, entre autres commodités, signalent les erreurs de syntaxe.
-
La création du firmware (si le langage est compilé) à partir du code source. Pour cela un ensemble de programmes appelé chaîne de compilation est requis. On trouve généralement dans une chaîne de compilation : un assembleur, un compilateur, un éditeur de liens, un simulateur de microcontrôleur, un programmeur de firmware…
-
Le débogage du firmware (si le langage est compilé) ou du code source (si le langage est interprété). Ecrire un code source sans erreurs de syntaxe ne garantit pas que celui-ci va s’exécuter comme on l’imaginait (ce serait merveilleux !). Un programme complexe contiendra certainement des quantités d’erreurs subtiles et plantera dans des situations que le programmeur n’aura pas prévues ; on parle de bugs ou bogues. Pour les traquer et les corriger plus facilement, le programmeur pourra utiliser un outil appelé débogueur qui permet d’exécuter pas à pas le programme, d’afficher les valeurs qu’il inscrit dans les registres, de mettre en pause son déroulement dans certaines conditions prédéfinies, etc.
Les environnements de développement intégrés (IDE) rassemblent tous ces outils et bien d’autres.
La figure qui suit illustre une chaîne de compilation standard pour microcontrôleurs équipés de cœurs ARM Cortex M :
Source : Definitive Guide to ARM(r) Cortex(r) M3 – M4, Thez, Yiu Joseph
Les langages de programmation, les bibliothèques et les IDE pour STM32
Le tableau qui suit énumère plusieurs langages, bibliothèques et IDE populaires et gratuits pour les microcontrôleurs STM32 :
Les champs d’applications préconisés sont évidemment subjectifs, inspirés d’observations “sur le terrain” ; nous ne prétendons pas nous substituer ici aux recommandations de l’Education Nationale, des instituts de formation, etc. MicroPython est également adapté pour le prototypage électronique et il est tout à fait possible qu’il soit un jour largement adopté par les professionnels du fait que la puissance des microcontrôleurs ne cesse d’augmenter, ce qui lui assure d’excellentes performances, bien que nécessairement inférieures à celles de firmwares compilés.
Comme nous l’avons déjà expliqué, votre projet dictera le langage et la bibliothèque les plus appropriés :
- Vous souhaitez développer rapidement un prototype pour un client ? Dans ce cas STM32duino ou Mbed feront probablement l’affaire.
- Vous travaillez sur une application qui enregistre et analyse du son ? Alors il vaudrait mieux que vous utilisiez HAL, STM32Cube et STM32CubeIDE pour optimiser les accès à la mémoire et les algorithmes de traitement du signal.
- Votre objectif est purement pédagogique avec des collégiens ou des lycéens ? Alors (Micro)Python est probablement la meilleure solution !
- Etc.
Pour aborder la programmation d’un microcontrôleur STM32 (le STM32F103) en C/C++ nous vous conseillons cette formation avec la bibliothèque LL ainsi que le site STM32 Wiki d’initiation à l’écosystème STM32Cube.
Pour finir, mentionnons les environnements de programmation par blocs essentiellement destinés à l’éducation. A cette date, un petit nombre d’entre eux supportent des cartes de prototypage pour microcontrôleurs STM32 de STMicroelectronics. Parmi ces initiatives citons MakeCode de Microsoft, l’environnement en ligne de la société Vittascience ou encore STudio4Education.
Ces outils logiciels, généralement exécutés dans des navigateurs Internet, permettent de construire très facilement des firmwares en assemblant à la souris des blocs graphiques inspirés des instructions des langages de haut niveau. Il affichent souvent en parallèle un listing en langage C, Python ou Java qui représente le code source équivalent au programme construit avec les blocs. Par cette approche, les élèves de collèges et lycées peuvent acquérir l’intuition de ce qu’est un algorithme et créer des applications ludiques, tout en ayant un premier aperçu des langages de programmation qu’ils rencontreront potentiellement plus tard dans leurs études.