LOCODUINO

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

mardi 19 mars 2024

Visiteurs connectés : 35

L’assembleur (7)

Techniques de programmation

.
Par : Christian

DIFFICULTÉ :

Après avoir lu les articles précédents, vous êtes maintenant capables de concevoir des routines écrites en assembleur au sein de programmes plus complexes écrits en C/C++. En opérant ainsi, vous exploitez la convivialité et l’efficacité du langage C/C++ tout en lui apportant un outil supplémentaire capable d’offrir un code plus compact, s’exécutant plus rapidement et respectant un timing très précis. Mais pour que l’assembleur devienne plus efficace que le C/C++ dans ces trois domaines, il convient de maîtriser les techniques de programmation liées à ce langage, techniques dont vous n’aviez pas à vous soucier en C/C++ qui s’occupait de beaucoup de choses à votre place. Cet article va donc parler de notions qui seront peut-être nouvelles pour vous, comme l’adressage, la gestion de la pile ou le timing.

<

Un programme ne fait rien d’autre que manipuler des informations, elles-mêmes représentées par des variables plus ou moins complexes. Avec le langage C/C++, il suffit de donner un nom à cette variable après lui avoir choisi un type (int, boolean, long, etc.) et chaque fois qu’on veut utiliser la variable, il suffit de la rappeler par son nom. C’est aussi simple qu’un smartphone : plus besoin de se rappeler du numéro de téléphone d’un ami pour pouvoir l’appeler. Avec l’assembleur, les choses ne sont pas aussi simples. Les variables sont contenues dans un ou plusieurs octets rangés en mémoire à des adresses mémoires bien définies qu’il faudra mémoriser afin d’aller chercher la variable quand on en a besoin ou simplement pour la stocker là où on pourra la retrouver.

Il est donc intéressant de comprendre comment manipuler l’adresse d’une variable afin d’accéder à la valeur de cette dernière dans le but de l’utiliser dans un programme. Mais avant de voir les techniques d’adressage, il est nécessaire de faire un petit retour sur les espaces mémoires des microcontrôleurs AVR.

Rappelez-vous de la mémoire

Ne faut-il pas déjà avoir de la mémoire pour être capable de se rappeler ? Justement, nous avons décrit la mémoire des microcontrôleurs AVR dans l’article L’assembleur (2). Grâce à son architecture Harvard, notre microcontrôleur AVR dispose de deux zones de mémoires : une réservée au programme et une réservée aux données. Chaque zone dispose de ses propres adresses.

La figure 1 montre la zone de mémoire qui permet de stocker des données, ce que Microchip appelle « Data Space » ou espace de données. Sur un ATmega328P, cette zone est constituée des 32 registres généraux, des 64 registres d’entrées-sorties, des 160 registres étendus d’entrées-sorties, puis de la mémoire SRAM qui permet à l’utilisateur de stocker ses propres données pour les retrouver et les manipuler [1]. Cette zone s’étend donc de l’adresse 0x0000 jusqu’à une adresse dont la valeur dépend de la quantité de mémoire SRAM dont dispose le MCU. Cette adresse la plus forte de l’espace de données s’appelle RAMEND.

Figure 1
Figure 1
L’espace de données

Dans l’espace de données de l’ATmega328P, les registres généraux occupent les adresses 0x0000 à 0x001F, les registres d’I/O occupent les adresses 0x0020 à 0x005F, les registres étendus d’I/O occupent les registres 0x0060 à 0x00FF. La SRAM commence à l’adresse 0x0100 jusqu’à RAMEND.

La figure 2 montre l’espace mémoire de programme, organisé en mots de 16 bits. Les adresses vont de 0x0000 à une valeur appelée FLASHEND qui dépend de la quantité de mémoire Flash dont dispose le microcontrôleur. Par exemple, pour l’ATmega328P, la valeur de FLASHEND vaut 0x3FFF (soit 16383) et la mémoire est organisée en 16K x 16. La partie droite de la figure 2 représente l’octet le moins significatif (LSB pour Least Significant Byte ou encore Low Byte) du mot de 16 bits, et la partie gauche l’octet le plus significatif (MSB pour Most Significant Byte ou encore High Byte).

Figure 2
Figure 2
Une représentation de la mémoire de programme

