← Teil 1. Erste Bekanntschaft
Teil 3. Indirekte Adressierung und Flusskontrolle →
Assembler Code Generator Library für AVR-Mikrocontroller
Teil 2. Erste Schritte
Wie geplant werden wir in diesem Teil die Funktionen der Programmierung mit der NanoRTOS-Bibliothek genauer betrachten. Diejenigen, die mit dem Lesen dieses Beitrags begonnen haben, können sich mit der allgemeinen Beschreibung und den Funktionen der Bibliothek im vorherigen Artikel vertraut machen. Aufgrund des begrenzten Umfangs der geplanten Veröffentlichung wird davon ausgegangen, dass ein angesehener Leser mit der C # -Programmierung zumindest minimal vertraut ist und auch die Architektur und Programmierung in Assemblersprache für AVR-Controller der Mega-Serie versteht.
Das Studium einer Technologie lässt sich am besten mit der Ausführung von Beispielen kombinieren. Ich empfehle daher, die Bibliothek selbst von https://drive.google.com/open?id=1FfBBpxlJkWC027ikYpn6NXbOGp7DS-5B herunterzuladen und zu versuchen, sie mit dem Konsolenanwendungsprojekt zu verbinden. Bei Erfolg können Sie sicher weitermachen. Sie können jede C # -Anwendung oder jedes UnitTest-Projekt als Shell zum Ausführen von Beispielen verwenden. Letzteres gefällt mir persönlich besser, da Sie so mehrere verschiedene Beispiele an einem Ort speichern und nach Bedarf ausführen können. In jedem Fall wird aus Platzgründen nur der Text des Beispiels in die Auflistung aufgenommen.
Die Arbeit mit der Bibliothek sollte immer mit einer Ansage wie einem Mikrocontroller beginnen. Da die Parameter und der Satz von Peripheriegeräten vom Controller-Typ abhängen, wirkt sich die Auswahl eines bestimmten Controllers auf die Bildung von Assembler-Code aus. Die Deklarationszeile der Steuerung, für die das Programm geschrieben wurde, lautet wie folgt
var m = new Mega328();
Ferner können zusätzliche Einstellungen des Mikrocontrollers folgen, wie beispielsweise Taktparameter oder die Zuweisung von Systemfunktionen für Ausgänge. Wenn Sie beispielsweise die Berechtigung zum Zurücksetzen der Hardware verwenden, wird die Verwendung der Ausgabe als Port vermieden. Alle Controller-Parameter haben Standardwerte, und in den Beispielen werden sie weggelassen, außer wenn dies wichtig ist. In realen Projekten empfehle ich Ihnen jedoch, sie immer zu installieren. Beispielsweise könnte eine Uhreinstellung so aussehen
m.FCLK = 16000000; m.CKDIV8 = false;
Diese Einstellung bedeutet, dass der Mikrocontroller von einem Quarzresonator oder einer externen Quelle mit einer Frequenz von 16 MHz getaktet wird und der Frequenzteiler für Peripheriegeräte ausgeschaltet wird.
Die Textfunktion der statischen Klasse AVRASM ist für die Ausgabe der Arbeit verantwortlich. Diese Funktion wird immer am Ende des Codes aufgerufen, um das Ergebnis in Assembler-Form auszugeben. Die zuvor zugewiesene Instanz der Controller-Klasse empfängt die Funktion als Parameter. Der einfachste Rahmen des Programms für die Arbeit mit der Bibliothek hat daher die folgende Form
var m = new Mega328();
Wenn wir versuchen, das Programm auszuführen, sollte es erfolgreich sein, aber es wird kein Code generiert. Trotz der Sinnlosigkeit des Ergebnisses lässt dies dennoch den Schluss zu, dass die Bibliothek keinen Wrapping-Code generiert.
Wir haben bereits gelernt, wie man ein leeres Programm erstellt. Versuchen wir nun, Code darin zu erstellen. Beginnen wir mit dem primitivsten. Mal sehen, wie wir das Inkrementproblem einer 8-Bit-Variablen in einer beliebigen ROZ-Zelle lösen können. Aus Assembler-Sicht ist dies der Befehl inc [register] . Fügen Sie die folgenden Zeilen in den Hauptteil des Programms unserer Beschaffung ein
var r = m.REG(); r++;
Der Zweck der Teams liegt auf der Hand. Der erste Befehl ordnet die Variable r einem der Prozessorregister zu. Der zweite Befehl spricht über die Notwendigkeit, diese Variable zu erhöhen. Nach der Ausführung erhalten wir das erste Ergebnis der Codeausführung.
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF R0000 = r20 inc R0000 .DSEG
Schauen wir uns genauer an, was als Ergebnis passiert ist. Die ersten vier Befehle sind die Initialisierung des Stapelzeigers. Als nächstes folgt die Definition des Variablennamens. Und schließlich unsere Inc , für die alles gestartet wurde. Es wurde nichts extra angezeigt, außer der Initialisierung des Stapels. Die Frage, die sich beim Betrachten dieses Codes stellen kann, ist, welche Art von R0000 ? Wir haben eine Variable namens r ? In einem C # -Programm kann ein Programmierer unter Verwendung des Gültigkeitsbereichs ganz bewusst und legal dieselben Namen verwenden. Aus Assembler-Sicht müssen alle Beschriftungen und Definitionen eindeutig sein. Um den Programmierer nicht zu zwingen, die Eindeutigkeit von Namen zu überwachen, werden die Namen standardmäßig vom System generiert. Es gibt jedoch eine Situation, in der Sie zu Debugging-Zwecken den bewussten Namen weiterhin vom Programm in den Ausgabecode übertragen möchten, damit er leicht gefunden werden kann. Nicht beängstigend. Ersetzen Sie m.REG () durch m.REG (”r”) und führen Sie den Code erneut aus. Als Ergebnis werden wir Folgendes sehen
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r20 inc r .DSEG
Also, mit der Benennung der Register aussortiert. Nun wollen wir sehen, warum plötzlich Register ab 20 und nicht ab 0 zugewiesen wurden. Um diese Frage zu beantworten, erinnern wir uns, dass Register ab 16 eine großartige Gelegenheit haben, sie mit Konstanten zu initialisieren. Und da diese Funktion sehr gefragt ist, beginnen wir die Verteilung ab der oberen Hälfte, um die Optimierungsmöglichkeiten zu erhöhen. Dann ist es trotzdem nicht klar - warum c20 und nicht 16? Der Grund ist, dass die Übersetzung einer Reihe von Befehlen in Assembler-Code ohne die Verwendung temporärer Speicherzellen nicht möglich ist. Für diese Zwecke haben wir 4 Zellen von 16 bis 19 zugewiesen. Dies bedeutet nicht, dass sie für den Programmierer völlig unzugänglich geworden sind. Es ist nur so, dass der Zugriff auf sie etwas anders organisiert ist, so dass der Programmierer sich der möglichen Einschränkungen ihrer Verwendung bewusst ist und bewusst handelt. Wir entfernen die Definition des Registers r aus dem Code und ersetzen die darauf folgende Zeile durch
m.TempL ++;
Schauen wir uns das Ergebnis an
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 inc TempL .DSEG
Hierbei ist anscheinend zu beachten, dass der Ausgabe-Assembler für die korrekte Interpretation der Verbindungsdatei mit Definitionen und Makros Common.inc aus dem Entwicklungspaket benötigt. Es enthält tatsächlich alle erforderlichen Makros und Definitionen, einschließlich der Namensübereinstimmung für temporäre Speicherzellen. TempL = r16, TempH = r17, TempQL = r18, TempQH = r19. In diesem Fall haben wir keinen einzigen Befehl verwendet, der temporäre Speicherzellen zum Arbeiten verwenden würde. Daher ist unsere Entscheidung, ihn im TempL-Vorgang zu verwenden, durchaus akzeptabel. Und was sollen wir tun, wenn wir absolut sicher sind, dass keine Zuordnung von Konstanten zu unserer Variablen nicht leuchtet und wir keine wertvollen Zellen der oberen Hälfte dafür ausgeben wollen? Geben Sie unsere Definition in den Quellcode zurück, indem Sie sie in var r = m.REGL ("r") ändern. und bewerten Sie das Ergebnis der Arbeit
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 .DEF r = r4 inc r .DSEG
Das Ziel ist erreicht. Wir haben es geschafft, der Bibliothek zu erklären, wo sich die Variable unserer Meinung nach befinden sollte. Gehen wir weiter. Mal sehen, was passiert, wenn mehrere Variablen gleichzeitig deklariert werden. Wir kopieren unsere Definition und Aktionen noch ein paar Mal. Zur Abwechslung setzen wir eine neue Variable zurück und reduzieren den Wert der anderen um 1. Das Ergebnis sollte ungefähr so sein.
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
Großartig Das wurde verlangt. Nun wollen wir sehen, wie wir das Register für andere Zwecke freigeben können, wenn wir es nicht mehr benötigen. Hier ist leider bisher alles von Hand. C # -Regeln für Sichtbarkeitsgrenzen und die automatische Freigabe von Variablen im Ausland für den Codegenerierungsmodus funktionieren noch nicht. Mal sehen, wie Sie die Zelle bei Bedarf noch freigeben können. Fügen Sie unserem Programm nur eine Zeile hinzu und sehen Sie sich das Ergebnis an.
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
Es ist leicht zu erkennen, dass das vierte von uns freigegebene Register wieder zur Verwendung verfügbar wurde. Unter Berücksichtigung der Tatsache, dass jede neue Variablendeklaration das Register erfasst, können wir den Schluss ziehen, dass Sie beim Kompilieren des Programms die Register rechtzeitig freigeben müssen, wenn Sie nicht auf eine Situation stoßen möchten, in der sie knapp werden.
Bei der Analyse der Beispiele haben wir bereits gezeigt, wie Unicast-Operationen an Registern ausgeführt werden. Nun wollen wir sehen, wie es mit Multicast läuft. Die Prozessorarchitektur erlaubt maximal zwei Adressenbefehle (für besonders korrosive, mit Ausnahme von zwei Befehlen, für die das Ergebnis in festen Registern abgelegt wird). Dies sollte so verstanden werden, dass der erste Operand in der Operation nach ihrer Ausführung das Ergebnis enthält. Für diese Art von Operation wird die spezielle Syntax [register1] [operation] = [register2] bereitgestellt. Mal sehen, wie es in der Praxis aussieht. Versuchen wir, zwei Registervariablen zu deklarieren und hinzuzufügen.
var m = new Mega328(); var op1 = m.REG(); var op2 = m.REG(); op1 += op2; var t = AVRASM.Text(m);
Als Ergebnis werden wir sehen
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
Ich habe bekommen, was sie erwartet hatten. Sie können selbst mit den Operationen experimentieren -, &, | und stellen Sie sicher, dass das Ergebnis nicht schlechter ist.
Bisher reicht all dies eindeutig nicht aus, um selbst das einfachste Programm zu schreiben. Tatsache ist, dass wir die Initialisierung der Register selbst noch nicht angesprochen haben. Die Architektur des Mikrocontrollers ermöglicht es Ihnen, die Register mit einer Konstanten, dem Wert eines anderen Registers, dem Wert der RAM-Speicherzelle an einer bestimmten Adresse, dem Wert der RAM-Speicherzelle am Zeiger in einem speziellen Registerpaar, dem Wert der Eingabe- / Ausgabezelle an einer bestimmten Adresse sowie dem Wert der Programmspeicherzelle am Zeiger zu initialisieren in ein spezielles Registerpaar gelegt. Wir werden uns später mit der indirekten Adressierung befassen, aber im Moment werden wir einfachere Fälle betrachten. Wir werden das folgende Testprogramm schreiben und ausführen.
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);
Hier haben wir zwei Variablen mit der Nummer und dem Symbol deklariert und initialisiert und dann den Wert der Variablen op2 in die Zelle op1 kopiert. Offensichtlich muss die Zahl im Bereich von 0 bis 255 liegen, damit kein Fehler auftritt. Das Ergebnis wird sein
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
Aus dem Beispiel ist ersichtlich, dass für alle aufgelisteten Operationen eine Funktion verwendet wird und die Bibliothek selbst den richtigen Satz von Assembler-Befehlen generiert. Wie schon oft erwähnt, ist das direkte Laden von Daten in das Register mit dem Befehl ldi nur für die obere Hälfte der Register verfügbar. Machen wir unsere Bibliothek komplizierter, indem wir das Programm so ändern, dass es Register für Variablen aus der unteren Hälfte zuweist.
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);
Wir bekommen
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
Die Bibliothek hat diese Aufgabe gemeistert und gleichzeitig die minimal mögliche Anzahl von Teams ausgegeben. Gleichzeitig haben wir gesehen, warum wir temporäre Speicherregister zuweisen müssen. Nun, zum Schluss wollen wir sehen, wie Mathematik für die Arbeit mit Konstanten implementiert wird. Wir wissen um die Existenz des Subi-Assembler-Befehls, um die Konstante vom Register zu subtrahieren, und jetzt werden wir versuchen, sie anhand der Bibliothek zu beschreiben.
var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 -= 10; var t = AVRASM.Text(m);
Das Ergebnis wird sein
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
Es stellte sich heraus. Und wie verhält sich die Bibliothek, wenn es keinen Assembler-Befehl gibt, der die erforderliche Operation ausführen kann? Zum Beispiel, wenn wir nicht subtrahieren, sondern eine Konstante hinzufügen wollen. Lass uns versuchen zu sehen
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
Die Bibliothek wurde durch Subtrahieren eines negativen Werts verlassen. Mal sehen, wie es mit der Schicht läuft. Verschieben Sie den Wert des Registers um 5 nach rechts.
var m = new Mega328(); var op1 = m.REG(); op1.Load(0x10); op1 >>= 5; var t = AVRASM.Text(m);
Das Ergebnis wird sein
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
Hier ist nicht alles offensichtlich, aber es laufen 2 Teams schneller als wenn eine Frontallösung von fünf Schichtteams verwendet würde.
In diesem Artikel haben wir die Verwendung der Bibliothek für die Arbeit mit Registerarithmetik untersucht. Im nächsten Artikel werden wir weiterhin beschreiben, wie die Bibliothek mit Zeigern arbeitet, und Methoden zur Steuerung des Ablaufs der Befehlsausführung (Schleifen, Übergänge usw.) betrachten.