La puissance magique des macros ou comment simplifier la vie d'un programmeur assembleur AVR

Beaucoup de choses ont été écrites sur les macros dans l'assembleur. Et dans la documentation, et dans divers articles. Mais dans la plupart des cas, cela se résume soit à une simple liste de directives avec une brève description de leurs fonctions, soit à un ensemble d'exemples disparates de macros prêtes à l'emploi.
Le but de cet article est de décrire une approche spécifique de la programmation en langage assembleur pour générer le code le plus simple et lisible à l'aide de macros. L'article ne décrira pas la syntaxe des commandes et directives individuelles. Une description détaillée a déjà été donnée par le constructeur . Nous nous concentrerons sur la façon d'utiliser ces opportunités pour résoudre des problèmes spécifiques.


À un moment donné, ATMEL a essayé et développé une gamme de microcontrôleurs huit bits avec une architecture de très haute qualité et un système de commande simple, mais en même temps très puissant. Mais, comme vous le savez, il n'y a pas de limite à la perfection, et certaines instructions fréquemment utilisées ne suffisent pas. Heureusement, l'assembleur de macros, aimablement et absolument gratuit fourni par le fabricant, peut considérablement simplifier le code grâce à l'utilisation de directives. Avant de passer directement aux macros, nous effectuerons quelques étapes préliminaires


Définition des constantes


.EQU FOSC = 16000000 .EQU CLK8 = 0 

Ces deux définitions vous permettent de vous débarrasser des "nombres magiques" dans les macros, où les valeurs des registres sont calculées en fonction de la fréquence du processeur et de l'état du fusible du diviseur périphérique. La première définition est la fréquence du cristal du processeur en hertz, la seconde est l'état du diviseur de fréquence périphérique.


Enregistrer la dénomination


 .DEF TempL = r16 .DEF TempH = r17 .DEF TempQL = r18 .DEF TempQH = r19 .DEF AL = r0 .DEF AH = r1 .DEF AQL = r2 .DEF AQH = r3 

Un registre un peu redondant à première vue qui peut être utilisé dans les macros. Il suffit de quatre registres pour Temp si nous avons affaire à des valeurs de 32 bits (par exemple, dans les opérations de multiplication de deux nombres de 16 bits). Si nous sommes sûrs que deux registres de stockage temporaires sont suffisants pour nous d'utiliser dans les macros, alors TempQL et TempQH ne peuvent pas être déterminés. Les définitions de A sont nécessaires pour les macros utilisant des opérations de multiplication. AQ n'est plus nécessaire si nous n'utilisons pas l'arithmétique 32 bits avec nos macros.


Macros pour implémenter des commandes simples


Maintenant que nous avons compris le nom des registres, nous allons commencer à implémenter les commandes manquantes et commencer par essayer de simplifier celles existantes. L'assembleur AVR a une caractéristique maladroite. Pour l'entrée et la sortie, les 64 premiers ports utilisent les commandes d' entrée / sortie et pour les lds / st restants. Afin de ne pas regarder la documentation à chaque fois à la recherche de la commande nécessaire pour un port spécifique, nous allons créer un ensemble de commandes universelles qui substitueront indépendamment les valeurs nécessaires.


 .MACRO XOUT .IF @0<64 out @0,@1 .ELSE sts @0,@1 .ENDIF .ENDMACRO .MACRO XIN .IF @1<64 in @0,@1 .ELSE lds @0,@1 .ENDIF .ENDMACRO 

Pour que la substitution fonctionne correctement, la compilation conditionnelle est utilisée dans la macro. Dans le cas où l'adresse du port est inférieure à 64, la première section conditionnelle est exécutée, sinon la seconde. Nos macros répètent complètement la fonctionnalité des commandes standard pour travailler avec les ports d'entrée / sortie.Par conséquent, pour indiquer que notre équipe possède des fonctionnalités avancées, nous ajoutons le préfixe de dénomination standard X.
L'une des commandes les plus courantes qui ne sont pas disponibles dans l'assembleur, mais qui sont constamment requises, est la commande pour écrire des constantes dans les registres d'entrée de sortie. L'implémentation de macro pour cette commande ressemblera à ceci


 .MACRO OUTI ldi TempL,@1 .IF @0<64 out @0, TempL .ELSE sts @0, TempL .ENDIF .ENDMACRO 

Dans ce cas, le nom dans la macro, afin de ne pas violer la logique de dénomination des commandes, ajoutez au nom standard le suffixe I , utilisé par le développeur pour désigner les commandes de travail avec les constantes. Dans cette macro, nous utilisons le registre TempL précédemment défini pour le fonctionnement .
Dans certains cas, pas un seul registre n'est requis, mais une paire entière stockant une valeur de 16 bits. Créer une nouvelle macro pour écrire une valeur 16 bits dans une paire de registres d'E / S


 .MACRO OUTIW ldi TempL,HIGH(@1) .IF @0<64 out @0H, TempL .ELSE sts @0H, TempL .ENDIF ldi TempL,LOW(@1) .IF @0<64 out @0L, TempL .ELSE sts @0L, TempL .ENDIF .ENDMACRO 

