Pfad: Home => AVR-Überblick => Absolute Beginner => Allererste Schritte
AVR-ICs

AVR-Assembler lernen - die allerersten Schritte

Es gibt schon viele kurze und lange Anleitungen zu AVR-Assembler. Das folgende wendet sich an absolute Laien und soll die allerersten Schritte beim Lernen erleichtern.

1. Vergessen Sie erst mal alles, was sie bislang über Programmiersprachen wissen

Wenn Sie schon irgendetwas über Programmiersprachen wie C, diverse Basic-Spielarten, Java, oder was auch immer wissen: legen Sie alles beiseite, was Sie darüber wissen. Es macht hier keinen Sinn, steht Ihnen beim Lernen im Weg herum und behindert Sie ganz ernsthaft. Beherzigen Sie also eine Grundregel der Pädagogik und des Alltagswissens: Lernen von Neuem beginnt immer mit der gründlichen Zerstörung von altem, schon Gewusstem. Auch wenn es weh tut: Wenn Sie das nicht machen, lernen Sie nix und enden mit so typischen Sätzen wie "Warum kann man das nicht so machen wie ich es schon kenne?" oder auch "Das ist mir zu kompliziert, das lerne ich nie!". Das schon Gewusste steht Ihnen echt im Weg herum und blockiert Sie.

1.1 Unser gewohnter Umgang mit Programmiersprachen

Ebenen eines Computers Warum ist das so? Assembler ist ganz, ganz anders als alle anderen Programmiersprachen und alle anderen Anwendungen, die auf Rechnern laufen.

So sehen die verschiedenen Ebenen aus, aus denen so ein üblicher Computer besteht und der unser Bild und unsere Sichtweise bestimmt. In der Mitte schwebt der eigentliche Prozessor, der zwar die ganze Arbeit macht, aber so gut wie gar nicht als solcher wahrgenommen wird. Von dem weiß ich eigentlich nix, außer dass er AMD-64-Bit-Irgendwie heißt. Ich sehe nur die äußerste Schicht: ich starte ein OpenOffice und schreibe damit Briefe oder male Grafiken oder konstruiere Tabellen, ich starte GIMP und bearbeite das Foto einer Dampfmaschine oder ich öffne einen Firefox-Browser und surfe zum SV-Darmstadt-98-Fanradio. Jeder dieser Schritte hat viele Millionen Einzelschritte im Prozessor ausgelöst, die ich aber weder gesehen noch irgendwie bemerkt habe oder auch nur im Ansatz verstanden habe. Wenn vor der Ausführung jedes Prozessorbefehls eine Abfrage käme, ob ich das auch wirklich tun will, würde mir der Prozessor bewusster werden. Aber dann wäre das Spiel Darmstadt-98 gegen Borussia Dortmund lange herum, bis mein Fanradio dann doch eine Woche später den Endstand anzeigt (11.2.2017: 2:1).

Schon die Schicht, die unter meinen Anwendungsprogrammen werkelt, das Betriebssystem des Computers, ist mir herzlich egal. Ob das nun ein Windows, ein Linux oder irgendetwas anderes ist, braucht mich als User gar nicht interessieren. Es sei denn, mein OpenOffice gibt es für dieses Betriebssystem gar nicht. Oder mein WLAN-Chip wird vom Betriebssystem nicht erkannt. Dann ist es Essig mit Surfen und ich muss in die Untiefen des Betriebssystems eintauchen, um das Problem zu lösen.

Und mein Verständnis reicht schon gar nicht tiefer hinab bis zum Prozessor. Der kann mir sowas von egal sein.

Was mache ich nun, wenn ich irgendwas z. B. in Java programmiere? Ich schreibe ein paar Programmzeilen, die das machen sollen, was ich brauche. Die werden dann kompiliert, oder vom Firefox interpretiert wenn es JavaScript war, und mein Browser macht dann, was ich ihm mit den Programmzeilen gesagt habe. Und malt ein buntes Viereck im Firefox auf den Bildschirm.

