dimanche 31 décembre 2017

Arduino - C+ASM

Dans un précédent article disponible ici nous avons vu comment faire pour utiliser TextWrangler (ou BBEdit) en lieu et place de l'éditeur de texte de l'Ide Arduino, avec une  coloration de syntaxe et un Apple Script permettant de lancer directement la compilation et le transfert. Dans un second article, disponible ici, nous avons vu comment installer AVRA et AVRDUDE ainsi qu'un module AppleScript pour TextWrangler afin de programmer notre Arduino en Assembleur.
Puisque nous avons tout pour travailler en C et en Assembleur avec TextWrangler, nous allons voir cette fois comment mélanger ces 2 langages, sans se prendre la tête à taper du code inline.

Pourquoi mélanger le C et l'assembleur?

L'assembleur possédait, possède et possédera toujours un énorme avantage sur les langages dit "évolués": celui de vous fournir une connexion directe avec le processeur en lui fournissant exactement les instructions nécessaires, ni plus, ni moins.
Un langage évolué quant à lui, permet l'usage de fonctions plus complexes mais qui doivent être évidement traduites en langage machine par le compilateur, cette traduction produisant des résultats plus ou moins efficaces.
Ainsi, le simple "blink" qui fait clignoter une LED de notre Arduino, occupe environ 900 octets de FLASH quand il est écrit en C et moins de 200 quand il est écrit en assembleur.
Code plus petit signifiant moins d'instructions pour le processeur, signifie donc aussi exécution plus rapide.

Ceci étant ce gain de vitesse et de taille n'est pas toujours intéressant.
En effet si on regarde nos codes Arduino, on voit que très souvent nous rajoutons des delay() pour que les Led ne clignotent pas trop vite ou pour que l'utilisateur ait le temps de réagir. Or si on passe notre temps à attendre, pourquoi faire du code assembleur pour aller plus vite?
De même, si nous regardons la consommation mémoire nous constatons que la mémoire qui est la plus rapidement saturée ce n'est pas la FLASH donc la mémoire qui contient le code machine, mais la SRAM qui contient les variables. Or l'assembleur va essentiellement permettre d'obtenir un résultat plus petit au niveau du code donc de l'occupation de la FLASH. De plus, si nous arrivons en limite de FLASH, c'est généralement parce que notre projet est très gros et dans ce cas il y a de fortes chances qu'il nécessite un grand nombre de branchements. En fait c'est au niveau de la SRAM et du nombre de PIN que nous allons le plus souvent atteindre les limites de notre Arduino nous obligeant à passer par exemple du UNO au Mega.

Par contre, imaginons que nous soyons sur un projet Arduino avec de la communication: une capture d'image puis un envoi via un module HC-12 donc sur une grande distance. Plus nous allons augmenter la distance de transmission, plus nous allons perdre en vitesse. Une solution consisterait à compresser les données. Ainsi, si notre image nécessite 6 secondes de transmission et que nous compressons les données pour qu'elles n'occupent plus que la moitié, nous pourrons transmettre en 3 secondes. Sauf qu'il va falloir compresser et que cela va prendre du temps. Si la compression prend 4 secondes cela ne vaut plus la peine, car au lieu de 6 secondes de transmission de données non-compressées, nous aurons 4 secondes de compression plus 3 secondes de transmission soit un total de 7 secondes. Cela vaut donc la peine à condition que la compression prenne moins de temps que le gain de temps de transmission. Et plus la compression sera rapide et plus cela sera intéressant. Nous avons ici un exemple d'application de l'assembleur.
Mais pour tout le reste de notre développement, autant utiliser le C car le développement est plus rapide (on tapes moins de code).
En clair, la solution serait de mélanger le C et l'assembleur: on utilise le C pour le corps de notre programme et l'assembleur pour les parties nécessitant un code plus compact et/ou plus rapide.

Comment mélanger?

