Pfad: Home => AVR-Überblick => Programmiertechniken => Assemblerkonzept

Das Konzept hinter der Sprache Assembler

Achtung! Bei dieser Seite geht es um die Programmierung von Mikrocontrollern, nicht um PCs mit Linux- oder Windows-Betriebssystem und ähnliche Elefanten, sondern um kleine Mäuse. Es geht auch nicht um die Programmierung von Ethernet-Megamaschinen, sondern um die Frage, warum man als Anfänger eher mit Assembler beginnen sollte als mit einer Hochsprache.

Sie erläutert, was das Konzept hinter Assembler ist, was Hochsprachenprogrammierer vergessen müssen, um Assembler zu lernen und warum Assembler manchmal fälschlich als "Maschinensprache" bezeichnet wird.

Die Hardware von Mikrocontrollern

Was hat die Hardware von Mikrocontrollern mit Assembler zu tun? Viel, wie aus dem Folgenden hervorgeht.
Das Konzept bei Assembler ist, die Ressourcen des Prozessors zugänglich zu machen. Unter Ressourcen sind dabei alle Hardwarebestandteile zu verstehen, also Zugänglich machen heißt dabei: unmittelbar zugänglich und nicht über Treiber oder irgendwelche Interfaces, die ein Betriebssystem bereitstellt. Das heißt, Sie bedienen die serielle Schnittstelle oder den AD-Wandler selbst, nicht irgendeine andere Schicht zwischen Ihnen und dem Rechner erledigt das für sie. Als Belohnung für die Mühe steht Ihnen die ganze Welt des Prozessors zur Verfügung und nicht nur der Teil, den der Compilerhersteller für Sie ausgewählt und zur Verwendung vorgesehen hat.

Die Arbeitsweise der CPU

Am Wichtigsten ist die Fähigkeit der zentralen Steuereinheit, Instruktionen aus dem Programmspeicher (Flash) zu holen (Instruction fetch), in auszuführende Schritte zu zerlegen und diese Schritte dann auszuführen. Die Instruktionen stehen dabei in den AVR als 16-bittige Zahlenwerte im Flash-Speicher und werden von dort schrittweise abgeholt. Der Zahlenwert übersetzt sich dann z. B. in die Addition der beiden Register R0 und R1 und das Speichern des Ergebnisses im Register R0. Register sind dabei Speicherzellen, die 8 Bits enthalten und direkt gelesen und beschrieben werden können.
An einigen Beispielen soll das genauer gezeigt werden.

CPU-OperationKode (binär)Kode(Hex)
CPU schlafen legen1001.0101.1000.10009588
Register R1 zu Register R0 addieren0000.1100.0000.00010C01
Register R1 von Register R0 subtrahieren0001.1000.0000.00011801
Konstante 170 in Register R16 schreiben1110.1010.0000.1010EA0A
Multipliziere Register R3 mit Register R2,
Ergebnis in R1 und R0
1001.1100.0011.00109C32

Wenn die CPU also hexadezimal 9588 aus dem Flashspeicher liest, stellt sie ihre Tätigkeit ein und holt keine weiteren Instuktionen mehr aus dem Speicher. Keine Angst, da ist noch ein weiterer Schutzmechanismus davorgeschaltet, bevor sie das tatsächlich ausführt. Und man kann sie auch wieder aus diesem Zustand aufwecken.
Liest sie hexadezimal 0C01, addiert sie R0 und R1 und schreibt das Ergebnis in das Register R0. Das läuft dann etwa so ab:

execution Im ersten Schritt wird das Befehlswort geholt und in Ausführungsschritte der Arithmetic Logical Unit (ALU) zerlegt.

Im zweiten Schritt werden die beiden ausgewählten Register an die ALU-Eingänge angelegt und mit den Inhalten die Operation Addition ausgeführt.

Im dritten Schritt wird das Zielregister R0 an den Ergebnisausgang der ALU angeschlossen und das Resultat in das Register geschrieben.