Wie geht das vor sich? Beim Kompilieren erzeugt der Compiler eine wilde Mischung aus Prozessoraktionen, Betriebssystemaufrufen und Treiberaktionen, um das Gewünschte zu realisieren. Wie er die Aufgabe löst, ist wieder total intransparent und interessiert uns auch nicht weiter. Obwohl wir programmiert haben, bleiben wir weiterhin Dumm-User, weil wir von dem, was da im Innern des Rechners dabei abgeht, rein gar nix raffen (und das auch gar nicht wissen und verstehen müssen). Es reicht, wenn es tut was es soll.

Programmieren lernen heißt hier also: unverstandenes Zeugs zusammenschreiben, das in Millionen unverstandenen Zwischenschritten letztlich zur gewünschten Aktion führt. Und wenn das nicht klappt, probieren wir solange rum bis es irgendwie hinhaut. Das ist ein Reiz-Reaktions-Schema ohne jeden Verstand oder auch eine Blackbox in edelster Verpackung. Genauso gut könnte man Würfeln als Schulfach einführen und es zur besseren Tarnung als "Angewandte Stochastik" aufhübschen.

Wenn wir in einer Hochsprache wie z. B. Pascal eine Programmzeile hinschreiben wie
C := A + B;

dann addiert der Rechner halt zwei Zahlen, die irgendwo in zwei Speicherbereichen herumstehen, die wir A und B genannt haben. Das Ergebnis kommt in einen Speicherbereich, den wir C genannt haben. Wo diese Speicherbereiche physisch im Speicher herumstehen, haben wir dem Compiler überlassen, der das für uns entschieden hat. Wissen tun wir es nicht, wir brauchen uns aber gar nicht damit herumschlagen, solange das Ergebnis in C korrekt ist. Und es wäre auch gar nicht so einfach, das herauszufinden. Und es wäre auch irgendwie "Heute hier, morgen da!", weil darüber auch noch andere darüber mitentscheiden (das Betriebssystem und seine Speicherverwaltung). Alles Entscheidungen, um die wir uns rein gar nicht kümmern müssen (und es auch gar nicht können).

Damit der Compiler weiß, wie groß er die Speicherbereiche für A, B und C machen muss, muss man ihm vorher mitteilen, von welchem Typ diese drei Zahlen sind. Irgendwo im Quellcode steht daher eine Zeile von der Art
Var A,B,C: Integer;, oder
Var A,B,C:Real;

Daraus entscheidet der Compiler dann für uns, wo die Speicherbereiche hinkommen und wie groß sie sind und welche Einzelschritte beim Addieren zu erledigen sind. Jedenfalls nicht so, dass wir wüssten, was da jetzt abgeht.

Nicht so bei Assembler: alle diese Entscheidungen, die wir so gerne der Maschinerie des Compilers überlassen, müssen wir hier selber treffen. Und alle Einzelschrittchen, die zum Addieren zweier Zahlen nötig sind, müssen wir auch selber hinschreiben. Das ist zwar mühsamer, aber im Endergebnis wissen wir dafür auch ganz genau, was da abgeht, weil wir es selber erfunden haben. Das ist das gänzlich Neue an Assembler, das wir lernen müssen: jedes Fitzelchen an Entscheidung, und sogar genau den Platz, den wir für das Speichern der drei Zahlen benutzen, müssen wir selber festlegen, das nimmt uns kein Compiler ab. Dafür redet uns auch rein gar nix in unsere Entscheidungen rein: Assembler macht uns zum Herren von allem und jedem - zum Preis von etwas mehr Aufwand.

1.2 Mikrocontroller

ATmega16 ATmega8 ATtiny12 Beim Mikrocontroller ist eigentlich alles ganz anders als beim normalen Computer: ein nackiger Chip mit mehr oder wenigen Anschlusspins, dem fehlt das ganze Zeugs außen drumherum. Nur ein bisschen Speicher, gar kein Betriebssystem, und schon gar keine Anwenderprogramme. Und deshalb müssen wir an den mit Bits und Bytes ran, der versteht gar nix anderes als diese Ursprache, die schon die ersten Zuse-Rechner beherrschten.

