L’assembleur (1)

Pourquoi programmer en assembleur ?

. Par : Christian. URL : https://www.locoduino.org/spip.php?article280

Une des causes de succès des cartes Arduino est qu’on peut écrire les programmes dans un langage facilement compréhensible pour l’humain puisqu’il ressemble, en quelque sorte, à de l’anglais technique. Pourquoi alors évoquer dans cette série d’articles un langage compliqué à comprendre et compliqué à mettre au point ? Par nostalgie ? Par challenge ? Pour frimer ? Il existe des raisons un peu plus objectives pour s’intéresser à l’assembleur et nous allons les évoquer.

Disons-le une bonne fois pour toutes, cette série d’articles n’est pas écrite pour vous inciter à programmer en assembleur mais nous allons voir que dans certains cas, l’assembleur est la seule solution possible pour que le programme fonctionne comme on le voudrait. Cette série d’articles est plutôt conçue pour vous aider à découvrir en quoi consiste l’assembleur et comment l’aborder. Les articles sont classés dans la catégorie « expert » (3 étoiles en haut du texte) pour la simple raison que nous ne pourrons pas, en quelques articles, écrire un cours complet d’assembleur qui nécessiterait à lui seul un livre entier. Vous vous poserez forcément des questions à la lecture des différents textes mais nous savons bien qu’un expert a suffisamment d’autonomie pour résoudre des problèmes par lui-même en effectuant une recherche sur internet. De plus, la pratique de l’assembleur requiert une excellente connaissance de la structure même du microcontrôleur, de ses ressources, de son organisation mémoire, de ses registres de contrôle ; écrire un programme en assembleur demande de tout contrôler par soi-même, de penser à tout, ce qui exclut les débutants de sa pratique. Apprendre à programmer en assembleur va vous demander beaucoup plus de temps et d’efforts que ce que vous avez déjà fourni pour découvrir Arduino. Si vous vous sentez prêts pour cette balade initiatique, alors accrochez-vous car ça va décoiffer !

Un seul langage pour votre microcontrôleur

Tout ce que comprend un microcontrôleur, ce sont des mots binaires constitués d’un certain nombre de bits égaux à 0 ou 1. Le PIC 16F84 utilise par exemple des mots binaires de 12 bits alors que l’ATmega328P (celui des cartes Uno) utilise des mots binaires de 16 bits [1]. Chaque mot binaire correspond à une instruction que le microcontrôleur saura exécuter (certaines instructions ont besoin de deux mots binaires, mais c’est assez rare). Un programme en langage machine est donc une succession de mots binaires et on voit bien que chaque microcontrôleur a son propre langage machine et qu’un programme écrit pour l’ATmega328P ne pourra pas fonctionner pour le PIC 16F84.

Il n’est pas très facile pour un simple humain de comprendre une suite de mots binaires. C’est pourquoi on représente chaque instruction par quelques caractères de texte comprenant une notation mnémonique [2] rappelant ce que fait la commande (par exemple ADD pour une addition) et des arguments (par exemple pour expliquer quoi additionner). Cette façon de représenter le code machine s’appelle l’assembleur mais il faut bien se rappeler que ce n’est que du texte et que ce texte doit ensuite être converti en mots binaires par un programme de traduction (appelé programme d’assemblage et souvent par abus de langage assembleur également).

Chaque microcontrôleur possède son assembleur, c’est-à-dire son jeu de mnémoniques correspondant au jeu d’instructions que le microcontrôleur sait exécuter. D’un microcontrôleur à un autre, l’assembleur est très différent, ce qui constitue le principal défaut de l’assembleur : sa non portabilité. Lorsque vous écriviez un programme pour Arduino, l’IDE se chargeait de le traduire en langage machine propre au microcontrôleur qui équipe la carte (voir à ce sujet l’article Du sketch à l’exécutable). Changer de carte était alors très facile : le même programme était traduit de façon différente par l’IDE en fonction du nouveau microcontrôleur cible. Réfléchissez bien à cela si vous voulez écrire un programme en assembleur : si vous changez de microcontrôleur, il faudra réécrire le programme au moins en partie, voire en totalité !

Les microcontrôleurs sont classés en deux catégories : les RISC (Reduced Instruction Set Computer) et les CISC (Complex Instruction Set Computer) [3]. Les RISC comportent moins d’instructions, ce qui facilite le travail d’apprentissage de l’assembleur mais complique la réalisation de tâches complexes là où un CISC aura peut-être des instructions pour faire ces tâches. Les microcontrôleurs AVR qui équipent nos cartes Arduino sont des RISC, avec un jeu de 135 instructions (à comparer aux 35 instructions du microcontrôleur RISC PIC 16F8X). Ce nombre de 135 instructions constitue un excellent compromis entre facilité d’apprentissage et possibilités tout de même assez étendues.

