Bibliothèque de générateur de code assembleur pour microcontrôleurs AVR. 2e partie

← Partie 1. Première connaissance


Partie 3. Adressage indirect et contrôle de flux →


Bibliothèque de générateur de code d'assembleur pour microcontrôleurs AVR


Partie 2. Pour commencer


Comme prévu, dans cette partie, nous examinerons plus en détail les caractéristiques de la programmation à l'aide de la bibliothèque NanoRTOS. Ceux qui ont commencé à lire ce post peuvent se familiariser avec la description générale et les capacités de la bibliothèque dans l' article précédent . En raison de la portée limitée de la publication prévue, il est supposé qu'un lecteur respecté connaît au moins très peu la programmation C # et a également une compréhension de l'architecture et de la programmation en langage d'assemblage pour les contrôleurs AVR de la série Mega.


L'étude de toute technologie est mieux combinée à l'exécution d'exemples, je suggère donc de télécharger la bibliothèque elle-même à partir de https://drive.google.com/open?id=1FfBBpxlJkWC027ikYpn6NXbOGp7DS-5B et d'essayer de la connecter au projet d'application console. En cas de succès, vous pouvez continuer en toute sécurité. Vous pouvez utiliser n'importe quelle application C # ou projet UnitTest comme shell pour exécuter des exemples. Personnellement, j'aime davantage ce dernier, car il vous permet de stocker plusieurs exemples différents en un seul endroit et de les exécuter selon vos besoins. Dans tous les cas, seul le texte de l'exemple sera inclus dans les annonces pour économiser de l'espace.


Le travail avec la bibliothèque doit toujours commencer par une annonce telle qu'un microcontrôleur. Étant donné que les paramètres et l'ensemble des périphériques dépendent du type de contrôleur, le choix d'un contrôleur spécifique affecte la formation du code assembleur. La ligne de déclaration du contrôleur pour lequel le programme est écrit est la suivante


var m = new Mega328(); 

En outre, des réglages supplémentaires du microcontrôleur peuvent suivre, tels que des paramètres d'horloge ou l'attribution de fonctions système pour les sorties. Par exemple, l'autorisation d'utiliser la réinitialisation matérielle élimine l'utilisation de la sortie comme port. Tous les paramètres du contrôleur ont des valeurs par défaut, et dans les exemples nous les omettrons, sauf quand c'est important, mais dans les projets réels je vous conseille de toujours les installer. Par exemple, un réglage d'horloge pourrait ressembler à ceci


  m.FCLK = 16000000; m.CKDIV8 = false; 

Ce paramètre signifie que le microcontrôleur est cadencé par un résonateur à quartz ou une source externe avec une fréquence de 16 MHz et que le séparateur de fréquence pour les périphériques est désactivé.
La fonction Texte de la classe statique AVRASM est responsable de la sortie du travail. Cette fonction sera toujours appelée à la fin du code pour afficher le résultat sous forme d'assembleur. Instance précédemment affectée de la classe de contrôleur, la fonction reçoit comme paramètre. Ainsi, le cadre le plus simple du programme pour travailler avec la bibliothèque prend la forme suivante


 var m = new Mega328(); //     //      //          var t = AVRASM.Text(m); //    t 

Si nous essayons d'exécuter le programme, il devrait réussir, mais il ne générera aucun code. Malgré la futilité du résultat, cela donne néanmoins des raisons de conclure que la bibliothèque ne génère aucun code d'habillage.


Nous avons déjà appris à créer un programme vide. Essayons maintenant de créer du code dedans. Commençons par les plus primitifs. Voyons comment résoudre le problème d'incrémentation d'une variable à huit bits située dans une cellule RON arbitraire. Du point de vue de l'assembleur, il s'agit de la commande inc [register] . Insérez les lignes suivantes dans le corps du programme de nos achats


  var r = m.REG(); r++; 

Le but des équipes est assez évident. La première commande associe la variable r à l'un des registres du processeur. La deuxième commande parle de la nécessité d'incrémenter cette variable. Après l'exécution, nous obtenons le premier résultat de l'exécution du code.


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 inc R0000 .DSEG 

Examinons de plus près ce qui s'est passé en conséquence. Les quatre premières commandes sont l'initialisation du pointeur de pile. Vient ensuite la définition du nom de la variable. Et enfin, notre inc , pour lequel tout a commencé. Rien de plus, à part l'initialisation de la pile qui n'apparaissait pas. La question qui peut se poser en regardant ce code est quel type de R0000 ? Nous avons une variable nommée r ? Dans un programme C #, un programmeur peut utiliser consciemment et légalement les mêmes noms, en utilisant scope. Du point de vue de l'assembleur, toutes les étiquettes et définitions doivent être uniques. Afin de ne pas forcer le programmeur à surveiller l'unicité des noms, par défaut, les noms sont générés par le système. Cependant, il existe une situation où, à des fins de débogage, vous souhaitez toujours transférer le nom conscient du programme vers le code de sortie, afin qu'il puisse être facilement trouvé. Pas effrayant. Remplacez m.REG () par m.REG (”r”) et exécutez à nouveau le code. En conséquence, nous verrons ce qui suit


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r20 inc r .DSEG 

