LOCODUINO

Forum de discussion
Dépôt GIT Locoduino
Flux RSS

mercredi 19 juin 2019

82 visiteurs en ce moment

La compilation d’un projet Arduino

Mais que fait réellement l’IDE ?

. Par : Thierry

Lorsque vous appuyez sur le bouton « vérifier », l’IDE Arduino lance un programme appelé « compilateur ». Ce compilateur C++ produit quelque chose que votre Arduino comprendra. Mais que va faire ce compilateur de votre prose ?

Un peu d’histoire…

Dans l’ordre, on a eu le Big Bang, les dinosaures, Jésus, Mahomet, la cocotte minute, et le BASIC !

Pour être sérieux, rappelons nous que les premiers ordinateurs, pendant la seconde guerre mondiale, étaient programmés à l’aide de câbles qui reliaient des lampes, ancêtres des transistors. Aujourd’hui la programmation est logicielle, plus matérielle. L’autre différence est dans la taille du processeur. Une poche pour un Uno, un hangar pour ces engins pourtant bien moins puissants que le plus petit de nos micro-contrôleur.

Cette activité, rapidement baptisée la programmation, s’est vite transformée du binaire du début, vers le langage machine (l’assembleur) appelé ainsi parce qu’il n’est que la transcription en langage clair (ou presque) des instructions du processeur. Sont apparus ensuite les langages évolués Cobol, Fortran et beaucoup d’autres qui masquent la complexité du matériel pour une syntaxe qui s’approche -de plus ou moins près- du langage parlé…

En 1972 deux chercheurs des laboratoires Bell, Dennis Rithchie et Ken Thompson ont créé un langage maison baptisé C, issu d’un premier essai B (forcément…). C’est encore l’un des langages les plus utilisé et dont la syntaxe a influencé de nombreux langages plus récents comme le C++, le C# [1] ou encore Java.

Le binaire des débuts était directement envoyé ou, comme on l’a vu, câblé sur la machine, mais les langages suivants sont des langages compilés. Pour un langage compilé, le compilateur, qui transforme le texte tapé par le programmeur en binaire compréhensible par le processeur, est utilisé une seule fois avant l’exécution du programme et le résultat est stocké dans un fichier exécutable. Dans un langage interprété, chaque instruction est compilée au moment de son exécution, et le résultat est perdu. La prochaine exécution de la même instruction devra être encore compilée… Ça explique les temps d’exécution beaucoup plus important de ces langages par rapport aux langages compilés. Le vieux GWBasic, mais aussi Java, Php et d’autres sont des langages interprétés [2] qui ne concernent pas l’Arduino.

Un compilateur ?

Le rôle d’un compilateur est de traduire un texte en quelque chose qu’un micro-processeur ou, dans notre cas, un micro-contrôleur puisse comprendre et exécuter. Mais pourquoi diable est-ce qu’il ne comprend pas directement ce que l’on tape ?

Parce que le texte qui constitue un programme est trop complexe pour être exécuté directement par un micro-contrôleur ou un micro-processeur. En théorie, il serait possible de construire une machine capable de le faire mais cette machine mobiliserait une grande quantité de ressources matérielles et serait totalement inefficace. Aucun ordinateur aujourd’hui, et encore moins un micro-processeur ou un micro-contrôleur, ne fonctionne en langage naturel.

Il faut en effet se placer à un niveau de complexité permettant une mise en œuvre aisée avec de l’électronique numérique. Un micro-contrôleur n’est par conséquent capable d’exécuter que des instructions très simples, beaucoup plus simples que les instructions complexes utilisées en C ou C++. L’ensemble des instructions qu’un micro-contrôleur est capable d’exécuter s’appelle le langage machine ou encore le jeu d’instructions. Chaque famille de micro-contrôleur possède son jeu d’instructions propre. Ainsi, dans le monde Arduino, les micro-contrôleurs AVR d’Atmel partagent le même jeu d’instructions, mais le Due et le Zero ont un jeu d’instructions différent. On voit ici le premier avantage de la compilation, le programmeur utilise un langage qui n’est pas celui du micro-contrôleur et ceci permet d’écrire des programmes uniques sans se préoccuper plus que nécessaire du micro-contrôleur cible.