La mémoire de programme est supposée contenir les instructions du programme ; l’adresse de l’instruction à exécuter est contenue dans un registre appelé PC (Program Counter) qu’il suffit d’incrémenter pour aller chercher l’instruction suivante. Certaines instructions peuvent aussi écrire dans PC pour forcer le MCU à aller chercher une instruction à un autre endroit de la mémoire afin d’exécuter un sous-programme. Il faut aussi se rappeler qu’on peut utiliser la mémoire de programme pour y stocker des valeurs constantes de données, comme par exemple une chaîne de caractères. Ceci évite d’encombrer la mémoire SRAM qui est généralement petite. En assembleur, il faut faire attention que ces données ne soient pas interprétées comme des instructions, ce qui aurait pour résultat de faire planter le programme.

Un programme qui s’exécute a besoin d’aller chercher des informations soit dans l’espace mémoire de programme (instructions par exemple), soit dans l’espace mémoire des données (variables par exemple) et il doit pour cela prendre en compte l’adresse de cette information stockée en mémoire. Pour optimiser cela, le microcontrôleur dispose de plusieurs modes d’adressage que nous allons voir maintenant.

Les modes d’adressage des microcontrôleurs AVR

Les microcontrôleurs AVR disposent de 13 modes d’adressage différents ; suivant le modèle de MCU, tous ne sont pas disponibles. Ces modes d’adressage peuvent être classés en plusieurs grandes catégories :

  • Adressage direct : des informations sur la localisation de l’opérande en mémoire sont contenues dans l’instruction.
  • Adressage indirect : des informations sur la localisation de l’opérande en mémoire sont contenues ailleurs que dans l’instruction, par exemple dans un registre.
  • Adressage relatif : l’adresse pour continuer l’exécution du programme est calculée à partir de l’adresse de l’instruction en cours d’exécution.

Quand on parle « d’informations sur la localisation de l’opérande en mémoire », on sous-entend soit son adresse mémoire, soit une information permettant de localiser cette adresse, par exemple un numéro de registre de travail. Certains modes d’adressage concernent la mémoire de données uniquement alors que d’autres concernent la mémoire de programme uniquement. Nous allons décrire maintenant les différents cas possibles ; les figures 3 à 15 sont extraites du document « AVR Instruction Set Manual » de Microchip.

Adressage de la mémoire de données

On rappelle que la mémoire de données s’étend de 0x0000 à RAMEND, valeur qui dépend du modèle de microcontrôleur (voir figure 1).

Adressage direct simple registre

L’instruction qui utilise ce type d’adressage opère dans l’espace mémoire réservé aux 32 registres de travail et n’en utilise qu’un seul. Le numéro de registre (de 0 à 31, ce qui nécessite 5 bits seulement) correspond à son adresse. La figure 3 montre ce genre d’adressage : l’instruction est composée d’un code opératoire sur 11 bits et du numéro du registre sur 5 bits (ceux-ci n’étant pas forcément à la suite) qui contient l’opérande.

Figure 3
Figure 3
Adressage direct simple registre (source Microchip)

Exemples : DEC Rd. Cette instruction décrémente le registre Rd et place le résultat dans Rd (d compris entre 0 et 31). Un seul registre est nécessaire et l’instruction est codée en binaire par 1001 010d dddd 1010. De même, INC Rd incrémente le registre Rd et place le résultat dans Rd (registre de destination) ; son codage en binaire vaut 1001 010d dddd 0011.

Adressage direct double registre

Cette fois, l’instruction nécessite deux registres Rd et Rr (d et r étant deux nombres différents entre 0 et 31). Comme le montre la figure 4, l’instruction contient un code opératoire sur 6 bits et les numéros des deux registres (deux fois 5 bits). Un des deux registres est appelé registre de destination (Rd) car c’est dans ce registre que sera stocké le résultat de l’opération une fois celle-ci terminée. Cela veut dire que l’information initiale du contenu du registre Rd est perdue car remplacée par le résultat ; si vous avez besoin de garder cette information, c’est à vous de la sauvegarder avant d’effectuer l’opération.

Figure 4
Figure 4
Adressage direct double registre (source Microchip)

Exemple : ADD Rd, Rr (addition sans retenue) additionne deux registres Rd et Rr et place le résultat dans le registre Rd (d et r compris entre 0 et 31). L’instruction a pour code opératoire 000011 suivi des numéros des deux registres mais pas dans l’ordre. La valeur binaire est 0000 11rd dddd rrrr (notez que les bits de Rr ne sont pas à la suite).