Donc, avec la dénomination des registres triés. Voyons maintenant pourquoi tout à coup les registres ont commencé à être attribués à partir de 20, et non à partir de 0? Afin de répondre à cette question, nous rappelons qu'à partir de 16, les registres ont une grande opportunité de les initialiser avec des constantes. Et comme cette fonctionnalité est très demandée, nous commençons la distribution à partir de la moitié supérieure pour augmenter les opportunités d'optimisation. Alors tout de même ce n'est pas clair - pourquoi c20 et pas 16? La raison en est que la traduction d'un certain nombre de commandes en code assembleur est impossible sans l'utilisation de cellules de stockage temporaires. À ces fins, nous avons alloué 4 cellules de 16 à 19. Cela ne signifie pas qu'elles sont devenues complètement inaccessibles au programmeur. C’est juste que leur accès est organisé un peu différemment afin que le programmeur soit conscient des limites possibles de leur utilisation et agisse consciemment. Nous supprimons la définition du registre r du code et remplaçons la ligne qui le suit par
m.TempL ++;
Regardons le résultat


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 inc TempL .DSEG 

Ici, apparemment, il convient de noter que l'assembleur de sortie nécessite pour l'interprétation correcte du fichier de connexion avec les définitions et les macros Common.inc du package de développement. Il contient en fait toutes les macros et définitions nécessaires, y compris la correspondance des noms pour les cellules de stockage temporaires. À savoir, TempL = r16, TempH = r17, TempQL = r18, TempQH = r19. Dans ce cas, nous n'avons pas utilisé une seule commande qui utiliserait des cellules de stockage temporaires pour fonctionner, donc notre décision de l'utiliser dans l'opération TempL est tout à fait acceptable. Et que devons-nous faire si nous sommes absolument sûrs qu'aucune affectation de constantes à notre variable ne brille et que nous ne voulons pas y dépenser de précieuses cellules de la moitié supérieure? Retournez notre définition au code source en la changeant en var r = m.REGL ("r"); et évaluer le résultat du travail


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DSEG 

Le but est atteint. Nous avons réussi à expliquer à la bibliothèque où, à notre avis, la variable devait se trouver. Allons plus loin. Voyons ce qui se passe si plusieurs variables sont déclarées à la fois. Nous copions notre définition et nos actions plusieurs fois. Pour une modification, nous allons réinitialiser une nouvelle variable et réduire la valeur de l'autre de 1. Le résultat devrait être quelque chose comme ça.


  var m = new Mega328(); var r = m.REGL("r"); r++; var rr = m.REGL("rr"); rr--; var rrr = m.REGL("rrr"); rrr.Clear(); var t = AVRASM.Text(m);  . RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DEF rr = r5 dec rr .DEF rrr = r6 clr rrr .DSEG 

Super C'est ce qui a été demandé. Voyons maintenant comment nous pouvons libérer le registre à d'autres fins si nous n'en avons plus besoin. Ici, malheureusement, jusqu'à présent, tout est à la main. Les règles C # sur les limites de visibilité et la libération automatique des variables lorsque vous allez à l'étranger pour le mode de génération de code ne fonctionnent pas encore. Voyons comment vous pouvez toujours libérer la cellule si nécessaire. Ajoutez une seule ligne à notre programme et regardez le résultat.


  var m = new Mega328(); var r = m.REGL("r"); r++; var rr = m.REGL("rr"); rr--; r.Dispose(); var rrr = m.REGL("rrr"); rrr.Clear(); var t = AVRASM.Text(m); 

 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DEF rr = r5 dec rr .UNDEF r .DEF rrr = r4 clr rrr .DSEG 

Il est facile de voir que le quatrième registre que nous avons libéré est redevenu disponible. Compte tenu du fait que chaque nouvelle déclaration de variable capture le registre, nous pouvons conclure que lors de la compilation du programme, vous devez libérer les registres à temps si vous ne voulez pas rencontrer une situation où ils deviennent rares.