Liest die Ausführungsmaschine 9C23 aus dem Flash, dann multipliziert sie die beiden Register R3 und R2 und schreibt das Ergebnis nach R1 (oberes Byte) und R0 (unteres Byte). Hat die ALU gar keine Hardware zum Multiplizieren (z. B. in einem ATtiny13, dann bewirkt 9C23 rein gar nix.
Im Prinzip kann die CPU also 65.536 verschiedene Operationen ausführen. Weil aber zum Beispiel nicht nur 170 in das Register R16 geladen werden können soll, sondern jede beliebige Konstante zwischen 0 und 255 in jedes Register zwischen R16 und R31, verbraucht alleine diese Ladeinstruktion 256*16 = 4.096 der 65.536 theoretisch möglichen Instruktionen. Die Direktladeinstruktionen für die Konstante c in die Register R16..R31 (r4:r3:r2:r1:r0, r4 ist dabei immer 1) bauen sich dabei folgendermaßen auf:

Bit151413121110 09080706050403 020100
Inhalt1110c7c6 c5c4r3r2r1r0c3 c2c1c0

Warum diese Bits so gemischt platziert sind, bleibt das Geheimnis von ATMEL.

Die Addition und die Subtraktion verbraten je 32*32 = 1.024 Kombinationen und sind mit den Zielregistern R0..R31 (z4..z0) und den Quellregistern R0..R31 (q4..q0) so aufgebaut:

Bit151413121110 09080706050403 020100
Inhalt Addieren00001 1q4z4z3z2z1z0 q3q2q1q0
Inhalt Subtrahieren00011 0q4z4z3z2z1z0 q3q2q1q0

Auch hier kümmern wir uns nicht weiter um die Platzierung, weil wir uns die auch nicht weiter merken müssen. Es geht hier nur darum zu verstehen, wie die CPU in ihrem Innersten tickt.

Instruktionen in Assembler

Damit sich der Programmierer keine 16-bittigen Zahlen und deren verrückte Platzierung merken muss, sind in Assembler dafür verständliche Abkürzungen an deren Stelle gesetzt, sogenannte Mnemonics (frei übersetzt Gedächtnisstützen). So lautet dann die Entsprechung der Instruktion, die den Schlafmodus bewirkt, einfach "SLEEP". Liest der Assembler "SLEEP", macht er daraus hex 9588. Das SLEEP kann sich im Gegensatz zu 9588 hexidezimal auch jemand wie ich merken, der schon bei der eigenen Telefonnummer Schwierigkeiten hat.
Addieren heißt einfach "ADD". Um die beiden Register auch noch gleich in einer leicht lesbaren Form mit anzugeben, lautet die gesamte Instruktion "ADD R0,R1". Der gesamte Ausdruck übersetzt sich in ein einziges binäres 16-Bit-Wort, nämlich 0C01. Die Übersetzung erledigt der Assembler.

Die Formulierung übersetzt der Assembler in exakt das 16-Bit-Wort, das dann im Flash-Speicher steht, von der CPU dort abgeholt, in ihre interne Ablauffolge umgewandelt und ausgeführt wird. Für jede Instruktion, die die CPU versteht, gibt es ein entsprechendes Mnemonic. Umgekehrt: es gibt keine Mnemonics, denen nicht exakt ein bestimmter Ablauf einer CPU-Instruktion entspricht. Die Fähigkeit der CPU bestimmt bei Assembler also den Sprachumfang. Die "Sprache" der CPU selbst ist also bestimmend, die Mnemonics sind bloß repräsentative Statthalter für die Fähigkeiten der CPU selbst.

Unterschied zu Hochsprachen

Hier ist für Hochsprachler noch ein Hinweis nötig. In Hochsprachen sind die Sprachkonstrukte weder von der Hardware noch von den Fähigkeiten der CPU abhängig. Die Sprachkonstrukte laufen auf einer Vielzahl von Prozessoren, sofern es für die Sprache einen Compiler gibt, der die erdachten Schlüsselwörter in CPU-Operationen übersetzen kann. Ein GOTO in Basic mag so ähnlich aussehen wie ein JMP in Assembler, dahinter steckt aber ein ganz anderes Konzept.

Die Sache mit der Übertragbarkeit auf andere Prozessoren funktioniert natürlich nur, solange der Controller und seine CPU das mitmacht. Letztlich bestimmt dann die Hochsprache, welcher Prozessor geht und welcher nicht. Ein ärmlicher Kompromiss, weil er unter Umständen ganze Welten mit ihren vielfältigen Möglichkeiten als ungeeignet ausschließt.

Deutlich wird das bei der oben gezeigten Instruktion "MUL". In Assembler kann die CPU das Multiplizieren entweder selbst erledigen oder der Assemblerprogrammierer muss sich selbst eine Multiplikationsroutine ausdenken, die das erledigt. In einem für einen ATtiny13 geschriebenen Assembler-Quellcode MUL zu verwenden, gibt einfach nur eine Fehlermeldung (Illegal instruction). In einer Hochsprache wird der Compiler bei einer Multiplikation feststellen, ob eine Multiplikationseinheit vorhanden ist, dann erzeugt er den Code entsprechend. Ist keine da, fügt er entsprechenden Code ein, ohne dass der Programmierer das bemerkt. Und vielleicht auch noch gleich eine Integer-, Longword- und andere Multiplikationsroutinen, ein ganzes Paket eben. Und schon reicht das Flash eines Tiny nicht mehr aus und es muss unbedingt ein Mega mit Dutzenden unbeschalteter Pins sein damit der Elephant unbenutzter Routinen reinpasst.

Assembler und Maschinensprache

Assembler wird wegen seiner Nähe zur Hardware auch oft als Maschinensprache bezeichnet. Das ist nicht ganz exakt, weil die CPU selbst nur 16-bittige Worte, aber nicht die Zeichenfolge "ADD R0,R1" versteht. Der Ausdruck "Maschinensprache" ist daher eher so zu verstehen, dass der Sprachumfang exakt dem der Maschine entspricht und der Assembler nur menschenverständliche Kürzel in maschinenverständliche Binärworte verwandelt.

Interpreterkonzept und Assembler

Bei einem Interpreter übersetzt die CPU selbst den menschenverständlichen Code erst in ihr verständliche Binärworte. Der Interpreter würde die Zeichenfolge "A = A + B" einlesen (neun Zeichen), würde die Leerzeichen darin herausschälen, die Parameter A und B identifizieren, würde das "+" in eine Addition übersetzen und letztlich vielleicht das Gleiche ausführen wie die Assemblerformulierung weiter oben.

Im Unterschied zum Assembler oben, bei der die CPU nach dem Assemblieren sofort ihr Lieblingsfutter, 16-bittige Befehlsworte, vorgeworfen bekommt, ist die CPU beim Interpreterkonzept überwiegend erst mit Übersetzungsarbeiten beschäftigt. Da zum Übersetzen vielleicht 20 oder 200 CPU-Schritte erforderlich sind, bevor die eigentliche Arbeit geleistet werden kann, ist ihre Ausführungsgeschwindigkeit nur als lahm zu bezeichnen. Was bei schnellen Prozessoren noch verschmerzt werden kann, ist bei zeitkritischen Abläufen prohibitiv: niemand weiss, was die CPU wann macht und wie lange sie dafür tatsächlich braucht.
Die Vereinfachung, sich nicht mit irgendwelchen Ausführungszeiten befassen zu müssen, bewirkt, dass der Programmierer sich damit gar nicht befassen kann, weil ihm die nötigen Informationen darüber auch noch vorenthalten werden.

Hochsprachenkonzepte und Assembler

Hochsprachen ziehen zwischen die CPU und den Quelltext noch weitere undurchaubare Ebenen ein. Ein durchgängiges Konzept sind z. B. sogenannte Variablen. Das sind Speicherplätze, die für eine Zahl, einen Text oder auch nur einen einzigen Wahrheitswert vorbereitet werden und im Quelltext der Hochsprache für den betreffenden Speicherplatz stehen. Vergessen Sie für das Erlernen von Assembler zu allererst das ganze Variablenkonzept, es führt Sie nur an der Nase herum und hält Sie davon ab, das Konzept zu durchblicken. Assembler kennt nur Bits und Bytes, Variablen sind ihm völlig fremd.
Hochsprachen fordern zum Beispiel, dass Variablen vor ihrer Verwendung zu deklarieren sind, also z. B. als Byte (8-Bit) oder Double-Word (16-Bit-Wort). Compiler platzieren solche Variablen dann je nach Geschmack irgendwo in den Speicherraum oder in die im AVR reichlich vorhandenen 32 Register. Ob er den Speicherort, wie der Assemblerprogrammierer, mit Übersicht und Nachdenken entscheidet, ist von den Kosten für den Compiler abhängig. Der Programmierer kann nur noch versuchen herauszufinden, wo die Variable denn letztlich steckt. Die Verfügungsgewalt darüber hat er dem Compiler überlassen.

Die Instruktion "A = A + B" ist jetzt auch noch typgeprüft: ist A ein Zeichen (Char) und B eine Zahl (1), dann wird die Formulierung so nicht akzeptiert, weil man Zeichencodes angeblich nicht zum Addieren benutzen kann. Der Schutzeffekt dieses Verbots ist ungefähr Null, aber Hochsprachenprogrammierer glauben dass sie dadurch vor Unsinn bewahrt werden. In Assembler hindert nichts und niemand daran, zu einem Byte z. B. sieben dazu zu zählen oder 48 abzuziehen, egal ob es sich bei dem Inhalt des Bytes um ein Zeichen oder eine Zahl handelt. Was in dem Register R0 drin steht, entscheidet der Programmierer, nicht der Compiler. Ob eine Operation mit dem Inhalt Sinn macht, entscheidet der Programmierer und nicht der Compiler. Ob vier Register einen 32-Bit-Wert repräsentieren oder vier ASCII-Zeichen, ob die in auf- oder absteigender Reihenfolge oder völlig durcheinander liegen, kann sich der Assemblerprogrammierer auch aussuchen, der Hochsprachenprogrammierer nicht. Typen gibt es in Assembler nicht, alles besteht aus beliebig vielen Bits und Bytes an bliebigen Orten im verfügbaren weitläufigen Speicherraum.

Von ähnlichem Effekt sind auch die anderen Regeln, denen sich der Programmierer in Hochsprachen zu unterwerfen hat. Angeblich ist es sicherer und übersichtlicher, alles in Unterprogrammen zu programmieren, ausser nach festgelegten Regeln nicht im Programmablauf herum zu springen und immer Werte als Parameter zu übergeben und Resulate auch so entgegen zu nehmen. Vergessen Sie einstweilen die meisten dieser Regeln, Assembler braucht auch welche, aber ganz andere, die Sie sich noch dazu alle selbst ausdenken müssen.
Hochsprachenprogrammierer haben noch ein Konzept verinnerlicht, das beim Erlernen von Assembler nur hinderlich ist: die Trennung in diverse Ebenen, in Hardware, Treiber und andere Interfaces. In Assembler ist das alles nicht getrennt, eine Trennung macht auch keinen sonderlichen Sinn, weil Sie die Trennung dauernd wieder durchlöchern müssen, um Ihr Problem optimal lösen zu können.
Da viele Hochsprachenregeln in Mikrocontrollern keinen Sinn machen und das alles so puristisch auch gar nicht geht, erfindet der Hochsprachenprogrammierer gerne allerlei Möglichkeiten, um bei Bedarf diese Restriktionen zu umgehen. In Assembler stellen sich diese Fragen erst gar nicht. Alles ist im direkten Zugriff, jede Speicherzelle verfügbar, nix ist gegen Zugriff abgeschirmt, alles kann beliebig verstellt und kaputt gemacht werden. Die Verantwortung dafür bleibt alleine beim Programmierer, der seinen Grips anstrengen muss, um drohende Konflikte bei Zugriffen zu vermeiden.
Dem fehlenden Schutz steht die totale Freiheit gegenüber. Lösen Sie sich einstweilen von den Fesseln, Sie werden sich andere sinnvolle Fesseln angewöhnen, die Sie vor Unheil bewahren.

Was ist nun genau warum einfacher?

Alles was der Assembler-Programmierer an Worten und Begriffen braucht, steht im Datenblatt des Prozessors: die Instruktions- und die Porttabelle. Fertig. Mit diesen Begrifflichkeiten kann er alles erschlagen. Keine andere Dokumentation ist vonnöten. Wie der Timer gestartet wird (ist "Timer.Start(8)" nun wirklich leichter zu verstehen als "LDI R16,0x02 und OUT TCCR0,R16"), wie er angehalten wird (CLR R16 und OUT TCCR0,R16), wie er auf Null gesetzt wird (CLR R16 und OUT TCCNT0,R16), steht nicht in der Dokumentation irgendeiner Hochsprache mit irgendeiner mehr oder weniger eingängiger Spezialbegrifflichkeit, sondern im Device Databook allein. Ob der Timer 8- oder 16-bittig ist, entscheidet der Programmierer in Assembler selbst, bei Basic der Compiler und bei C vielleicht auch der Programmierer, wenn er genügend mutig ist.

Was ist dann an der Hochsprache leichter, wenn man statt "A = A * B" schreiben muss "MUL R16,R17"? Kaum etwas. Außer A und B sind nicht als Byte definiert. Dann muss der Assemblerprogrammierer überlegen, weil sein Hardware-MUL nur Bytes multipliziert. Wie man 24-bittige Zahlen mit 16-bittigen Zahlen mit und ohne Hardware-Multiplikator multipliziert, kann man lernen oder aus dem Internet abkupfern. Jedenfalls kein Grund, in eine Hochsprache mit ihren Bibliotheken zu flüchten, nur weil man zu faul zum Lernen ist.

Mit Assembler lernt der Programmierer die Hardware kennen, weil er ohne die Ersatzkrücke Compiler auskommen muss, die ihm zwar alles abnimmt, aber damit auch alles aus der Hand nimmt. Als Belohnung für seine intensive Beschäftigung mit der Hardware hat er die auch beliebig im Griff und kann, wenn er will, auch mit einer Baudrate von 45,45 bpm über die serielle Schnittstelle senden und empfangen. Eine Geschwindigkeit, die kein Windowsrechner zulässt, weil die Krücke Treiber halt nun mal nur ganzzahlige Vielfache von 75 kennt, weil frühere Fernschreiber das mit einem Getriebe auch schon hatten und flugs von 75 auf 300 umgeschaltet werden konnten.

Wer Assembler kann, hat ein Gefühl dafür, was der Prozessor kann. Wer dann bei komplexen Aufgabenstellungen zu einer Hochsprache übergeht, hat die Entscheidung rationaler gefällt als wenn er mangels Können erst gar nix anderes kennt und sich mit Work-Arounds für nicht in der Hochsprache implementierte Features die Nacht um die Ohren schlagen muss. Der Assemblerprogrammierer hat zwar viel mehr selbst zu machen, weil ihm die ganzen famosen Bibliotheken fehlen, kann aber über so viel abseitiges Herumgeeiere nur lächeln, weil ihm die ganze Welt des Prozessors unterwürfig zur Verfügung steht.

©2010 by info@avr-asm-tutorial.net