Programmer un microcontrôleur

Comme n’importe quel ordinateur, un microcontrôleur doit être programmé. Pour cela il faut choisir un langage, une API (acronyme anglais pour « Application Programming Interface ») 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 :


Le langage d'assemblage


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 :


Listings C et ASM comparés


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 :

  1. 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 ;
  2. 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 ;
  3. 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 compilés


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 :

  1. Un firmware appelé interpréteur est installé et exécuté sur le microcontrôleur cible dès son démarrage ;
  2. Ensuite, on écrit le listing de notre programme dans un fichier qui est également recopié dans la mémoire du microcontrôleur ;
  3. 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 :


Les langages interprétés


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(3) # 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 API (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 Application Programming Interfaces (API)

Que le langage que vous utilisez soit compilé ou interprété, vous aurez forcément recours à une Application Programming Interface (API) ou interface de programmation en français. Pour simplifier sans en trahir le concept, une API est un ensemble de programmes et de procédures déjà écrits, regroupés par thématiques dans des bibliothèques de codes source 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 API. 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 l’API 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 l’API STM32duino, extension de l’API Arduino aux puces STM32. Avec STM32duino la gestion des entrées-sorties est considérablement simplifiée par des bibliothèques é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 l’API 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 l’API qui vous permettront de réaliser votre application avec le moins d’efforts.

Les Integrated Developement Environments (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…
    La figure qui suit illustre une chaîne de compilation standard pour microcontrôleurs équipés de coeurs ARM Cortex M :


Chaîne de compilation ARM


Source : Definitive Guide to ARM(r) Cortex(r) M3 – M4, Thez, Yiu Joseph


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

Les langages de programmation, les API et les IDE pour STM32

Le tableau qui suit énumère plusieurs langages, API et IDE populaires et gratuits pour les microcontrôleurs STM32 :


Quelques API gratuites pour l'embarqué


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 recommendations 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’augementer, ce qui lui assure d’excellentes performances, bien que nécessairement inférieures à celles des firmwares compilés. Un bel exemple de ce potentiel est illustré par les modules caméras OpenMV, qui exécutent des algorithmes de traitement d’images vidéo (en temps réel) et d’intelligence artificielle grâce à des microcontrôleurs STM32 de la famille H7 (par exemple le STM32H743II) dans un environnement MicroPython.

Comme nous l’avons déjà expliqué, votre projet dictera le langage et l’API 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.

Si vous souhaitez bien comprendre les fondamentaux de la programmation du microcontrôleur STM32F103 en C/C++ nous vous conseillons cette formation avec l’API LL et les sites 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.