Adressage direct des entrées-sorties

Les instructions qui utilisent ce type d’adressage opèrent dans l’espace mémoire des registres d’entrées-sorties et ceux-ci sont au nombre de 64. Il faut donc 6 bits pour repérer le numéro du registre d’I/O concerné et Rr/Rd permet de repérer le registre de travail source ou destination. La figure 5 montre ce genre d’adressage.

Figure 5
Figure 5
Adressage direct des entrées-sorties (source Microchip)

Exemple : OUT Rr,A qui transfère le contenu du registre source Rr vers le registre d’entrée-sortie A (r compris entre 0 et 31 et A compris entre 0 et 63). La valeur binaire de l’instruction est 1011 1AAr rrrr AAAA (une fois de plus, les bits codant le registre d’I/O ne sont pas à la suite).

Adressage direct de la mémoire de données

L’instruction est composée de deux mots de 16 bits, le deuxième contenant l’adresse de l’opérande comprise entre 0x0000 et RAMEND. C’est donc tout l’espace de données qui peut être adressé de cette façon. La figure 6 montre ce genre d’adressage.

Figure 6
Figure 6
Adressage direct de la mémoire de données (source Microchip)

Exemple : LDS Rd,k qui transfère un octet de l’espace de données dont l’adresse vaut k vers le registre de destination Rd (d compris entre 0 et 31, et k compris entre 0 et 65535). Cette instruction nécessite deux mots. Le premier mot contient l’information sur le numéro de registre qui sert de destination et sa valeur binaire est 1001 000d dddd 0000. Le deuxième mot contient l’adresse sur 16 bits (k).

Adressage indirect de la mémoire de données

C’est un peu la même chose que l’adressage direct, mais cette fois l’adresse de l’opérande est contenue dans un registre de 16 bits. Ce registre est appelé X-Register ou Y-Register ou Z-Register et est constitué de deux registres de travail, comme cela a été expliqué dans l’article L’assembleur (2) (R26 et R27 pour X-Register, R28 et R29 pour Y-Register et enfin, R30 et R31 pour Z-Register). La figure 7 montre ce genre d’adressage.

Figure 7
Figure 7
Adressage indirect de la mémoire de données (source Microchip)

Exemples : LD Rd,X ou LD Rd,Y ou encore LD Rd,Z.

Adressage indirect avec déplacement

L’adresse est obtenue en ajoutant au contenu du registre Y-Register ou Z-Register (mais pas X-Register) un déplacement codé sur 6 bits, comme le montre la figure 8. L’instruction contient donc un code opératoire sur 5 bits, un registre source ou destination sur 5 bits et le déplacement noté q sur 6 bits.

Figure 8
Figure 8
Adressage indirect avec déplacement (source Microchip)

Exemple : LDD Rd,Y+q avec d compris entre 0 et 31 et q compris entre 0 et 63.

Adressage indirect avec pré-décrémentation

Le contenu du registre (X, Y ou Z-Register) est décrémenté avant l’opération pour obtenir l’adresse de l’opérande. Ce mode d’adressage se révèle particulièrement utile pour balayer une table de valeurs. La figure 9 montre ce genre d’adressage.

Figure 9
Figure 9
Adressage indirect avec pré-décrémentation (source Microchip)

Exemple : LD Rd,-Z avec d compris entre 0 et 31. Le signe – permet de pré-décrémenter le registre (X, Y ou Z, dans cet exemple c’est Z).

Adressage indirect avec post-incrémentation

C’est un peu la même chose que le mode précédent, mais cette fois, le contenu du registre X, Y ou Z est incrémenté après l’opération comme le montre la figure 10. L’adresse de l’opérande est donc ce qui est contenu dans le registre X, Y ou Z avant que l’incrémentation soit faite. Ce mode d’adressage est également utile pour balayer une table de valeurs.

Figure 10
Figure 10
Adressage indirect avec post-incrémentation (source Microchip)

Exemple : LD Rd,Y+ avec d compris entre 0 et 31. Le signe + permet de post-incrémenter le registre (X, Y ou Z, dans cet exemple c’est Y).

Adressage de la mémoire de programme