Aufbau eines Mikrocontrollers Das hier ist nun die Innerei eines nackigen Mikrocontrollers. Der hat zwar einiges an Hardware, aber weder ein Betriebssystem (und entsprechende Treiber für die Hardware) noch eine Tastatur oder einen Bildschirm. Dafür hat er aber
  1. einen Flashspeicher für das ablaufende Programm, und
  2. eingebauten flüchtigen Speicher (der sich leert, wenn die Betriebsspannung ausgeht, oder besser: sich mit lauter Einsen füllt), und
  3. einen nichtflüchtigen Speicher EEPROM, der seinen Inhalt auch beim Abschalten der Betriebsspannung behält, und
  4. jede Menge weitere Hardware wie Zeitgeber (Timer) und Zähler (Counter), AD-Wandler, u.v.a.m., und vor allem
  5. eine ziemlich große Anzahl an Pins, die vom Programm als Ein- oder Ausgang geschaltet werden können und über die Informationen in Form von Bits ein- oder ausgegeben werden können.
Das Gebilde versteht aber rein gar nix anderes als Nullen und Einsen. In seinem Flashspeicher stehen nach dem Programmieren solche Nullen und Einsen herum und werden als Befehle oder auch als Daten interpretiert. Auch im Speicher oder im EEPROM stehen nur Nullen und Einsen, und alle andere Hardware versteht auch nur Nullen und Einsen. Außer die AD-Wandler-Eingänge, die verstehen auch angelegte analoge Spannungen und wandeln sie in Nullen und Einsen um (wenn wir es ihnen mit der richtigen Abfolge von Nullen und Einsen sagen).

Damit ist jetzt schon klar: wer mit diesen Mikrocontrollern was anfangen will, muss Nullen und Einsen und sich in der Binärwelt auskennen. Im Gegensatz zu Java, C oder Pascal ist binär hier Pflicht und ohne das geht es einfach nicht.

Womit wir auch schon bei Assembler als Programmiersprache sind: der Prozessor versteht halt nur mal nur das, was in seiner zentralen Recheneinheit (CPU, Central Processing Unit) auch verstanden wird. Und das ist halt der mächtige, aber beschränkte Befehlssatz der AVR-CPUs. Die Binärkombination 0000.0000.0000.0000 versteht die CPU so: tue mal für einen Takt lang gar nichts. Und, wenn sie das aus dem Flashspeicher so gelesen hat, macht sie eben halt rein gar nichts und führt danach den nächsten Befehl aus.

2 Assembler als Sprache

Bekommt der Mikrocontroller Spannung und ist sein Reset auf hoher Spannung (annähernd seiner Betriebsspannung), so legt die CPU los: sie setzt ihren Adresszähler auf Null, liest die 16 Bits ein, die im Flashspeicher an dieser Stelle stehen und führt sie aus.

2.1 Befehlsbearbeitung durch die CPU

Stößt die CPU im Flashspeicher auf die Bitkombination 0000.1100.0000.0000, dann addiert sie den im Register R0 stehenden Inhalt mit sich selbst und schreibt das Resultat in das gleiche Register. Ein Register ist dabei so eine Art interner Speicherplatz für acht Bit breite Binärzahlen, auf die die CPU direkt und ohne Umschweife zugreifen und mit denen daher gerechnet werden kann. Die AVRs haben davon 32 Stück (R0 bis R31), was die allermeisten anderen CPUs dieser Welt bei weitem übertrifft. Für mich ein wichtiger Grund, gar nicht erst nach den popeligen PIC-Controllern zu schauen: die haben gerade mal nur ein einziges, und schon das Addieren zweier kleiner Zahlen ist bei denen ein Hängen und Würgen.

Nebenbei haben wir da auch schon ein paar wichtige Entscheidungen getroffen: unsere Zahl steht im Register R0 und ist acht Bit groß. Hätte uns sonst der Compiler abgenommen und uns entmündigt.

Was steht nach dem Addieren im Register R0? Nun, nur Nullen. Die Inhalte aller 32 Register wurden beim Start des Prozessors nämlich auf Null gesetzt. Und Null plus Null gibt nun mal Null. Oder binär hingeschrieben:
  0000.0000
+ 0000.0000
-----------
= 0000.0000
Echt langweilig.

Daher muss jetzt erst mal was in dieses Register rein: eine Eins. Dafür gibt es eine einfache Möglichkeit: Den Inhalt des Registers R0 (war: Null) um Eins erhöhen. Die entsprechende Bitkombination lautet: 1001.0100.0000.0011. Das zählt Eins dazu. (Die Bitkombinationen bloß nicht merken, die werden wir schnell wieder los!)