En analysant les exemples, nous avons déjà montré en cours de route comment effectuer des opérations de monodiffusion sur des registres. Voyons maintenant comment les choses se passent avec la multidiffusion. L'architecture du processeur permet un maximum d'instructions à deux adresses (pour celles particulièrement corrosives, à l'exception de deux instructions pour lesquelles le résultat est placé dans des registres fixes). Ceci doit être compris de telle manière que le premier opérande de l'opération après son exécution contiendra le résultat. La syntaxe spéciale [registre1] [opération] = [registre2] est fournie pour ce type d'opération. Voyons à quoi cela ressemble dans la pratique. Essayons de déclarer et d'ajouter deux variables de registre.


  var m = new Mega328(); var op1 = m.REG(); var op2 = m.REG(); op1 += op2; var t = AVRASM.Text(m); 

En conséquence, nous verrons


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 add R0000,R0001 .DSEG 

J'ai obtenu ce qu'ils attendaient. Vous pouvez expérimenter vous-même les opérations -, &, | et assurez-vous que le résultat n'est pas pire.
Jusqu'à présent, tout ce qui précède n'est clairement pas suffisant pour écrire même le programme le plus simple. Le fait est que nous n'avons pas encore abordé l'initialisation des registres eux-mêmes. L'architecture du microcontrôleur vous permet d'initialiser les registres avec une constante, la valeur d'un autre registre, la valeur de la cellule de mémoire RAM à une adresse spécifique, la valeur de la cellule de mémoire RAM au pointeur situé dans une paire spéciale de registres, la valeur de la cellule d'entrée / sortie à une adresse spécifique, ainsi que la valeur de la cellule de mémoire programme au niveau du pointeur placé dans une paire spéciale de registres. Nous traiterons de l'adressage indirect plus tard, mais pour l'instant nous considérerons des cas plus simples. Nous allons écrire et exécuter le programme de test suivant.


  var m = new Mega328(); var op1 = m.REG(); var op2 = m.REG(); op1.Load(0x10); op2.Load('s'); op1.Load(op2); var t = AVRASM.Text(m); 

Ici, nous avons déclaré et initialisé deux variables avec le nombre et le symbole, puis copié la valeur de la variable op2 dans la cellule op1. De toute évidence, le nombre doit être compris entre 0 et 255 pour qu'aucune erreur ne se produise. Le résultat sera


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 .DEF R0001 = r21 ldi R0000,16 ldi R0001,'s' mov R0000,R0001 .DSEG 

L'exemple montre que pour toutes les opérations répertoriées, une fonction est utilisée et que la bibliothèque elle-même forme l'ensemble correct de commandes d'assembleur. Comme cela a été mentionné à plusieurs reprises, le chargement direct des données dans le registre avec la commande ldi n'est disponible que pour la moitié supérieure des registres. Rendons notre bibliothèque plus compliquée en changeant le programme afin qu'il alloue des registres pour les variables de la moitié inférieure.


  var m = new Mega328(); var op1 = m.REGL(); var op2 = m.REGL(); op1.Load(0x10); op2.Load('s'); op1.Load(op2); var t = AVRASM.Text(m); 

Nous obtenons


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r4 .DEF R0001 = r5 ldi TempL,16 mov R0000,TempL ldi TempL,'s' mov R0001,TempL mov R0000,R0001 .DSEG 

La bibliothèque a fait face à cette tâche, dépensant en même temps le nombre minimum d'équipes possible. Et en même temps, nous avons vu pourquoi nous devions allouer des registres de stockage temporaires. Enfin, voyons comment les mathématiques sont implémentées pour travailler avec des constantes. Nous connaissons l'existence de la commande assembleur subi pour soustraire la constante du registre, et maintenant nous allons essayer de la décrire en termes de bibliothèque.


  var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 -= 10; var t = AVRASM.Text(m); 

Le résultat sera


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 subi R0000,0x0A .DSEG 

Cela s'est avéré. Et comment la bibliothèque se comportera-t-elle s'il n'y a aucune commande d'assembleur qui peut effectuer l'opération nécessaire? Par exemple, si nous voulons ne pas soustraire, mais ajouter une constante. Essayons de voir


  var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 += 10; var t = AVRASM.Text(m); 

 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 subi R0000,0xF6 .DSEG 

La bibliothèque est sortie en soustrayant une valeur négative. Voyons comment les choses évoluent avec le changement. Déplacez la valeur du registre de 5 vers la droite.


  var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 >>= 5; var t = AVRASM.Text(m); 

Le résultat sera


 RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 ldi R0000,16 swap R0000 andi R0000,15 lsr R0000 .DSEG 

Tout n'est pas évident ici, mais il fonctionne 2 équipes plus rapidement que si une solution frontale de cinq équipes de quart était utilisée.


Ainsi, dans cet article, nous avons examiné l'utilisation de la bibliothèque pour travailler avec l'arithmétique des registres. Dans le prochain article, nous continuerons à décrire le fonctionnement de la bibliothèque avec les pointeurs et à considérer les méthodes de contrôle du flux d'exécution des commandes (boucles, transitions, etc.)

Source: https://habr.com/ru/post/fr462701/


All Articles