Pfad:
Home =>
AVR-Überblick =>
Programmiertechniken => Hochsprache vs. Assembler
(This page in English:
)
Programmiertechnik für Anfänger in AVR Assemblersprache
Die wesentlichen Unterschiede zwischen Hochsprachen und Assembler
Die beliebte Frage des Anfängers, mit was er denn bitte schön
anfangen sollte, mit C oder mit asm, wird ihm heute meistens abgenommen:
der Arduino kann nur C und hat etliche Hürden vor die Verwendung
von asm eingebaut. Also fängt er halt mit C an. Was macht man also,
wenn man denn schon C kann? Man lernt halt was Neues dazu.
Wenn man in Assembler einsteigt, kann man meistens schon andere
Programmiersprachen auswendig. Das prägt: man fragt sich dauernd,
warum das in Assembler alles so ganz anders geht als in der bislang
gewohnten Sprachumgebung.
Für denjenigen, der sich beim Lernen von Assembler das immer
wieder fragt, ist dieses Dokument geschrieben. Es zeigt an einem
Beispiel, wo die wichtigsten Unterschiede zwischen Hochsprachen
und AVR-Assembler liegen, zeigt aber auch Gemeinsamkeiten auf.
Das soll ein wenig beim Lernen helfen und Lernhürden
überwinden helfen.
Bei den Hochsprachen verwende ich hier in den Beispielen Pascal,
weil ich das gut behersche. Andere, wie C, Java, VBA oder PHP,
funktionieren ganz ähnlich, nur halt mit { und } statt mit
begin und end;. Und auch die Unterschiede zu Assembler sind
bei allen Sprachen grundsätzlich diesselben.
Das Beispiel: eine Uhr mit Sekunden-Ticker
Die Beispiel-Aufgabe: eine Uhr mit Sekunden, Minuten und Stunden
soll gestellt und um eine Sekunde erhöht werden. Die Uhr soll
getestet werden, ob sie richtig geht und nach einem Tag wieder
bei Null ankommt. Die Uhrzeit soll auch in eine Zeichenkette
verwandelt werden, die so aussieht: "01:23:45".
Zuerst zeige ich, wie das in Pascal geht. Dann kommt das Ganze in
Assembler. Und Du wirst merken, wo genau die Unterschiede sind und
warum diese Unterschiede so sind wie sie nun mal sind.
1 Die Aufgabe in Pascal
1.1 Variablendefinitionen in Pascal
Die Variablen für die Zeit könnte man in Pascal so definieren:
Var bSek,bMin,bStd: Byte;
Merke: in vielen Hochsprachen geht nix ohne solche Ankündigungen
an den Compiler, sei es VAR, DIM oder was weiß ich nicht alles.
Manche kommen aber auch ohne das aus und machen es sich selber.
Soweit so einfach. Was fängt der Pascal-Compiler nun damit an?
Er legt irgendwo im Speicher drei Bytes an. Nein, nicht ganz genau:
er legt nur drei Adressen fest, unter denen er später die
Sekunden, Minuten und Stunden ansprechen möchte. In den so
angelegten Speicherzellen steht noch irgendwas von vorher drin, es
kommt erst später etwas rein.
Physisch ist bis jetzt noch gar nichts passiert. In den adressierten
Speicherzellen steht nur Müll drin. In der vom Compiler
produzierten ausführbaren Datei steht auch noch gar nix drin,
das VAR wird vom Compiler einfach verschluckt und bleibt erst mal
folgenlos.
1.2 Uhr stellen in Pascal
Aber jetzt geht es endlich los: wir schreiben die Uhr-Variablen.
Das geschehe in einer im Programm später aufgerufenen Prozedur
zum Rücksetzen oder Stellen der Uhr. Die könnte dann zum
Beispiel so aussehen:
Procedure UhrStellen(Std,Min,Sek:Byte);
begin
bSek:=Sek;
bMin:=Min;
bStd:=Std;
end;
Und im späteren würde die Uhr dann mit
{ Setzt die Uhr zurueck }
Procedure UhrRuecksetzen;
begin
UhrStellen(0,0,0);
end;
{ Stellt die Uhr auf eine feste Startzeit }
Procedure UhrStartzeit;
begin
UhrStellen(1,23,44);
end;
rückgesetzt oder auf eine vorgegebene Uhrzeit eingestellt.
Damit stünde dann im Speicher an den Orten bSek, bMin und
bStd die tatsächlich rückgesetzte oder die
voreingestellte Uhrzeit herum.
Endlich ist mal was passiert.
1.3 Uhr um eine Sekunde weiterdrehen in Pascal
Wenn wir nun die Uhr um eine Sekunde weiterdrehen wollen, dann
brauchen wir dafür eine Prozedur UhrTick. Wenn wir um
00:00:00 noch einen Tageswechsel veranlassen wollen, muss uns
die Tickprozedur noch einen Wahrheitswert zurückgeben
und wird zu einer Function mit einem Rückgabewert:
Function UhrTick:Boolean;
begin
Result:=False;
Inc(bSek);
If bSek >= 60 Then
begin
bSek:=0;
Inc(bMin);
If bMin >= 60 Then
begin
bMin:=0;
Inc(bStd);
If bStd >= 24 Then
begin
bStd:=0;
Result:=True;
end;
end;
end;
end;
Der Rückgabewert "Result" kommt nun bei jedem
Aufruf mit einem Wahrheitswert False oder True zurück
und kann dann weiterverwendet werden.
1.4 Uhr testen in Pascal
Um sicher zu gehen, dass die Uhr auch korrekt funktioniert,
müssen wir die Uhr auf Null setzen und die Tickroutine
60*60*24 = 86.400 mal aufrufen. Danach sollte die Uhr wieder
auf Null sein. Wir könnten die Uhr auch so lange
aufrufen, bis ein True zurückkommt. In Pascal ginge
das dann so:
Procedure UhrTest;
Var n:LongInt;
begin
UhrRuecksetzen;
n:=0;
Repeat Inc(n) Until UhrTick;
Writeln(n);
end;
Was da nun der Compiler macht ist erstens mal eine mehr-bytige
Variable mit Namen "n" zu definieren. Die ist nötig,
weil die 86.400 weder in ein 8-bittiges Byte noch in ein
16-bittiges Wort passen werden. Mit LongInt wird dann genügend
viel zählbar. Wenn es noch höher würde, müssten
wir einen INT64 für n nehmen, das höchste der Gefühle
in Pascal.
Und hier stoßen wir auf Ähnlichkeiten und Unterschiede
zu Assembler. Auch der AVR-Assembler ist mit mehr als 8 Bit langen
Zahlen erst mal überfordert. Aber wenn wir ihm erlauben, weitere
Speicherstellen dazu zu nehmen, können wir auch große
Zahlen handhaben. Da so ein kleiner AVR alleine schon 32 Register
hat, kann er so bis zu 256 Bit lange Zahlen. Das kann so kein Pascal,
auch kein 64-Bit-Prozessor. Und wenn es noch mehr sein muss, nimmt
der Assembler-Programmierer halt noch das SRAM dazu. Gigantisch!
Wieder kümmert sich nur der Compiler darum, wo genau
nun die vielen Bytes von n herumliegen, den Programmierer kümmert
das nicht weiter. Vermutlich wird n irgendwo auf dem Stapel herumliegen,
wir wissen es nicht.
Mit dem Repeat zählen wir die Sekunden, bis der Tag zu Ende
ist. Er sollte mit einem n von 86.400 enden, das uns die Kommandozeile
mit dem Writeln dann auch anzeigen wird.
1.5 Umwandlung der Uhrzeit in eine Zeichenkette in Pascal
Wenn wir jetzt noch die Uhrzeit in eine Zeichenkette umwandeln
wollen, müssen wir führende Nullen vor die Sekunden-,
Minuten- und Stundenwerte schreiben. Da das drei Mal gemacht
werden muss, machen wir das mit einer Funktion, die empfangene
Bytes mit führenden Nullen als String zurückliefert.
Das sähe dann als Quellcode in Pascal so aus:
Function ByteInStr(b:Byte):String;
begin
If b < 10 Then
Result:='0'+IntToStr(b) else
Result:=IntToStr(b);
end;
Function ZeitInStr:String;
begin
Result:='Zeit = '+ByteInStr(bStd)+':'+ByteInStr(bMin)+':'+ByteInStr(bSek);
end;
Wenn wir nun irgendwo "Writeln(ZeitInStr);" aufrufen,
kriegen wir die Uhrzeit genau so wie sie soll auch auf die
Kommandozeile zurückgeliefert und angezeigt.
Damit hätten wir alles zusammen, was man so braucht, um die
Uhrzeit zu setzen, ticken zu lassen, zu testen und sie auf dem Bildschirm
in ansprechender Form auszugeben. Und das in der Hochsprache Pascal.
2 Das Ganze in Assembler
Für alle genannten Operationen gibt es in Assembler analoge
Konstruktionen. Allerdings hat es in Assembler noch einige
Überraschungen.
2.1 Zwei Orte für die Zeit in Assembler
Wie teilt man dem Assembler mit, dass er bitte schön drei
Speicherplätze für die Nutzung als Zeit bereitlegen
möge?
So ein AVR bietet zwei Möglichkeiten, um diese zu platzieren:
- in dreien seiner insgesamt 32 Register, oder
- in seinem üppigen statischen RAM, auch SRAM genannt.
Das ist jetzt schon ein gewaltiger Unterschied zu Pascal: wir sind
als Assembler-Programmierer verpflichtet, den genauen Ort für
alles und jedes angeben und festlegen zu müssen. Der Assembler
macht so was, im Gegensatz zum Pascal-Compiler, nicht von sich aus:
wir müssen ihm mitteilen, wo er bitte schön alles hintun
soll.
Und das zwingt den Programmierer eben auch zu Entscheidungen: hat er
freie Register zur Genüge, wird er Möglichkeit 1 nehmen.
Wenn nicht, halt 2. Wenn er mit 1 mal angefangen hat und seine
Mega-Maschine immer größer und Register-hungriger wurde,
muss er irgendwann die drei Register für Wichtigeres freischaufeln
und die ganze Uhrzeit-Mimik, die nur in jeder Sekunde ein einziges
Mal tatsächlich verwendet wird, in das SRAM verlegen. Wir sehen:
die Entscheidungsfreiheit des Assembler-Programmierers hat zwar den
Nachteil, dass er sich um was kümmern muss, das der
Pascal-Programmierer nicht mal kennt, dass er aber auch alle Dinge
in seinem Griff behält und optimieren kann.
Entsprechend sehen die beiden Möglichkeiten etwas unterschiedlich
aus. Zuerst die Variante mit den Registern:
.def rSek = R17
.def rMin = R18
.def rStd = R19
Das macht jetzt dasselbe wie die Variablendefinition in der Hochsprache:
es legt einfach drei Namen fest. Es sagt aber auch, wohin die Uhrzeit
genau gelegt werden soll: in drei Register. Hier sind es die drei
Register R17, R18 und R19.
Und warum nicht R0, R1 und R2, am Anfang des Registerparks, den so ein
AVR (außer den ATtiny4/5/9/10) nun mal hat? Nun, das weiß
der Assembler-Programmierer schon aus dem Kopf, dass er dort weder
die "LDI"-Instruktion zum Stellen der Uhr noch die
"CPI"-Instruktion zum Feststellen verwenden könnte, ob
die Uhr schon 60 Sekunden oder Minuten oder auch 24 Stunden erreicht
hat. Wenn er die Uhrzeit in R0/R1/R2 verlegen will, braucht er jedesmal
zwei Instruktionen, um sie zu stellen oder vergleichen zu können.
Und der Assembler-Programmierer hasst nichts so sehr wie unnütze
Instruktionen, die er einfach mit R17/R18/R19 vermeiden kann: die
können LDI und CPI ohne weiteres.
Wie macht der Pascal-Compiler das? Wir wissen es nicht, und nur ein
tiefer Blick in den vom Compiler produzierten Assembler-Code könnte
uns das offenbaren. Wer's mag, kann es herausfinden. Man muss es nicht.
Nicht so der Assembler: der will es vom Programmierer ganz genau wissen.
Der selbst muss die AVR-Instruktion genau angeben, die die Uhr stellt
oder die Sekunden, Minuten oder Stunden mit irgendwas vergleicht. Und
sich höchstselbst um etwas kümmern, das der
Pascal-Programmierer nicht mal kennt.
Wie beim "Var" auch sind auch beim .def dies nur Mitteilungen
an den Assembler, damit ist noch nix geschrieben, nur Platz definiert
für die Uhrzeit.
Soll das Ganze in das SRAM, dann sähe das so aus:
.dseg
.org SRAM_START
sSek:
.byte 1
sMin:
.byte 1
sStd:
.byte 1
Die drei Label sSek, sMin und sStd haben jetzt in den Interna des
Assemblers Adressen. Assemblieren wir das mit gavrasm mit der Option
-s (oder mit avr_sim, in dem ein gavrasm werkelt) und schauen uns
die Symbolliste im produzierten Assembler-Listing an, dann sähen
wir das hier (andere Assembler zeigen einem das nicht!):
List of symbols:
Type nDef nUsed Decimalval Hexval Name
T 1 1 19 13 ATTINY13
L 1 0 96 60 SSEK
L 1 0 97 61 SMIN
L 1 0 98 62 SSTD
Die drei Labels im SRAM (Typ L) haben die Adressen 96, 97 und 98.
Wenn Du statt des Tiny13 einen großen ATmega verwendest, liegen
die drei halt woanders, weil bei denen das SRAM woanders beginnt.
Auch hier passiert mit den Speicherzellen im SRAM, 96 bis 98, rein
gar nix, wie beim "Var" auch. Der Assembler weiß jetzt
aber, wo "sSek", "sMin" und "sStd" liegen,
wo deren Adressen sind.
Damit hätten wir schon einen ganz gehörigen Unterschied: in
Assembler gibt der Programmierer den Ort, an dem irgendwas passieren soll,
ganz genau und schon in den ersten Zeilen des Codes an. Bei Hochsprachen
interessiert dieser Ort gar nicht, nur der Compiler weiß ihn und
kümmert sich alleine drum.
Das sind in Assembler im SRAM jetzt mehr als doppelt so viele Zeilen
als bei der Platzierung in Registern. Aber auch immer dasselbe, nur
halt woanders (und in Pascal total intransparent). Passiert ist bis
jetzt noch gar nix, nur drei Namen sind vergeben.
Rücksetzen der Uhr in Assembler
Auch beim Rücksetzen der Uhr und bei ihrem Stellen sind nun zwei
verschiedene Varianten nötig. Zuerst das Rücksetzen mit den
drei Registern:
; Uhr neu starten
UhrStartReg:
clr rSek
clr rMin
clr rStd
ret
Aber selbst dafür gibt es noch eine weitere Möglichkeit, die
genau dasselbe tut:
; Uhr neu starten
UhrStartReg:
ldi rSek,0
ldi rMin,0
ldi rStd,0
ret
Mach doch, was Du willst.
Nun können wir mit einem beherzten "rcall UhrStartReg" an
irgendeiner Stelle des Programms die Uhr in Registern auf Null stellen.
Aber gemach: was in Hochsprachen ganz automatisch gemacht wird
(nämlich eine Stapelverwaltung), muss der Assembler-Programmierer
erst dem Chip mühsam beibringen: den Stapel einzurichten.
Start:
; Init des Stapels
.ifdef SPH
ldi R16,High(RAMEND)
out SPH,R16
.endif
ldi R16,Low(RAMEND)
out SPL,R16
Das setzt den 8- oder 16-Bit-Stapelzeiger auf das Ende des SRAMs. Wenn
wir nun "rcall UhrStartReg" aufrufen, dann legt er die
aktuelle Ausführungsadresse auf diesem Stapel ab und kehrt bei
dem "ret" dorthin auch wieder zurück.
Im Unterschied zum Hochsprachenprogrammierer muss sich der
Assemblerprogrammierer hiermit um den Stapel höchstselbst
kümmern. Macht er das nicht, weil er es nicht weiß oder
vergisst, dann passieren im programmierten Chip ganz eigenartige
Dinge, jedenfalls funktioniert rein gar nix wie es soll. Der
Assembler-Programmierer hat die Pflicht, zu entscheiden, ob mit
Stapel gearbeitet werden soll oder nicht. Kein Assembler wird ihn
irgendwie warnen, wenn er das nicht tut. Der Assembler behandelt
den Programmierer nicht als Kind: "wenn er keinen Stapel
einrichtet, wird er schon wissen, was er tut, da rede ich ihm gar
nicht drein!"
Ein Vorteil hat der Assembler-Programmierer: er kann jetzt die mit dem
Namen "UhrStartReg:" benannte Prozedur nun hinschreiben, wo
er möchte (nur nicht an den Anfang des Programmspeichers oder
mehr als 2-Kilo-Worte entfernt vom rjmp-Aufruf). Viele Hochsprachen
verlangen, dass verwendete Prozeduren und Funktionen immer erst
definiert werden (oder mit FORWARD deklariert sind), bevor sie im
Programm verwendet werden können. Da Assembler immer zwei
Durchläufe durchführen (two-pass-assembler), braucht man
solche Regeln hier nicht beachten.
Hier noch die Assembler-Formulierung für das Nullsetzen der
Zeit, wenn die im SRAM platziert ist:
UhrStartSram:
ldi R16,0
sts sSek,R16
sts sMin,R16
sts sStd,R16
ret
Auch nicht sehr viel anders, aber anders. Statt dem LDI jetzt ein
STS (STore in Sram), aber mit zwei Parametern: der Adresse und dem
zu setzenden Wert in einem Register (R16).
2.2 Uhr stellen in Assembler
Schon ein wenig interessanter und vielfältiger sind die
Möglichkeiten, die Uhr zu Beginn auf eine feste Zeit einzustellen.
Am einfachsten ist es, einfach die Register mit einem festen Wert
zu beschreiben:
; Uhr in Registern stellen auf 01:23:44
UhrStellenReg:
ldi rSek,44
ldi rMin,23
ldi rStd,1
ret
Bei der Variante mit SRAM geht das dann so:
; Uhr im SRAM stellen auf 01:23:44
UhrStellenSram:
ldi R16,44
sts sSek,R16
ldi R16,23
sts sMin,R16
ldi R16,1
sts sStd,R16
ret
So, jetzt soll die Uhr morgen aber mit 20:00:01 zu
Laufen beginnen (eine Schrecksekunde nach Tagesschau).
Jetzt sucht sich der Assembler-Programmierer mühsam
alle Stellen im Programm zusammen, in denen rSek/sSek,
rMin/sMin und rStd/sStd vorkommen und schreibt dort
die neuen Zeiten dahin. Weil die aber sehr oft vorkommen,
findet er auch viele Vorkommen, die rein gar nix mit
Uhr-Stellen zu tun haben.
Um das zu vermeiden, schreibt er stattdessen in den
Kopf seines Quellcodes die folgenden drei Zeilen:
.equ cUhrStartStd = 20
.equ cUhrStartMin = 0
.equ cUhrStartSek = 1
Anstelle von "ldi rStd,1" schreibt er nun
"ldi rStd,cUhrStartStd", und braucht sich
fürderhin nur noch um die Konstanten im Kopf zu
kümmern.
Selbiges würde er in in Pascal mit den Zeilen
Const
cUhrStartStd = 20;
cUhrStartMin = 0;
cUhrStartSek = 1;
und mit dem Aufruf
"UhrStellen(cUhrStartStd,cUhrStartMin,cUhrStartSek)
auch erreichen.
Folglich: nur um Nuancen unterschiedlich. Nur, dass man
".equ Name,Zahl" überall im ganzen Quelltext
und "Const" besser nur im Kopf der Pascal-Quelle
schreiben sollte. Und ".equ Name,Zahl" kann man
sogar hinter die Zeilen schreiben, in denen Name schon
verwendet wird, ohne dass man das, wie in Pascal nötig
vorher als FORWARD deklariert.
Und wo bleiben jetzt die Aufrufparameter von
"UhrStellen", die in Pascal angeben, auf welchen
Wert die Uhr gestellt werden soll? Hier kommt dann mal
wieder ein echter Unterschied: Aufrufparameter gibt es in
Assembler rein gar nicht. Dafür gibt es in Assembler
immer und überall Zugriff auf alles! So was wie
übergebene Parameter, mit denen nur die aufgerufene
Prozedur oder Funktion etwas anfangen kann, die aber
ansonsten unsichtbar sind, gibt es in Assembler nicht.
Wenn ein aufgerufenes Unterprogramm in Assembler was
kriegen soll, muss das irgendwo in Registern, im SRAM
oder auch in irgendwelchen Portregistern schon rumstehen.
Wo das steht, ist in Assembler völlig egal: das
Unterprogramm holt es sich da ab, wo es herumsteht.
Was der Compiler so alles veranstalten muss, wenn er Aufrufparameter
an eine Prozedur oder Funktion übergeben muss, ist dem Assembler
herzlich egal und wurscht: so was tut man nicht und kann es auch gar
nicht. Und verschwendet auch keine Zeit dafür.
Dafür ist alles in Assembler überall und immer PUBLIC und
GLOBAL. Diese beiden Worte, auf die der Hochsprachen-Programmierer
immerzu fixiert starrt, weil sie darüber entscheiden, ob es geht
oder nicht geht, kennt der Assembler-Programmierer gar nicht. Er kann
jede Speicherzelle immer und von überall her kaputtschreiben.
Nix ist irgendwie geschützt, alles ist immer möglich. Der
Preis dieser Freiheit ist: Spagehtti-Code und von irgendwoher hat
da irgendwas den Timer abgestellt. Schutz spielt sich im Kopf des
Assembler-Programmierers ab, kein Compiler nimmt ihm irgendwas
davon ab: der Assembler lässt alles zu, verhindert nichts,
lässt selbst den übelsten Unsinn machen. It's all up
to you.
Übergabewerte an aufgerufene Unterprogramme machen also in
Assembler keinerlei Sinn, weil sowieso alles zugänglich ist.
Selbiges gilt auch für die Resultate von Funktionen,
wie das Beispiel der Tick-Routine in Assembler im nächsten
Kapitel zeigt.
2.3 Die Tick-Routine in Assembler
So sähe die Tick-Routine in Assembler aus, wenn die Uhr
in Registern untergebracht ist:
Tick:
inc rSek
cpi rSek,60
brcs TickRet ; Springe wenn kleiner 60
clr rSek
inc rMin
cpi rMin,60
brcs TickRet ; Springe wenn kleiner 60
clr rMin
inc rStd
cpi rStd,24
brcs TickRet ; Springe wenn kleiner 24
clr rStd
TickRet:
ret
"inc" erhöht den Registerinhalt um Eins,
"cpi" vergleicht den Registerinhalt mit 60 oder 24,
und "brcs" verzweigt (branch), wenn die Carry-Flagge
des CPU-Statusregisters gesetzt ist (BRanch on Carry Set).
Sind die 60 Sekunden erreicht, wird rSek auf Null gesetzt
(CLeaR) und die Minuten erhöht. Und so weiter bis der
Tag um ist.
Und wo bleibt nun das "Result" der Funktion? Nun, es
gibt sogar zwei davon. Beide befinden sich im Statusregister
SREG und sind ein Bit darin:
- die Z-Flagge: sie gibt an, ob bei der letzten Addition,
Subtraktion oder einem Vergleich (CPI) Gleichheit war. Da
bei allen Operationen, bei denen die Berechnung vorzeitig
verlassen wurde (mit brcs), der Vergleich mit UNGLEICH
ausgegangen war (weil der Vergleichswert ja noch nicht
erreicht wurde), ist die Z-Flagge nicht gesetzt. Erst
beim Vergleich mit 24 war sie gesetzt, wenn der Tag zu
Ende ist. Auch beim "clr rStd" wird die
Z-Flagge gesetzt. Nur wenn der Tag zu Ende ist, kehrt
die Routine mit gesetzter Z-Flagge zurück.
- die C-Flagge: sie ist dann gesetzt, wenn beim Vergleich
die Zahl, mit der verglichen wurde, größer
war. So ergibt "cpi rSek,60" immer ein
gesetztes Carry, solange rSek noch kleiner als 60 ist.
Mit brcs verzweigt dann das Programm mit gesetzter
C-Flagge zur Rückkehr. Nicht, wenn beim Vergleich
"cpi rStd,24" der Tag um ist. Auch die
Instruktion "clr rStd" löscht die
C-Flagge. Nur wenn der Tag zu Ende ist, kehrt die
Routine mit gelöschter C-Flagge zurück.
Das Tagesende kann bei der Rückkehr von der Tick-Routine
also sowohl an einer gesetzten Z-Flagge als auch an einer
gelöschten C-Flagge erkannt werden. Beide können
verwendet werden, um festzustellen, ob das Datum um einen Tag
erhöht werden muss. Mit brne (Z-Flagge gelöscht)
oder mit brcs (C-Flagge gesetzt) kann die Datumsanpassung
einfach übersprungen werden.
Auch hier dasselbe Prinzip: bei der Rückkehr von einer
Routine können von dieser beliebige Resultate
übergeben werden. Ob es ein Bit, zwei Bits, ein Byte,
zwei Bytes oder 50 Bytes oder wie in diesem Fall eine
Flagge im Statusregister, kann sich der Assembler-Programmierer
selbst aussuchen. Da sowieso alles PUBLIC und GLOBAL ist,
kann das Ergebnis auch stehen wo es will. Hier war der
Übergeber das SREG, das zwei Flaggen zurückgab.
Das SREG kann von überall her und zu jeder Zeit gelesen
und zu weiteren Sprungentscheidungen herangezogen werden.
Fazit: In Assembler vergiss das Konzept mit der Übergabe
von Parametern beim Aufruf von Prozeduren und Funktionen,
einschließlich der Rückgabe von Resultaten. Das
Konzept macht in Assembler keinen Sinn, weil alle Programmteile
alles dürfen und weil der Programmierer überall und
jederzeit alleine darüber entscheidet, was mit welchem
Register, welcher SRAM-Speicherzelle oder mit welcher
EEPROM-Zelle wann geschehen soll.
2.4 Die Testroutine in Assembler
Das Pendant zur "UhrTest"-Routine in Pascal ist
in Assembler etwas länger, weil wir ja immer angeben
müssen, wo alles hinkommen soll und weil wir noch den
Stapel einrichten müssen.
Die Testroutine zählt, nach wievielen Aufrufen ein Tag
zu Ende ist. In Assembler geht das so (mit der Zeit in
Registern):
.def rCnt0 = R0 ; Zaehler, Byte 0
.def rCnt1 = R1 ; dto., Byte 1
.def rCnt2 = R2 ; dto., Byte 2
.def rmp = R16 ; Vielzweckregister
.def rSek = R17 ; Sekunden
.def rMin = R18 ; Minuten
.def rStd = R19 ; Stunden
;
Start:
.ifdef SPH
ldi rmp,High(RAMEND)
out SPH,rmp
.endif
ldi rmp,Low(RAMEND)
out SPL,rmp
; Uhr auf Null
StartUhr:
rcall UhrStart
clr rCnt0 ; Zaehler auf Null
clr rCnt1
clr rCnt2
Loop:
inc rCnt0 ; Tick zaehlen
brne Tick
inc rCnt1
brne Tick
inc rCnt2
Tick:
rcall UhrTick ; Eine Sekunde aufwaerts
brcs Loop
nop ; Hier Breakpoint setzen
rjmp StartUhr
;
; Unterprogramme
; Uhr starten
UhrStart:
clr rSek
clr rMin
clr rStd
ret
; Uhr um eine Sekunde vorwärts
UhrTick:
inc rSek
cpi rSek,60
brcs UhrTickEnd
clr rSek
inc rMin
cpi rMin,60
brcs UhrTickEnd
clr rMin
inc rStd
cpi rStd,24
brcs UhrTickEnd
clr rStd
UhrtTickEnd:
ret
Das ist nun auch ziemlich straight-forward. Die beiden Aufrufe
von "UhrStart" und "UhrTick" sind fast wie
in der Hochsprache, nur die Verwendung der Flaggen Z und C in
Assembler gibt es in Hochsprachen rein gar nicht: Statusregister
der CPU sind in Hochsprachen streng verbotene Gebiete, nur
Assembler kann und darf das - und benutzt so was wirklich oft.
2.5 Die Zeit als Zeichenkette in Assembler
Auch bei der Umwandlung der drei Variablen in eine Zeichenkette -
beim Pascal war das eine Funktion, die einen String zurückgab -
laufen wir in eine Besonderheit von AVR-Assembler: da eine Funktion
nix (oder alles) zurückgibt, muss der String schon an eine
Stelle geschrieben werden, die der AVR halt so hat und von wo sie
das Hauptprogramm dann abholen kann.
Und noch was: da der AVR auch rein gar kein Kommandozeilenfenster
kennt (dafür ist er einfach zu klein), muss schon ein Bereich im
SRAM dafür herhalten, den String zu beherbigen. Und im
Simulator können wir uns dann auch angucken, ob der String auch
wirklich die Uhrzeit zeigt. Sonst müssten wir dem kleinen Kerl
eine LCD oder eine serielle Schnittstelle anbauen, damit er
darüber die Uhrzeit in die große weite Welt hinausposaunen
kann.
Auch das Assembler-Programm beginnt mit einer Routine, die in einem
Register (hier: rmp) übergebene Bytes mit führenden Nullen
in eine zweistellige Zeichenfolge umwandelt. Diese Routine zieht
solange zehn von der Zahl ab, bis ein Überlauf auftritt (brcc
bedeutet BRanch on Carry Clear). Der Zähler, hier rmp2, beginnt
bei dem ASCII-Zeichen '0' - 1, da rmp2 schon vor dem ersten Abziehen
ausgeführt wird. Falls das schon beim ersten Abziehen ein Carry
ergibt, dann steht korrekterweise eine ASCII-'0' im Register rmp2.
Auch das macht jetzt wieder mal einen Unterschied zu Hochsprache: der
Assembler-Programmierer ist ständig am Code-Optimieren und spart
Instruktionen, wo er nur kann. Hätte er den Zähler einfach auf
Null gesetzt, dann müsste er am Ende Eins abziehen und auch noch
die ASCII-'0' dazu zählen. Diese eine Instruktion hat er sich
gespart, indem er den Zähler nicht auf Null, sondern auf ASCII-'0'
minus Eins setzt. Den Pascal-Programmierer juckt so was rein gar nicht,
er denkt nicht mal darüber nach. Allein das Umpacken der erzeugten
ASCII-Zeichen in einen String (auf dem Stapel, die Adresse ist nur dem
Compiler bekannt) dauert ja auch schon länger. Und das nimmt er
fraglos in Kauf - oder merkt es gar nicht.
Beim Abspeichern der Zehner und der Einer verwendet der
AVR-Assembler-Programmierer gerne Zeiger: hier das Registerpaar Z. Es
besteht aus den beiden Registern R31 (MSB) und R30 (LSB). Mit
"st Z+,rmp2" schreibt er den ASCII-Zehner-Zähler in rmp2
in den SRAM-Speicher, auf den Z jeweils zeigt. Anschließend
erhöht der Prozessor auch noch den Zeiger um Eins (Z+), so dass
er gleich auf die nächste Adresse zeigt.
Und so sieht der ganze Quellcode der Umwandlerei aus (rechts das Ergebnis
im SRAM, in der untersten Zeile tobt sich wegen der RCALLs der Stapel aus):
; SRAM-Position fuer Uhrzeit
.dseg
UhrString:
.byte 8
;
; Code fuer Uhrzeit
.cseg
; Byte in rmp als ASCII-Zeichen in Z
; mit fuehrenden Nullen
Int2Str:
ldi rmp2,'0'-1
Int2Str1:
inc rmp2
subi rmp,10
brcc Int2Str1
st Z+,rmp2
subi rmp,-'0'-10
st Z+,rmp
ret
; Zeit in eine Zeichenkette im Sram umwandeln
ClkString:
ldi ZH,High(UhrString)
ldi ZL,Low(UhrString)
ldi rmp,'Z'
st Z+,rmp
ldi rmp,'e'
st Z+,rmp
ldi rmp,'i'
st Z+,rmp
ldi rmp,'t'
st Z+,rmp
ldi rmp,' '
st Z+,rmp
ldi rmp,'='
st Z+,rmp
ldi rmp,' '
st Z+,rmp
mov rmp,rStd
rcall Int2Str
ldi rmp,':'
st Z+,rmp
mov rmp,rMin
rcall Int2Str
ldi rmp,':'
st Z+,rmp
mov rmp,rSek
rcall Int2Str
ret
Das ist jetzt in der Summe aber ganz viel länger als in Pascal.
Weil aber jede dieser Zeilen genau einer einzigen Prozessoroperation
entspricht, was in Pascal beileibe nicht der Fall ist, kriegt man
mit diesem Programmcode ein sehr kleines, schlankes Ausführbares
hin. Siehe weiter unten.
3 Gegenüberstellung von Pascal- und AVR-Asm-Programm
Hier in der Tabelle sind mal die wichtigsten Eigenschaften der
ausführbaren Programme gegenübergestellt.
Parameter | Pascal-Uhr | AVR-ASM-Uhr |
Prozessortakt | 1,8 GHz | 1 MHz |
Dauer eines Tags | 4 ms | 1,30 s |
daraus: Dauer pro Tick | 46,30 ns | 15,05 µs |
dto. pro MHz Takt | 83,33 µs |
Größe | mit FPC: 115 kB | 57 Worte (= 114 Byte) |
Das Folgende fällt daran auf:
- Der Prozessortakt meines Laptop ist 1.800-mal höher als
eines Standard-AVRs. Gut, man könnte den AVR noch maximal 20-fach
schneller machen, aber wozu sollte das gut sein (außer für
Batterie-Leer)?
- Mein Laptop absolviert die 86.400 Sekunden eines Tages in 4 ms,
wofür der AVR etwas mehr als eine Sekunde braucht. Aber eine Sekunde
pro Tag ist nun auch nicht gerade eine beeindruckende Dauer und eher
nicht der Rede wert.
- Pro Tick errechnet sich daraus für den Laptop 46,3 ns,
für den AVR 15,05 µs. Das ist um das 300-fache schneller.
- Hätte mein Laptop den gleichen Prozessortakt wie ein AVR,
dann bräuchte er mit 83µs mehr als sechsfach so lang wie
ein AVR. Daran sieht man, wie verschwenderisch Laptops mit der
Rechenzeit umgehen: da ist so ein AVR bei gleichem Takt viel, viel
besser. Wenn man heute noch, wie das früher bei geleasten
IBM-Mainframes üblich war, pro Rechenminute bezahlen
müsste: alle würden nur noch Assembler machen.
- Und zum Schluss noch die Programmgröße. Die ist mit
115 kB um das 1.000-fache größer. Wozu braucht der
Pascal das alles? Nun, er schleppt jede Menge Unsinn mit sich herum,
den er niemals braucht. Seine ganze Speicherverwaltung ist dabei,
damit er die famosen Aufruf- und Rückgabespektakel veranstalten
kann. Und das Fenster will auch angezeigt sein. Was man halt als
Pascal-Programm so braucht. Das braucht der AVR so alles gar nicht,
kennt er auch gar nicht, um im Endeffekt dennoch dasselbe zu tun.
4 Fazit
Ein wichtiges Fazit: wenn Du als Hochsprachen-Programmierer in Assembler
einsteigst, musst Du Dich um ein paar wenige Dinge, wie die Platzierung
von allem und jedem oder das Initiieren der Stapelverwaltung, selber
kümmern, die Dir bislang der Compiler abgenommen hat. Dafür
bist Du jetzt Herr über alle Dinge und Du weißt exakt, was
Du tust. Das hat Dich bislang nicht interessiert, das muss es jetzt aber.
Und? Habe ich zuviel versprochen, dass Assembler in Mikroprozessoren
optimal zu Hause ist und wirkt? Und dass es sich lohnt, in dieser
Sprache genausogut zu Hause zu sein wie in irgendwelchen Hochsprachen
auf dem PC oder Laptop?
Kein vernünftiger Mensch wird auf dem PC oder Laptop in Assembler
programmieren. Aber um einen AVR dazu zu kriegen, mittels acht
verschiedenen Interrupt-Quellen zeitkritische Signale zu senden und
zu empfangen, und das innerhalb von Mikrosekunden, wenn es sein muss.
Und das Ganze dann auch noch in die 4 kB Flashspeicher passend zu
kriegen, dafür ist nur Assembler wirklich geeignet.
Also, ran an die Buletten und Assembler gelernt. Und den knappen
Flashspeicher nicht mit unnützem Datenmüll vollstopfen, der
sowieso niemals gebraucht wird und nur als Füllstoff vom Compiler
in die Speicherzellen gefüllt wird. Das kann mit Assembler nicht
passieren: da kommt nur rein, was auch Sinn macht und was Du auch
wirklich dort haben willst!
Zum Seitenanfang
©2022 by http://www.avr-asm-tutorial.net