Chacune des instructions complexes du C ou du C++ est donc traduite par le compilateur en une ou plusieurs instructions simples. Qu’en est-il exactement ? Au fil des articles nous avons déjà évoqué les mémoires présentes dans l’Arduino : flash où le programme est stocké, SRAM ou les variables sont stockées et EEPROM qui a un statut à part. À cela s’ajoute les registres. Il s’agit de plusieurs petites mémoires où sont stockées les données sur lesquelles le micro-contrôleur calcule. Un AVR (Arduino Uno) en possède 32. Ainsi, pour simplement ajouter 1 à une variable, il faudra :

  1. copier la valeur contenue dans cette variable de la mémoire vers un registre ;
  2. ajouter 1 à cette valeur dans le registre ;
  3. copier la valeur contenue dans le registre vers la variable en mémoire.

3 instructions du micro-contrôleur. Pour illustrer ceci considérons la portion de sketch suivant :

  1. uint8_t a;
  2.  
  3. void setup()
  4. {
  5. a++;
  6. }

La compilation pour un Arduino Uno donnera :

000002be <setup>:
 2be:    80 91 60 00    lds r24, 0x0060
 2c2:    8f 5f          subi r24, 0xFF
 2c4:    80 93 60 00    sts 0x0060, r24
 2c8:    08 95          ret

Nous voyons ici le résultat du travail du compilateur et la traduction de notre fonction setup en code machine. La fonction setup est implantée dans la mémoire flash de l’adresse 2be (c’est de l’hexadécimal) à l’adresse 2c9 et est composée de 4 instructions machine. Les adresses où se trouve chacune des instructions sont en première colonne. Les chiffres en seconde colonne sont les instructions machines elles-même, ce sont tout simplement des nombres. Ce sont ces nombres qui seront mis dans la flash du micro-contrôleur. On voit que les 1ere et 3e instructions occupent 4 octets chacune alors que les 2e et 4e n’en occupent que 2. La dernière colonne est la représentation de ces instructions machine sous une forme plus compréhensible pour nous : l’assembleur.

La première instruction, lds, pour Load Direct from Data Space, copie la valeur contenue dans a (on apprend au passage que a est mis en SRAM à l’adresse 0x60) dans le registre r24. La seconde subi, pour Subtract Immediate, soustrait 0xFF à la valeur stockée dans r24. C’est assez surprenant, une addition a été remplacée par une soustraction ! En réalité l’AVR ne possède pas d’instruction d’addition avec une constante (un immédiat est une constante stockée directement dans l’instruction elle-même). Et donc le compilateur utilise une soustraction en prenant l’opposé de la constante. En effet, 0xFF vaut -1. La troisième instruction sts, pour Store Direct to Data Space effectue l’opération inverse de lds. Enfin, ret est l’instruction qui permet de revenir d’une fonction.

On ne va pas aller plus loin, le propos est juste de mesurer la distance qui existe entre le texte que l’on écrit et ce que le micro-contrôleur exécute effectivement. On voit que cette distance est grande et que se passer de compilateur aurait un impact pour le moins significatif sur le travail du programmeur. A noter que très peu des micro-contrôleurs antérieurs à l’Arduino, souvent employés de manière industrielle, et pour beaucoup encore en usage aujourd’hui, étaient programmables via un langage évolué comme le C…

Compilons, compilons, il en restera toujours quelque chose…

Une bonne partie du travail de la compilation est du traitement de fichiers textes et de chaînes de caractères, une autre partie est la production du code machine et son optimisation : enlever le code inutilisé, factoriser ou simplifier lorsque c’est possible…

Le compilateur C/C++

Voyons plus spécifiquement le travail du compilateur C/C++ qui nous occupe : GCC utilisé par l’IDE Arduino. GCC est un produit diffusé sous licence libre et amélioré/maintenu par une importante communauté. Il est disponible sur toutes les plateformes du marché : Mac, Linux, Windows et d’autres. C’est un ’cross-compiler’, c’est à dire qu’il est capable de générer du code pour la machine sur laquelle il est exécuté, mais aussi pour beaucoup d’autres plateformes, y compris les processeurs Atmel qui équipent nos Arduino.

Les sources

Les sources C se composent de deux types de fichiers, le fichier d’entête suffixé .h , et le fichier source suffixé .c . En C++, les suffixes deviennent .hpp et .cpp, mais les compilateurs C/C++ modernes comme GCC savent tous mélanger du C et du C++ et reconnaissent les deux types d’extension.

L’entête

Le rôle du fichier d’entête est de faire connaitre au reste du monde les fonctions, constantes et déclarations d’un fichier source.