Connaître l’assembleur, c’est parler microcontrôleur !

Si vous devez effectuer un voyage en Chine, vous avez deux possibilités pour parler avec des Chinois : vous utilisez l’anglais qui n’est ni votre langue, ni celle des Chinois mais de nombreux Chinois parlent l’anglais, surtout dans les grandes villes, ou bien vous apprenez à parler Chinois, ce qui vous permettra de vous faire comprendre y compris dans les campagnes. Le premier cas utilise une langue intermédiaire qui doit être traduite (par le Chinois lui-même ou éventuellement un interprète) et correspond à la programmation des cartes Arduino en C ou C++. La traduction n’est pas toujours très efficace. Le deuxième cas correspond à l’assembleur, une langue qui est la plus proche du langage machine et si vous la maîtrisez un peu, vous pourrez vous faire comprendre avec beaucoup d’efficacité (notez aussi que si vous ne la maîtrisez pas, ce sera une catastrophe et vous serez bel et bien perdu !).

Ce qui doit vous motiver à utiliser l’assembleur, au moins en partie dans vos programmes, c’est la recherche de l’efficacité qui peut se constater dans deux domaines : la compacité du code et le timing rigoureux d’une fonction. Et là, je suis désolé de le dire mais l’IDE d’Arduino n’est pas optimisé pour produire du code compact et parfois n’est pas toujours adapté pour respecter le timing d’une fonction.

Compacité du code

L’assembleur permet d’obtenir un code machine qui soit le plus compact possible. À titre d’exemple, prenez le programme Blink donné dans l’IDE (version 1.8.13) ; le code machine généré pour une carte Uno (microcontrôleur ATmega328P) occupe 924 octets de mémoire flash pour cinq instructions. Le même programme écrit en assembleur pour Atmega328P n’occupe que… 15 instructions soit 30 octets ! La figure 1 montre les listings des deux programmes : d’un côté cinq instructions (dont pinMode, digitalWrite et delay) pour un résultat à 924 octets et de l’autre 15 instructions (on n’y reviendra ultérieurement) pour 30 octets puisqu’une instruction est un mot de 16 bits soit deux octets.

Figure 1
Figure 1
Le programme Blink pour Arduino à gauche et en assembleur à droite.

Cet exemple est très frappant pour voir à quel point l’assembleur permet d’obtenir un code très compact. L’IDE d’Arduino accepte du code assembleur en ligne. L’instruction est du type :
asm("sbi 0x05, 5");

Ce qui est entre guillemets est une instruction en assembleur. Avec plusieurs lignes on peut reconstituer un petit programme en assembleur comme le montre la figure 2 qui reprend le listing assembleur de la figure 1.

Figure 2
Figure 2
Assembleur en ligne pour l’IDE d’Arduino.

Cependant, le résultat est décevant puisque la compilation pour ATmega328P génère un code machine de 474 octets comme on peut le voir. Il est donc nécessaire pour programmer en assembleur d’utiliser d’autres outils que ceux proposés par l’IDE d’Arduino. Nous allons voir maintenant une autre raison de passer à des outils plus performants même pour programmer nos cartes Arduino en C/C++.

Timing d’une fonction

Dans certains cas, l’IDE d’Arduino ne permet pas d’obtenir ce qu’on veut. Consultez la page du site Arduino qui décrit la fonction delayMicroseconds() et examinez le petit programme fourni en fin de page et repris dans la figure 3.

Figure 3
Figure 3
Selon Arduino, ce programme tout simple ne donnera pas le résultat escompté.

Si vous examinez à l’oscilloscope le signal obtenu sur la sortie (ici broche 8), vous constaterez un signal carré cyclique dont la demi-période fait 53,5 µs au lieu de 50 µs comme le voulait le programmeur et parfois ce signal montre une demi-période de presque 60 µs. Comme il est dit sur la page du site d’Arduino, cette approximation est due à l’exécution de code qui a été rajouté par l’IDE. Tout ceci est expliqué en détails sur cette vidéo (en anglais) :

Comme on peut le voir dans la vidéo, il est tout à fait possible de s’en sortir avec l’IDE mais cela demande de bien comprendre ce qui se passe au niveau du processus de construction du code machine et surtout de savoir ce qu’on peut se permettre de faire ou ne pas faire. Trois choses sont à retenir de cette vidéo :
-  Certaines applications ne sont pas possibles avec les fonctions standard d’Arduino ou les bibliothèques.
-  Pour de meilleures performances (rapidité et compacité), il vaut mieux accéder directement aux registres du microcontrôleur.
-  Parfois il faut combattre le code ajouté par l’IDE pour que le projet fonctionne correctement.

Résumé des avantages et inconvénients de l’assembleur et du C