Wenn wir danach den Addierbefehl anschließen, dann passiert Folgendes:
  0000.0001
+ 0000.0001
-----------
= 0000.0010
Eins und Eins gibt zwei (dezimal), aber zwei gibt es binär nicht, deshalb wird, wie beim Addieren von Fünf und Fünf in der Dezimalwelt, ein Übertrag in die nächsthöhere Stelle geschoben. Aus der dezimalen Zwei wird binär 10.

Addieren wir die Zahl noch mal zu sich selbst, kriegen wir
  0000.0010
+ 0000.0010
-----------
= 0000.0100
Wir sehen schon: addieren mit sich selbst ist in der Binärwelt dasselbe wie einmal Links-Schieben, wobei in die freie unterste Bit-Stelle eine Null reinkommt.

Nun hat unser Programm schon drei CPU-Operationen: Erhöhen, Addieren, Addieren. Und es stellt sich die Frage: was macht der Prozessor, wenn er mit den dreien fertig ist? Nun, er läuft und läuft und läuft, sogar wenn es gar kein Programm mehr im Speicher gibt oder wenn der Speicher alle ist. Dann fängt er am Ende halt wieder bei Null an. Aber ohne dabei die Register zu löschen, das macht er nur beim Spannungsausfall!

2.2 Mnemonics

Kein Mensch kann sich 16-stellige Binärzahlen merken, wie sie die CPU versteht (außer einigen Gedächtnisgenies). Und definitiv geht einem spätestens beim Schreiben der dritten Binärzahl schon die Lust aus, so einen langwierigen Quatsch hinzuschreiben.

Um sich die länglichen Binärzahlen zu ersparen, hat man einfache Gedächtnisstützen erfunden. So heißt 1001.0100.0000.0011 einfach "inc R0", und 0000.1100.0000.0000 einfach "add R0,R0". Man sieht die Absicht: "inc" kommt vom englischen "Increase" und "add" versteht sich in beiden Sprachen von selbst. "inc" und "add" sind Mnemonics oder menschen-definierte Gedächtnisstützen. Die Angaben "R0" und "R0,R0" sind hingegen Parameter, im zweiten Fall durch ein Komma getrennt.

Das Mnemonic von Tue-nichts-Befehl lautet übrigens "nop", von englisch "no operation".

Nichts anderes ist und macht ein Assembler: er sucht aus dem Text Mnemonics und ihre Parameter und wandelt sie in die entsprechenden Binärcodes um. Füttert man die nacheinander zusammengestellten Binärcodes in den Flashspeicher, versteht das die CPU auf Anhieb und macht was sie soll. Das Zusammenstellen der Binärcodes heißt Assemblieren (von englisch "to assemble", deutsch zusammenbauen oder zusammenstellen), das Programm dazu ist ein Assembler, der umzuwandelnde Quellcode wird aber auch als vom Typ Assembler bezeichnet.

Wichtig: Nur solche Binärkombinationen haben ein Mnemonic, die auch von der CPU verstanden werden. AVR-Assembler bildet also nur das nach, was die AVR-CPU auch tatsächlich kann. Alles andere sind unknown programming objects (UPOs).

2.3 Hexadezimal statt binär

Und noch eine wesentliche Schreibvereinfachung wurde erfunden und wird in Assembler verwendet: Hexadezimalzahlen. Eine Hexadezimalziffer fasst vier Bits zu einem Zeichen zusammen, aus 8 Binärstellen werden derer zwei Hexadezimalziffern und aus 16 derer vier.

Bei binär 0000 bis 1001 ist es noch einfach: aus denen wird hexadezimal 0 bis 9. 1010 wird A, 1011 wird B, usw. bis 1111, das zu F wird. Da übliche Assembler sowieso den gesamten Text in Großbuchstaben umwandeln, ist es egal, ob a oder A bzw. f oder F geschrieben wird. Nicht egal ist es, ob 99 eine Dezimalzahl oder eine Hexadezimalzahl kennzeichnet. Deshalb kriegen Hexadezimalzahlen in Assembler vor die Zahl "0X" oder "0x", und aus 99 wird mit 0x99 dezimal 153.

Analog dazu kriegen Binärzahlen ein "0b" davor. So ist 0b101 dezimal nicht 101 sondern 5. Nur wo nix davorsteht geht der Assembler davon aus, dass die folgende Zahl dezimal ist.