Pour mélanger le code C et le code ASM, il existe 3 solutions.
La première consiste à utiliser du code 'inline'. Pour cela on tape le code assembleur en plein milieu du code C, en ajoutant des informations pour que le compilateur sache que c'est de l'assembleur. Vous trouverez ici un document expliquant comment faire.
https://web.stanford.edu/class/ee281/projects/aut2002/yingzong-mouse/media/GCCAVRInlAsmCB.pdf
Le problème c'est que la syntaxe de l'assembleur n'est pas forcément très claire et là, en devant ajouter des guillemets, des retours chariots etc... pour se conformer à cette syntaxe un peu spéciale, cela devient encore moins clair. En plus nous tapons en plein milieu de notre code en C, ce qui veut dire que nous ne pouvons pas exécuter notre code Assembleur tout seul, par exemple avec AVRA.

La seconde solution consiste à s'insérer dans le processus de création de l'exécutable, au niveau du linker. C'est la solution la plus communément utilisée. Lorsque vous compilez votre code C, cela ne génère pas tout de suite un fichier exécutable. Il y a génération d'un fichier intermédiaire, un fichier (objet) dont l'extension est généralement ".o" ou ".obj". Tous les fichiers C de votre projet vont donc donner des fichiers objets et c'est un autre outil, le "linker" qui va lier ces fichiers entre eux pour générer le fichier exécutable. Sachant que de son côté l'assembleur génère lui aussi ces fichiers intermédiaires, on a donc là une solution apparement simple: le compilateur C produit des fichiers objets, l'assembleur produit des fichiers objets, nous fournissons tous ces fichiers objets au linker qui va tout coller ensemble et nous produire notre exécutable.


Le contenu du dossier après assemblage avec AVRA

En théorie, c'est une excellente idée. Le problème c'est que les fichiers objets contiennent des références les uns aux autres. En effet, un fichier objet peut contenir des fonctions qui sont appelées à partir du code contenu dans un autre fichier objet. A la compilation, chaque fichier C donc chaque fichier objet pourra paraître bon, et c'est le linker qui découvrira que telle fonction appelé dans le code d'un fichier n'existe en fait dans aucun des autres fichiers du projet. Mais pour découvrir cela et coller correctement ensemble les fichers .o encore faut-il qu'ils soient reconnus par le linker.
Le problème c'est qu'apparement le compilateur GCC utilisé par l'Ide Arduino, produit des fichiers objets dans un format différent de ceux produits par l'assembleur AVRA. On ne peut donc pas tout fournir au "linker"...

La vérité est ailleurs

Une recherche dans les dossiers de nos projets soulève une interrogation: où sont les fichiers intermédiaires donc les fichiers "objet" de nos projets Arduino?
De plus, tout le monde affirme sur les différents forums que l'Arduino se programme en C++. Mais dans ce cas, pourquoi nos fichiers ne portent pas l'extension ".cpp" mais une extension ".ino"?
La réponse à ses deux interrogations va nous permettre de comprendre quelques détails dans la gestion des projets et va nous permettre de mettre au point une troisème méthode pour mélanger, cette fois-ci très facilement, du C et de l'assembleur!
Lorsque l'on développe avec XCode ou Android Studio, chaque projet se compose d'un grand nombre de dossiers et de fichiers. Or, si nous commençons à déplacer ou à renommer ces fichiers ou ces dossiers, plus rien ne fonctionne.

L'Ide Arduino ne possède pas un tel sysème de projet. Son système est beaucoup plus simple mais quand même assez "tordu": quand on crée un projet sur Arduino, ce projet doit être mis dans un dossier et le fichier ".ino" doit avoir le même nom que ce dossier. On peut renommer le dossier mais dans ce cas on doit aussi renomer le fichier pour que les deux aient le même nom.
Or le fichier ".ino" que vous générez, n'est pas un totalement un fichier C++. Il y ressemble beaucoup mais il lui manque quelques informations qui font que ce fichier n'est pas utilisable directement par GCC, le compilateur. Quand vous cliquez sur "Téléverser", l'Ide Arduino ouvre le dossier du projet et y cherche un fichier ".ino" avec le même nom que le dossier. Il charge ce fichier .ino et en fait une copie qu'il modifie en y ajoutant les éléments manquants, ceci afin d'en faire un vrai fichier C++. Il sauve ensuite cette copie en y ajoutant l'extension ".cpp". Sur Mac OSX, ce fichier est sauvé dans le compte utilisateur, dans Documents/Arduino/sketch. Si votre sketch se nomme "essai.ino", ce nouveau fichier s'appelera "essai.ino.cpp"
Si vous jetez un oeil dans le dossier Documents/Arduino vous y verrez aussi un dossier "core" avec tout un tas de fichiers ".o" pour les fonctions de base de l'Arduino.

