Die magische Kraft von Makros oder wie man einem AVR-Assembler-Programmierer das Leben leichter macht

In Assembler wurde viel über Makros geschrieben. Und in der Dokumentation und in verschiedenen Artikeln. In den meisten Fällen handelt es sich jedoch entweder um eine einfache Auflistung von Anweisungen mit einer kurzen Beschreibung ihrer Funktionen oder um eine Reihe unterschiedlicher Beispiele für vorgefertigte Makros.
In diesem Artikel wird ein spezifischer Ansatz für die Assembler-Programmierung beschrieben, mit dem mithilfe von Makros der einfachste und lesbarste Code generiert werden kann. Der Artikel beschreibt nicht die Syntax einzelner Befehle und Anweisungen. Eine detaillierte Beschreibung wurde bereits vom Hersteller gegeben . Wir werden uns darauf konzentrieren, wie wir diese Möglichkeiten nutzen können, um bestimmte Probleme zu lösen.


Zu einer Zeit versuchte und entwickelte ATMEL eine Reihe von 8-Bit-Mikrocontrollern mit einer sehr hochwertigen Architektur und einem einfachen, aber gleichzeitig sehr leistungsfähigen Befehlssystem. Wie Sie wissen, sind der Perfektion jedoch keine Grenzen gesetzt, und einige häufig verwendete Anweisungen reichen nicht aus. Glücklicherweise kann der vom Hersteller freundlicherweise und absolut kostenlos bereitgestellte Makro-Assembler den Code mithilfe von Anweisungen erheblich vereinfachen. Bevor wir direkt zu den Makros übergehen, werden wir einige vorbereitende Schritte ausführen


Definition von Konstanten


.EQU FOSC = 16000000 .EQU CLK8 = 0 

Mit diesen beiden Definitionen können Sie "magische Zahlen" in Makros entfernen, bei denen die Werte der Register basierend auf der Prozessorfrequenz und dem Zustand der Sicherung des peripheren Teilers berechnet werden. Die erste Definition ist die Frequenz des Prozessorkristalls in Hertz, die zweite ist der Zustand des peripheren Frequenzteilers.


Namensgebung registrieren


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

Ein auf den ersten Blick etwas redundantes Namensregister, das in Makros verwendet werden kann. Es werden nur vier Register für Temp benötigt, wenn es sich um 32-Bit-Werte handelt (z. B. bei Operationen zum Multiplizieren von zwei 16-Bit-Zahlen). Wenn wir sicher sind, dass zwei temporäre Speicherregister für die Verwendung in Makros ausreichen, können TempQL und TempQH nicht bestimmt werden. Definitionen für A werden für Makros benötigt, die Multiplikationsoperationen verwenden. AQ ist nicht mehr erforderlich, wenn wir mit unseren Makros keine 32-Bit-Arithmetik verwenden.


Makros zur Implementierung einfacher Befehle


Nachdem wir die Benennung der Register herausgefunden haben, werden wir die fehlenden Befehle implementieren und zunächst versuchen, die vorhandenen zu vereinfachen. Der AVR-Assembler verfügt über eine unangenehme Funktion. Für die Ein- und Ausgabe verwenden die ersten 64 Ports die Befehle in / out und für die verbleibenden lds / sts . Um nicht jedes Mal in der Dokumentation nach dem erforderlichen Befehl für einen bestimmten Port zu suchen, erstellen wir eine Reihe universeller Befehle, die die erforderlichen Werte unabhängig voneinander ersetzen.


 .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 

Damit die Ersetzung ordnungsgemäß funktioniert, wird im Makro die bedingte Kompilierung verwendet. In dem Fall, in dem die Portadresse kleiner als 64 ist, wird der erste bedingte Abschnitt ausgeführt, andernfalls der zweite. Unsere Makros wiederholen die Funktionalität von Standardbefehlen für die Arbeit mit Eingabe- / Ausgabeports vollständig. Um anzuzeigen, dass unser Team über erweiterte Funktionen verfügt, fügen wir das Standard-Namenspräfix X hinzu.
Einer der häufigsten Befehle, die im Assembler nicht verfügbar sind, aber ständig benötigt werden, ist der Befehl zum Schreiben von Konstanten in die Ausgabeeingangsregister. Die Makroimplementierung für diesen Befehl sieht folgendermaßen aus


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