2.4 Die No-No's bei Assembler - für Hochsprachenverwöhnte

Das Folgende ist in Assembler gründlich zu vergessen: Damit macht der Großteil der erlernten Konzepte und Regeln aus Hochsprachen in Assembler keinen Sinn. Sehr wohl macht es aber viel Sinn, sich über optimierte Datenstrukturen Gedanken zu machen (was kommt wo hin und in welcher Reihenfolge) und Spaghetticode (nutz- und hirnloses Herumspringen im Code) zu vermeiden. Das in Hochsprachen übliche "Erst mal anfangen, dazupfriemeln kann man immer!"-Konzept führt bei Assembler schnell zu undurchschaubaren Code- und Datenstrukturen, die nachträglich zu sortieren viel aufwändiger ist als es von vornherein systematisch zu planen.

3 Simulieren des Programmablaufs

Um das erste Assembler-Programm zu schreiben, starten wir Notepad (im Windows auch Editor genannt) oder Wordpad (beides im Startmenue unter Zubehör) oder unter Linux mit KWrite oder einem anderen einfachen Editor. Wir tippen die folgenden Zeilen ein:

.nolist
.include "tn13def.inc"
.list
  inc R0
  add R0,R0
  add R0,R0

Die erzeugte Datei speichern wir in einem Ordner mit einem aussagekräftigen Namen, z. B. im Verzeichnis "avr-assembler/code1/" als Datei "code1.asm".

3.1 Assemblieren

Zum Assemblieren dieser Assembler-Quellcode-Datei kann man jeden AVR-Assembler einsetzen, z. B. meinen eigenen, gavrasm. Den gibt es hier zum kostenlosen Herunterladen, fertig kompiliert für 64-Bit-Linux oder -Windows.

Kommandozeile mit CD Hat man die ausführbare Datei "gavrasm" (Linux, als ausführbar markieren) oder "gavrasm.exe" (Windows) im gleichen Verzeichnis abgelegt, kann man eine Kommandozeile starten (in Linux unter System und Konsole, in Windows unter Start-Programme- Zubehör-Eingabeaufforderung). In die tippt man zunächst ein, wohin man navigieren möchte (in den Ordner mit gavrasm und der Quellcode-Datei, mit CD "[Ganzer Pfadname]"). Dann startet man gavrasm (in Linux mit ./gavrasm -seb code1.asm, in Windows mit gavrasm.exe -seb code1.asm).

Assembler bei der Arbeit Nun sollte der Assembler sein Werk verrichten und mit Meldungen glänzen.

Die wichtigste davon ist: "keine Fehler". Die Warnung weiter oben können wir ignorieren.

DIR mit neuen Dateien Wenn wir nun mit DIR (Linux: ls) den Verzeichnisinhalt auflisten lassen, sehen wir zwei neue Dateien:
  1. Code1.hex: das ist die Datei, deren Inhalt in den Flashspeicher gebrannt werden muss, damit die CPU das dann ausführt, und
  2. Code1.lst: das ist die Datei, mit der der Assembler seine Assemblierarbeit kommentiert.


Assembler-Listing Die zweite der beiden Dateien sieht so im Editor so aus.

Die fünfte Codezeile (inc R0) wurde vom Assembler mit hexadezimal 9403 an der Adresse 000000 übersetzt. Die sechste und die siebte Zeile (beide add R0,R0) wurde in hexadezimal 0C00 an den Adressen 1 und 2 übersetzt. Aus nichts anderem besteht auch die erste, die Hex-Datei. Aber in einem etwas eigenartigen Format (in Intel-Hex).

Außerdem hat er noch eine Liste der Symbole ausgeworfen, in der aber nichts arg sinnvolles drinsteht außer der AVR-Typ.

In Kürze also und zum Merken: Ein Assembler übersetzt Assembler-Mnemonics in Binärcode für den Mikrocontroller, damit der genau nur das kriegt, was er auch versteht, nämlich binären Code.

3.2 Der Code im Simulator

Und jetzt alles wieder schnell vergessen: das mit der Kommandozeile braucht man nämlich gar nicht. Und ist auch viel zu umständlich. Es geht viel komfortabler, nachdem ich im vergangenen Jahr den Simulator avr_sim gebastelt habe. Und auch den gibt es kostenlos, nämlich hier. Als ausführbare Lin64- oder Win64-Datei.

