← Teil 2. Erste Schritte
Teil 4. Peripheriegeräte programmieren und Interrupts behandeln →
Assembler Code Generator Library für AVR-Mikrocontroller
Teil 3. Indirekte Adressierung und Flusskontrolle
Im vorherigen Teil haben wir uns ausführlich mit der Arbeit mit 8-Bit-Registervariablen befasst. Wenn Sie den vorherigen Beitrag verpasst haben, empfehle ich Ihnen, ihn zu lesen. Dort finden Sie einen Link zur Bibliothek, um die Beispiele im Artikel selbst auszuprobieren. Für diejenigen, die die Bibliothek früher heruntergeladen haben, empfehle ich das Herunterladen der neuesten Version, da die Bibliothek ständig aktualisiert wird und einige Beispiele in der alten Version der Bibliothek möglicherweise nicht funktionieren.
Leider reichen die Bittiefen der zuvor betrachteten Registervariablen eindeutig nicht aus, um als Speicherzeiger verwendet zu werden. Bevor wir direkt mit der Diskussion der Zeiger fortfahren, betrachten wir daher eine andere Klasse der Datenbeschreibung. Die meisten Befehle in der AVR-Mega-Architektur sind so konzipiert, dass sie nur mit Registeroperanden funktionieren, d. H. Beide Operanden und das Ergebnis sind 8 Bit groß. Es gibt jedoch eine Reihe von Operationen, bei denen zwei nacheinander angeordnete ROZ-Register als ein einziges 16-Bit-Register betrachtet werden. Es gibt nur wenige solcher Operationen, und sie konzentrieren sich hauptsächlich auf die Arbeit mit Zeigern.
Aus Sicht der Syntax der Bibliothek entspricht die Arbeit mit einem Registerpaar fast der Arbeit mit einer Registervariablen. Stellen Sie sich ein kleines Beispiel vor, in dem wir versuchen, mit einem Registerpaar zu arbeiten. Um hier und unten Platz zu sparen, geben wir das Ergebnis der Ausführung nur dann an, wenn bestimmte Merkmale der Codegenerierung erläutert werden müssen.
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);
In diesem Beispiel haben wir mit dem Befehl DREG () zwei 2-Byte-Variablen deklariert, die sich in Registerpaaren befinden. Mit den folgenden Befehlen haben wir ihnen den Anfangswert zugewiesen und eine Reihe von arithmetischen Operationen ausgeführt. Wie Sie dem Beispiel entnehmen können, entspricht die Syntax für die Arbeit mit einem Registerpaar weitgehend der für die Arbeit mit einem regulären Register. Ein Registerpaar kann auch als eine Variable betrachtet werden, die aus zwei unabhängigen Registern besteht. Auf das Register wird als Satz von zwei 8-Bit-Registern über die High- Eigenschaft für den Zugriff auf die oberen 8 Bits als 8-Bit-Register und die Low- Eigenschaft für den Zugriff auf die unteren 8 Bits zugegriffen. Der Code sieht folgendermaßen aus
var m = new Mega328(); var dr1 = m.DREG(); dr1.Load(0xAA55); dr1.Low--; dr1.High += dr1.Low; var t = AVRASM.Text(m);
Wie Sie dem Beispiel entnehmen können, können wir mit High und Low als unabhängige Registervariablen arbeiten, einschließlich der Ausführung verschiedener arithmetischer und logischer Operationen zwischen ihnen.
Nachdem wir Variablen mit doppelter Länge herausgefunden haben, können wir beschreiben, wie mit Variablen im Speicher gearbeitet wird. In der Bibliothek können Sie mit 8, 16-Bit-Variablen und Arrays von Bytes beliebiger Länge arbeiten. Betrachten Sie ein Beispiel für die Zuweisung von Speicherplatz für Variablen im RAM.
var m = new Mega328(); var bt = m.BYTE();
Mal sehen, was passiert ist.
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
Im Abschnitt Datendefinition haben wir eine Speicherzuordnung. Beachten Sie, dass sich die Zuordnungsreihenfolge von der Deklaration von Variablen unterscheidet. Das ist kein Zufall. Die Zuweisung des Speichers für Variablen erfolgt nach dem Sortieren in absteigender Reihenfolge nach den folgenden Kriterien (in absteigender Reihenfolge der Wichtigkeit). Das maximale Divisor-Vielfache vom Grad 2 → Die Größe des zugewiesenen Speichers. Dies bedeutet, dass, wenn wir 4 Arrays mit einer Größe von 64, 48, 40 und 16 Bytes zuweisen möchten, die Zuordnungsreihenfolge unabhängig von der Deklarationsreihenfolge folgendermaßen aussieht:
Länge 64 - Maximale Divisor-Vielfache von Grad 2 = 64
Länge 48 - Maximale Divisor-Vielfache von Grad 2 = 16
Länge 16 - Maximales Divisor-Vielfaches von Grad 2 = 16
Länge 40 - Maximale Divisor-Vielfache von Grad 2 = 8
Dies geschieht, um die Steuerung der Grenzen des Arrays zu vereinfachen.
und reduzieren Sie die Größe des Codes in Operationen mit Zeigern. Wir können keine Operationen mit Variablen im Speicher direkt ausführen, daher steht uns nur das Lesen / Schreiben zum Registrieren von Variablen zur Verfügung. Der einfachste Weg, mit Variablen im Speicher zu arbeiten, ist die direkte Adressierung.
var m = new Mega328(); var bt = m.BYTE();
In diesem Beispiel haben wir eine Variable im Speicher und eine Registervariable deklariert. Danach haben wir der Variablen den Wert 0x55 zugewiesen und ihn in die Variable im Speicher geschrieben. Dann gelöscht und wieder hergestellt.
Um mit Array-Elementen zu arbeiten, verwenden wir die folgende Syntax
var rr = m.REG(); var arr = m.ARRAY(10); rr.MLoad(arr[5]);
Die Nummerierung der Elemente im Array beginnt mit 0. Somit wird im obigen Beispiel der Wert 6 des Array-Elements in die Zelle rr geschrieben.
Jetzt können Sie zur indirekten Adressierung wechseln. Die Bibliothek hat einen eigenen Datentyp für einen Zeiger auf den RAM-Speicher - MEMPtr . Mal sehen, wie wir es nutzen können. Wir ändern unser vorheriges Beispiel so, dass die Arbeit mit der Variablen im Speicher über den Zeiger ausgeführt wird.
var m = new Mega328(); var bt1 = m.BYTE(); var bt2 = m.BYTE(); var rr = m.REG(); var ptr = m.MEMPTR();
Aus dem Text ist ersichtlich, dass wir zuerst den ptr-Zeiger deklariert und dann Schreib- und Leseoperationen damit ausgeführt haben. Zusätzlich zur Möglichkeit, die Lese- / Schreibadresse im Befehl während der Ausführung zu ändern, vereinfacht die Verwendung des Zeigers die Arbeit mit Arrays und kombiniert die Lese- / Schreiboperation mit dem Inkrementieren / Dekrementieren des Zeigers. Schauen wir uns ein Programm an, das ein Array mit einem bestimmten Wert füllen kann.
var m = new Mega328(); var bt1 = m.ARRAY(4);
In diesem Beispiel haben wir die Möglichkeit genutzt, einen Zeiger beim Schreiben in den Speicher zu erhöhen.
Als nächstes gehen wir zur Fähigkeit der Bibliothek über, den Befehlsfluss zu steuern. Wenn es einfacher ist, wie man bedingte und bedingungslose Sprünge und Schleifen mithilfe der Bibliothek programmiert. Der einfachste Weg, dies zu verwalten, ist die Verwendung von Beschriftungsnavigationsbefehlen. Beschriftungen in einem Programm werden auf zwei verschiedene Arten deklariert. Das erste ist, dass wir mit dem AVRASM.Label- Team ein Label für die zukünftige Verwendung erstellen, es aber nicht in den Programmcode einfügen. Diese Methode wird verwendet, um Vorwärtssprünge zu erstellen, dh in Fällen, in denen der Sprungbefehl vor der Beschriftung stehen muss. Um die Beschriftung an der gewünschten Stelle des Assembler-Codes festzulegen, müssen Sie den Befehl AVRASM.newLabel ([Variable der zuvor erstellten Beschriftung]) ausführen . Um zurück zu gehen, können Sie eine einfachere Syntax verwenden, indem Sie eine Bezeichnung festlegen und ihren Wert einer Variablen mit einem Befehl AVRASM.newLabel () ohne Parameter zuweisen .
Die einfachste Art des Übergangs ist ein bedingungsloser Übergang. Um es aufzurufen, verwenden wir den Befehl GO ([jump_mark]] . Mal sehen, wie es mit einem Beispiel aussieht.
var m = new Mega328(); var r = m.REG();
Bedingte Übergänge haben mehr Kontrolle über den Ausführungsfluss. Ihr Verhalten hängt vom Status der Operationsflags ab, und dies ermöglicht es, den Operationsfluss abhängig vom Ergebnis ihrer Ausführung zu steuern. Die Bibliothek verwendet die IF- Funktion, um einen Befehlsblock zu beschreiben, der nur unter bestimmten Bedingungen ausgeführt werden sollte. Schauen wir uns ein Beispiel an.
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);
Da die Syntax des IF- Befehls nicht ganz bekannt ist, sollten Sie sie genauer betrachten. Das erste Argument hier ist die Übergangsbedingung. Das Folgende ist die Methode, in der der Codeblock platziert wird, die ausgeführt werden sollte, wenn die Bedingung erfüllt ist. Eine Variante der Funktion ist die Fähigkeit, einen alternativen Zweig zu beschreiben, d. H. Einen Codeblock, der ausgeführt werden muss, wenn die Bedingung nicht erfüllt ist. Zusätzlich können Sie auf die Funktion AVRASM.Comment () achten , mit der wir dem Ausgabe-Assembler Kommentare hinzufügen können.
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);
Das Ergebnis in diesem Fall sieht wie folgt aus
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
Die vorhergehenden Beispiele zeigen eine bedingte Verzweigungsoption, bei der ein Vergleichsbefehl zum Bestimmen der Verzweigungsbedingungen verwendet wird. In einigen Fällen ist dies nicht erforderlich, da die Übergangsbedingungen durch den Status der Flags nach der letzten ausgeführten Operation bestimmt werden sollten. Die folgende Syntax wird für solche Fälle bereitgestellt.
var m = new Mega328(); var rr1 = m.REG(); rr1.Load(0x22); rr1--; m.IFEMPTY(() =>AVRASM.Comment(", 0")); var t = AVRASM.Text(m);
In diesem Beispiel überprüft die IFEMPTY- Funktion den Status des Z-Flags nach einem Inkrement und führt den Code des bedingten Blocks aus, wenn er 0 erreicht.
Am flexibelsten in Bezug auf die Verwendung kann die LOOP- Funktion betrachtet werden. Es dient zur bequemen Beschreibung von Programmzyklen. Betrachten Sie ihre Unterschrift
LOOP(Register iter, Action<Register, string> Condition, Action<Register, string> body)
Der Parameter iter weist eine Registervariable zu, die als Iterator in einer Schleife verwendet werden kann. Der zweite Parameter enthält einen Codeblock, der die Bedingungen für das Verlassen der Schleife beschreibt. Der zugewiesene Iterator und die Startbezeichnung der zurückzugebenden Schleife werden an diesen Codeblock übergeben. Der letzte Parameter wird verwendet, um den Codeblock des Hauptkörpers der Schleife zu beschreiben. Das einfachste Beispiel für die Verwendung der LOOP- Funktion ist eine Stub-Schleife, dh eine Endlosschleife, um zur gleichen Zeile zu springen. Die Syntax lautet in diesem Fall wie folgt
m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { });
Das Kompilierungsergebnis ist unten angegeben.
L0002: xjmp L0002
Kehren wir zu unserem Beispiel des Füllens eines Arrays mit einem bestimmten Wert zurück und ändern Sie ihn so, dass das Füllen in einer Schleife ausgeführt wird
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]);
Der Ausgabecode sieht in diesem Fall wie folgt aus
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
Eine andere Möglichkeit, Übergänge zu organisieren, sind indirekt adressierte Übergänge. Das für sie am nächsten liegende Analogon in Hochsprachen ist ein Zeiger auf eine Funktion. Der Zeiger zeigt in diesem Fall nicht auf den RAM-Speicher, sondern auf den Programmcode. Da AVR über eine Harvard-Architektur verfügt und einen eigenen Befehlssatz für den Zugriff auf den Programmspeicher verwendet, wird ROMPtr anstelle von MEMPtr wie oben beschrieben als Zeiger verwendet. Der Anwendungsfall für indirekt adressierte Übergänge kann durch das folgende Beispiel veranschaulicht werden.
var m = new Mega328(); var block1 = AVRASM.Label; var block2 = AVRASM.Label; var block3 = AVRASM.Label; var ptr = m.ROMPTR(); ptr.Load(block1);
In diesem Beispiel haben wir 3 Befehlsblöcke. Nach Abschluss jedes Blocks wird die Steuerung zurück an den indirekt adressierten Verzweigungsbefehl übertragen. Da wir am Ende des Befehlsblocks den Übergangsvektor jedes Mal auf einen neuen Block setzen, sieht die Ausführung wie Block1 → Block2 → Block3 → Block1 ... usw. in einem Kreis aus. Dieser Befehl ermöglicht zusammen mit bedingten Verzweigungsbefehlen einfache und bequeme Mittel der Sprache, um solche ziemlich komplexen Algorithmen wie eine Zustandsmaschine zu beschreiben.
Eine komplexere Version eines indirekt adressierten Zweigs ist der Befehl SWITCH . Es wird kein Zeiger auf eine Übergangsbezeichnung für den Übergang verwendet, sondern ein Zeiger auf eine Variable im Speicher, in dem die Adresse der Übergangsbezeichnung gespeichert ist.
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();
In diesem Beispiel lautet die Übergangssequenz wie folgt: Block1 → Block2 → Block3 → Block1 → Block3 → Block1 → Block3 → Block1 ... Wir konnten einen Algorithmus implementieren, bei dem die Block2-Befehle nur im ersten Zyklus ausgeführt werden.
Im nächsten Teil des Beitrags werden wir uns mit der Arbeit mit Peripheriegeräten, der Implementierung von Interrupts, Routinen und vielem mehr befassen.