Prenons un petit source :

  1. // Fichier len.c
  2. int len(const char *inText)
  3. {
  4. // retourne la longueur de la chaîne passée en argument.
  5. ...
  6. return length;
  7. }

Si je veux pouvoir utiliser cette fonction dans mon croquis, elle doit être reconnue

  1. void setup()
  2. {
  3. char text[]="Locoduino";
  4. int len = len(text); // Erreur de compilation : fonction inconnue.
  5. }
  6. void loop()
  7. {
  8. }

Tel quel, le compilateur va produire une erreur signalant une fonction inconnue. Pour que ça marche, il faut dire à GCC que cette fonction est déclarée autre part :

  1. int len(const char *inText); // déclaration de la fonction
  2. void setup()
  3. {
  4. char text[]="Locoduino";
  5. int len = len(text); // Utilisation d'une fonction connue.
  6. }
  7. void loop()
  8. {
  9. }

Et là c’est bon. Mais imaginons que cette petite fonction ’len’ est utilisée dans douze autres sources. Si je change l’interface de len dans len.c en disant par exemple que je limite la longueur à un byte :

// Fichier len.c
byte len(const char *inText)
{
 // retourne la longueur de la chaîne passée en argument.
 ...
 return length;
}

A nouveau, la compilation va échouer parce que GCC ne trouvera pas la fonction avec un byte en valeur de retour. Et pour corriger, je dois passer dans les douze sources pour changer la déclaration ! Alors plutôt que d’écrire la déclaration au début de chaque source qui utilise la fonction, nous allons créer un fichier d’entête :

  1. // Entête len.h pour len.c
  2.  
  3. byte len(const char *inText);

Et je vais utiliser ce fichier partout où j’en ai besoin :

  1. #include <len.h>
  2.  
  3. void setup()
  4. {
  5. char text[]="Locoduino";
  6. int len = len(text);
  7. }
  8. void loop()
  9. {
  10. }

On l’aura compris, le rôle de #include (’inclure’ en bon Français) est de remplacer la ligne par le contenu du fichier pointé.

Et maintenant, si je me dis que finalement ce n’était pas une bonne idée de limiter à un byte, je change len.h et len.c en remettant un int en valeur de retour, je lance la compilation et c’est tout !

Le rôle de fichiers d’entête ne se limite pas à la déclaration de fonctions, on peut (ou plutôt on doit) y inclure toutes les déclarations avec une portée qui s’étend au delà du fichier d’origine. Cela inclut les define, les constantes simples, les enum, les structures, les classes, les fonctions… Par exemple, ajoutons une contrainte de longueur à la chaîne que len() peut analyser :

  1. // Fichier len.c
  2.  
  3. #define MAXSTRLEN 255
  4.  
  5. int len(const char *inText)
  6. {
  7. // retourne la longueur de la chaîne passée en argument.
  8. ...
  9. // Taille maxi atteinte...
  10. if (length > MAXSTRLEN)
  11. return MAXSTRLEN;
  12. ...
  13. return length;
  14. }

Il est logique dans ce cas de permettre à ceux qui emploient la fonction de connaître cette limite et de dimensionner leurs chaînes de caractères en conséquence :

  1. // Entête len.h pour len.c
  2.  
  3. #define MAXSTRLEN 255
  4.  
  5. byte len(const char *inText);
  1. // Source len.c
  2. #include <len.h>
  3.  
  4. void setup()
  5. {
  6. char text[MAXSTRLEN]="Locoduino";
  7. int len = len(text);
  8. }
  9. void loop()
  10. {
  11. }

On peut faire quantité de choses à un fichier d’entête, y compris y appeler d’autres fichiers d’entête… Il faut juste avoir conscience que le contenu de ce fichier sera compilé avec chaque source qui l’appelle.

Mais que fait exactement l’IDE Arduino ?

L’édition

L’IDE est d’abord un éditeur de fichiers textes, ceux qui constituent votre croquis. En effet, tous les fichiers présents dans votre répertoire de croquis sont édités ensemble dans l’IDE, et aussi compilés ensemble pour obtenir le résultat. Le fichier .ino en est le centre puisque c’est lui qui contient setup et loop, et vous ne pourriez pas déplacer ces fonctions dans un autre fichier source. Par contre avec la commande ’Nouvel onglet’ du menu, vous pouvez créer un nouveau fichier .h ou .c .

La compilation