On rappelle que la mémoire de programme s’étend de 0x0000 à FLASHEND, valeur qui dépend du modèle de microcontrôleur (voir figure 2). L’adressage de la mémoire de programme permet deux choses : se rendre à une partie du programme pour exécuter les instructions qui suivent (l’instruction qui fait cela est du type JMP ou CALL comme nous le verrons plus loin) ou bien accéder à une donnée qui reste constante tout au long du programme et qui a été placée dans cet espace mémoire pour économiser la place en SRAM. Nous allons donc commencer par étudier les modes d’adressage influençant le déroulement du programme.

Adressage direct de la mémoire de programme

Ce mode d’adressage est réservé aux instructions JMP (saut vers une certaine instruction) ou CALL (appel d’un sous-programme). Suivant le modèle de MCU, la taille du registre PC (Program Counter) peut être de 22 bits (cas pour l’ATmega2560 par exemple). L’instruction contient donc toute l’information nécessaire pour remplir le registre PC avec l’adresse correspondant à l’instruction à exécuter. Les 16 bits les moins significatifs (LSB pour Least Significant Bits) sont contenus dans le deuxième mot de l’instruction alors que les 6 bits les plus significatifs (MSB pour Most Significant Bits) sont contenus dans le premier mot de l’instruction. L’exécution du programme se poursuit alors à l’adresse contenue dans l’instruction. C’est ce que montre la figure 11.

Figure 11
Figure 11
Adressage direct de la mémoire programme (source Microchip)

Exemples : JMP k qui opère un saut vers une adresse à l’intérieur de tout l’espace mémoire de programme (4M words soit 8M octets). La valeur k (comprise entre 0 et 4M) est chargée dans PC. Cette instruction est codée sur deux mots. Il en est de même pour CALL k.

Adressage indirect de la mémoire de programme

Ce mode d’adressage est utilisé par les instructions IJMP et ICALL. Cette fois, le PC est chargé avec le contenu du Z-Register (sur 16 bits) et l’exécution du programme se poursuit à l’adresse contenue initialement dans Z-Register. C’est ce que montre la figure 12.

Figure 12
Figure 12
Adressage indirect de la mémoire programme (source Microchip)

Exemple : IJMP qui effectue un saut vers l’adresse contenue dans le registre Z. Ce registre n’ayant que 16 bits, le saut n’est possible que dans la plus basse section des 64 K-words (128 KB) de l’espace de mémoire de programme.

Adressage relatif de la mémoire de programme

Ce mode d’adressage est utilisé par les instructions RJMP et RCALL. Comme le montre la figure 13, le PC est incrémenté puis additionné à une valeur k codée sur 12 bits (appelée adresse relative et comprise entre -2048 et 2047) et le programme se poursuit à l’adresse PC + 1 + k. Pour les microcontrôleurs AVR dont la mémoire de programme n’excède pas 2 K-words (4 KB), ces deux instructions peuvent adresser tout l’espace mémoire de programme depuis n’importe quelle localisation.

Figure 13
Figure 13
Adressage relatif de la mémoire de programme (source Microchip)

Exemples : RJMP k et RCALL k (voir l’exemple pratique donné un peu plus loin).

Adressage de constante présente en mémoire programme

Pour terminer, nous allons voir comment accéder à des données constantes placées en mémoire de programme. Les instructions qui permettent cela sont LPM (Load Program Memory) et ELPM (Extended Load Program Memory) ainsi que SPM (Store Program Memory). Pour ce mode d’adressage, c’est le registre Z (Z-Register) qui est utilisé. Dans certains cas (MCU avec plus de 64 KB de mémoire programme), ce registre n’est pas suffisant et on doit lui adjoindre un autre registre appelé RAMPZ qui une fois concaténé avec Z permet d’obtenir la taille suffisante en nombre de bits pour accéder à l’ensemble de cette mémoire étendue (cas de l’ATmega2560) ; on utilise alors l’instruction ELPM.

Adressage de constante présente en mémoire programme avec les instructions LPM, ELPM et SPM

Comme le montre la figure 14, les 15 bits les plus significatifs (MSB pour Most Significant Bits) du registre Z permettent de sélectionner l’adresse du mot de mémoire. Pour l’instruction LPM, le bit le moins significatif (LSB pour Least Significant Bit ou encore bit 0) permet de sélectionner l’octet composant le mot ; si ce bit vaut 0, on sélectionne l’octet le moins significatif du mot, s’il vaut 1, on sélectionne l’octet le plus significatif. Pour l’instruction SPM, le bit 0 doit être égal à zéro.

