Dans cet article, je vous propose une solution au challenge lancé dans l’article précédent, écrire un programme en assembleur pour créer un chenillard. Pour ma part, cela a constitué mon premier programme en assembleur pour une carte Uno et mes débuts pour prendre en main Studio 7. Le programme a fonctionné du premier coup, preuve que ce n’est pas si compliqué de concevoir en assembleur, mais ce genre de programme est assez simple. Je vais donc essayer de rassembler mes souvenirs pour vous expliquer comment je m’y étais pris.
L’assembleur (5)
Première application pour le modélisme ferroviaire
. Par :
DIFFICULTÉ :
Qu’est-ce qu’un chenillard ?
Ne cherchez pas ce terme dans un dictionnaire car il n’existe pas ! Pourtant, il est couramment utilisé en électronique pour désigner un dispositif qui fait cheminer un flash lumineux de lampe en lampe comme on peut le voir sur la route pour signaler qu’une voie de circulation se rabat sur la voie adjacente ou bien pour baliser un virage particulièrement dangereux. Sa reproduction en modélisme ferroviaire est donc un grand classique et peut être réalisée avec une carte Uno. C’est d’ailleurs un exercice que nous avons proposé aux débutants dès le début de Locoduino dans le premier article que nous avons écrit ! (Un chenillard de DEL)
Le principe d’un chenillard
Le principe est simple : on allume la première LED, on attend un très court temps car on veut reproduire un flash, on éteint la première LED, puis on applique la même chose à la LED suivante et ainsi de suite.
Le listing suivant vous le montre en langage C :
// Initialisation des lignes 4 à 9 en sortie void setup () { } // Fonction loop void loop () { // Extinction de toutes les DEL au départ du programme for (byte i = 4 ; i <= 9 ; i++) { } // Boucle pour faire flasher les DEL for (byte i = 4 ; i <= 9 ; i++) { } // délai de 500 millisecondes // Recommence la séquence }
Nous allons donc essayer de transcrire ce programme en assembleur. Pour avoir compris le programme Blink développé précédemment (article L’assembleur (3)), nous savons déjà initialiser une ligne en sortie et allumer ou éteindre cette ligne. On sait également produire une temporisation. Il faut maintenant un moyen pour passer à la ligne suivante dans l’ordre des sorties de la carte Uno et pour cela, pourquoi ne pas utiliser une instruction qui décale les bits d’un registre ?
Réaffectation des lignes de sortie
Comme vous le voyez plus haut, notre programme en C utilisait les sorties 4 à 9 de la carte Uno pour créer le chenillard. Comme on le voit sur la figure 1, les sorties 4 à 7 sont reliées au port D (PD4:7) du microcontrôleur et les sorties 8 et 9 au port B (PB0:1).
Il serait en fait plus judicieux de travailler avec un seul port d’entrée-sortie, par exemple le port D avec les lignes PD2:7 qui correspondent aux sorties 2 à 7 de la carte Uno. Un des réflexes à avoir quand on programme en assembleur, c’est de bien prendre en compte l’aspect matériel du microcontrôleur afin de l’utiliser le plus efficacement possible. Donc, c’est décidé, nous allons créer un chenillard sur les sorties 2 à 7 de notre carte Uno, ce qui fait seulement utiliser le port D.
Fonctionnement logiciel d’un port d’entrée-sortie
Un port d’entrée-sortie est défini par trois registres de 8 bits, deux dans lesquels on peut lire et écrire (Read/Write) appelés registre de données PORT et registre de direction DDR, un troisième en lecture seule appelé registre d’entrée PIN. La figure 2 montre ce qu’il en est pour notre port D.
Comme nous allons utiliser le port D en sortie, seuls les deux premiers registres nous intéressent. Le registre de direction sert justement à définir chacune des lignes du port soit en entrée (le bit doit être zéro) soit en sortie (le bit doit être 1). Le registre de données sert à mettre la ligne à l’état HIGH (le bit doit être à 1) ou à l’état LOW (le bit doit être à 0). Si les LED sont reliées par leur anode, c’est un état HIGH qui les allume et un état LOW qui les éteint.
Nous avons déjà une bonne idée de comment concevoir notre programme. Quelques conseils avant de se mettre devant notre console.
Un peu d’organisation
La première chose à faire avant de se mettre à coder, c’est déjà de sortir un bloc de papier et un crayon gomme ; et oui, on aura certainement à corriger certaines idées, donc la gomme est bien utile. Sur ce bloc, on peut écrire toutes les idées à étudier, dessiner nos registres pour bien comprendre comment les 8 bits sont organisés, choisir les instructions, définir les différentes phases, esquisser les sous-programmes nécessaires : dans notre cas, il nous faut un sous-programme pour attendre mais on peut reprendre celui du programme Blink en l’adaptant aux durées à obtenir.
Pour choisir les instructions, le mieux est de les avoir sous la main ; je me rappelle que pour ce premier programme, j’ai imprimé le jeu d’instructions de l’ATmega328P pour m’y référer et je l’ai encore à l’heure actuelle, rangé dans des pochettes plastiques. Cela aide au travail de conception qui peut alors se faire ordinateur éteint, sur votre bloc de papier et avec cette documentation restreinte.
Décalage des lignes allumées
Le travail de réflexion nous amène à définir le contenu du registre DDRD qui sera en binaire 11111100 : les lignes 2 à 7 en sortie. Pour allumer la première LED (la ligne 2), le registre de données PORTD sera en binaire 00000100 (ligne 2 à l’état HIGH). Pour passer à la ligne de sortie suivante, il faut décaler ce registre vers la gauche d’une position, ce qui éteint la ligne allumée et allume la ligne suivante. Il faut trouver les instructions capables de réaliser cela ; examinons donc les instructions concernant les bits. Comme le montre la figure 3, aucune ne permet de décaler les bits d’un port. Il faut donc travailler avec des registres et ensuite transférer son contenu dans le port.
L’instruction LSL permet un décalage vers la gauche. Cette instruction agit également sur le flag de carry C qui récupère le bit 7 ; c’est un moyen de savoir qu’il faut ensuite revenir à la ligne de sortie 2. L’instruction ROL devrait aussi pouvoir convenir ; en plus du décalage, le bit 7 est transféré dans le bit de carry C puis C est réinjecté dans le bit 0, ce qui crée une rotation des bits du registre. Finalement, j’ai décidé de prendre LSL et de voir si cela fonctionnerait. Au moment où je récupère un bit à 1 dans le bit carry, il est temps d’attendre une pause plus longue puis de recommencer le processus. Un branchement va donc s’imposer à ce moment-là.
Ébauche du programme
Mon brouillon (que je n’ai hélas pas gardé) devait ressembler à cela :
Setup :
initialiser le port D en sortie pour les lignes 2 à 7
allumer LED_BUILTIN comme témoin de fin de setup (non obligatoire)
Loop :
chenillard éteint, attendre pause longue (entre chaque salve de flash)
allumer ligne 2 du port
Boucle :
attendre pose courte
décaler les bits à gauche (passer de ligne n à ligne n+1)
tant que C = 0 revenir à Boucle (instruction BRCC)
dès que C = 1, revenir à Loop car séquence entièrement effectuée
Delay :
sous-programme pour attendre (boucles imbriquées)
La seule question qui se posait était : faut-il remettre le flag C à zéro dans le programme ou bien est-ce fait automatiquement lors du prochain décalage ? Logiquement, lors du prochain décalage, le flag C devait recevoir un bit égal à zéro, donc il se réinitialise.
Autre chose existait sur mon brouillon, un calcul estimatif du temps à attendre avec le sous-programme delay.
Le programme
Voici le listing du programme écrit en assembleur et téléversé dans ma carte Uno grâce à Studio 7 :
; ; Chenillard_asm_Uno.asm ; Sur sorties 2 à 7 de carte Uno ; ; Created: 07/12/2020 10:17:47 ; Author : Christian ; Amended : 06/02/2021 ; Utilise LSL (Logical Shift Left) pour déplacer le flash ; Durée de flash approximativement 74 ms ; Durée entre salve approximativement 1500 ms ; Replace with your application code ldi r17,0b11111100 out DDRD, r17 ; lignes 2 à 7 en sortie sbi DDRB,5 ; ligne 13 en sortie sbi PORTB,5 ; ligne 13 on - LED_BUILTIN on (fin de setup) start: ldi r17,0b00000000 out PORTD,r17 ; chenillard éteint ldi r16,120 ; R16 règle durée de temporisation rcall delay ; durée de pause longue ldi r17,0b00000100 out PORTD,r17 ; ligne 2 on in r5,PORTD ; le port D est mis dans R5 loop_ppale: ; boucle principale du programme ldi r16,6 ; R16 règle durée de temporisation rcall delay ; durée de flash court lsl r5 ; décalage à gauche de R5 out PORTD,r5 ; PORTD décalé vers gauche brcc loop_ppale ; recommence décalage tant que C = 0 rjmp start ; C = 1 donc reprend toute la séquence delay: ; SBR de temporisation clr r8 ; R8 = 0 clr r9 ; R9 = 0 loop: ; boucles imbriquées avec R8, R9 et R16 dec r8 brne loop dec r9 brne loop dec r16 brne loop ret ; retour de sous-programme
L’écriture dans le port se fait d’abord en écrivant dans un registre (instruction LDI qui n’accepte que les registres R16 à R31, ici on a choisi R17 pour ce rôle) puis en transférant ce registre dans le port avec l’instruction OUT. Le transfert dans le sens inverse (port dans registre) se fait avec l’instruction IN. Quand il n’y a qu’un seul bit à écrire, on utilise SBI (pour PB5 qui est ligne de la LED_BUILTIN de la carte Uno). Le décalage se fait sur un registre (ici R5) qui ensuite est copié dans le port. Nous aurions pu réutiliser R17 au lieu de R5, mais le fait de réutiliser un registre utilisé auparavant implique de se poser la question : dois-je sauvegarder sa valeur avant d’en faire autre chose ? Ici, il n’y aurait eu aucun problème, mais comme on dispose de 32 registres, j’ai préféré pour cet exemple en utiliser un nouveau, ce qui m’a permis aussi d’expliquer l’instruction IN par la même occasion. Le sous-programme de temporisation utilise les registres R8, R9 et R16 et a déjà été commenté dans l’article L’assembleur (3).
Ce programme a fonctionné du premier coup, mais j’avais déjà un peu l’expérience de la programmation en assembleur sur MCU PIC16F84. J’ai tout de même changé la valeur chargée dans le registre R16 pour régler la durée du flash. En effet, je m’étais bel et bien trompé dans mon calcul ! J’ai donc divisé par 2 la valeur chargée dans R16, reprogrammé ma carte et le résultat m’a bien plu. Le calcul exact de la durée de temporisation a été fait dans l’article L’assembleur (3) ; dans ce programme, la durée du flash est de 74 ms et on attend 1500 ms entre chaque salve. On peut aussi régler ces deux temporisations par tâtonnements ; l’important est de réaliser quelque chose qui reproduise au mieux la réalité.
Comme expliqué un peu plus haut, voici le listing montrant la solution qui utilise R17 au lieu de R5, ce qui évite la relecture du PORT D :
start: ldi r17,0b00000000 out PORTD,r17 ldi r16,120 rcall delay ldi r17,0b00000100 out PORTD,r17 loop_ppale: ldi r16,6 rcall delay lsl r17 out PORTD,r17 brcc loop_ppale rjmp start
Cette solution est légèrement optimisée par rapport à la précédente. Encore une fois, il y a différentes façons de concevoir un programme : soit utiliser un registre différent pour chacune des opérations à faire, soit réutiliser un registre déjà employé auparavant mais en s’assurant que cela n’aura aucune conséquence fâcheuse pour la suite des opérations (éventuellement sauvegarder son contenu pour le récupérer ultérieurement). Il faut simplement agir en connaissance de cause.
Le programme initial écrit en C avec l’IDE d’Arduino utilisait 1014 octets de mémoire flash ; celui-ci écrit en assembleur en utilise 52 pour faire la même chose. Programmer en assembleur requiert de savoir comment fonctionnent le MCU et ses périphériques (ici les ports d’entrée-sortie) ; cela n’était pas nécessaire avec la programmation en C où il suffisait d’exprimer ce qu’on veut obtenir. Ce qu’il faut surtout retenir, c’est qu’écrire un programme en assembleur commence toujours par une grande réflexion sur la façon dont on va le construire. J’espère que l’exemple développé aujourd’hui vous servira pour mettre au point vos propres programmes.