Au moment de la compilation, l’IDE va créer un répertoire temporaire avec un nom tout aussi temporaire. Ce répertoire existera tant que l’IDE restera ouvert sur un croquis donné, et les compilations successives stockeront leurs résultats ici. Sous Windows, ce répertoire est dans temp (dans l’explorateur de fichier tapez %temp% dans la barre d’adresse) et son nom commence par arduino_build…

Avant de demander au compilateur GCC de faire son travail, une intervention est faite par la surcouche Arduino qui s’appelle ’Wiring’ pour modifier votre source et le rendre compilable et complet. Cette surcouche va tenir compte aussi du type d’Arduino qui est demandé : Nano, Uno, Mega ou autre…

Il va ajouter l’include de arduino.h mais aussi les déclarations de setup et loop :

  1. #line 1 "sketch_sep10b.ino"
  2. #include "Arduino.h"
  3. void setup();
  4. void loop();
  5. #line 1
  6. void setup() {
  7. }
  8.  
  9. void loop() {
  10. }

Le premier ordre pré-processeur (voir plus bas…) #line est seulement là pour donner le nom du fichier tel qu’il est connu pour le créateur. Celui qui sera effectivement compilé ne s’appellera plus comme ça, et ne sera pas au même endroit ! Le second #line est là pour imposer au compilateur quoi dire si une erreur se produit. Pour le créateur du croquis, la ligne 1 est bien la ligne avec ’void setup()’.

Une fois le source rendu compilable, la compilation est lancée sur le croquis modifié, mais aussi sur plusieurs sources du noyau Atmel comme HardwareSerial.cpp, ou de Wiring lui même comme Wiring.c . Tous les résultats de compilation, les fichiers objet .o (voir plus loin) sont conservés tant que rien ne justifie de tout recompiler, comme par exemple de changer de type d’Arduino… C’est ce qui explique que la première compilation est beaucoup plus longue que les suivantes.

La compilation se passe en trois phases

Prè-processeur