Dans cette macro, nous utilisons les fonctions LOW et HIGH intégrées pour extraire l'octet bas et haut d'une valeur 16 bits. Dans le nom de la macro, ajoutez les suffixes I et W à la commande pour indiquer que dans ce cas, la commande fonctionne avec une valeur de 16 bits (mot).
Pas moins souvent dans les programmes, il y a chargement de paires de registres, par exemple pour mettre des pointeurs en mémoire. Créons une telle macro


 .MACRO ldiw ldi @0L, LOW(@1) ldi @0H, HIGH(@1) .ENDMACRO 

Dans cette macro, nous utilisons le fait que la dénomination standard des registres et des ports chez le fabricant implique le postfixe L pour le bas et le postfix H pour la partie supérieure de la valeur à deux octets. Si vous suivez cette règle lorsque vous nommez vos propres variables, la macro fonctionnera correctement, y compris avec elles. La beauté des macros réside également dans le fait qu'elles fournissent une substitution simple, par conséquent, dans le cas où le deuxième opérande est un nombre, et dans le cas où il s'agit du nom de l'étiquette, la macro fonctionnera correctement.


Macros pour implémenter des commandes complexes.


Lorsqu'il s'agit d'opérations plus complexes, les macros ne sont généralement pas utilisées, préférant les routines. Cependant, dans ces cas, les macros peuvent vous faciliter la vie et rendre le code plus lisible. Dans ce cas, la compilation conditionnelle vient à la rescousse. Une approche de programmation pourrait ressembler à ceci:
Nous plaçons toutes nos routines dans un fichier séparé, que nous nommerons, par exemple, Library.inc . Chaque sous-programme de ce fichier aura le formulaire suivant


 _sub0: .IFDEF __sub0 ; -----    ----- ret .ENDIF 

Dans ce cas, la présence de la définition __sub0 signifie que le sous-programme doit être inclus dans le code résultant. Sinon, il est ignoré.
Ensuite, dans un fichier séparé Macro.inc, nous définissons les macros du formulaire


 .MACRO SUB0 .IFNDEF __sub0 .DEF __sub0 .ENDIF ; ---          call _sub0 .ENDMACRO 

Lorsque vous utilisez cette macro, nous vérifions la définition de __sub0 et, si elle est manquante, nous effectuons la détermination. Par conséquent, l'utilisation d'une macro déverrouille l'inclusion de code de sous-programme dans le fichier de sortie. Dans le cas de l'utilisation de routines dans des macros, le code du programme principal prendra la forme suivante


 .INCLUDE “Macro.inc” ;----    ---- .INCLUDE “Library.inc” 

À titre d'exemple, nous donnons une implémentation d'une macro pour diviser des entiers non signés 8 bits. Nous gardons la logique du constructeur et plaçons le résultat en AL (r0) , et le reste de la division en AH (r1) . Le sous-programme se présente comme suit


 _div8u: .IFDEF __ div8u ;AH -  ;AL  ;TempL -  ;TempH -  ;TempQL -  clr AL; clr AH; ldi TempQL,9 d8u_1: rol TempL dec TempQL brne d8u_2 ret d8u_2: rol A sub AH, TempH brcc d8u_3 add AH,TempH clc rjmp d8u_1 d8u_3: sec rjmp d8u_1 .ENDIF 

La définition de macro pour utiliser cette routine sera la suivante


 .MACRO DIV8U .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 mov TempH, @1 call _div8u .ENDMACRO 

Si vous le souhaitez, vous pouvez ajouter une version pour travailler avec une constante


 .MACRO DIV8UI .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 ldi TempH, @1 call _div8u .ENDMACRO 

Par conséquent, l'utilisation de l'opération de division dans le texte du programme est triviale


 DIV8U r10, r11 ; r0 = r10/r11 r1 = r10 % r11 DIV8UI r10, 35 ; r0 = r10/35 r1 = r10 % 35 

En utilisant la compilation conditionnelle, nous pouvons placer toutes les routines qui pourraient nous être utiles dans Library.inc . Dans ce cas, seuls ceux qui ont été appelés au moins une fois apparaîtront dans le code de sortie. Faites attention à la position de l'étiquette d'entrée. La sortie de l'étiquette au-delà des limites de la condition est due aux fonctionnalités du compilateur. Si vous placez l'étiquette dans le corps du bloc conditionnel, le compilateur peut générer une erreur. La présence de balises inutilisées dans le code n'est pas effrayante, car la présence d'un nombre quelconque de balises n'affecte pas le résultat.


Macros périphériques