Figure 14
Figure 14
Adressage de constantes avec LPM, ELPM, SPM (source Microchip)

Exemples : LPM Rd,Z avec d compris entre 0 et 31 ou bien LPM (aucun opérande, donc c’est R0 qui est utilisé pour recevoir l’octet localisé par le registre Z).

Adressage post-incrémenté avec les instructions LPM Z+ et ELPM Z+

C’est quasiment le même mode d’adressage que le précédent comme le montre la figure 15. Le registre Z est utilisé pour accéder au mot et le bit 0 de ce registre permet de sélectionner l’octet qui compose le mot. Une fois l’opération effectuée, le registre Z est incrémenté. Si l’instruction ELPM Z+ est utilisée, le registre RAMPZ est utilisé pour étendre le registre Z.

Figure 15
Figure 15
Adressage post-incrémenté avec LPM Z+, ELPM Z+ (source Microchip)

Exemples : LPM Rd,Z+ avec d compris entre 0 et 31 ou bien ELPM Rd,Z+ (dans ce cas, c’est RAMPZ:Z qui est post-incrémenté).

L’adressage d’un point de vue pratique

Tout ce que nous venons de décrire doit vous paraître extrêmement théorique et vous vous demandez peut-être en quoi cela présente une importance pour vous. On peut produire du code en se contentant d’utiliser quelques instructions, toujours les mêmes, celles qu’on connaît le mieux. Mais pour optimiser son code, le rendre plus compact et plus rapide, il est nécessaire d’utiliser toutes les possibilités dont on dispose et la connaissance des différents modes d’adressage est alors indispensable.

Au-delà de l’optimisation du code, il faut déjà produire un programme qui ne plante pas et encore une fois, ne pas choisir des instructions inadaptées à ce qu’on veut faire. Une instruction qui utilise un ou deux registres de travail ne posera aucun problème : elle est codée sur un seul mot et s’exécute en un seul cycle. Que les bits qui repèrent le ou les registres ne soient pas à la suite n’est qu’une curiosité ; l’important est que l’information soit complète et donc que tous les bits soient bien là.

Par contre, les instructions qui utilisent un adressage indirect, peuvent faire planter le programme si elles sont mal utilisées. Par exemple, nous avons vu que certaines instructions utilisent les registres X, Y ou Z qui doivent être chargés au préalable, ce qu’il faut savoir faire sans se tromper entre octet de poids faible et octet de poids fort. Si vous avez été attentifs, vous avez déjà l’information nécessaire pour utiliser ces registres. Mais charger un tel registre prend de l’espace programme et du temps d’exécution ; c’est pourquoi il vaut mieux utiliser les instructions qui réalisent un pré-décrément ou bien un post-incrément de ces registres chaque fois qu’on veut accéder à des valeurs qui se suivent en mémoire comme celles d’un tableau par exemple. Bien évidemment, il ne faut pas se tromper entre incrément et décrément !

Une autre catégorie d’instructions qui peut faire planter le programme est celle qui utilise l’adressage relatif ; vous ne pouvez pas aller chercher une instruction qui se trouverait très en amont ou très en aval de l’adresse de l’instruction que vous êtes en train d’exécuter. Vous êtes limité en translation en quelque sorte. Se dire qu’on utilisera systématiquement JMP à la place de RJMP, c’est perdre en optimisation : JMP utilise deux mots alors que RJMP n’en utilise qu’un seul et JMP s’exécute en trois cycles alors que RJMP n’en nécessite que deux.

Lorsque plusieurs instructions vous semblent possibles pour réaliser la même chose, le choix de l’instruction doit se faire en prenant en compte le nombre de mots pour la coder, le temps d’exécution en cycles, la façon dont l’instruction va modifier les flags du registre d’état SREG, mais également comment l’instruction agit sur le Program Counter et sur le pointeur de pile (voir plus loin). Toutes ces données se trouvent dans le document déjà cité « AVR Instruction Set Manual » dont la lecture doit aussi être complétée par l’étude de la datasheet du microcontrôleur utilisé. On se rend compte que faire le meilleur choix possible pour coder un programme demande beaucoup d’expérience, donc beaucoup de travail pour bien maîtriser toutes les données.