La première partie est du traitement de chaînes et de fichiers. C’est le pré-processeur qui s’en charge. Comme son nom l’indique son rôle est de pré mâcher les sources avant de les envoyer au compilateur proprement dit. Chaque source .c ou .cpp va être transformée jusqu’à ne plus avoir d’ordre pré-processeur dans le texte. Ces ordres sont peu nombreux et commencent tous par dièse (’#’). Notez également l’absence de ’ ;’ à la fin de ces commandes.

- include
C’est l’ordre le plus commun puisqu’il est présent dans quasiment tous les sources. Il sert à prendre le contenu d’un fichier pour l’insérer dans un autre. Par convention on essaie de n’utiliser que des fichiers d’entête h ou hpp, mais dans des cas particuliers il est tout à fait possible d’inclure d’autres .c ou .cpp ou même des fichiers avec des extensions exotiques !

A noter une petite différence entre le #include <fichier.h> et le #include "fichier.h" . Avec les guillemets, le compilateur cherchera le fichier inclus parmi les sources du projet et des librairies référencées. Avec les ’<>’, il va d’abord chercher dans les fichiers de Atmel et du compilateur, et s’il ne trouve pas il va se tourner vers le projet compilé et ses références. L’include de Arduino.h par exemple doit se faire avec ’<>’ parce que ce fichier fait partie du noyau Wiring de GCC, pas de votre croquis.

- define
Un define permet de remplacer un texte par un autre :

  1. #define MAXSTRLEN 255
  2. char text1[MAXSTRLEN];
  3. char text2[MAXSTRLEN * 2];

devient

  1. char text1[255];
  2. char text2[255 * 2];

Il peut aussi servir à remplacer des expressions :

#define COPY(SOURCE, DEST)    strncpy(DEST, SOURCE, MAXSTRLEN)


COPY(str1, str2) ;
devient
strncpy(str2, str1, 255) ;

Notez l’inversion des arguments DEST et SOURCE entre COPY en strncpy.
Chaque compilateur C fournit un ensemble de fonctions de base dites ’standard’ qui constituent un socle de fonctionnalités disponible pour toutes les applications. Ces fonctions, comme le langage lui-même, ont été normalisées par l’ANSI depuis des décennies. Parmi ces fonctions de base, strncpy permet de copier une chaîne d’un emplacement mémoire vers un autre avec une taille maximum, mais les arguments donnent la destination en premier, et la source en second, ce qui m’a toujours choqué. Ma macro COPY prend donc les arguments dans le sens qui me plait et les renvoient comme strncpy les attend… C’est un choix personnel, bien entendu. vous faites ce que vous voulez !

Enfin, il est possible de ’défaire’ un define avec undef :

  1. #define MAXSTRLEN 255
  2. #undef MAXSTRLEN
  3.  
  4. char text[MAXSTRLEN]; // erreur de compilation : MAXSTRLEN indéfini !

- if/else/endif
Il est possible de tester l’existence ou la valeur des defines, et ainsi compiler ou pas certaines parties du code. C’est cette fonctionnalité qui est employée pour choisir le code à exécuter en fonction du modèle d’Arduino sélectionné dans l’IDE. Pour cela, l’IDE crée des defines sur la ligne de commande directement à l’appel de GCC. Par exemple dans hardware/arduino/avr/variants/standard/pins_arduino.h :

  1. #if defined(__AVR_ATmega8__)
  2. #define digitalPinHasPWM(p) ((p) == 9 || (p) == 10 || (p) == 11)
  3. #else
  4. #define digitalPinHasPWM(p) ((p) == 3 || (p) == 5 || (p) == 6 || (p) == 9 || (p) == 10 || (p) == 11)
  5. #endif

Si le define ’__AVR_ATmega8__’ existe, c’est à dire si l’Arduino concerné est équipé d’un micro contrôleur ATmega8 comme les tout premiers Arduino, le compilateur va conserver le premier #define digitalPinHasPWM(p) ..., sinon il prendra l’autre ligne dans le #else . Un #if doit toujours se terminer par un #endif.

Plusieurs syntaxes sont possibles :

#if defined(__AVR_ATmega8__)
#if defined __AVR_ATmega8__
#ifdef __AVR_ATmega8__

Le test inverse est

#if not defined(__AVR_ATmega8__)
#if !defined __AVR_ATmega8__
#ifndef __AVR_ATmega8__

- error
Le but est de provoquer une erreur du compilateur… Mais pourquoi en vouloir alors qu’on s’obstine à les éradiquer ? Imaginons que nous voulions absolument que MAXSTRLEN soit défini :

  1. #ifndef MAXSTRLEN
  2. #error MAXSTRLEN is not defined !
  3. #endif

S’il n’est pas défini, l’IDE arrêtera la compilation avec le message :

sketch_test:2 : error : #error MAXSTRLEN is not defined !
#error MAXSTRLEN is not defined !

Dans le même ordre d’idée, on pourrait empêcher la compilation pour un modèle d’Arduino incompatible, ou si l’on a oublié d’inclure telle ou telle bibliothèque…

- pragma
Ce type d’ordre sert à guider le compilateur, à lui interdire certains warnings par exemple, ou à donner des directives pour l’optimisation du code produit. Son utilisation assez rare avec l’Arduino. Un exemple :
#pragma GCC optimize ("-O2")
A partir de cette ligne, l’optimisation appliquée par le compilateur sera de niveau 2. Le chapitre sur la compilation expliquera le principe des optimisations.

Pour terminer avec le pré-processeur, voyons le résultat de son travail sur notre petit exemple :

  1. // Entête len.h pour len.c
  2.  
  3. #define MAXSTRLEN 255
  4.  
  5. byte len(const char *inText);
  1. // Fichier len.c
  2.  
  3. #define MAXSTRLEN 255
  4.  
  5. int len(const char *inText)
  6. {
  7. // retourne la longueur de la chaîne passée en argument.
  8. ...
  9. // Taille maxi atteinte...
  10. if (length > MAXSTRLEN)
  11. return MAXSTRLEN;
  12. ...
  13. return length;
  14. }
  1. // Source testlen.ino
  2. #include <len.h>
  3.  
  4. #ifdef MAXSTRLEN
  5. char text[MAXSTRLEN];
  6. #define COPY(SOURCE, DEST) strncpy(DEST, SOURCE, MAXSTRLEN);
  7. #else
  8. char text[80];
  9. #define COPY(SOURCE, DEST) strncpy(DEST, SOURCE, 80);
  10. #endif
  11.  
  12. void setup()
  13. {
  14. COPY("Locoduino", text);
  15. int len = len(text);
  16. }
  17. void loop()
  18. {
  19. }

Cela va devenir un fichier intermédiaire de compilation :

int len(const char *inText);

char text[255];

void setup()
{
 strncpy(text, "Locoduino", 255);
 int len = len(text);
}
void loop()
{
}

On a donc à ce moment là un fichier par source c ou cpp dépouillé de ses instructions pré-processeur, de ses remarques, de ses lignes vides (j’en ai remis pour plus de clarté…)…
La main est ensuite passée au compilateur.

Compilateur

Le compilateur va traduire le fichier résultant du pré-processeur en ordres compréhensibles pour le micro-contrôleur. Le résultat de la compilation sera un fichier .o dit fichier objet. Ce fichier comprend deux parties : la première regroupe la version compilée des fonctions trouvées dans le source pré-processé, c’est à dire setup et loop, tandis que la seconde recense les fonctions dites ’externes’ employées par ce source. Ici c’est len() et strncpy() qui sont notées puisqu’absentes de ce source.
On se retrouve avec un fichier objet pour chaque source.

Le rôle du compilateur n’est pas seulement un travail de traduction de texte, c’est aussi et surtout un outil qui va essayer de comprendre le code demandé, et d’y ajouter des optimisations autant que possible.

Prenons quelques lignes de code :

  1. const int a = 2;
  2. const int b = 6;
  3. const int c = 8;
  4.  
  5. int delta = b*b - 4*a*c;

qui donnera au final, après disparition des constantes, et calcul à la compilation d’une expression de toutes façons constante :

int delta = -28;

Autre cas :

  1. for (unsigned i = 0; i < strlen(string); ++i)
  2. {
  3. ...
  4. }

Dans une boucle, le test est effectué à chaque pas de la boucle. Si ce test est une expression, elle sera évaluée de nombreuses fois avec ses appels de fonction comme ici avec strlen. Si la chaîne ’string’ ne change pas pendant la boucle, le compilateur peut sortir l’appel à strlen dans une variable locale créée pour l’occasion et ainsi accélérer le traitement de la boucle :

  1. unsigned length = strlen(string);
  2.  
  3. for (unsigned i = 0; i < length; ++i)
  4. {
  5. ...
  6. }

C’est le compilateur qui voit si ’string’ est modifié ou pas pendant la boucle, mais il peut échouer à voir la modification si elle est loin dans les appels de fonction successifs… D’où la possibilité pour un compilateur de provoquer un bug là où le code semble fonctionner. C’est ce qui a poussé les concepteurs des compilateurs à fournir le moyen de maîtriser le niveau d’optimisation.

Table 1 : Résultats des optimisations
Niveau d’optimisationTaille en octetsTemps en seconde
-Os (priorité taille : défault) 19558 17.8
-O0 (sans optimisation) 31382 44.7
-O1 20428 17.0
-O2 20500 12.7
-O3 25550 12.2

Sur cet exemple de compilation d’un .ino de traitement graphique sur un écran Lcd, on voit d’abord que l’optimisation opérée par le compilateur a une forte influence, à la fois sur la taille du programme, et sur le temps d’exécution. D’autre part, sans optimisation (O0), les deux sont épouvantables ! Enfin, selon le niveau, on va privilégier l’un ou l’autre… Dans tous les cas, une optimisation peut provoquer des problèmes et rendre un programme inopérant. Malgré tout, on ne peux pas s’en passer totalement, sauf à accepter de gros programmes qui s’exécutent lentement ! Comme toujours, c’est une affaire de compromis.

Linkeur

Le Linkeur, éditeur de liens dans notre belle langue, est chargé de recoller tous ces morceaux, ces fichiers objets, et de fabriquer un résultat unique à exécuter, ou dans le cas de l’Atmel, à transmettre au micro-contrôleur. Il reçoit en argument la liste des fichiers à assembler, nos objets, mais aussi une liste de librairies (fichiers .lib, .l ou .so) qui contiennent des objets tout faits, comme les fonctions de base Arduino pinMode ou digitalWrite, ou l’objet Serial, ou encore les librairies standard du C. Armé de tout cela, le linkeur va vérifier que toutes les pièces du puzzle de ce qui lui a été demandé sont bien présentes avant de construire le résultat final. C’est à ce moment là que vous verrez sortir des erreurs du type "undefined reference to `function()’".
Par exemple, si on avait déclaré dans notre len.h une fonction EnMajuscule() qui mettrait la chaîne passée en argument en majuscule :

  1. byte len(const char *inText);
  2. char *EnMajuscule(const char *);}