AVR-Assembler-Simulator avr_sim Nach dem Auspacken und dem Starten von avr_sim wird man erst mal nach dem Standardpfad für Quellcode-Dateien gefragt (woraufhin wir zum Ordner avr-assembler navigieren). Aus dem Menü Project wählen wir "New from asm" und navigieren zur Datei "code1.asm".

avr_sim Typauswahl Jetzt nervt avr_sim mit der Frage, in welcher der drei Verpackungen der Prozessor ATtiny13 denn simuliert werden soll. Nachdem wir das mit Ok quittiert haben, sehen wir unseren Assembler-Quellcode im Editor vom Simulator.

Editor im avr_sim Der Inhalt der Datei taucht jetzt im Editor auf, und wir können nach Herzenslust darin herumfummeln und Codezeilen einfügen oder löschen.

Im linken Projektfenster gibt es allerhand Interessantes zum Projekt und zu den Innereien des ATtiny13. So erfahren wir, dass er mit 1,2 MHz getaktet ist (ja, er liest pro Sekunde 1,2 Millionen Befehle aus seinem Flashspeicher und führt sie aus), dass er einen 8-Bit-Timer und keinen 16-Bit-Timer hat und dass er vier AD-Wandlerkanäle hat.

Und das Schönste an dem Fenster ist: es hat jetzt einen Menüeintrag "Assemble". Wenn wir auf den klicken, dann werkelt ein eingebauter gavrasm-Assembler an der Datei Code1.asm.

Assemblieren im avr_sim Der Assembler sollte zum Schluss kommen, dass alles korrekt assembliert ist.

avr_sim zeigt jetzt automatisch das Listing an und hat einen weiteren Menüeintrag: "Simulate". Wenn wir den anklicken, öffnet sich das Simulatorfenster.

Simulatorfenster Hier sehen wir im Fenster "Simulation status" den Programmzähler in hexadezimal, die Anzahl Instruktionen, die schon simuliert sind, den Stapelzeiger und die Taktfrequenz. Im Fenster "SREG" sehen wir die Flaggen der CPU im sogenannten Status-Register). Und im Fenster "Register" die Inhalte aller 32 Register des Chips (alles Nullen, Anzeige in hexadezimal).

Mit dem Menüeintrag "Step" können wir nun unseren Code schrittweise ausfüren.

Simulieren von inc R0 Was Wunder: im Register R0 steht jetzt wie vorhergeplant eine 1.

Beim Status hat sich auch was getan: der Programmzähler ist jetzt auf 1, die Anzahl ausgeführter Instruktionen ebenfalls und die abgelaufene Zeit zeigt die Dauer eines Takts an.

Auch im Listing im Editorfenster hat sich was getan: das kleine >-Zeichen im gelben Feld zeigt jetzt auf die nächste Instruktion.

Und so können wir nun schrittweise durch den Quellcode steppen und schauen, was das jeweils mit den Registern macht.

Fehlermeldung Ende Code Nach dem dritten Schritt zeigt jetzt der Programmzähler aber auf eine Speicherzelle im Flash, der gar keinen Code enthält. Der Simulator hat das gemerkt, der reale Chip würde es nicht merken und einfach immer so weiter machen.

4 Mehr zum Assembler-Lernen

Es gibt natürlich noch viel mehr Instruktionen als nur NOP, INC und ADD. Die sind hier alle einzeln erläutert.

Und so ein AVR-Assembler-Projekt macht auch nur richtig Spaß, wenn man den Hexcode in einen realen Chip brennt und der dann macht, was er soll. Hier gibt es einen ganzen Kurs mit solchen kleinen Anwendungen und dem zugehörigen Quellcode in Assembler.

Wer noch mehr praktische Beispiele braucht, findet sie hier oder hier nach Anwendungsarten sortiert.

Natürlich kann der Simulator noch viel mehr. Alles, was er kann, ist im jeweiligen Handbuch dargestellt, das es zum Simulator hier zum Download gibt.

Viel Erfolg beim Lernen.

Kommentare zu dieser Seite bitte hierhin.



zum Seitenanfang


©2019 by http://www.avr-asm-tutorial.net