Mais stop! Nous avons raté un détail. Regardez le fichier ci-dessous. C'est un bout de code tout simple. Une fonction setup et une fonction loop qui appelle une fonction blink, le tout dans un seul fichier. Créez un dossier et déposez ce fichier en lui donnant le même nom que le dossier, par exemple "tst_blink.ino" dans le dossier "tst_blink". Téléversez: la Led clignote, rien de bizarre. Maintenant, sélectionnez la fonction "blink" qui est en bas de ce fichier, découpez là et sauvez là dans un autre fichier que vous appelerez par exemple "bloc.ino" et que vous sauverez dans le même dossier.

Vous aurez donc un dossier "tst_blink" avec deux fichiers: "tst_blink.ino" et "bloc.ino". Cliquez dans l'éditeur pour téléverser "tst_blink.ino". Suprise, ça continue à fonctionner tout pareil... Ouvrez maintenant le dossier Documents/Arduino/sketch. Vous y verrez "tst_blink.ino.cpp" donc le fichier qui a été dupliqué par l'ide Arduino. Mais ouvrez le avec un éditeur de texte: surprise! Il contient à la fois les données de "tst_blink.ino" et celle de "bloc.ino".
En fait, l'Ide Arduino se repère sur le nom du dossier, prend le fichier qui à la même nom, le charge puis y recopie tous les autres fichiers ".ino" qu'il trouve dans le même  dossier.
C'est ce qui explique que si vous avez des anciennes versions de votre code dans le dossier de travail, plus rien ne marche car l'Arduino incorporera tout dans le code C++ qu'il enverra à GCC pour la compilation.

La solution

En réfléchissant un peu, nous tenons notre solution! Nous allons créer un dossier de projet, y mettre notre code C dans un fichier .ino avec le même nom que ce dossier. Nous allons ensuite créer un fichier .asm dans lequel nous allons taper notre code assembleur au format AVRA. Nous allons pouvoir l'assembler avec AVRA, le modifier etc... Ce fichier nous le sauverons lui aussi dans le dossier de notre projet, mais avec un nom différent. Nous aurons par exemple un dossier "essai", un fichier "essai.ino" et un fichier "bloc.asm". Nous allons ensuite programmer un module AppleScript pour TextWrangler (ou BBEdit, c'est pareil). Lorsque nous déclencherons ce module il chargera "bloc.asm", le transformera pour le mettre au format "inline", puis le sauvera sous le nom "bloc.ino" dans le dossier. Ensuite notre script appelera l'ide Arduino. Celui-ci chargera "essai.ino" y incorporera "bloc.ino" et enverra le résultat à GCC!

Le Script

Voici le code AppleScript. Pour l'installer, reportez vous à l'article sur l'utilisation de TextWrangler comme éditeur pour l'Arduino (ici). Le principe d'installation est identique.



Attention: il faut régler les préférences de l'Editeur AppleScript. Cliquez dans le menu "Editeur de script", option "Préférences" puis sur l'onglet "Modifications" et cochez "Tabulations et sauts de ligne dans les chaînes". Ceci permet de faire des scripts qui écrivent des tabulations et des sauts de lignes sans que ceux-ci ne soient "éxécutées" dans l'éditeur.