Un exemple pratique

Voici le dump d’un programme Blink quasiment identique à celui que nous avons créé dans les articles précédents. Il donne à gauche les adresses d’implantation, la valeur du code en hexadécimal, la notation mnémonique, un commentaire. Il a été obtenu avec l’IDE d’Arduino, raison pour laquelle il est implanté à l’adresse 90 car ce programme était appelé depuis la fonction setup. On remarque aussi que dans le sous-programme , l’initialisation des registres R8 et R9 est faite avec un ou exclusif (EOR R8, R8) au lieu de clear register (CLR R8). Cela revient au même puisque la documentation explique que CLR applique un ou exclusif entre le registre et lui-même.

00000090 <blink>:
  90:	25 9a       	sbi	0x04, 5     ; 4

00000092 <start>:
  92:	2d 9a       	sbi	0x05, 5     ; 5
  94:	08 e2       	ldi	r16, 0x28   ; 40
  96:	04 d0       	rcall  .+8             ; 0xa0 <delay>
  98:	2d 98       	cbi	0x05, 5     ; 5
  9a:	08 e2       	ldi	r16, 0x28   ; 40
  9c:	01 d0       	rcall  .+2             ; 0xa0 <delay>
  9e:	f9 cf       	rjmp  .-14     	; 0x92 <start>

000000a0 <delay>:
  a0:	88 24       	eor	r8, r8
  a2:	99 24       	eor	r9, r9

000000a4 <loop>:
  a4:	8a 94       	dec	r8
  a6:	f1 f7       	brne  .-4      	; 0xa4 <loop>
  a8:	9a 94       	dec	r9
  aa:	e1 f7       	brne  .-8      	; 0xa4 <loop>
  ac:	0a 95       	dec	r16
  ae:	d1 f7       	brne  .-12     	; 0xa4 <loop>
  b0:	08 95       	ret

Ce qu’il y a d’intéressant dans ce dump, c’est de voir comment est calculé le déplacement pour des instructions comme RCALL, RJMP ou bien BRNE (le déplacement relatif est indiqué juste après la notation mnémonique). On retrouve bien ce qui a été expliqué plus haut.

La notion de pile et comment la gérer

La pile (Stack en anglais) est une partie de la mémoire SRAM utilisée pour stocker temporairement des données et les retrouver lorsque c’est nécessaire. On en a déjà un peu parlé dans l’article L’assembleur (2). Les données qu’on stocke dans la pile peuvent être vues comme des wagons de marchandises qu’on garerait temporairement sur une voie de garage ; le dernier wagon garé sera le premier wagon qu’on peut récupérer et si on veut récupérer le wagon le plus proche du heurtoir, il faudra auparavant avoir fait sortir les wagons plus proches de l’entrée de la voie de garage. Avec les données, c’est la même chose : la dernière donnée stockée dans la pile sera la première donnée récupérée de la pile et on parle alors de pile LIFO (pour Last In First Out). Il est donc important de connaître en permanence l’adresse mémoire qui permettra de stocker la prochaine donnée : celle-ci est conservée dans un registre appelé pointeur de pile (Stack Pointer ou SP) et chaque fois qu’on utilise la pile (pour stocker ou récupérer une donnée), le registre SP est mis à jour. Plus on stocke des données et plus la pile occupe de l’espace en mémoire. Plus on récupère des données et moins la pile occupe de l’espace. Il faut bien-sûr veiller à ce que la pile ne vienne pas écraser les valeurs de nos variables stockées aussi en SRAM.

La figure 16 montre la mémoire SRAM qui commence par exemple à l’adresse 0x0100 (cas de l’ATmega328P) et se termine à l’adresse RAMEND. Les adresses vont en croissant de haut en bas ; l’adresse la plus haute (RAMEND) est donc située au bas de la figure. Comme le registre SP est généralement initialisé avec la valeur RAMEND, le premier emplacement mémoire pour stocker une donnée est à l’adresse RAMEND (case en vert sombre sur la figure). Pour stocker une valeur dans la pile, on doit d’abord mettre cette valeur dans un registre de travail Rr puis copier ce registre à l’adresse contenue dans SP avec l’instruction PUSH Rr. Une fois que l’instruction PUSH est terminée, SP est décrémenté : il pointe maintenant sur la position de SRAM située juste au-dessus (case en vert clair sur la figure). Pour récupérer la donnée et la mettre dans le registre Rd, on doit utiliser l’instruction POP Rd qui va commencer par incrémenter SP (donc il pointe maintenant sur la case en vert sombre) et ensuite copier dans le registre Rd la donnée pointée par SP [2].

