LOCODUINO

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

jeudi 15 avril 2021

28 visiteurs en ce moment

L’assembleur (5)

Première application pour le modélisme ferroviaire

. Par : Christian

DIFFICULTÉ :

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.

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 :

  1. // Initialisation des lignes 4 à 9 en sortie
  2. void setup () {
  3. pinMode (4, OUTPUT) ;
  4. pinMode (5, OUTPUT) ;
  5. pinMode (6, OUTPUT) ;
  6. pinMode (7, OUTPUT) ;
  7. pinMode (8, OUTPUT) ;
  8. pinMode (9, OUTPUT) ;
  9. }
  10.  
  11. // Fonction loop
  12. void loop () {
  13. // Extinction de toutes les DEL au départ du programme
  14. for (byte i = 4 ; i <= 9 ; i++) {
  15. digitalWrite (i, LOW) ; // éteint la DEL reliée à la broche i
  16. }
  17.  
  18. // Boucle pour faire flasher les DEL
  19. for (byte i = 4 ; i <= 9 ; i++) {
  20. digitalWrite (i, HIGH) ; // allume la DEL sur broche i
  21. delay (50) ; // durée du flash 50 millisecondes
  22. digitalWrite (i, LOW) ; // éteint la DEL
  23. }
  24.  
  25. // délai de 500 millisecondes
  26. delay (500) ;
  27.  
  28. // Recommence la séquence
  29. }

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

Figure 1
Figure 1
Une partie du schéma de la carte Uno (source Arduino)

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.

Figure 2
Figure 2
Les registres du PORT D (source Microchip)

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.

Figure 3
Figure 3
Instructions concernant les bits (source Microchip)

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 :

  1. ;
  2. ; Chenillard_asm_Uno.asm
  3. ; Sur sorties 2 à 7 de carte Uno
  4. ;
  5. ; Created: 07/12/2020 10:17:47
  6. ; Author : Christian
  7. ; Amended : 06/02/2021
  8. ; Utilise LSL (Logical Shift Left) pour déplacer le flash
  9. ; Durée de flash approximativement 74 ms
  10. ; Durée entre salve approximativement 1500 ms
  11.  
  12.  
  13. ; Replace with your application code
  14. ldi r17,0b11111100
  15. out DDRD, r17 ; lignes 2 à 7 en sortie
  16. sbi DDRB,5 ; ligne 13 en sortie
  17. sbi PORTB,5 ; ligne 13 on - LED_BUILTIN on (fin de setup)
  18.  
  19. start:
  20. ldi r17,0b00000000
  21. out PORTD,r17 ; chenillard éteint
  22. ldi r16,120 ; R16 règle durée de temporisation
  23. rcall delay ; durée de pause longue
  24. ldi r17,0b00000100
  25. out PORTD,r17 ; ligne 2 on
  26. in r5,PORTD ; le port D est mis dans R5
  27.  
  28. loop_ppale: ; boucle principale du programme
  29. ldi r16,6 ; R16 règle durée de temporisation
  30. rcall delay ; durée de flash court
  31. lsl r5 ; décalage à gauche de R5
  32. out PORTD,r5 ; PORTD décalé vers gauche
  33. brcc loop_ppale ; recommence décalage tant que C = 0
  34. rjmp start ; C = 1 donc reprend toute la séquence
  35.  
  36. delay: ; SBR de temporisation
  37. clr r8 ; R8 = 0
  38. clr r9 ; R9 = 0
  39. loop: ; boucles imbriquées avec R8, R9 et R16
  40. dec r8
  41. brne loop
  42. dec r9
  43. brne loop
  44. dec r16
  45. brne loop
  46. ret ; retour de sous-programme

Télécharger

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 :

  1. start:
  2. ldi r17,0b00000000
  3. out PORTD,r17
  4. ldi r16,120
  5. rcall delay
  6. ldi r17,0b00000100
  7. out PORTD,r17
  8. loop_ppale:
  9. ldi r16,6
  10. rcall delay
  11. lsl r17
  12. out PORTD,r17
  13. brcc loop_ppale
  14. 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.

Réagissez à « L’assembleur (5) »

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 (1)

TCOs en Processing (2)

Comment réussir son projet Arduino

Passage à niveau géré par Arduino (1)

Passage à niveau géré par Arduino (2)

Passage à niveau géré par Arduino (3)

Passage à niveau géré par Arduino (4)

Passage à niveau géré par Arduino (5)

Du sketch à l’exécutable

Programmer vos Arduino avec un fichier hexadécimal

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

L’assembleur (8)


Christian

L’assembleur (7)


Christian

L’assembleur (6)


Christian

L’assembleur (5)


Christian

L’assembleur (4)


Christian

L’assembleur (3)


Christian

L’assembleur (2)


Christian

L’assembleur (1)


Christian

Programmer vos Arduino avec un fichier hexadécimal


Christian

Du sketch à l’exécutable


Christian

Les articles les plus lus

Les Timers (I)

Les interruptions (1)

Comment gérer le temps dans un programme ?

Instructions conditionnelles : le if … else

Répéter des instructions : les boucles

Bien utiliser l’IDE d’Arduino (1)

Ces tableaux qui peuvent nous simplifier le développement Arduino

Les chaînes de caractères

Calculer avec l’Arduino (1)

Les Timers (III)