In diesem Fall fügt der Name im Makro zum Standardnamen das Postfix I hinzu , das vom Entwickler verwendet wird, um die Befehle für die Arbeit mit Konstanten zu kennzeichnen, um die Befehlsbenennungslogik nicht zu verletzen. In diesem Makro verwenden wir das zuvor definierte TempL- Register für den Betrieb .
In einigen Fällen ist kein einziges Register erforderlich, sondern ein ganzes Paar, das einen 16-Bit-Wert speichert. Erstellen Sie ein neues Makro, um einen 16-Bit-Wert in ein Paar von E / A-Registern zu schreiben


 .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 

In diesem Makro verwenden wir die integrierten Funktionen LOW und HIGH , um das Low- und High-Byte aus einem 16-Bit-Wert zu extrahieren. Fügen Sie im Makronamen die Postfixes I und W zum Befehl hinzu, um anzuzeigen, dass der Befehl in diesem Fall mit einem 16-Bit-Wert (Wort) arbeitet.
Nicht seltener werden in Programmen Registerpaare geladen, um beispielsweise Zeiger auf den Speicher zu setzen. Lassen Sie uns ein solches Makro erstellen


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

In diesem Makro verwenden wir die Tatsache, dass die Standardbenennung von Registern und Ports beim Hersteller den Postfix L für den unteren und den Postfix H für den oberen Teil des Doppelbytewerts impliziert. Wenn Sie diese Regel beim Benennen Ihrer eigenen Variablen befolgen, funktioniert das Makro ordnungsgemäß, auch mit diesen. Das Schöne an Makros liegt auch in der Tatsache, dass sie eine einfache Ersetzung bieten. Wenn also der zweite Operand eine Zahl ist und wenn dies der Name des Labels ist, funktioniert das Makro korrekt.


Makros zur Implementierung komplexer Befehle.


Wenn es um komplexere Operationen geht, werden Makros im Allgemeinen nicht verwendet und bevorzugen Routinen. In diesen Fällen können Makros jedoch das Leben erleichtern und den Code lesbarer machen. In diesem Fall hilft die bedingte Kompilierung. Ein Programmieransatz könnte folgendermaßen aussehen:
Wir platzieren alle unsere Routinen in einer separaten Datei, die wir beispielsweise Library.inc nennen werden . Jedes Unterprogramm in dieser Datei hat das folgende Formular


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

In diesem Fall bedeutet das Vorhandensein der Definition __sub0, dass das Unterprogramm in den resultierenden Code aufgenommen werden muss. Andernfalls wird es ignoriert.
Als nächstes definieren wir in einer separaten Datei Macro.inc Makros des Formulars


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

Wenn wir dieses Makro verwenden, prüfen wir die Definition von __sub0 und führen, falls es fehlt, die Bestimmung durch. Infolgedessen wird durch die Verwendung eines Makros die Aufnahme von Unterprogrammcode in die Ausgabedatei entsperrt. Bei der Verwendung von Routinen in Makros hat der Code des Hauptprogramms die folgende Form


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

Als Beispiel geben wir eine Implementierung eines Makros zum Teilen von 8-Bit-Ganzzahlen ohne Vorzeichen. Wir behalten die Logik des Herstellers bei und platzieren das Ergebnis in AL (r0) und den Rest der Division in AH (r1) . Das Unterprogramm sieht wie folgt aus


 _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 

Die Makrodefinition für die Verwendung dieser Routine lautet wie folgt


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

Bei Bedarf können Sie eine Version für die Arbeit mit einer Konstanten hinzufügen


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

Daher ist die Verwendung der Teilungsoperation im Programmtext trivial


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