Figure 16
Figure 16
La zone de la pile

Dans l’exemple donné ci-dessus, c’est vous qui utilisez la pile pour y stocker temporairement des valeurs que vous voulez retrouver par la suite. Parfois, c’est le microcontrôleur lui-même qui utilise la pile. Par exemple lorsqu’il appelle un sous-programme : avant de se brancher à l’adresse où commence le sous-programme, il doit déjà sauvegarder l’adresse où il est afin d’y revenir une fois le sous-programme terminé. Mais pour sauvegarder une adresse de programme, il faut deux octets si la mémoire est au plus 128 KB, voire trois pour une mémoire plus étendue (jusqu’à 8 MB). Il faut donc empiler ces octets sur la pile et décrémenter en conséquence SP (de 2 ou de 3 unités).

De la même façon, l’instruction RET qui permet de sortir d’un sous-programme et revenir au programme appelant, va charger PC avec le contenu de la pile (deux ou trois octets) et incrémenter en conséquence (de 2 ou 3 unités) le registre SP.

Des appels successifs de sous-programmes par des sous-programmes vont donc faire grossir la taille de la pile et si on n’y prend pas garde, cette pile peut venir corrompre la zone où sont stockées des variables du programme. Il en va de même avec une utilisation excessive des instructions PUSH. De plus, la pile peut aussi être corrompue si on n’y prend pas garde. Par exemple, dans un sous-programme, vous empilez trois valeurs de registres que vous voulez sauvegarder temporairement mais vous n’en récupérez que deux avant que le sous-programme se termine. Votre pile est corrompue et lors de l’instruction RET, le PC sera chargé avec une valeur qui n’est pas celle attendue ; le programme fera alors n’importe quoi. La pile peut aussi être corrompue par un nombre excessif de variables que le programme stocke en mémoire SRAM. La zone de variables va s’étendre du haut vers le bas de la figure 16 alors que la pile va s’étendre du bas vers le haut : les problèmes surviennent lorsque les deux frontières se rejoignent et il devient difficile de comprendre le comportement du microcontrôleur.

Le Stack Pointer étant un registre situé à une certaine adresse, il est tout à fait possible d’écrire une valeur à l’intérieur. Cela permet d’implanter la pile à une adresse différente de RAMEND avec pour conséquence, un rapprochement de la zone réservée à la pile de la zone réservée aux variables. Il faut donc avoir de très bonnes raisons pour faire cela et en plus, il faut le faire avant toute utilisation de la pile.

Calibration du temps dans une fonction

Un des avantages de l’assembleur est de pouvoir calibrer le temps d’exécution d’un sous-programme ou d’une fonction, ce qui peut permettre d’obtenir des signaux électroniques bien calibrés. Bien évidemment, la précision dans le temps d’un microcontrôleur n’est pas celle d’une horloge atomique. De plus, on ne peut pas descendre au-dessous du temps nécessaire pour un cycle d’horloge, soit 62,5 nanoseconde pour un MCU qui utilise un quartz de 16 MHz. Cela veut dire que les temps d’exécution ne pourront être qu’un multiple de 62,5 ns ou encore un nombre entier de cycles.

Par exemple, le programme Blink de l’article L’assembleur (3) devait fonctionner à la fréquence d’un Hz ; le temps d’allumage ou d’extinction doit donc être égal à une demi-seconde, ce qui dure 8 millions de cycles (avec un quartz à 16 MHz). Trois boucles imbriquées étaient utilisées pour se rapprocher du résultat et en chargeant la dernière boucle avec la valeur 40, le temps d’allumage ou d’extinction obtenu était égal à 0,492805312 seconde [3]. Si on charge la dernière boucle avec 41, le résultat est meilleur mais on dépasse légèrement la demi-seconde [4].

