Inhaltsverzeichnis
AVR Assembler Einführung
Es gibt mehrere Gründe, sich mit dem AVR-Assembler zu beschäftigen:
- reines Interesse
- Man hat eben keinen anderen Compiler, Bascom kostet ja was und GCC-C kann man möglicherweise genausowenig, also warum nicht gleich.
- Endlich Programmieren ohne Sprach-Restriktionen
- (theoretisch) sagenhaft schneller und kurzer Code
Dadurch ergeben sich aber auch verschiedene Haupt-Zielgruppen für eine Einführung. Die eine ist in einer oder mehreren anderen Sprachen durchaus versiert, und möchte endlich auch mal richtig in die Tiefen der Hardware steigen. Die andere ist totaler Neueinsteiger und hat eben gehört, daß man Micro-Controller eben am besten mit Assembler programmiert.
Unterschiede zu anderen Sprachen
Bildhaft ist der Unterschied der: Beim Assembler hat man ein weißes Stück Papier (ohne Linien) und ein Alphabet von A-Z. Aus diesen Buchstaben soll man nun erstmal Worte suchen und dann damit einen sinnvollen Text verfassen.
Bei "höheren" Sprachen ist das Papier zumindest mal liniert, und dazu kriegt man auch ein Wörtebuch und die Grammatik. Teile des Textes sind schon vorgeschrieben und ich muß nurmehr bei den ..... Punkten was einsetzen.
- Daten
Höherer Sprachen kennen alle möglichen Datentypen in allen möglichen Längen, integer, floating point, Strings. Dadurch sind aber auch gleich die möglichen und zulässigen Operationen damit schon eingeschränkt.
Beim (AVR) Assembler gibt es nur zwei Typen: Bytes mit Vorzeichen und Bytes ohne Vorzeichen. Aus. Und es gibt wenige Einschränkungen, was man damit machen kann.
- Vor- und Nachteile
Wenn man den Assembler mit höheren Sprachen vergleicht, um vielleicht Vor- und Nachteile herausarbeiten zu können, kommt man schnell drauf, daß in jedem Vorteil für eine der Seiten auch schon der Nachteil drinnen steckt. Was ich beim Assembler an Performance heraushole, weil ich z.B. "Goodies" eines Controllers ausnutze, bezahle ich, wenn ich mir einen anderen kaufe und dann das ganze Programm umbauen muß.
Struktur eines AVR-Maschinenprogrammes
Bei höheren Sprachen wird diese Struktur vom Compiler/Linker erzeugt, beim Assembler muß man sich selbst darum kümmern.
Der Controller beginnt mit der Abarbeitung links oben bei der Programmspeicheradresse 0000. Wenn man Interrupts verwenden will, muß man gleich das nächste Bereich (ISR-Vectoren) überspringen (mit "JMP"). Wenn nicht, kann man sich diese Vektoren aber auch wegdenken.
Man landet so oder so bei den Befehlen, die der Initialisierung dienen (Setzen der Startbedingung). Das ist zumindest die Festlegung des Stack-Pointers, sonst kann man keine "CALLS" oder Interrupts durchführen.
Danach kommt man von oben in die Haupt-Programm-Schleife, die (üblicherweise) immer wieder ohne Ende durchlaufen wird.
Das "End" ist bei Microcontrollern daher auch kaum wirklich notwendig. Wenn es da ist, ist das eine ewige Wiederholung eines einzigen Befehls.
Source-Code Muster
Das folgende Codebeispiel ist eine reine Basis-Initialisierung ohne irgendeine erkennbare Funktion. Für den Einstieg ist es am besten, das einfach abzuschreiben, wie es ist, und dann an den bezeichnenten Stellen mit eigenen Befehlen nach und nach zu erweitern.
.NOLIST ; List-Output unterdrücken .INCLUDE <m8def.inc> ; das gibt es für jeden Controllertyp .LIST ; List-Output wieder aufdrehen .CSEG ; was nun folgt, gehört in den FLASH-Speicher ;------------------------------------------------------ ; Start Adresse 0000 ;------------------------------------------------------ RESET: jmp INIT ; springen nach "INIT" ;------------------------------------------------------ ; ISR VECTORS ;------------------------------------------------------ ; ..... hier kommen dann die Sprungadressen für die Interrupts rein ; dazu kommen wir noch .ORG INT_VECTORS_SIZE ; dadurch haben wir für die Vektoren Platz gelassen INIT: ;------------------------------------------------------ ; INITIALIZE ;------------------------------------------------------ ldi r24,high(RAMEND) ;Stack Pointer setzen out SPH,r24 ; "RAMEND" ist in m8def.inc (s.o.) festgelegt ldi r24,low(RAMEND) ; out SPL,r24 ; ;------------------------------------------------------ ; eigene Initialisierungen ;------------------------------------------------------ ;.... ;.... ;.... ;------------------------------------------------------ ; HAUPTSCHLEIFE ;------------------------------------------------------ Hauptschleife: ;.... eigene befehle ;.... eigene befehle ;.... eigene befehle rjmp Hauptschleife ; immer wiederholen ;------------------------------------------------------ ; ENDE ;------------------------------------------------------ Ende: rjmp Ende
Der Ablauf der Übersetzung
Der Assembler ist ja selbst auch nur ein Programm und zu Beginn recht spartanisch ausgestattet: Er hat einen Daten-Buffer zur Verfügung, in den er den Maschinencode hineinschreiben soll, einen Pointer (Zeiger) dazu, damit er immer weiß, wo er grad ist, und eine allgemeine Tabelle, wo er Texte mit Zahlen verknüpfen kann. Alles ist zu Beginn leer bzw. auf NULL. Mit diesen Voraussetzungen fängt er an, unseren Source-Text zeilenweise zu lesen. Und in jeder Zeile (die nicht leer ist) erwartet er eine Anweisung, was zu tun ist
PreCompiler Anweisungen
Das sind Anweisungen an den Assembler selbst und erzeugen keine Maschinenbefehle. Das ob. Source-Fragment hat auch gleich mit sowas begonnen
.NOLIST
Normalerweise schreibt der Assembler ja eine Übersetzungsliste. Nach dieser Anweisung läßt er das bleiben
.INCLUDE <m8def.inc> ; das gibt es für jeden Controllertyp
sagt ihm, er soll jetzt erstmal die (text) datei "m8def.inc" lesen und genauso behandeln, als wäre diese Datei mit dem Editor hier reinkopiert worden.
.LIST
Ab nun wird wieder alles in der Übersetzungsliste protokolliert.
.CSEG
was danach folgt, bezieht sich auf den FLASH-Programm-Speicher
Gleich ein Hinweis: Der "AVR Assembler 2" hat als Kennzeichen für den Precompiler ein "#" und andere Schlüsselwörter. Zum allgemeinen Verständnis ist das aber vorerst egal.
Bemerkungen / Kommentare
;------------------------------------------------------
Alles, was NACH einem Strichpunkt steht, schreibt er einfach nur in die Übersetzungsliste, sonst macht er nix.
Label (Sprungmarke)
RESET:
Was gleich ganz links in der Zeile beginnt (und mit einem Doppelpunkt abgeschlossen ist) schreibt er einfach in die genannte Tabelle und den momentanen Wert des Pointers dazu. Sonst hat das im Moment keine weitere Bedeutung. Natürlich, wenn er den gleichen Text schon mal hatte, gibt es eine Fehlermeldung, denn ein Label muss eindeutig sein
Commands
jmp INIT ; springen nach "INIT"
Alles, was nicht ganz links steht und auch nix anderes ist, betrachtet der Assembler als "Command". Normalerweise ist das eben eine AVR-Instruktion aus dem "Instruction Set". Hier ist das "JMP", also im Programm einfach woanders weitermachen. Gut, aber wo ? Der Rechner braucht nun unbedingt eine Zahl, denn "INIT" sagt ihm ja nichts. Also sieht er nun in der Tabelle nach, ob er den Text "INIT" schon mal hatte. Hatte er nicht, den zu der Stelle weiter hinten, wo "INIT:" als Sprungmarke steht (s.o) ist er ja noch garnicht gekommen.
Ich kann leider nicht genau sagen, welche Strategie der AVR-Assembler nun anwendet, dazu gibt es keine Doku. Manche Assembler lesen erstmal überhaupt nur den Sourcetext von vorn bis hinten durch, um alle "Sprungmarken" zu sammeln (Pass I) und fangen dann nochmal von vorne an (Pass II), diesmal mit schon gefüllter Tabelle. Andere machen das etwas gefinkelter. Ist uns aber eigentlich egal.
Jedenfalls findet er letztlich den Text "INIT" und daneben steht die Nummer der Speicherstelle, wo wir eben "INIT:" hingeschrieben haben. Und nun schreibt der Assembler also den Maschinencode für "JMP" in den Buffer und dazu die Zahl aus der Tabelle. Das war mal ein Befehl, also addiert er auf seinen Zeiger eins drauf und ist mit diesem Befehl fertig.
ORG / JMP
Das, was für den Controller beim Ablauf dann ein "JMP" ist, ist das ".ORG" für den Compiler. Wenn also bislang der Assembler den FLASH-Speicher von 0 - nn mit Befehlen befüllt hat, macht er durch
.ORG INT_VECTORS_SIZE
mit der Stelle weiter, die er über die gleiche Tabelle unter "INT_VECTORS_SIZE" gefunden hat. Dieser Tabelleneintrag, genauso wie "RAMEND" an anderer Stelle, entsteht aus der Datei "m8def.inc". Wie im vorliegenen Fall macht man diese "Assembler-Jumps", um Speicherstellen freizuhalten.
Merkwürdiges
Befehlsadressen / Byteadressen
Ein Maschinenbefehl besteht aus maximal drei, bit-mäßig verschachtelten Teilen. Die meisten Befehle brauchen dafür genau 16 Bit (also zwei Byte), manchmal reicht das nicht, da gehören dann nochmal 16 Bit dazu. Kleiner als 16 Bit ist aber keiner. Aus diesem Grund wäre es natürlich sinnlos, Adressen für Befehle in Bytes auszudrücken, wenn es eh immer Vielfache von 2 sein müssen, das kleinste Bit einer solchen Adresse wäre ja immer null.
Also adressiert man Commands in "Worten" von 16 - Bit bzw. 2 Bytes
Beim Ablegen eines Labels (Sprungmarke) werden also Wortadressen in der Tabelle angelegt. Solange der Wert auch als Befehls-Adresse verwendet wird (z.B. "JMP Label"), braucht das nicht zu kümmern, denn genauso will es der AVR ja dann auch haben. Wenn dort aber Daten stehen und wir wollen diese adressieren, müssen wird den Wert mal 2 nehmen (oder einmal nach links schieben, was dasselbe ist), sonst landen wir im Nirwana.
3 ist nicht gleich 3
Eine Zahl bedeutet also bei der Maschinensprache unter Umständen ganz was anderes, je nachdem, in welchem Zusammenhang sie verwendet wird. Bei allen Befehlen, die nur für die Register 16 - 31 gehen, steht im Maschinencode für das Register 16 nicht 16 drin, sondern 0. die "16" addiert er dann bei der Befehlsausführung drauf (für den Techniker: er braucht ja nur das Bit 24 hochzuziehen)
GPR ist nicht gleich GPR
Was auch beim Einstieg leicht irritieren kann ist der Umstand, daß die 32 sogenannten "General Purpose Register" irgendwie so gar nicht generell und gleich sind. Manche Befehle kann ich mit allen 32 Registern machen, einige nur mit R16-R31, andere von R24 aufwärts und dann sind da noch R26 bis R31, die heissen dann manchmal auch noch X, Y und Z
SFR ist gar nicht gleich GPR
Special Function Register sind überhaupt seltsam, die meisten GPR-Befehle gehen da gleich garnicht, dafür gibt es andererseits ein paar Bit-Manipulationen, die gehen wiederum nur mit diesen. Dann gibt es auch Bit-Abfragen, die gehen da wie dort und bewirken auch das Gleiche, aber dafür heissen sie dann anders.
Assembler-Befehle
Viele dieser Probleme nimmt uns der Assembler ab, viele aber nicht. Dort erscheinen die Instruktionen immer so
CMD | ARG1 | ARG2 |
wobei die Argumente 1 u. 2 optionell, d.h. bei Bedarf erscheinen. Command ist natürlich immer da. Trotz der Hilfe des Assemblers durch "Warnings" und "Errors" ist bei den Argumenten aber immer Vorsicht geboten. Wir haben gehört, daß Arg 1 und Arg 2 im Grunde einfach nur zwei Zahlen sind, die dann zusammen mit dem Command-Code zum Maschinenbefehl werden. Wenn es dem Assembler also auch nur irgendwie gelingt, aus dem, was wir da hinschreiben, Zahlen zu machen die er umwandeln kann, nimmt er sie auch.
Zwischenbilianz
Einiges dieser sonderbaren Dinge lassen sich leicht erklären, manche schwer, manche mangels Unterlagen darüber garnicht. Deswegen versuch ich es hier auch gleich garnicht.
So oder so sind es Fakten, an die man sich am besten einfach gewöhnt. Das scheint und ist am Anfang sicher schwer, solange man noch mit den Befehlen einzeln kämpft. RISC-Befehle werden aber erst in bestimmten Gruppierungen überhaupt wirklich sinnvoll, und durch diese sich immer wieder wiederholenden Gruppen wird es sehr schnell leichter.
Beim Briefe schreiben denkt man ja auch nicht in Buchstaben, das wäre mühsam, sondern in Worten oder in ganzen Sätzen.
Load & Store
Die meisten Befehle für den AVR laufen nach dem Load & Store (Laden und Speichern) Prinzip ab. Das heißt, die Daten werden zuerst aus dem SRAM-Speicher (oder den I/O bzw. SFR Registern) in die allgemeinen Register (GPR) geladen, dort verarbeitet, und danach wieder in den SRAM oder die SFR zurückgestellt.
Das bedeutet, um z.B. ein Byte aus der einen Ecke des SRAM in eine andere zu kopieren, brauchen wir wenigstens 3 Maschineninstruktionen.
Das ist natürlich eine recht allgemeine Darstellung, aber es hilft, das Instruction Set des AVR schon ein wenig besser zu verstehen.
Denn erstens gibt es zu (fast) jeder Art "Load"-Befehl ein Äquivalent als "Store", und zum Finden genügt es meist, statt "L" an der selben Stelle ein "S" zu nehmen.
LDS GPR, adresse ; lade das Byte von der Adresse in ein GPR STS adresse, GPR ; speichere den Inhalt von GPR in der Adresse
Wenn es um die I/O Register (SFR) geht:
IN GPR, I/O-Register ; GPR <-----I/O Register OUT I/O-Register, GPR ; I/O Register <---- GPR
Für feste Werte (Literals) gibt es natürlich keine Store-Variante, denn diese Werte stehen ja im FLASH (Programm) Speicher, und da kann man nicht einfach einzelne Bytes verändern.
LDI GPR, wert
Beispiele
An PORTC haben wir (mit Vorwiderständen) ein paar LED angeschlossen. Und die sollen einfach nur zeigen, welche 0-er und 1-er am PORTB anliegen.
; Eigene Initialisierungen LDI R24, 0 ; der wert "0" ins allgemeine Register R24 OUT DDRB, R24 ; das kommt ins Controll-register f. PortB ; dadurch sind diese PINs als INPUT festgelegt LDI R24, 255 ; der wert "255" = FF ins allgemeine Register R24 OUT DDRC, R24 ; das kommt ins Controll-register f. PortC ; dadurch ist das Port auf OUTPUT gestellt
; Eigene Befehle (in der Hauptschleife) IN R24, PINB ; die Bits vom PortB ins allgemeine Register R24 OUT PORTC, r24 ; schreiben wir nach PortC ; wo ein 1-er ist, leuchtet nun die LED
Wenn wir diesr Befehle in das Programm-Schema oben einfügen, haben wir also das erste Assemblerprogramm schon geschrieben.
Autor: PicNick