← Partie 2. Pour commencer
Partie 4. Programmation des périphériques et gestion des interruptions →
Bibliothèque de générateur de code d'assembleur pour microcontrôleurs AVR
Partie 3. Adressage indirect et contrôle de flux
Dans la partie précédente, nous avons insisté de manière suffisamment détaillée sur le travail avec des variables de registre à 8 bits. Si vous avez manqué le post précédent, je vous conseille de le lire. Vous y trouverez un lien vers la bibliothèque pour essayer vous-même les exemples de l'article. Pour ceux qui ont téléchargé la bibliothèque plus tôt, je recommande de télécharger la dernière version, car la bibliothèque est constamment mise à jour et certains exemples peuvent ne pas fonctionner dans l'ancienne version de la bibliothèque.
Malheureusement, les profondeurs de bits des variables de registre précédemment considérées ne sont clairement pas suffisantes pour être utilisées comme pointeurs de mémoire. Par conséquent, avant de passer directement à la discussion des pointeurs, nous considérons une autre classe de description des données. La plupart des commandes de l'architecture AVR Mega sont conçues pour fonctionner uniquement avec des opérandes de registre, c'est-à-dire que les deux opérandes et le résultat ont une taille de 8 bits. Cependant, il existe un certain nombre d'opérations où deux registres RON situés consécutivement sont considérés comme un seul registre 16 bits. Il existe peu d'opérations de ce type et elles sont principalement axées sur l'utilisation de pointeurs.
Du point de vue de la syntaxe de la bibliothèque, travailler avec une paire de registres est presque le même que travailler avec une variable de registre. Prenons un petit exemple où nous essayons de travailler avec une paire de registres. Afin d'économiser de l'espace ici et ci-dessous, nous ne donnerons le résultat de l'exécution que là où il est nécessaire d'expliquer certaines fonctionnalités de la génération de code.
var m = new Mega328(); var dr1 = m.DREG(); var dr2 = m.DREG(); dr1.Load(0xAA55); dr2.Load(0x55AA); dr1++; dr1--; dr1 += 0x100; dr1 += dr2; dr2 *= dr1; dr2 /= dr1; var t = AVRASM.Text(m);
Dans cet exemple, nous avons déclaré deux variables de 2 octets situées dans des paires de registres à l'aide de la commande DREG (). Avec les commandes suivantes, nous leur avons attribué la valeur initiale et effectué une série d'opérations arithmétiques. Comme vous pouvez le voir dans l'exemple, la syntaxe pour travailler avec une paire de registres est largement la même que pour travailler avec un registre normal. Une paire de registres peut également être considérée comme une variable composée de deux registres indépendants. Le registre est accessible sous la forme d'un ensemble de deux registres 8 bits via la propriété High pour accéder aux 8 bits supérieurs en tant que registre 8 bits, et la propriété Low pour accéder aux 8 bits inférieurs. Le code ressemblera à ceci
var m = new Mega328(); var dr1 = m.DREG(); dr1.Load(0xAA55); dr1.Low--; dr1.High += dr1.Low; var t = AVRASM.Text(m);
Comme vous pouvez le voir dans l'exemple, nous pouvons travailler avec High et Low en tant que variables de registre indépendantes, y compris effectuer diverses opérations arithmétiques et logiques entre elles.
Maintenant que nous avons trouvé des variables à double longueur, nous pouvons commencer à décrire comment travailler avec des variables en mémoire. La bibliothèque vous permet de travailler avec des variables de 8, 16 bits et des tableaux d'octets de longueur arbitraire. Prenons un exemple d'allocation d'espace pour des variables en RAM.
var m = new Mega328(); var bt = m.BYTE();
Voyons ce qui s'est passé.
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DSEG L0002: .BYTE 16 L0001: .BYTE 2 L0000: .BYTE 1
Dans la section de définition des données, nous avons une allocation de mémoire. Notez que l'ordre d'allocation est différent de la déclaration de variables. Ce n'est pas un hasard. L'allocation de mémoire pour les variables se produit après un tri décroissant selon les critères suivants (par ordre décroissant d'importance) Le multiple diviseur maximum de degré 2 → La taille de la mémoire allouée. Cela signifie que si nous voulons allouer 4 tableaux de 64, 48, 40 et 16 octets, l'ordre d'allocation, quel que soit l'ordre de déclaration, ressemblera à ceci:
Longueur 64 - Multiples diviseurs maximum de degré 2 = 64
Longueur 48 - Multiples diviseurs maximum de degré 2 = 16
Longueur 16 - Multiple diviseur maximal de degré 2 = 16
Longueur 40 - Multiples diviseurs maximum de degré 2 = 8
Ceci est fait afin de simplifier le contrôle des limites du tableau.
et réduire la taille du code dans les opérations avec des pointeurs. Nous ne pouvons effectuer directement aucune opération avec des variables en mémoire, donc tout ce qui nous est disponible est de lire / écrire pour enregistrer des variables. L'adressage direct est le moyen le plus simple de travailler avec des variables en mémoire.
var m = new Mega328(); var bt = m.BYTE();
Dans cet exemple, nous avons déclaré une variable en mémoire et une variable de registre. Après cela, nous avons attribué à la variable la valeur 0x55 et l'avons écrite dans la variable en mémoire. Puis effacé et restauré en arrière.
Pour travailler avec des éléments de tableau, nous utilisons la syntaxe suivante
var rr = m.REG(); var arr = m.ARRAY(10); rr.MLoad(arr[5]);
La numérotation des éléments du tableau commence par 0. Ainsi, dans l'exemple ci-dessus, la valeur 6 de l'élément du tableau est écrite dans la cellule rr.
Vous pouvez maintenant passer à l'adressage indirect. La bibliothèque a son propre type de données pour un pointeur vers l'espace mémoire RAM - MEMPtr . Voyons comment nous pouvons l'utiliser. Nous modifions notre exemple précédent pour que le travail avec la variable en mémoire s'effectue via le pointeur.
var m = new Mega328(); var bt1 = m.BYTE(); var bt2 = m.BYTE(); var rr = m.REG(); var ptr = m.MEMPTR();
Il peut être vu à partir du texte que nous avons d'abord déclaré le pointeur ptr , puis effectué des opérations d'écriture et de lecture avec lui. Outre la possibilité de modifier l'adresse de lecture / écriture dans la commande lors de l'exécution, l'utilisation du pointeur simplifie le travail avec les tableaux, combinant l'opération de lecture / écriture avec l'incrémentation / décrémentation du pointeur. Regardons un programme qui peut remplir un tableau avec une valeur spécifique.
var m = new Mega328(); var bt1 = m.ARRAY(4);
Dans cet exemple, nous avons profité de la possibilité d'incrémenter un pointeur lors de l'écriture dans la mémoire.
Ensuite, nous passons à la capacité de la bibliothèque à contrôler le flux de commandes. Si c'est plus facile, comment programmer des sauts et des boucles conditionnels et inconditionnels à l'aide de la bibliothèque. La façon la plus simple de gérer cela est d'utiliser les commandes de navigation des étiquettes. Les étiquettes d'un programme sont déclarées de deux manières différentes. La première est qu'avec l'équipe AVRASM.Label , nous créons une étiquette pour une utilisation future, mais ne l'insérons pas dans le code du programme. Cette méthode est utilisée pour créer des sauts en avant, c'est-à-dire dans les cas où la commande de saut doit précéder l'étiquette. Pour définir l'étiquette à l'emplacement requis du code assembleur, vous devez exécuter la commande AVRASM.newLabel ([variable de l'étiquette précédemment créée]) . Pour revenir en arrière, vous pouvez utiliser une syntaxe plus simple en définissant une étiquette et en affectant sa valeur à une variable avec une seule commande AVRASM.newLabel () sans paramètres.
Le type de transition le plus simple est une transition inconditionnelle. Pour l'appeler, nous utilisons la commande GO ([jump_mark]] . Voyons à quoi cela ressemble avec un exemple.
var m = new Mega328(); var r = m.REG();
Les transitions conditionnelles ont plus de contrôle sur le flux d'exécution. Leur comportement dépend de l'état des drapeaux d'opération et cela permet de contrôler le flux des opérations en fonction du résultat de leur exécution. La bibliothèque utilise la fonction IF pour décrire un bloc de commandes qui ne doit être exécuté que sous certaines conditions. Regardons un exemple.
var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); rr1.Load(0x22); rr2.Load(0x33); m.IF(rr1 == rr2, () => { AVRASM.Comment(" - , "); }); var t = AVRASM.Text(m);
Étant donné que la syntaxe de la commande IF n'est pas très familière, examinez-la plus en détail. Le premier argument ici est la condition de transition. Voici la méthode dans laquelle le bloc de code est placé, qui doit être exécutée si la condition est remplie. Une variante de la fonction est la possibilité de décrire une branche alternative, c'est-à-dire un bloc de code qui doit être exécuté si la condition n'est pas remplie. De plus, vous pouvez faire attention à la fonction AVRASM.Comment () , avec laquelle nous pouvons ajouter des commentaires à l'assembleur de sortie.
var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); rr1.Load(0x22); rr2.Load(0x33); m.IF(rr1 == rr2, () => { AVRASM.Comment(" - , "); },()=> { AVRASM.Comment(" - , "); }); AVRASM.Comment(" "); var t = AVRASM.Text(m);
Le résultat dans ce cas se présente comme suit
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi R0000,34 ldi R0001,51 cp R0000,R0001 brne L0002 ;--- - , --- xjmp L0004 L0002: ;--- - , --- L0004: ;--- --- .DSEG
Les exemples précédents présentent une option de branchement conditionnel dans laquelle une commande de comparaison est utilisée pour déterminer les conditions de branchement. Dans certains cas, cela n'est pas nécessaire, car les conditions de transition doivent être déterminées par l'état des fanions après la dernière opération effectuée. La syntaxe suivante est fournie pour de tels cas.
var m = new Mega328(); var rr1 = m.REG(); rr1.Load(0x22); rr1--; m.IFEMPTY(() =>AVRASM.Comment(", 0")); var t = AVRASM.Text(m);
Dans cet exemple, la fonction IFEMPTY vérifie l'état de l'indicateur Z après un incrément et exécute le code du bloc conditionnel lorsqu'il atteint 0.
La plus flexible en termes d'utilisation peut être considérée comme la fonction LOOP . Il est destiné à une description pratique des cycles de programme. Considérez sa signature
LOOP(Register iter, Action<Register, string> Condition, Action<Register, string> body)
Le paramètre iter attribue une variable de registre qui peut être utilisée comme itérateur dans une boucle. Le deuxième paramètre contient un bloc de code qui décrit les conditions de sortie de la boucle. L'itérateur affecté et l'étiquette de début de la boucle à renvoyer sont passés à ce bloc de code. Le dernier paramètre est utilisé pour décrire le bloc de code du corps principal de la boucle. L'exemple le plus simple d'utilisation de la fonction LOOP est une boucle de stub, c'est-à-dire une boucle infinie pour passer à la même ligne. La syntaxe dans ce cas sera la suivante
m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { });
Le résultat de la compilation est donné ci-dessous.
L0002: xjmp L0002
Revenons à notre exemple de remplissage d'un tableau avec une certaine valeur et changez-le pour que le remplissage soit effectué en boucle
var m = new Mega328(); var rr1 = m.REG(); var rr2 = m.REG(); var arr = m.ARRAY(16); var ptr = m.MEMPTR(); ptr.Load(arr[0]);
Dans ce cas, le code de sortie se présente comme suit
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi YL, LOW(L0002+0) ldi YH, HIGH(L0002+0) ldi R0001,16 ldi R0000,170 L0003: st Y+,R0000 dec R0001 brne L0003 L0004: .DSEG L0002: .BYTE 16
Une autre façon d'organiser les transitions est de passer par des transitions adressées indirectement. L'analogue le plus proche dans les langues de haut niveau pour eux est un pointeur vers une fonction. Dans ce cas, le pointeur ne pointe pas vers l'espace RAM, mais vers le code du programme. Comme AVR a une architecture Harvard et utilise son propre ensemble d'instructions spécifiques pour accéder à la mémoire du programme, ROMPtr est utilisé comme pointeur plutôt que MEMPtr décrit ci-dessus. Le cas d'utilisation des transitions adressées indirectement peut être illustré par l'exemple suivant.
var m = new Mega328(); var block1 = AVRASM.Label; var block2 = AVRASM.Label; var block3 = AVRASM.Label; var ptr = m.ROMPTR(); ptr.Load(block1);
Dans cet exemple, nous avons 3 blocs de commandes. À la fin de chaque bloc, le contrôle est retransféré à la commande de branchement adressée indirectement. Étant donné qu'à la fin du bloc de commande, nous définissons le vecteur de transition sur un nouveau bloc à chaque fois, l'exécution ressemblera à Block1 → Block2 → Block3 → Block1 ... et ainsi de suite dans un cercle. Cette commande, associée aux commandes de branchement conditionnel, permet à un langage simple et pratique de décrire des algorithmes assez complexes comme une machine à états.
Une version plus sophistiquée d'une branche adressée indirectement est la commande SWITCH . Il n'utilise pas de pointeur sur une étiquette de transition pour la transition, mais un pointeur sur une variable dans la mémoire dans laquelle l'adresse de l'étiquette de transition est stockée.
var m = new Mega328(); var block1 = AVRASM.Label; var block2 = AVRASM.Label; var block3 = AVRASM.Label; var arr = m.ARRAY(6); var ptr = m.MEMPTR();
Dans cet exemple, la séquence de transition sera la suivante: Block1 → Block2 → Block3 → Block1 → Block3 → Block1 → Block3 → Block1 ... Nous avons pu implémenter un algorithme dans lequel les commandes Block2 ne sont exécutées qu'au premier cycle.
Dans la prochaine partie de l'article, nous envisagerons de travailler avec des périphériques, de mettre en œuvre des interruptions, des routines et bien plus encore.