Mit der bedingten Kompilierung können wir alle Routinen, die für uns nützlich sein könnten, in Library.inc platzieren . In diesem Fall werden im Ausgabecode nur diejenigen angezeigt, die mindestens einmal aufgerufen wurden. Achten Sie auf die Position des Eintragsetiketts. Die Ausgabe des Labels über die Grenzen der Bedingung hinaus ist auf den Compiler zurückzuführen. Wenn Sie die Beschriftung in den Hauptteil des bedingten Blocks einfügen, gibt der Compiler möglicherweise einen Fehler aus. Das Vorhandensein nicht verwendeter Tags im Code ist nicht beängstigend, da das Vorhandensein einer beliebigen Anzahl von Tags das Ergebnis nicht beeinflusst.


Periphere Makros


Eine der Vorgänge, bei denen es schwierig ist, auf die Dokumentation des Herstellers zu verzichten, ist die Initialisierung von Peripheriegeräten. Selbst bei Verwendung mnemonischer Bezeichnungen von Registern und Bits aus dem Code kann es schwierig sein zu verstehen, in welchem ​​Modus ein Gerät konfiguriert ist, insbesondere da der Modus manchmal durch eine Kombination von Bitwerten verschiedener Register konfiguriert wird. Mal sehen, wie Makros mit dem USART- Beispiel verwendet werden können.
Beginnen wir mit dem Initialisierungsmakro für den asynchronen Modus.


 .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 

Durch die Verwendung des Makros konnten wir die Initialisierung der USART- Setup-Register durch Werte ersetzen, die unverständlich waren, ohne die Dokumentation durch eine Zeile zu lesen, die selbst diejenigen verarbeiten konnten, die zum ersten Mal auf diesen Controller gestoßen waren. In diesem Makro wurde schließlich auch klar, warum wir die Frequenz- und Divisorkonstanten bestimmt haben. Nun, es sollte beachtet werden, dass trotz des beeindruckenden Codes des Makros selbst das resultierende das gleiche Aussehen hat, als ob wir die Initialisierung auf die übliche Weise schreiben würden.
Zum Abschluss von USART hier noch ein paar kleine Makros


  .MACRO USART_SEND_ASYNC outi UDR0, @0 .ENDMACRO 

Es gibt nur eine Zeile, aber mit diesem Makro können Sie besser sehen, wo das Programm Daten in USART anzeigt. Wenn wir davon ausgehen, im synchronen Modus ohne Interrupts zu arbeiten, ist es besser, anstelle von USART_SEND_ASYNC das folgende Makro zu verwenden


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

In diesem Fall aktivieren wir die Überprüfung der Portbelegung und zeigen Daten nur an, wenn der Port frei ist. Offensichtlich funktioniert dieser Ansatz für die Arbeit mit Peripheriegeräten für jedes Gerät und nicht nur für USART .


Vergleich von Programmen ohne und mit Makros.


Schauen wir uns ein kleines Beispiel an und vergleichen Sie den Code, der ohne Verwendung von Makros geschrieben wurde, mit dem Code, in dem sie verwendet werden. Nehmen Sie zum Beispiel ein Programm, das die klassische "Hallo Welt!" über Hardware- UART zum Terminal.


  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 

Und hier ist das gleiche Programm, aber mit Makros geschrieben


 .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 

In diesem Beispiel haben wir die oben beschriebenen Makros verwendet, mit denen wir den Programmcode erheblich vereinfachen und verständlicher machen konnten. Der Binärcode in beiden Programmen ist absolut identisch.


Fazit


Die Verwendung von Makros kann den Assembler-Code des Programms erheblich reduzieren, um es verständlicher und lesbarer zu machen. Mit der bedingten Kompilierung können Sie universelle Befehle und Prozedurbibliotheken erstellen, ohne redundanten Ausgabecode zu erstellen. Als Nachteil kann man auf eine im Vergleich zu den Standards von Hochsprachen sehr bescheidene Menge zulässiger Operationen und Einschränkungen hinweisen, wenn Daten als "vorwärts" deklariert werden. Diese Einschränkung erlaubt es beispielsweise nicht, mittels Makros einen vollwertigen universellen Befehl für jmp / rjmp-Übergänge zu schreiben, und erhöht den Code des Makros selbst erheblich, wenn komplexe Logik implementiert wird.

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


All Articles