On pourrait ainsi l’utiliser dans le .ino ou dans les .cpp associés au projet. Comme il ne se base que sur les sources, le compilateur ne pourrait pas savoir si une telle fonction EnMajuscule existe quelque part dans les objets qui seront réunis à la fin par le linkeur. Sa déclaration lui suffit pour compiler, et c’est ce qu’il fait.

Le linkeur, lui, tente de recoller les morceaux épars, tous les objets et les librairies [3], et il va s’apercevoir qu’à la fin, une référence vers une fonction n’est pas résolue. C’est à ce moment que surgit l’erreur.

Au final, le résultat est un fichier .hex présent dans le répertoire temporaire. C’est lui qui va être téléversé vers l’Arduino.

Pour conclure

A la lecture de tout ce que doit faire un compilateur, et vous vous doutez bien que je n’ai vraiment pas approfondi les sujets, on comprend mieux le temps de compilation qui peut sembler long pour un fichier .ino qui est somme toute souvent assez court… Et ces temps deviennent encore plus importants lorsque l’on commence à toucher à des plateformes comme ARM, ESP ou STM qui ajoutent une couche conséquente dans les bibliothèques standard du C. Mais maintenant au moins, vous avez une idée de ce qui se trame derrière le rideau !

[1le ’#’ représente l’assemblage de quatre ’+’, ce qui place le C# comme le successeur de C++ !

[2certains langages comme Java sont à la fois compilés et interprétés. Java est compilé vers un binaire qui ne correspond à aucune machine réelle et appelé byte code. Ce byte code est ensuite interprété

[3au sens fichiers .lib ou similaires, pas les bibliothèques de l’Arduino…

2 Messages

Réagissez à « La compilation d’un projet Arduino »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Programmation »

Comment gérer le temps dans un programme ?

La programmation, qu’est ce que c’est

Types, constantes et variables

Installation de l’IDE Arduino

Répéter des instructions : les boucles

Les interruptions (1)

Instructions conditionnelles : le if … else

Instructions conditionnelles : le switch … case

Comment concevoir rationnellement votre système

Comment gérer l’aléatoire ?

Calculer avec l’Arduino (1)

Calculer avec l’Arduino (2)

La compilation d’un projet Arduino

Les structures

Systèmes de numération

Les fonctions

Trois façons de déclarer des constantes

Transcription d’un programme simple en programmation objet

Ces tableaux qui peuvent nous simplifier le développement Arduino

Les chaînes de caractères

Trucs, astuces et choses à ne pas faire !

Processing pour nos trains

Arduino : toute première fois !

Démarrer en Processing (1)

TCOs en Processing

TCOs en Processing (2)

Comment réussir son projet Arduino

Le monde des objets (1)

Le monde des objets (2)

Le monde des objets (3)

Le monde des objets (4)

Les pointeurs (1)

Les pointeurs (2)

Les Timers (I)

Les Timers (II)

Les Timers (III)

Les Timers (IV)

Les Timers (V)

Bien utiliser l’IDE d’Arduino (1)

Bien utiliser l’IDE d’Arduino (2)

Piloter son Arduino avec son navigateur web et Node.js (1)

Piloter son Arduino avec son navigateur web et Node.js (2)

Piloter son Arduino avec son navigateur web et Node.js (3)

Piloter son Arduino avec son navigateur web et Node.js (4)

Les derniers articles

La compilation d’un projet Arduino


Thierry

TCOs en Processing (2)


Pierre59

Comment concevoir rationnellement votre système


Jean-Luc

Comment réussir son projet Arduino


Christian

Piloter son Arduino avec son navigateur web et Node.js (4)


bobyAndCo

TCOs en Processing


Pierre59

Ces tableaux qui peuvent nous simplifier le développement Arduino


bobyAndCo

Processing pour nos trains


Pierre59

Piloter son Arduino avec son navigateur web et Node.js (3)


bobyAndCo

Piloter son Arduino avec son navigateur web et Node.js (2)


bobyAndCo

Les articles les plus lus

Les interruptions (1)

Les Timers (I)

Ces tableaux qui peuvent nous simplifier le développement Arduino

Les chaînes de caractères

Comment gérer le temps dans un programme ?

Instructions conditionnelles : le if … else

Arduino : toute première fois !

Piloter son Arduino avec son navigateur web et Node.js (1)

Calculer avec l’Arduino (1)

Bien utiliser l’IDE d’Arduino (1)