Faisons un petit tour dans ce script, bien que la masse de commentaires devraient vous permettre de vous en sortir. On commence par prélever le texte, soit la zone sélectionnée, soit tout. Puis on va dans la fonction de génération du code ASM. On remplace les tabulations et les doubles espaces dans tout le texte, puis on analyse celui-ci, ligne par ligne. On supprime les commentaires et on nettoie les espaces de fin de ligne. Puis nous regardons si cette ligne contient un ".EQU" c'est-à-dire l'équivalent d'un #define en Assembleur. Si c'est le cas nous en prélevons le nom et la valeur.
Nous allons ensuite regarder si nous devons déposer le code ASM ou non. J'ai en effet trouvé intéressant de ne pas forcément tout "traduire" en code Inline. Afin de délimiter la ou les zones de code à "traduire", on doit simplement ajouter "_start_asm:" pour marquer le début d'une zone et "_end_asm:" pour en marquer la fin. On peut ajouter autant de start et de end que l'on veut mais ils ne doivent évidement pas être imbriqués.
Nous faisons ensuite un chercher-remplacer des éventuels ".equ" et nous ajoutons la chaine au code final.
Nous allons ensuite sauver le fichier (subroutine save_asm). Nous prenons le chemin complet du fichier, nous changeons l'extension pour mettre ".ino" et nous sauvons le code généré. Ensuite, nous prenons le nom du dernier dossier du chemin: puisque c'est le nom du dossier du projet, c'est donc aussi le nom du fichier .ino principal! Nous construisons donc le chemin pour pointer vers ce fichier .ino principal (qui est donc notre fichier en C, avec setup() et loop() et nous demandons à l'utilisateur s'il veut qu'on déclenche l'ide Arduino pour "Téléverser". S'il accepte, on lance l'Ide, qui va donc prendre le fichier C ".ino" et va le traiter en y incorporant notre code ASM.



Assez souvent le premier déclenchement de l'Ide Arduino ouvre celui-ci, mais il ne téléverse rien. Mais dès le second déclenchement, ça marche.

Une fois ce script installé dans le dossier avec les autres scripts pour TextWrangler, il apparaîtra dans le menu avec l'icône en forme de parchemin avec le nom sous lequel vous avez sauvé le script (pour moi c'est "Arduino ASM+C.scpt")



Deux petits détails: en haut du script se trouve "tell application "TextWrangler"". Si vous utilisez BBEDit, remplacer par "tell application "BBEdit"". Tout en bas du code se trouve "set MyArduinoPath to quoted form of POSIX path" avec le chemin vers l'Ide Arduino. En ce qui me concerne l'Ide est dans un dossier "Dev" ce qui explique ce chemin. A vous de modifier suivant votre configuration.

Un exemple

Mettons notre méthode en pratique avec un bout de code C qui va appeler une fonction en Assembleur pour faire un simple "blink".
Dossier: c_asm_blink" dans lequel je met le fichier c_asm_blink.ino ci-dessous
C'est un fichier C classique, avec notre setup() et notre boucle loop(). Dans celle-ci nous appelons notre fonction ams_blink qui sera mise dans notre fichier assembleur.
cet appel se fait avec une ligne d'assembleur inline avec l'instrution "call".
Donc asm ("call asm_blink");



Ensuite je crée un fichier blink.asm dans lequel je met le code ci-dessous.
C'est un fichier assembleur au format AVRA, qui commence par des commentaires puis des .equ et ensuite le code. Au départ nous réglons la PIN du Led en sortie. Comme ceci ne nous interesse pas, nous mettons le tag "_start_asm:" plus bas afin que seule la fin du fichier soit convertie (Note: si nous ne mettons pas de tag "_end_asm:" le conversion se fait jusqu'à la fin)
Je trouve bien ici le label "asm_blink:" qui permettra à ma fonction "loop()" de mon fichier C d'appeller ce code assembleur.

