Was hier folgt, ist nichts für Profis und Power-User, die mögen weiterblättern. Ich versuche hier, absolute Neueinsteiger nach und nach mit ein paar Grundinformationen zu versorgen.
Inhaltsverzeichnis
Assembler Einführung für Bascom-User
Wieso Bascom ?
Eine der einfachsten Möglichkeiten, sich an Assembler heranzutasten, ist es, den Bascom-Compiler als Workbench zu benutzen.
Die Vorteile:
- Das Drumherum mit der richtigen Initialisierung, auch der Perpipherie, kann man bequem von Bascom machen lassen, bis man sich halt auskennt.
- Wenn irgendeine Berechnung oder Teil-Funktion nervt oder nicht gleich richtig hinhaut, schreibt man halt doch ein paar Bascom-Statements.
- fürs Erste reicht die Demo-Version allemal
Die Nachteile:
- Gott-weiß-wie komfortabel ist der Bascom-Assembler natürlich nicht, aber es reicht.
- Bei manchen Befehlen ist es nicht klar, ob das ein Assembler oder ein Bascom-Befehl ist. In diesem Fall muß man ein "!" Rufzeichen davor setzen. Man erkennt das aber sofort, denn diese reservierten Bascom-Wort mach er sofort in Fettschrift. Trotzdem aufpassen !
Ein Grund-Programm
Nicht lachen, auch das ist ein Bascom-Programm:
$regfile = "m32def.dat" $asm $end Asm End
Das Programm macht natürlich überhaupt nix. Aber durch die paar Zeilen hat Bascom alle notwendigen Initialisierungen schon erledigt und wir brauchen uns um nichts zu kümmern. Zwischen "$asm" und "$end asm" kann man nun nach Herzenslust irgendwas Assemblermäßiges reinschreiben und mit dem Simulator rumprobieren.
Auch "REGFILE" müßte man nicht hinschreiben, dann gilt eben das, was man in "OPTIONS/COMPILER/CHIP" eingestellt hat.
Der Zentral-Prozessor (CPU)
Das ist der Kollege, dem man mit "Assembler-Instruktionen" davon überzeugen muß, irgendwas zu tun. Ohne den läuft garnix. Der hat als Hilfe einen "Befehlszähler", der immer auf den nächsten Befehl zeigt, der drankommt. Und dann hat er noch eine Reihe "Register", das sind kleine Zwischenspeicher, mit denen er arbeiten kann. Die heissen einfach "R0", "R1",...."R31", also 32 Stück, in jedes paßt genau ein Byte, und ein Byte, das wissen wir, besteht wiederum aus 8 Bits.
Daten-Transfer Operationen I
Bevor wir mit diesen Registern irgendetwas ausprobieren können, müssen wir erstmal gezielt bestimmte Werte reinschreiben können. Sowas heißt eben "Transfer". Da wir ja erst am Anfang sind, reicht uns zum Beispiel:
LDI R24, 14
Damit wird in das Register R24 der Binärwert von "14" reingestellt, das sind die Bits "00001110". Der maximale Wert, da es ja nur ein Byte ist, wäre "255", also "11111111". Für den Befehl "LDI" können wir übrigens leider nur die Register R16 - R31 setzen, das ist so eine Einschränkung von wegen "RISC" Architektur.
MOV R3, R24
Deswegen auch der zweite Befehl "MOV", damit wird im Beispiel der Inhalt von R24 in das Register R3 kopiert. Somit können wir mit maximal zwei Befehlen also jeder beliebige Register von R0 bis R31 mit beliebigen Werten laden. Natürlich gibt es noch eine Menge mehr an Transferbefehlen, aber Listen von Assembler-Befehlen gibt es schon genug, da brauchen wir hier nicht auch noch eine.
Arithmetisch-Logische Operationen
Laden wir mal zwei Register:
LDI R25, 17 LDI R24, 14
Und jetzt die Grund-Befehle, Varianten später:
- Arithmetisch
ADD R25, R24 addieren R25 + R24, Ergebnis nach R25 !SUB R25, R24 subtrahieren
- Logisch
!AND R25, R24 "UND" !OR R25, R24 "ODER" EOR R25, R24 "Exklusiv-ODER"
Das Ergebnis steht immer in Operand-1
Gleich mal ausprobieren
$regfile = "m32def.dat" $asm LDI R25, 17 ' Laden LDI R24, 14 ' Laden ADD R25, R24 'addieren 17 + 14, Ergebnis in R25 LDI R25, 17 'Nachladen, da R25 durch "ADD" ja verändert wurde !SUB R25, R24 'subtrahieren 17 - 14 LDI R25, 17 ' Laden LDI R24, 14 ' Laden !AND R25, R24 ' Es kommt überall dort "1" raus, wo sowohl r25 als auch R24 eine 1 haben LDI R25, 17 ' Laden LDI R24, 14 ' Laden !OR R25, R24 ' Es kommt überall dort "1" raus, wo r25 oder R24 eine 1 haben ' (ODER BEIDE !) LDI R25, 17 ' Laden LDI R24, 14 ' Laden EOR R25, R24 ' Es kommt überall dort "1" raus, wo ENTWEDER r25 oder R24 eine 1 haben ' (ABER NICHT BEIDE !) $end Asm End
Zum Probieren ist das am besten mit dem Simulator. (Register-Fenster öffnen und Einzelschritte)
Ergebnis prüfen
Normalerweise ist es ja nicht so, daß vor solchen Operationen die Rechenwerte direkt geladen werden, sondern die kommen ja von irgendwo aussen her. Und da muß man ja dann anders reagieren, je nachdem, ob die Werte gleich waren, ob r25 größer oder kleiner als r24 war, und so weiter.
Da helfen die "Flags" im Status-Register (SREG). Das ist zwar auch ein normales Byte, nur haben die einzelnen Bits darin eine spezielle Bedeutung und geben eben nähere Auskunft über die gerade abgelaufenen Operation. Nur das Wichtigste:
- ZERO-Bit Es wird automatisch gesetzt, wenn das Ergebnis genau NULL ergeben hat.
- CARRY-Bit Es wird automatisch gesetzt, wenn es einen "Übertrag" gegeben hat
Man kann diese (und noch andere) Flags sehen, wenn man im Simulator auf "µP" drückt.
Z = ZERO C = CARRY
Beispiele:
LDI R25, 17 LDI R24, 14 !SUB R25, R24
Zero & Carry sind nicht gesetzt, denn das Ergebnis ist ungleich NULL, und "17" ist außerdem größer als "14"
LDI R25, 17 LDI R24, 17 !SUB R25, R24
Jetzt ist Zero gesetzt, denn das Ergebnis ist gleich NULL
LDI R25, 12 LDI R24, 44 !SUB R25, R24
Jetzt ist das Carry-Bit gesetzt, denn "12" ist ja kleiner als "44", das Ergebnis ist also negativ, und ein "Übertrag" ist auch aufgetreten.
Vergleichen
"Vergleichen" ist für die ALU (Recheneinheit) das Gleiche wie Subtrahieren (SUB), nur daß das eigentliche Rechenergebnis nirgends hingeschrieben wird und NUR DIE FLAGS gesetzt werden.
CP R25, R24
Verzweigen
Wir haben ja gesagt, es wird verglichen, damit der Rechner je nach Vergleichs- der Rechenergebnis was anderes tut. "Was anderes tun" heißt anderer Code, also muß der "Befehlszähler" einen anderen Wert bekommen, damit der Programmablauf dort fortgesetzt wird. Dazu gibt es natürlich die "unbedingten" Varianten
JMP Zieladresse ' oder RJMP Zieladresse ' das nimmt man, wenn das Ziel in der Nähe ist
Oder eben die "Verzweigung unter bestimmten Bedingungen" (conditional branch)
BRxxx Zieladresse
Für "xxx" (Bedingung) gibt es nun eine ganze Reihe Möglichkeiten. Es gibt im Prinzip für jedes Bit im Status-Register (s.o) eine Abfrage "wenn gesetzt" und "wenn nicht gesetzt".
Die wohl wichtigsten sind die Möglichkeiten, die sich aus dem "ZERO"- und dem "CARRY"-Flag ergeben:
BREQ Zieladresse ' Verzweigen, wenn "GLEICH" (equal) Zero = 1 BRNE Zieladresse ' Verzweigen, wenn "NICHT GLEICH" (not equal) Zero = 0 BRLO Zieladresse ' Verzweigen, wenn "KLEINER" (lower) Carry = 1 BRSH Zieladresse ' Verzweigen, wenn "GLEICH ODER GRÖSSER" (same or higher) Carry = 0
Und, die Überraschung, ausgerechnet sowas Häufiges wie
Verzweigen, wenn "GRÖSSER"
gibt's überhaupt nicht. Nun, dazu müßten ja eigentlich zwei Flags abgefragt werden. "Größer" heißt nämlich CARRY = 0 UND ZERO = 0. Und das ist in der "RISC" Welt nicht drin, da wird gespart.
Beispiel
Lieber gleich ein Beispiel zum Ausprobieren und Festigen, das war ja doch etwas gebündelt. Aber davor gleich noch eins drauf: Eine "Zieladresse" ist der (im ganzen Programm) eindeutige Name eines Befehls (ein "Label"), der in der Zeile ganz links beginnt und mit Doppelpunkt abgeschlossen wird
Flußdiagramm
- Theoretisch sieht das ja so aus:
- Da es aber keiner Programmierspache möglich ist, alternativen Code nebeneinander zu schreiben, muß dieser Teil auf "Spaghetti"-Code umstrukturiert werden.
"Hochsprachen" machen das versteckt im Maschinencode, beim Assembler müssen wir selbst machen. Und natürlich auch "GOTO" (=JMP) verwenden, ein sonst in allen Büchern als "no, no" (=pfui) beschriebener Befehl.
Die Praxis
Programm_Beginn: ' das ist zum Beispiel gleich ein "Label" LDI R25, 12 ' R25 = 12 LDI R24, 44 ' R24 = 44 '-------------------------------------- ' nun der Vergleich '-------------------------------------- CP R25, R24 BREQ Label_1 ' Verzweigen nach "Ziel", wenn R25 = R24 LDI R16, 1 ' das machen wir (zum Beispiel), wenn R25 NICHT= r24 ist RJMP Label_2 'wir müssen unbedingt springen, sonst laufen wir ja ' in den Zweig "ist_gleich" rein Label_1: LDI R16, 0 ' das machen wir (zum Beispiel), wenn R25 = r24 ist '---------------------------- ' da treffen wir uns wieder Label_2: da geht er wieder gemeinsam weiter
Ich kann nur dringend empfehlen, sich mit diesem Beispiel zu beschäftigen und auch mit anderen Werten rumzuprobieren, das "bedingte Verzweigen" in allen Varianten ist das A und O der Programmiererei, beim Assembler eben auch ein bißchen verschärft.
Eine Alternative: Bedingtes "Skip"
Was der AVR noch anbietet, ist eine Reihe von "SKIP IF" Befehlen. Für unseren Registervergleich gibt es aber nur den
CPSE Register, Register
Befehl. Er bedeutet:
"Vergleiche die Register, und wenn die Inhalte gleich sind, überspringe den nächsten Befehl"
Das wird uns das Herumspringen und das Verwenden von Labeln erspart. Allerdings kann immer nur EIN Befehl übersprungen werden
eine Besonderheit hat der Befehl noch: Da er ja Vergleich und Bedingungsabfrage in Einem ist, werden auch keine Flags im Statusregister (SREG) verändert. Das ist praktisch, wenn man diese Flags durch eine andere Operation vorher gesetzt hat, und sie über diesen Vergleichs + Sprung - Befehl darüber-retten will. Das ist aber im Moment schon etwas fortgeschritten.
Kurze Zusammenfassung
- Wir können also beliebige Register mit beliebigen Werten laden,
- Wir können mit diesen Werten rechnen oder sie vergleichen
- Und je nach Vergleichs- oder Rechenergebnis unterschiedlichen Code durchlaufen.
- Man könnte aber auch ein paar Lehren daraus ziehen:
- die Register R16 - R31 braucht man unter Umständen für Zwischenschritte, um Werte in die Register R0 - R15 laden zu können. Man sollte also diese Register nicht zu schnell fest belegen und vollräumen, damit man dafür noch Spielraum behält.
- Auch doch recht simple IF .. ELSE Konstrukte können ein gewisses vorher überlegtes Konzept brauchen, sonst verliert man schnell den Überblick. Ein Blatt Papier und ein Bleistift sind also recht hilfreich. Assembler schreibt man nicht einfach in den Bildschirm rein.
Schleifen
Eigentlich ist das ja nichts speziell Assembler-spezifisches, aber was soll's.
Flußdiagramme
Es gibt zwei Grundmuster für Schleifen (Befehlswiederholungen).
- WHILE "solange Bedingung erfüllt ist, mache was"
- DO...LOOP WHILE "mache was, solange Bedingung erfüllt ist"
Der Unterschied ist wichtig: Bei "WHILE" wird nur was gemacht, wenn die Bedingung schon zutrifft, Bei "DO..WHILE" werden die Befehle auf jeden Fall wenigstens einmal ausgeführt, erst dann wird gecheckt, ob wiederholt werden soll.
Praxis
Theoretisch sieht das ja gut aus, und mit Hochsprachen kann man das auch meist so formulieren. Beim Assembler geht das aber nur so schön übersichtlich, wenn man nur eine einzelne Bedingung hat. Eine einfache Zähl-Schleife in der "WHILE" Version:
$regfile = "m32def.dat" $asm LDI r25, 0 ' R25 = 0 LDI r24, 1 ' R24 = 1 SchleifenBeginn: CPI R25, 12 ' Der Befehl ist neu: vergleiche R25 mit dem festen Wert "12" BREQ SchleifenAusgang ' Wenn R25 = 12, verlassen wir die Schleife ADD R25, R24 ' auf R25 den Wert von R24 draufaddieren RJMP SchleifenBeginn ' und wieder rauf zur Prüfung SchleifenAusgang: ... $end Asm End
Was geschieht, ist klar: R25 beginnt mit Null. Wenn der R25 NICHT= "12", addieren wir "1" auf R25 und wiederholen das Ganze. Wenn R25 = "12", verlassen wir die Schleife.
Nehmen wir aber an, wir hätten zwei Bedingungen (es geht hier nicht um Sinn oder Unsinn der Abfrage):
- WHILE R25 NICHT= "12 UND R24 = "1"
SchleifenBeginn: CPI R25, 12 ' Der Befehl ist neu: vergleiche R25 mit dem festen Wert "12" BREQ SchleifenAusgang ' Wenn R25 = 12, verlassen wir die Schleife CPI R24, 1 ' s.o BRNE SchleifenAusgang ' Wenn R24 NICHT= 1, verlassen wir die Schleife ADD R25, R24 ' auf R25 den Wert von R24 draufaddieren RJMP SchleifenBeginn ' und wieder rauf zur Prüfung SchleifenAusgang:
- WHILE R25 NICHT= "12 ODER R24 < R25
SchleifenBeginn: CPI R25, 12 BREQ R25_ist_12 SchleifenBody: ADD R25, R24 RJMP SchleifenBeginn R25_ist_12: CP R24, R25 BRLO SchleifenBody SchleifenAusgang: ...
Wenn wir da nicht im Kommentar dazuschreiben, worum es geht, kennt sich ein Fremder erst nach einiger Überlegung aus.
Tips
Mehrere Bedingungen in eine UND-ODER Beziehung sind immer fehleranfällig und leicht unübersichtlich
- Als Erstes immer die RICHTIGE (und am besten verständliche) Lösung suchen, und erst dann durch Umformungen die "SCHÖNE" Lösung.
- Also nochmal das obige "ODER" Beispiel, erst in der vollen Grundform
WHILE ( R25 NICHT= "12 ) ODER ( R24 < R25 )
SchleifenBeginn: CPI R25, 12 ' R25 <=> 12 BREQ R25_ist_12 JMP R25_ist_nicht_12 CP R24, R25 BRLO r24_ist_kleiner_r25 JMP r24_ist_nicht_kleiner_r25 ADD R25, R24 ' der "BODY" steht ja fest RJMP SchleifenBeginn ' das ist auch sicher SchleifenAusgang: ' ausgang gibt es (eigentlich) immer ...
Das fehlt was ? Ja, denn jetzt erst sollten wir die Ziele auch hinschreiben
1. Wir machen den "body" immer, wenn r25 nicht gleich 12
also schreiben wir das hin
SchleifenBeginn: CPI R25, 12 ' BREQ R25_ist_12 JMP R25_ist_nicht_12 ' abgehakt CP R24, R25 BRLO r24_ist_kleiner_r25 JMP r24_ist_nicht_kleiner_r25 R25_ist_nicht_12: ' ADD R25, R24 ' RJMP SchleifenBeginn ' SchleifenAusgang: ' ...
2. Wir machen den "body" immer, wenn r24 kleiner als r25
SchleifenBeginn: CPI R25, 12 BREQ R25_ist_12 JMP R25_ist_nicht_12 ' abgehakt CP R24, R25 BRLO r24_ist_kleiner_r25 ' abgehakt JMP r24_ist_nicht_kleiner_r25 ' r24_ist_kleiner_r25: R25_ist_nicht_12: ADD R25, R24 RJMP SchleifenBeginn SchleifenAusgang: ...
Anmerkung: wir können an der selben Stelle beliebig viele Label vergeben
3. Was ist, wenn r25 = 12 ? dann müssen wir die zweite Bedingung prüfen (ist ja ein ODER)
SchleifenBeginn: CPI R25, 12 BREQ R25_ist_12 ' abgehakt JMP R25_ist_nicht_12 ' abgehakt R25_ist_12: CP R24, R25 BRLO r24_ist_kleiner_r25 ' abgehakt JMP r24_ist_nicht_kleiner_r25 ' r24_ist_kleiner_r25: R25_ist_nicht_12: ADD R25, R24 RJMP SchleifenBeginn SchleifenAusgang: ...
4. Bleibt nurmehr "r24 ist nicht kleiner r25". Da geht's offenbar dann hin, wenn KEINE der Bedingungen erfüllt ist, also: raus aus der Schleife
SchleifenBeginn: CPI R25, 12 BREQ R25_ist_12 ' abgehakt JMP R25_ist_nicht_12 ' abgehakt R25_ist_12: CP R24, R25 BRLO r24_ist_kleiner_r25 ' abgehakt JMP r24_ist_nicht_kleiner_r25 ' abgehakt r24_ist_kleiner_r25: R25_ist_nicht_12: ADD R25, R24 RJMP SchleifenBeginn r24_ist_nicht_kleiner_r25: SchleifenAusgang: ...
Jetzt ist das Ganze zwar nicht elegant, aber richtig und leicht nachvollziehbar.
Wenn der Sprungbefehl und das Ziel unmittelbar hintereinander stehen, können wir uns den Sprung sparen. Also bauen wir etwas um, damit das auch so ist:
SchleifenBeginn: CPI R25, 12 BREQ R25_ist_12 ' JMP R25_ist_nicht_12 ' steht jetzt direkt dahinter r24_ist_kleiner_r25: 'den ganzen Block raufgeschoben R25_ist_nicht_12: ADD R25, R24 RJMP SchleifenBeginn R25_ist_12: CP R24, R25 BRLO r24_ist_kleiner_r25 ' JMP r24_ist_nicht_kleiner_r25 ' steht jetzt direkt dahinter r24_ist_nicht_kleiner_r25: SchleifenAusgang: ...
Und kürzen:
SchleifenBeginn: CPI R25, 12 BREQ R25_ist_12 r24_ist_kleiner_r25: ADD R25, R24 RJMP SchleifenBeginn R25_ist_12: CP R24, R25 BRLO r24_ist_kleiner_r25 SchleifenAusgang: ...
Ob das nun ein "WHILE" oder ein "DO..WHILE" wird, hängt nurmehr davon ab, wo wir zu Beginn in die Befehlsfolge reinspringen.
- Von oben weg, wie es dort steht, ist es eine "WHILE" Schleife
- Eine "DO..WHILE" Schleife (Bedingung am Ende prüfen) wird es, wenn wir zuerst mit dem "Body" beginnen. Also
JMP r24_ist_kleiner_r25 ' Erst die Aktion, DANN die Bedingung prüfen SchleifenBeginn: CPI R25, 12 BREQ R25_ist_12 r24_ist_kleiner_r25: ADD R25, R24 RJMP SchleifenBeginn R25_ist_12: CP R24, R25 BRLO r24_ist_kleiner_r25 SchleifenAusgang: ...
Ist doch praktisch ?
Assembler-mäßig ist das nun ok und erträglich. Aber mit dem theoretischen WHILE-Flußdiagramm hat das nun nicht mehr viel gemeinsam.
Weg vom Simulator auf den µC
Jetzt wird's Zeit, die ersten Schritte in die AVR-Realität zu machen, immer Simulator ist ja langweilig, fast so, als würden wir im Trockenen schwimmen lernen müssen.
Bis jetzt haben wir vom Bascom ja nur die äussere Programmhülle verwendet, jetzt soll er doch auch wirklich was tun.
Datenaustausch mit BasCom
Das funktioniert über Datenfelder, die wir irgendwie definieren müssen. Weil's so einfach ist, lassen wir das erstmal Bascom übernehmen. Das ist kein Rückschritt auf dem Weg zum Assemblerprogrammierer, denn das Definieren von Daten ist ja nichts anderes, als Felderen im SRAM (also im eigentlichen Arbeitsspeicher) Namen zuzuweisen. Über den Namen kann man diese Felder dann auch im Assembler ansprechen.
- Bascom-->Assembler
Damit Bascom ein Feld für uns definiert, sagen wir einfach (aber außerhalb der "$asm" / "$end asm" Bereiches)
DIM Meinfeld AS BYTE
Im Bascom, das weiß der Leser vielleicht ja schon, kann man da Werte reinschreiben, so wie wir das mit dem "LDI" Befehl bei Registern gemacht haben
Meinfeld = 85
Jetzt steht an irgendeiner Speicherstelle (egal wo, wir sprechen es eh' nur über den Namen an) der Binärwert von 85 (Hexadezimal &H55 oder in Bits &B01010101). Dieses Feld können wir aber nun auch mit dem Assembler lesen:
$regfile = "m32def.dat" Dim Meinfeld As Byte ' definition Meinfeld = 85 'Wertzuweisung $asm lds r24, {Meinfeld} '(das sind zwei geschwungene Klammern) ' das ist wieder ein neuer Befehl: "LDS" $end Asm End
"LDS" lädt in ein beliebiges Register den Wert von der Speicherstelle, die "Meinfeld" heißt.
- Assembler-->Bascom
Das geht auch umgekehrt. Wenn wir den Bascomteil noch etwas vervollständigen
$regfile = "m32def.dat" $crystal = 8000000 ' der Quartz, den wir verwenden $baud = 9600 ' die Baudrate für das Terminal ' dadurch macht Bascom alle Einstellungen, die wir für das ' Terminal brauchen Dim Meinfeld As Byte ' definition $asm LDI R24, 85 STS {Meinfeld}, R24 ' "STS" ist das Gegenstück zu "LDS", also vom Register zum Speicher $end Asm PRINT str(Meinfeld) ' Und schon gibt uns Bascom den Wert (dezimal) auf dem Terminal aus End
- Das ist der Vorteil den wir haben, wenn wir Assembler mit Bascom anfangen und nicht mit einem "richtigen" Assembler wie das AVR-Studio. Denn das Gefummel mit der Terminalausgabe erledigt alles Bascom, sonst müßten wir es selbst erst wo abschreiben oder lernen, bis wir auch nur einen Pieps auf dem Terminal sehen.
- Wenn es interessiert: Das, was wir zuletzt im Assembler geschrieben haben, ist auch genau das, was Bascom an Maschinencode produziert, wenn wir
Meinfeld = 85
hinschreiben
- Bascom-->Assembler-->Bascom
Noch immer können wir nur mit fest einprogrammierten Werten arbeiten, d.h. für was anderes als "85" müssen wir ändern, übersetzen und brennen.
Wir wollen nun versuchen, etwas über das Terminal einzugeben, bearbeiten und dann wieder ausgeben.
$regfile = "m32def.dat" $crystal = 8000000 ' der Quartz, den wir verwenden $baud = 9600 ' die Baudrate für das Terminal Dim Meinfeld As Byte DO Meinfeld = INKEY() IF Meinfeld <> 0 THEN $asm lds r24, {Meinfeld} '(das sind zwei geschwungene Klammern) ' Verarbeitung '............ STS {Meinfeld}, R24 $end Asm PRINT str(Meinfeld) END IF LOOP End