L’assembleur permet un contrôle total des ressources du microcontrôleur, son code est plus compact et plus rapide mais par contre moins efficace pour de grandes applications. De plus, ce code est non portable car propre au microcontrôleur cible, difficile à maintenir et aussi difficile à lire.

Le langage C permet un code mieux structuré, portable, facile à maintenir et plus efficace pour de grandes applications, mais cependant moins compact et moins rapide pour de petites applications et avec un contrôle limité sur les ressources du microcontrôleur.

L’idéal est donc d’utiliser le langage C en appelant certaines fonctions écrites en assembleur uniquement lorsque cela présente un avantage (respect d’un timing bien particulier par exemple). Comme nous le verrons ultérieurement, ceci est parfaitement possible, mais avant d’en arriver là, il est nécessaire déjà d’apprendre les bases.

D’autres outils de programmation que l’IDE

L’IDE a été conçu pour faciliter la vie de ceux qui veulent programmer des microcontrôleurs : il est simple à utiliser, plutôt universel, et relativement efficace pour la plupart des projets amateurs en matière d’électronique programmable (et notamment dans le domaine du modélisme ferroviaire). Mais comme on vient de le voir, il a aussi ses limites : il peut ajouter du code qui risque de modifier le timing d’une fonction et il ne fournit pas un code très compact. De plus, il ne permet pas de simuler l’application ni de la déboguer facilement. Avec un programme écrit avec l’IDE, il y a toujours la possibilité de rajouter temporairement des Serial.print pour suivre l’évolution des variables, mais ce système D ne peut s’appliquer à un programme écrit en assembleur. Un amateur éclairé (un expert qui s’intéresse de très près aux microcontrôleurs) a sans aucun doute intérêt à utiliser un outil plus complet.

Il en existe plusieurs : certains parlent par exemple de PlatformIO (que l’on retrouve dans certains livres consacrés à Arduino) mais pour ma part, j’ai adopté Microchip Studio 7 (anciennement Atmel Studio 7 mais Atmel a été racheté par Microchip) et je ne le regrette pas puisque cette suite permet de programmer en C/C++ les microcontrôleurs AVR et ARM de Microchip et en assembleur les microcontrôleurs AVR. De plus, il intègre un simulateur bien pratique pour déboguer les programmes. Il nécessite un programmateur ISP mais on peut utiliser une carte Arduino (Uno par exemple) comme programmateur (Arduino ISP) et cela marche très bien. La génération du code est souvent plus efficace puisqu’on ne rajoute pas ce qui a permis de rendre l’IDE si simple d’emploi. Studio 7 est également un environnement intégré de développement (au même titre que l’IDE).

Nous avons vu plus haut les limitations de l’assembleur en ligne avec l’IDE d’Arduino ; si vous voulez programmer en assembleur d’une façon plus qu’occasionnelle, je vous conseille de télécharger Studio 7 qui est gratuit sur le site de Microchip et de l’installer sur votre ordinateur [4]. Mais vous pouvez aussi attendre d’en savoir un peu plus sur l’assembleur car comme vous allez le constater très vite, programmer en assembleur n’est pas de tout repos ! De plus, vous verrez dans l’article L’assembleur (6) que l’IDE est parfois plus simple d’emploi pour rédiger un programme en C qui appelle des routines en assembleur. Au cours de cette série d’articles, nous aurons l’occasion de décrire l’utilisation des deux logiciels (IDE ou Studio 7) et vous pourrez ainsi vous faire une idée en fonction de vos besoins en programmation.

Conclusion de cette première partie

Utiliser l’assembleur n’est pas une nécessité absolue et dans la majorité de nos applications de modélisme, le langage Arduino et l’IDE font parfaitement l’affaire. Mais l’expert que vous êtes est peut-être arrivé à certaines limites qu’il serait intéressant de faire sauter grâce à une solution plus efficace. C’est dans cet esprit que je vous invite à me suivre dans ce survol de ce qu’est l’assembleur et ce qu’il permet.

[1Certaines instructions comme CALL ou JMP utilisent 32 bits (soit deux mots de 16 bits) comme nous le verrons par la suite.

[2D’après le dictionnaire de l’Académie Française, mnémonique est un adjectif, même si on l’emploie comme un substantif alors féminin. On doit donc parler de notation mnémonique ou de symbole mnémonique, mais dans le jargon de la microprogrammation, on utilise souvent ce terme comme un nom masculin ou féminin : le ou la mnémonique.

[3RISC et CISC font référence à la complexité sémantique des instructions, pas à leur nombre. Cependant, en introduisant des instructions de complexité élevée, on augmente leur nombre et les microcontrôleurs CISC ont donc un nombre d’instructions plus élevé que les RISC.

[4Ce logiciel n’est actuellement distribué que pour Windows.