Tout ceci étant en place, je lance mon script "Arduino ASM+C" via le menu avec le petit parchemin. Une alerte me demande si je veux lancer l'Ide Arduino. Je répond oui, l'Ide s'ouvre, charge le fichier c_asm_blink/c_asm_blink.ino, y incorpore blink.ino, envoi tout ça à GCC qui compile puis passe ça au linker. Le résultat est envoyé à la carte et.... ça blink!
Par curiosité, ouvrons le dossier c_asm_blink. Nous y voyons notre fichier C, notre fichier ASM mais aussi le fichier que le script à généré donc "blink.ino". En l"ouvrant nous y voyons le code assembleur au format Inline, code issu du traitement de notre fichier assembleur d'origine.
Si nous sommes encore plus curieux, nous pouvons allez voir dans le dossier Documents/Arduino/sketch. Nous y verrons le fichier c_asm_blink.ino.cpp et en l'ouvrant nous verrons qu'il contient à la fois le code C et le code assembleur.

Un deuxième exemple

Dans ce premier exemple nous avons appelé un bout de code assembleur qui travaillait tout seul, sans aucun lien avec le C (sauf l'appel). Dans ce second exemple, nous allons envoyer à notre fonction assembleur un mot écrit en minuscule. Le code assembleur va le mettre en majuscule et au retour le code C va l'afficher.

Le principe est le même: dossier "c_asm_string" dans lequel nous mettons notre code "c_asm_string.ino", ci-dessous.

Définition de notre chaîne, puis setup() avec réglage de la vitesse du port série puis affichage du mot avec un classique Serial.println(chaine);
Nous appelons ensuite notre fonction assembleur, qui se nomme "uppercase". L'appel se fait avec:
    asm("call uppercase"::"z"(chaine));
Le mot "asm" au début indique que c'est du code assembleur. Nous avons ensuite l'instruction d'appel ("call uppercase"), puis nous avons des ":". Dans un prochain article nous verrons cela plus en détal mais sachez que cela sert à passer des valeurs au code assembleur. Dans le cas présent, nous avons "z"(chaine) ce qui signifie que nous allons mettre l'adresse de notre chaine dans le registre z qui est un registre que le processeur utilise pour gérer les adresses.
En retour l'assembleur aura modifié la chaine que nous allons à nouveau afficher et qui, cette fois, sera en majuscule.

Nous créons maintenant notre fichier "uppercase.asm" que nous mettons dans le dossier "c_asm_string". Le code est très simple: nous sauvons le registre r16 sur la pile, pour le protéger. Puis nous mettons dans r16 un octet que l'on trouve à l'adresse z. Donc ici, c'est le premier caractère de notre chaine. Nous testons pour voir si c'est l'octet de fin (qui vaut 0). Si oui, nous sautons au label "exit" auquel nous récupérons la valeur de r16 que nous avions sauvé, et nous retournons au C (ret = return). Si r16 ne vaut pas 0, nous lui soustrayons 0x20 (soit 32 en décimal). Comme le code ascii d'une lettre minuscule est de 32 plus haut que celui de son équivalent en majuscule, en retirant 32 au code ascii de nos lettres minuscules nous les passons en majuscule. Nous remettons ensuite r16 dans le registre z en incrémentant celui-ci, ce qui le fait pointer sur le caractère suivant. Nous bouclons ensuite au label "loop:" et ainsi de suite jusqu'à la fin de la chaîne.

Quand nous sommes à la fin, nous sortons donc nous revenons dans le C qui affiche à nouveau la chaîne avec     Serial.println(chaine); montrant désormais la chaine en majuscule.

Comme tout à l'heure, il suffit de déclencher le script via le menu de TextWrangler pour que le .asm soit mis en .o et que tout soit compilé et téléversé.

 

Conclusion

Mélanger du C et de l'Assembleur devient ainsi beaucoup plus facile. Soit vous ne tapez que le code nécessaire (comme nous l'avons fait avec uppercase.asm) c'est-à-dire que vous ne pourrez sans doute pas l'assembler avec AVRA, soit vous tapez tout le code ASM avec les init, les EQU etc... ce qui vous permettra de l'assembler avec AVRA et de le tester. Dans ce cas, l'usage des tags "_start_asm:" et "_end_asm:" vous permettra de sélectionner les blocs à traduire en "inline".

Arduino - C+ASM

Dans un précédent article disponible ici nous avons vu comment faire pour utiliser TextWrangler (ou BBEdit) en lieu et place de l'édite...