Pour obtenir le résultat voulu, il faut donc s’arranger pour que le sous-programme effectue 8 millions de cycles. L’instruction NOP (No Operation) ne fait absolument rien mais nécessite un cycle pour être exécutée ; cela revient à faire attendre le microcontrôleur pendant un cycle. La solution à notre problème consiste à ajouter des instructions NOP dans les boucles imbriquées et à calculer quelles sont les conséquences sur la durée du sous-programme. Comme il ne faut pas en ajouter de trop, le mieux est de commencer par la dernière boucle et si ce n’est pas suffisant, essayer la boucle située juste avant. Par tâtonnement, on peut arriver à affiner le temps d’exécution du sous-programme.

Une autre solution consiste à utiliser les timers du microcontrôleur, comme cela a été fait dans l’article Les Timers (II). Il suffit alors de charger les bons registres avec les bonnes valeurs pour provoquer une interruption. Bien évidemment, le temps d’exécution de cette interruption doit aussi être pris en compte pour calibrer le signal.

Il existe donc plusieurs solutions pour calibrer dans le temps une routine en assembleur. Mais cette calibration peut demander de la réflexion et du travail pour arriver au meilleur résultat. Calcul et tâtonnement sont les deux outils à notre disposition pour y arriver.

Le temps d’exécution du sous-programme delay du programme Blink est de 7 884 885 cycles ; pour obtenir une demi-seconde (8 000 000 cycles), il manque donc 115 115 cycles. Ajoutons x NOP dans la boucle 1, y NOP dans la boucle 2 et z NOP dans la boucle 3 et refaisons le calcul du temps d’exécution. Nous obtenons l’équation suivante :
2621440.x + 10240.y + 40.z = 115115 (avec x, y, z entiers naturels).
Les coefficients représentent le nombre de fois que la boucle est parcourue (2621440 = 256 x 256 x 40 ; 10240 = 256 x 40).

On voit que x ne peut être qu’égal à 0.

Avec y = 10, il faudrait que z = 317 pour se rapprocher du résultat, et 317 instructions NOP encombreraient beaucoup la mémoire.

Si y = 11, il faut que z = 61 pour se rapprocher du résultat. Dans ce cas, la durée du sous-programme delay est de 7 999 965 cycles et il suffit de rajouter 35 NOP avant l’instruction RET pour arriver au compte. Au total, c’est 107 instructions NOP qu’on rajoute en mémoire simplement pour attendre. Pour éviter cela, il est possible soit d’inclure une attente supplémentaire avant de sortir du sous-programme soit de revoir le calcul du timing de delay avec d’autres valeurs pour chaque boucle jusqu’à obtenir un résultat plus satisfaisant. On voit avec cet exemple que si le but peut être atteint, il n’est pas toujours facile d’optimiser la solution [5].

Avec cet article, nous avons atteint le but que nous nous étions fixé initialement : vous faire découvrir un nouveau moyen de programmer vos microcontrôleurs avec un langage qui peut se révéler très efficace pour certains cas bien particuliers. Il n’est pas dans nos intentions de vous inciter à tout écrire en assembleur ; ce serait en effet dommage de ne pas profiter de la puissance du C pour de grandes applications. Mais comme il est possible de mélanger le C et l’assembleur, vous avez la possibilité de bénéficier du meilleur des deux langages : facilité d’écriture et de débogage pour le C et un code plus compact et mieux calibré en temps pour l’assembleur. À vous de voir si cela peut présenter un intérêt dans vos projets.

[1Seule une étude de la datasheet du composant permet de connaître le nombre de registres étendus d’entrées-sorties : de 0 pour des ATtiny à 416 pour l’ATmega2560 par exemple.

[2On peut évidemment choisir comme registre de destination Rd le registre Rr qui contenait initialement la donnée mise sur la pile

[3Voir le calcul dans l’article L’assembleur (3)

[4Le calcul donne 0,505125438 s.

[5Obtenir un timing rigoureux est plus facile sur des temporisations de quelques microsecondes.

4 Messages

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

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 »

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)

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)

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)

Les derniers articles

L’assembleur (9)


Christian

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

TCOs en Processing (2)


Pierre59

Les articles les plus lus

Les Timers (I)

Les interruptions (1)

Instructions conditionnelles : le if ... else

Bien utiliser l’IDE d’Arduino (1)

Ces tableaux qui peuvent nous simplifier le développement Arduino

Comment gérer le temps dans un programme ?

Les structures

Les Timers (III)

Les Timers (II)

Instructions conditionnelles : le switch ... case