L'une des opérations où il est difficile de se passer de la documentation du fabricant est d'initialiser les périphériques. Même avec l'utilisation de désignations mnémoniques de registres et de bits du code, il peut être difficile de comprendre dans quel mode un périphérique est configuré, d'autant plus que parfois le mode est configuré par une combinaison de valeurs de bits de différents registres. Voyons comment les macros peuvent être utilisées avec l'exemple USART .
Commençons par la macro d'initialisation en mode asynchrone.


 .MACRO USART_INIT ; speed, bytes, parity, stop-bits .IF CLK8 == 0 .SET DIVIDER = FOSC/16/@0-1 .ELSE .SET DIVIDER = FOSC/128/@0-1 .ENDIF ; Set baud rate to UBRR0 outi UBRR0H, HIGH(DIVIDER) outi UBRR0L, LOW(DIVIDER) ; Enable receiver and transmitter .SET UCSR0B_ = (1<<RXEN0)|(1<<TXEN0) outi UCSR0B, UCSR0B_ .SET UCSR0C_ = 0 .IF @2 == 'E' .SET UCSR0C_ |= (1<<UPM01) .ENDIF .IF @2 == 'O' .SET UCSR0C_ |= (1<<UPM00) .ENDIF .IF @3== 2 .SET UCSR0C_ |= (1<<USBS0) .ENDIF .IF @1== 6 .SET UCSR0C_ |= (1<<UCSZ00) .ENDIF .IF @1== 7 .SET UCSR0C_ |= (1<<UCSZ01) .ENDIF .IF @1== 8 .SET UCSR0C_ = UCSR0C_ |(1<<UCSZ01)|(1<<UCSZ00) .ENDIF .IF @1== 9 .SET UCSR0C_ |= (1<<UCSZ02)|(1<<UCSZ01)|(1<<UCSZ00) .ENDIF ; Set frame format outi UCSR0C,UCSR0C_ .ENDMACRO 

L'utilisation de la macro nous a permis de remplacer l'initialisation des registres de configuration USART par des valeurs incompréhensibles sans lire la documentation par une ligne que même ceux qui ont rencontré ce contrôleur pour la première fois pouvaient gérer. Dans cette macro, il est également finalement devenu clair pourquoi nous avons déterminé les constantes de fréquence et de diviseur. Eh bien, il convient de noter que malgré le code impressionnant de la macro elle-même, celle qui en résulte aura la même apparence que si nous écrivions l'initialisation de la manière habituelle.
Pour finir avec USART, voici encore quelques petites macros


  .MACRO USART_SEND_ASYNC outi UDR0, @0 .ENDMACRO 

Il n'y a qu'une seule ligne, mais l'utilisation de cette macro vous permettra de mieux voir où le programme affiche les données dans USART . Si nous supposons travailler en mode synchrone sans utiliser d'interruptions, alors au lieu de USART_SEND_ASYNC, il est préférable d'utiliser la macro ci-dessous


  .MACRO USART_SEND USART_Transmit: xin TempL, UCSR0A sbrs TempL, UDRE0 rjmp USART_Transmit outi UDR0, @0 .ENDMACRO 

Dans ce cas, nous activons la vérification de l'occupation du port et affichons les données uniquement lorsque le port est libre. De toute évidence, cette approche de l'utilisation des périphériques fonctionnera pour n'importe quel périphérique, et pas seulement pour USART .


Comparaison de programmes sans et utilisant des macros.


Regardons un petit exemple et comparons le code écrit sans utiliser de macros avec le code où elles sont utilisées. Par exemple, prenez un programme qui affiche le classique "Hello world!" au terminal via le matériel UART .


  RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 USART_Init: out UBRR0H, r17 out UBRR0L, r16 ldi r16, (1<<RXEN0)|(1<<TXEN0) out UCSRnB,r16 ldi r16, (1<<USBS0)|(3<<UCSZ00) out UCSR0C,r16 ldi ZL, LOW(STR<<1) ldi ZH, HIGH(STR<<1) LOOP: lpm r16, Z+ or r16,r16 breq END USART_Transmit: in r17, UCSR0A sbrs r17, UDRE0 rjmp USART_Transmit out UDR0,r16 rjmp LOOP END: rjmp END STR: .DB “Hello world!”,0 

Et voici le même programme, mais écrit à l'aide de macros


 .INCLUDE “macro.inc” .EQU FOSC = 16000000 .EQU CLK8 = 0 RESET: ldiw SP, RAMEND; USART_INIT 19200, 8, "N", 1 ldiw Z, STR<<1 LOOP: lpm TempL, Z+ test TempL breq END USART_SEND TempL rjmp LOOP END: rjmp END STR: .DB “Hello world!”,0 

Dans cet exemple, nous avons utilisé les macros décrites ci-dessus, ce qui nous a permis de simplifier considérablement le code du programme et de le rendre plus compréhensible. Le code binaire dans les deux programmes sera absolument identique.


Conclusion


L'utilisation de macros peut réduire considérablement le code assembleur du programme, pour le rendre plus compréhensible et lisible. La compilation conditionnelle vous permet de créer des commandes universelles et des bibliothèques de procédures sans créer de code de sortie redondant. Comme inconvénient, on peut signaler un ensemble très modeste par rapport aux langages de haut niveau d'opérations et de restrictions autorisées lors de la déclaration des données «en aval». Cette restriction ne permet pas, par exemple, d'écrire au moyen de macros une commande universelle à part entière pour les transitions jmp / rjmp et gonfle considérablement le code de la macro elle-même lors de l'implémentation d'une logique complexe.

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


All Articles