AVR-Tutorial: Stack: Unterschied zwischen den Versionen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche
Keine Bearbeitungszusammenfassung
(Yield noch einfacher als Stack-Wechsler)
 
(26 dazwischenliegende Versionen von 5 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
== Motivation für Sackhüpfen ==
== Motivation ==
bisher war es so, dass wenn die Programmausführung an einer anderen Stelle fortgesetzt werden soll, als sich durch die Abfolge der Befehle ergibt, mittels <b>rjmp</b> an diese andere Stelle gesprungen wurde.
Bisher war es so, dass, wenn die Programmausführung an einer anderen Stelle fortgesetzt werden soll, als sich durch die Abfolge der Befehle ergibt, mittels <code>rjmp</code> an diese andere Stelle gesprungen wurde.
Das ist aber oft nicht ausreichend. Oft möchte man den Fall haben, dass man aus der normalen Befehlsreihenfolge heraus eine andere Sequenz von Befehlen ausgeführt wird und wenn diese abgearbeitet ist, genau an die Aufrufstelle zurückgesprungen wird. Da diese eingeschobene Sequenz an vielen Stellen aufrufbar sein soll, gelingt es daher auch nicht, mittels eines <b>rjmp</b> wieder zur aufrufenden Stelle zurück zu kommen, denn dann müsste ja dieser Rücksprung je nachdem von wo der Hinsprung gekommen ist entsprechend modifiziert werden.
Das ist aber oft nicht ausreichend. Oft möchte man den Fall haben, dass man aus der normalen Befehlsreihenfolge heraus eine andere Sequenz von Befehlen ausgeführt wird und wenn diese abgearbeitet ist, genau an die Aufrufstelle zurückgesprungen wird. Da diese eingeschobene Sequenz an vielen Stellen aufrufbar sein soll, gelingt es daher auch nicht, mittels eines <code>rjmp</code> wieder zur aufrufenden Stelle zurück zu kommen, denn dann müsste ja dieser Rücksprung je nachdem von wo der Hinsprung gekommen ist entsprechend modifiziert werden.


Die Lösung dieses Dilemmas besteht in einem eigenen Befehl <b>rcall</b>. Ein <b>rcall</b> macht prinzipiell auch den Sprung zu einem Ziel, legt aber gleichzeitig auch noch die Adresse von wo der Sprung erfolgt ist in einem speziellen Speicherbereich ab, so dass sein 'Gegenspieler', der Befehl <b>ret</b> (wie Return) anhand dieser abgelegten Information wieder genau zu dieser Aufrufstelle zurückspringen kann. Diesen speziellen Speicherbereich nennt man den "<b>[http://www.youjizz.com Stack]"</b>. Stack bedeutet übersetzt soviel wie Stapel. Damit ist ein Speicher nach dem LIFO-Prinzip ("last in first out") gemeint. Das bedeutet, dass das zuletzt auf den Stapel gelegte Element auch zuerst wieder heruntergenommen wird. Es ist nicht möglich, Elemente irgendwo in der Mitte des Stapels herauszuziehen oder hineinzuschieben. Ein Stack (oder Stapel) funktioniert wie ein Stapel Teller. Der Teller, welcher zuletzt auf den Stapel gelegt wird, ist auch der erste, welcher wieder vom Stapel heruntergenommen wird. Und genau das wird in diesem Fall ja auch benötigt: jeder <b>rcall</b> legt seine Rücksprungadresse auf den Stack, so dass alle nachfolgenden <b>ret</b> jeweils in umgekehrter Reihenfolge wieder die richtigen Rücksprungadressen anspringen.
Die Lösung dieses Dilemmas besteht in einem eigenen Befehl '''<code>rcall</code>'''. Ein <code>rcall</code> macht prinzipiell auch den Sprung zu einem Ziel, legt aber gleichzeitig auch noch die Adresse, von wo der Sprung erfolgt ist, in einem speziellen Speicherbereich ab, so dass sein „Gegenspieler“, der Befehl '''<code>ret</code>''' (wie Return) anhand dieser abgelegten Information wieder genau zu dieser Aufrufstelle zurückspringen kann. Diesen speziellen Speicherbereich nennt man den „[[Stack]]“. Stack bedeutet übersetzt soviel wie Stapel. Damit ist ein Speicher nach dem LIFO-Prinzip (''last in first out'') gemeint. Das bedeutet, dass das zuletzt auf den Stapel gelegte Element auch zuerst wieder heruntergenommen wird. Es ist nicht möglich, Elemente irgendwo in der Mitte des Stapels herauszuziehen oder hineinzuschieben. Ein Stack (oder Stapel) funktioniert wie ein Stapel Teller. Der Teller, welcher zuletzt auf den Stapel gelegt wird, ist auch der erste, welcher wieder vom Stapel heruntergenommen wird. Und genau das wird in diesem Fall ja auch benötigt: jeder <code>rcall</code> legt seine Rücksprungadresse auf den Stack, so dass alle nachfolgenden <code>ret</code> jeweils in umgekehrter Reihenfolge wieder die richtigen Rücksprungadressen anspringen.


Bei allen aktuellen AVR-Controllern wird der Stack im [[Speicher#RAM|RAM]] angelegt. Der Stack wächst dabei von oben nach unten: Am Anfang wird der Stackpointer (Adresse der aktuellen Stapelposition) auf das Ende des RAMs gesetzt. Wird nun ein Element hinzugefügt, wird dieses an der momentanen Stackpointerposition abgespeichert und der Stackpointer um 1 erniedrigt. Soll ein Element vom Stack heruntergenommen werden, wird zuerst der Stackpointer um 1 erhöht und dann das Byte von der vom Stackpointer angezeigten Position gelesen.
Bei allen aktuellen AVR-Controllern wird der Stack im [[Speicher#RAM|RAM]] angelegt. Der Stack wächst dabei von oben nach unten: Am Anfang wird der Stackpointer (Adresse der aktuellen Stapelposition) auf das Ende des RAMs gesetzt. Wird nun ein Element hinzugefügt, wird dieses an der momentanen Stackpointerposition abgespeichert und der Stackpointer um 1 erniedrigt. Soll ein Element vom Stack heruntergenommen werden, wird zuerst der Stackpointer um 1 erhöht und dann das Byte von der vom Stackpointer angezeigten Position gelesen.


== Aufruf von Unterhosen ==
== Aufruf von Unterprogrammen ==
Dem Prozessor dient der Stack hauptsächlich dazu, Rücksprungadressen beim Aufruf von Unterprogrammen zu speichern, damit er später noch weiß, an welche Stelle zurückgekehrt werden muss, wenn das Unterprogramm mit '''ret''' oder die Interruptroutine mit '''reti''' beendet wird.  
Dem Prozessor dient der Stack hauptsächlich dazu, Rücksprungadressen beim Aufruf von Unterprogrammen zu speichern, damit er später noch weiß, an welche Stelle zurückgekehrt werden muss, wenn das Unterprogramm mit <code>ret</code> oder die Interruptroutine mit <code>reti</code> beendet wird.


Das folgende Beispielprogramm (AT90S4433) zeigt, wie der Stack dabei beeinflusst wird:  
Das folgende Beispielprogramm (AT90S4433) zeigt, wie der Stack dabei beeinflusst wird:


[http://www.elephanttube.com Download stack.asm]
[http://www.mikrocontroller.net/sourcecode/tutorial/stack.asm Download stack.asm]


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.include "4433def.inc"    ; bzw. 2333def.inc
.include "4433def.inc"    ; bzw. 2333def.inc


Zeile 28: Zeile 28:


sub1:
sub1:
                           ; hier könnten ein paar Befehle stehen
                           ; Hier könnten ein paar Befehle stehen.
         rcall sub2        ; sub2 aufrufen
         rcall sub2        ; sub2 aufrufen
                           ; hier könnten auch ein paar Befehle stehen
                           ; Hier könnten auch ein paar Befehle stehen.
         ret              ; wieder zurück
         ret              ; wieder zurück


sub2:
sub2:
                           ; hier stehen normalerweise die Befehle,
                           ; Hier stehen normalerweise die Befehle,
                           ; die in sub2 ausgeführt werden sollen
                           ; die in sub2 ausgeführt werden sollen.
         ret              ; wieder zurück
         ret              ; wieder zurück
</syntaxhighlight>
</syntaxhighlight>


'''.def temp = r16''' ist eine Assemblerdirektive. Diese sagt dem Assembler, dass er überall, wo er "temp" findet, stattdessen "r16" einsetzen soll. Das ist oft praktisch, damit man nicht mit den Registernamen durcheinander kommt. Eine Übersicht über die Assemblerdirektiven findet man [http://www.avr-asm-tutorial.net/avr_de/beginner/diraus.html hier].  
'''<code>.def temp = r16</code>''' ist eine Assemblerdirektive. Diese sagt dem Assembler, dass er überall, wo er „temp“ findet, stattdessen „r16“ einsetzen soll. Das ist oft praktisch, damit man nicht mit den Registernamen durcheinander kommt. Eine Übersicht über die Assemblerdirektiven findet man bei [http://www.avr-asm-tutorial.net/avr_de/beginner/diraus.html avr-asm-tutorial.net].


Bei Controllern, die mehr als 256 Byte RAM besitzen (z.&nbsp;B. ATmega8), passt die Adresse nicht mehr in ein Byte. Deswegen gibt es bei diesen Controllern das Stack-Pointer-Register aufgeteilt in '''SPL''' (Low) und '''SPH''' (High), in denen das Low- und das High-Byte der Adresse gespeichert wird. Damit es funktioniert, muss das Programm dann folgendermaßen geändert werden:  
Bei Controllern, die mehr als 256 Byte RAM besitzen (z.&nbsp;B. ATmega8), passt die Adresse nicht mehr in ein Byte. Deswegen gibt es bei diesen Controllern das Stack-Pointer-Register aufgeteilt in '''SPL''' (Low) und '''SPH''' (High), in denen das Low- und das High-Byte der Adresse gespeichert wird. Damit es funktioniert, muss das Programm dann folgendermaßen geändert werden:


[http://www.alohatube.com Download stack-bigmem.asm]
[http://www.mikrocontroller.net/sourcecode/tutorial/stack-bigmem.asm Download stack-bigmem.asm]


<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
.include "m8def.inc"
.include "m8def.inc"


Zeile 61: Zeile 61:


sub1:
sub1:
                                           ; hier könnten ein paar Befehle stehen
                                           ; Hier könnten ein paar Befehle stehen.
         rcall sub2                        ; sub2 aufrufen
         rcall sub2                        ; sub2 aufrufen
                                           ; hier könnten auch Befehle stehen
                                           ; Hier könnten auch Befehle stehen.
         ret                              ; wieder zurück
         ret                              ; wieder zurück


sub2:
sub2:
                                           ; hier stehen normalerweise die Befehle,
                                           ; Hier stehen normalerweise die Befehle,
                                           ; die in sub2 ausgeführt werden sollen
                                           ; die in sub2 ausgeführt werden sollen.
         ret                              ; wieder zurück
         ret                              ; wieder zurück
</syntaxhighlight>
</syntaxhighlight>


Natürlich ist es unsinnig, dieses Programm in einen Controller zu programmieren. Stattdessen sollte man es mal mit dem AVR-Studio simulieren, um die Funktion des Stacks zu verstehen.  
Natürlich wäre es unsinnig, dieses Programm in einen Controller zu programmieren. Stattdessen sollte man es mal mit dem AVR-Studio simulieren, um die Funktion des Stacks zu verstehen.


Als erstes wird mit '''Project/New''' ein neues Projekt erstellt, zu dem man dann mit '''Project/Add File''' eine Datei mit dem oben gezeigten Programm (stack.asm) hinzufügt. Nachdem man unter '''Project/Project Settings''' das '''Object Format for AVR-Studio''' ausgewählt hat, kann man das Programm mit Strg+F7 assemblieren und den Debug-Modus starten.  
Als erstes wird mit '''Project/New''' ein neues Projekt erstellt, zu dem man dann mit '''Project/Add File''' eine Datei mit dem oben gezeigten Programm (stack.asm) hinzufügt. Nachdem man unter '''Project/Project Settings''' das '''Object Format for AVR-Studio''' ausgewählt hat, kann man das Programm mit Strg+F7 assemblieren und den Debug-Modus starten.


Danach sollte man im Menu '''View''' die Fenster '''Processor''' und '''Memory''' öffnen und im Memory-Fenster '''Data''' auswählen.  
Danach sollte man im Menü '''View''' die Fenster '''Processor''' und '''Memory''' öffnen und im Memory-Fenster '''Data''' auswählen.


Das Fenster '''Processor'''
Das Fenster '''Processor'''
* ''Program Counter'': Adresse im Programmspeicher (FLASH), die gerade abgearbeitet wird
* ''Program Counter'': Adresse im Programmspeicher (Flash), die gerade abgearbeitet wird
* ''Stack Pointer'': Adresse im Datenspeicher (RAM), auf die der Stackpointer gerade zeigt
* ''Stack Pointer'': Adresse im Datenspeicher (RAM), auf die der Stackpointer gerade zeigt
* ''Cycle Counter'': Anzahl der Taktzyklen seit Beginn der Simulation
* ''Cycle Counter'': Anzahl der Taktzyklen seit Beginn der Simulation
* ''Time Elapsed'': Zeit, die seit dem Beginn der Simulation vergangen ist  
* ''Time Elapsed'': Zeit, die seit dem Beginn der Simulation vergangen ist


Im Fenster '''Memory''' wird der Inhalt des RAMs angezeigt.  
Im Fenster '''Memory''' wird der Inhalt des RAMs angezeigt.


Sind alle 3 Fenster gut auf einmal sichtbar, kann man anfangen, das Programm (in diesem Fall "stack.asm") mit der Taste F11 langsam Befehl für Befehl zu simulieren.  
Sind alle drei Fenster gut auf einmal sichtbar, kann man anfangen, das Programm (in diesem Fall „stack.asm“) mit der Taste F11 langsam Befehl für Befehl zu simulieren.


Wenn der gelbe Pfeil in der Zeile '''out SPL, temp''' vorbeikommt, kann man im Prozessor-Fenster sehen, wie der Stackpointer auf 0xDF (''ATmega8'': 0x45F) gesetzt wird. Wie man im Memory-Fenster sieht, ist das die letzte RAM-Adresse.  
Wenn der gelbe Pfeil in der Zeile '''<code>out SPL, temp</code>''' vorbeikommt, kann man im Prozessor-Fenster sehen, wie der Stackpointer auf 0xDF (''ATmega8'': 0x45F) gesetzt wird. Wie man im Memory-Fenster sieht, ist das die letzte RAM-Adresse.


Wenn der Pfeil auf dem Befehl '''rcall sub1''' steht, sollte man sich den Program Counter anschauen: Er steht auf 0x02 (''ATmega8'': 0x04).  
Wenn der Pfeil auf dem Befehl '''<code>rcall sub1</code>''' steht, sollte man sich den ''Program Counter'' anschauen: Er steht auf 0x02 (''ATmega8'': 0x04).


Drückt man jetzt nochmal auf F11, springt der Pfeil zum Unterprogramm sub1. Im RAM erscheint an der Stelle, auf die der Stackpointer vorher zeigte, die Zahl 0x03 (''ATmega8'': 0x05). Das ist die Adresse im ROM, an der das Hauptprogramm nach dem Abarbeiten des Unterprogramms fortgesetzt wird. Doch warum wurde der Stackpointer um 2 verkleinert? Das liegt daran, dass eine Programmspeicheradresse bis zu 2 Byte breit sein kann, und somit auch 2 Byte auf dem Stack benötigt werden, um die Adresse zu speichern.  
Drückt man jetzt nochmal auf F11, springt der Pfeil zum Unterprogramm <code>sub1</code>. Im RAM erscheint an der Stelle, auf die der Stackpointer vorher zeigte, die Zahl 0x03 (''ATmega8'': 0x05). Das ist die Adresse im ROM, an der das Hauptprogramm nach dem Abarbeiten des Unterprogramms fortgesetzt wird. Doch warum wurde der Stackpointer um 2 verkleinert? Das liegt daran, dass eine Programmspeicheradresse bis zu 2 Byte breit sein kann, und somit auch 2 Byte auf dem Stack benötigt werden, um die Adresse zu speichern.


Das gleiche passiert beim Aufruf von sub2.  
Das gleiche passiert beim Aufruf von <code>sub2</code>.


Zur Rückkehr aus dem mit rcall aufgerufenen Unterprogramm gibt es den Befehl '''ret'''. Dieser Befehl sorgt dafür, dass der Stackpointer wieder um 2 erhöht wird und die dabei eingelesene Adresse in den "Program Counter" kopiert wird, so dass das Programm dort fortgesetzt wird.  
Zur Rückkehr aus dem mit <code>rcall</code> aufgerufenen Unterprogramm gibt es den Befehl '''<code>ret</code>'''. Dieser Befehl sorgt dafür, dass der Stackpointer wieder um 2 erhöht wird und die dabei eingelesene Adresse in den ''Program Counter'' kopiert wird, so dass das Programm dort fortgesetzt wird.


Apropos Program Counter: Wer sehen will, wie so ein Programm aussieht, wenn es assembliert ist, sollte mal die Datei mit der Endung ".lst" im Projektverzeichnis öffnen. Die Datei sollte ungefähr so aussehen:  
A propos Program Counter: Wer sehen will, wie so ein Programm aussieht, wenn es assembliert ist, sollte mal die Datei mit der Endung .lst“ im Projektverzeichnis öffnen. Die Datei sollte ungefähr so aussehen:


http://www.mikrocontroller.net/images/listfile.gif
http://www.mikrocontroller.net/images/listfile.gif


Im blau umrahmten Bereich steht die Adresse des Befehls im Programmspeicher. Das ist auch die Zahl, die im Program Counter angezeigt wird, und die beim Aufruf eines Unterprogramms auf den Stack gelegt wird. Der grüne Bereich rechts daneben ist der OP-Code des Befehls, so wie er in den Programmspeicher des Controllers programmiert wird, und im roten Kasten stehen die "mnemonics": Das sind die Befehle, die man im Assembler eingibt.
Im blau umrahmten Bereich steht die Adresse des Befehls im Programmspeicher. Das ist auch die Zahl, die im Program Counter angezeigt wird, und die beim Aufruf eines Unterprogramms auf den Stack gelegt wird. Der grüne Bereich rechts daneben ist der OP-Code des Befehls, so wie er in den Programmspeicher des Controllers programmiert wird, und im roten Kasten stehen die „mnemonics“: Das sind die Befehle, die man im Assembler eingibt.
Der nicht eingerahmte Rest besteht aus Assemblerdirektiven, Labels (Sprungmarkierungen) und Kommentaren, die nicht direkt in OP-Code umgewandelt werden.
Der nicht eingerahmte Rest besteht aus Assemblerdirektiven, Labels (Sprungmarkierungen) und Kommentaren, die nicht direkt in OP-Code umgewandelt werden.
Der grün eingerahmte Bereich ist das eigentliche Programm, so wie es der µC versteht. Die jeweils erste Zahl im grünen Bereich steht für einen Befehl, den sog. OP-Code (OP = Operation). Die zweite Zahl codiert Argumente für diesen Befehl.
Der grün eingerahmte Bereich ist das eigentliche Programm, so wie es der µC versteht. Die jeweils erste Zahl im grünen Bereich steht für einen Befehl, den sog. OP-Code (OP = Operation). Die zweite Zahl codiert Argumente für diesen Befehl.


== Sichern von Registern ==
== Sichern von Registern ==
Eine weitere Anwendung des Stacks ist das "Sichern" von Registern. Wenn man z.&nbsp;B. im Hauptprogramm die Register R16, R17 und R18 verwendet, dann ist es i.d.R. erwünscht, dass diese Register durch aufgerufene Unterprogramme nicht beeinflusst werden. Man muss also nun entweder auf die Verwendung dieser Register innerhalb von Unterprogrammen verzichten, oder man sorgt dafür, dass am Ende jedes Unterprogramms der ursprüngliche Zustand der Register wiederhergestellt wird. Wie man sich leicht vorstellen kann ist ein "Stapelspeicher" dafür ideal: Zu Beginn des Unterprogramms legt man die Daten aus den zu sichernden Registern oben auf den Stapel, und am Ende holt man sie wieder (in der umgekehrten Reihenfolge) in die entsprechenden Register zurück. Das Hauptprogramm bekommt also wenn es fortgesetzt wird überhaupt nichts davon mit, dass die Register inzwischen anderweitig verwendet wurden.  
Eine weitere Anwendung des Stacks ist das „Sichern“ von Registern. Wenn man z.&nbsp;B. im Hauptprogramm die Register R16, R17 und R18 verwendet, dann ist es i.d.R. erwünscht, dass diese Register durch aufgerufene Unterprogramme nicht beeinflusst werden. Man muss also nun entweder auf die Verwendung dieser Register innerhalb von Unterprogrammen verzichten, oder man sorgt dafür, dass am Ende jedes Unterprogramms der ursprüngliche Zustand der Register wiederhergestellt wird. Wie man sich leicht vorstellen kann, ist ein „Stapelspeicher“ dafür ideal: Zu Beginn des Unterprogramms legt man die Daten aus den zu sichernden Registern oben auf den Stapel, und am Ende holt man sie wieder (in der umgekehrten Reihenfolge) in die entsprechenden Register zurück. Das Hauptprogramm bekommt also, wenn es fortgesetzt wird, überhaupt nichts davon mit, dass die Register inzwischen anderweitig verwendet wurden.


[http://www.youjizz.com Download stack-saveregs.asm]
[http://www.mikrocontroller.net/sourcecode/tutorial/stack-saveregs.asm Download stack-saveregs.asm]
<syntaxhighlight lang="avrasm">
<syntaxhighlight lang="asm">
.include "4433def.inc"            ; bzw. 2333def.inc
.include "4433def.inc"            ; bzw. 2333def.inc


Zeile 123: Zeile 123:
         ldi R17, 0b10101010      ; einen Wert ins Register R17 laden
         ldi R17, 0b10101010      ; einen Wert ins Register R17 laden


         rcall sub1                ; Unterprogramm "sub1" aufrufen
         rcall sub1                ; Unterprogramm „sub1“ aufrufen
   
   
         out PORTB, R17          ; Wert von R17 an den Port B ausgeben
         out PORTB, R17          ; Wert von R17 an den Port B ausgeben
Zeile 133: Zeile 133:
         push R17                ; Inhalt von R17 auf dem Stack speichern
         push R17                ; Inhalt von R17 auf dem Stack speichern


         ; hier kann nach belieben mit R17 gearbeitet werden,
         ; Hier kann nach belieben mit R17 gearbeitet werden,
         ; als Beispiel wird es hier auf 0 gesetzt
         ; als Beispiel wird es hier auf 0 gesetzt.


         ldi R17, 0
         ldi R17, 0
Zeile 141: Zeile 141:
         ret                      ; wieder zurück zum Hauptprogramm
         ret                      ; wieder zurück zum Hauptprogramm
</syntaxhighlight>
</syntaxhighlight>
Wenn man dieses Programm assembliert und in den Controller lädt, dann wird man feststellen, dass jede zweite LED an Port B leuchtet. Der ursprüngliche Wert von R17 blieb also erhalten, obwohl dazwischen ein Unterprogramm aufgerufen wurde, das R17 geändert hat.  
Wenn man dieses Programm assembliert und in den Controller lädt, dann wird man feststellen, dass jede zweite LED an Port B leuchtet. Der ursprüngliche Wert von R17 blieb also erhalten, obwohl dazwischen ein Unterprogramm aufgerufen wurde, das R17 geändert hat.


Auch in diesem Fall kann man bei der Simulation des Programms im AVR-Studio die Beeinflussung des Stacks durch die Befehle '''push''' und '''pop''' genau nachvollziehen.
Auch in diesem Fall kann man bei der Simulation des Programms im AVR-Studio die Beeinflussung des Stacks durch die Befehle '''<code>push</code>''' und '''<code>pop</code>''' genau nachvollziehen.


== Sprung zu beliebiger Vagina ==
== Sprung zu beliebiger Adresse ==
''Dieser Abschnitt ist veraltet, da nahezu alle ATmega/ATtiny Typen IJMP/ICALL unterstützen.''
''Dieser Abschnitt ist veraltet, da nahezu alle ATmega/ATtiny-Typen IJMP/ICALL unterstützen.''


Kleinere AVR besitzen keinen Befehl, um direkt zu einer Adresse zu springen, die in einem Registerpaar gespeichert ist. Man kann dies aber mit etwas Stack-Akrobatik erreichen. Dazu einfach zuerst den niederen Teil der Adresse, dann den höheren Teil der Adresse mit '''push''' auf den Stack legen und ein '''ret''' ausführen:
Kleinere AVR besitzen keinen Befehl, um direkt zu einer Adresse zu springen, die in einem Registerpaar gespeichert ist. Man kann dies aber mit etwas Stack-Akrobatik erreichen. Dazu einfach zuerst den niederen Teil der Adresse, dann den höheren Teil der Adresse mit '''<code>push</code>''' auf den Stack legen und ein '''<code>ret</code>''' ausführen:
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">
ldi ZH, high(testRoutine)
ldi ZH, high(testRoutine)
ldi ZL, low(testRoutine)
ldi ZL, low(testRoutine)
Zeile 162: Zeile 162:
</syntaxhighlight>
</syntaxhighlight>
Auf diese Art und Weise kann man auch Unterprogrammaufrufe durchführen:
Auf diese Art und Weise kann man auch Unterprogrammaufrufe durchführen:
<syntaxhighlight lang="avrasm">  
<syntaxhighlight lang="asm">  
ldi ZH, high(testRoutine)
ldi ZH, high(testRoutine)
ldi ZL, low(testRoutine)
ldi ZL, low(testRoutine)
Zeile 182: Zeile 182:
Bei diesen Befehlen muss das Sprungziel in ZH:ZL stehen.
Bei diesen Befehlen muss das Sprungziel in ZH:ZL stehen.


== Weitere Informationen (von Justin Bieber): ==
== Multitasking ==
* [http://www.mikrocontroller.net/attachment/301/Der-Stack-1.pdf Der Stack - Funktion und Nutzen (pdf)]
* [http://www.mikrocontroller.net/attachment/299/Der-Stack-2.pdf Der Stack - Parameterübergabe an Unterprogramme (pdf)]
* [http://www.mikrocontroller.net/attachment.php/676/Der-Stack-3.pdf Der Stack - Unterprogramme mit variabler Parameteranzahl (pdf) ]
Vor langer Zeit gab es einen Penis der keine Vagina finden konnte!
Also half Papa Penis ihm eine zu finden!
Sie gingen in das größte Vaginastudio der Welt!
Dort wollte Penis seine Unschuld Verlieren...


(Der in dieser Abhandlung angegebene Befehl ''MOV ZLow, SPL'' muss ''IN ZL, SPL'' heißen, da SPL und SPH I/O-Register sind. Ggf ist auch SPH zu berücksichtigen --> 2byte Stack-Pointer)
Mit nur wenig Aufwand lässt sich kooperatives Multitasking oder Multithreading zwischen 2 Threads realisieren.
Nahezu alle AVRs haben das Register SPL oder SPH:SPL zum Lesen und Verändern des Stapelzeigers.
Während für den Hauptthread der Stapelzeiger in irgendeiner Form initialisiert und ans Ende des RAM verweist,
muss für jeden weiteren Thread ein Stapel-Bereich angelegt werden.
Sinnvollerweise im nicht initialisierten RAM.
 
Für das Umschalten zwischen den beiden Stapeln, mithin der Threads, braucht es noch einen Speicher
für den jeweils anderen Stapelzeiger.
Für ATtinys mit 8-Bit-adressierbarem RAM (also nur SPL) und genau 2 Threads sieht das so aus:
 
<syntaxhighlight lang="c++" style="tab-size:8">
 
byte stacksave NOINIT;
 
void initThread(void(*p)()) { // p = Funktionszeiger
static byte stack[30] NOINIT;
byte*stackend=stack+sizeof stack;
stacksave=byte(unsigned(stackend-7)); // POP macht Pre-Inkrement auf AVR
// Die vier darüber liegenden Bytes werden zu W16 und Y
// für den Thread. Diese werden bei yield() thread-lokal gesichert.
stackend[-2]=unsigned(p)>>8; // Lo und Hi sind vertauscht auf dem Stack!
stackend[-1]=unsigned(p)&0xFF; // <p> kommt als Wortadresse, nicht halbieren
}
 
// Threadwechsel-Funktion, in Assembler implementiert
extern "C" void yield();
 
void worker() __attribute__((noreturn));
void worker() {
for(;;) {
// tue irgendwas
  yield(); // typischerweise im Innern von Funktionen, die auf ein Interrupt-Ereignis warten
}
}
 
// in der Initialisierungssequenz
initThread(worker);
 
// in der Hauptschleife
yield(); // zum Worker-Thread
 
</syntaxhighlight>
 
Und der Assembler-Teil:
 
<syntaxhighlight lang="asm">
.global yield
// Multithreading: Da die Anzahl der Threads von vornherein bekannt und 2 ist,
// ist diese Yield-Funktion (yield = Vorfahrt gewähren) einfach ein Stack-Toggler.
// Beachte: Obwohl es so aussieht als ob yield nur W24 verändert,
// verändert diese Funktion (aus Sicht des Aufrufers) alle Register
// und rettet nur W16 und Y.
// Interessanterweise müssen keine Interrupts gesperrt werden!
// (Das ist erforderlich bei AVR-Controllern _mit_ SPH, falls man nicht
// geschickterweise beide Stacks in eine „Page“ mit gleichem SPH sperrt.)
yield: push YH
push YL
push r17
push r16
in r24,SPL
lds r25,stacksave
sts stacksave,r24
out SPL,r25
pop r16
pop r17
pop YL
pop YH
ret // zum anderen Thread
</syntaxhighlight>
 
Für mehr als 2 Threads wird das Ganze deutlich aufwändiger und lohnt sich nur noch für ATmegas.
Diese Implementierung ist kompatibel zur avr-g++-ABI und „wegreservierten“ Registern R2..R15.
 
Das Ganze beißt sich prinzipiell ''nicht'' mit '''sleep_cpu()'''.
 
Preemptives Multitasking entsteht daraus, wenn die (eine) Yield-Funktion vom Timer zyklisch aufgerufen wird. Dabei müssen ALLE, wirklich ALLE Register (auch R0 und R1) (außer die wegreservierten) sowie die Flags gerettet werden. Das benötigt mehr als die o.a. 30 Byte Platz auf dem Stack.
 
== Weitere Informationen (von Lothar Müller) ==
* [http://www.mikrocontroller.net/attachment/301/Der-Stack-1.pdf Der Stack – Funktion und Nutzen (pdf)]
* [http://www.mikrocontroller.net/attachment/299/Der-Stack-2.pdf Der Stack – Parameterübergabe an Unterprogramme (pdf)]
* [http://www.mikrocontroller.net/attachment.php/676/Der-Stack-3.pdf Der Stack – Unterprogramme mit variabler Parameteranzahl (pdf)]
 
(Der in dieser Abhandlung angegebene Befehl '''<code>MOV ZLow, SPL</code>''' muss '''<code>IN ZL, SPL</code>''' heißen, da SPL und SPH I/O-Register sind. Ggf. ist auch SPH zu berücksichtigen → 2-Byte-Stack-Pointer.)


----
----

Aktuelle Version vom 22. März 2024, 13:36 Uhr

Motivation

Bisher war es so, dass, wenn die Programmausführung an einer anderen Stelle fortgesetzt werden soll, als sich durch die Abfolge der Befehle ergibt, mittels rjmp an diese andere Stelle gesprungen wurde. Das ist aber oft nicht ausreichend. Oft möchte man den Fall haben, dass man aus der normalen Befehlsreihenfolge heraus eine andere Sequenz von Befehlen ausgeführt wird und wenn diese abgearbeitet ist, genau an die Aufrufstelle zurückgesprungen wird. Da diese eingeschobene Sequenz an vielen Stellen aufrufbar sein soll, gelingt es daher auch nicht, mittels eines rjmp wieder zur aufrufenden Stelle zurück zu kommen, denn dann müsste ja dieser Rücksprung je nachdem von wo der Hinsprung gekommen ist entsprechend modifiziert werden.

Die Lösung dieses Dilemmas besteht in einem eigenen Befehl rcall. Ein rcall macht prinzipiell auch den Sprung zu einem Ziel, legt aber gleichzeitig auch noch die Adresse, von wo der Sprung erfolgt ist, in einem speziellen Speicherbereich ab, so dass sein „Gegenspieler“, der Befehl ret (wie Return) anhand dieser abgelegten Information wieder genau zu dieser Aufrufstelle zurückspringen kann. Diesen speziellen Speicherbereich nennt man den „Stack“. Stack bedeutet übersetzt soviel wie Stapel. Damit ist ein Speicher nach dem LIFO-Prinzip (last in first out) gemeint. Das bedeutet, dass das zuletzt auf den Stapel gelegte Element auch zuerst wieder heruntergenommen wird. Es ist nicht möglich, Elemente irgendwo in der Mitte des Stapels herauszuziehen oder hineinzuschieben. Ein Stack (oder Stapel) funktioniert wie ein Stapel Teller. Der Teller, welcher zuletzt auf den Stapel gelegt wird, ist auch der erste, welcher wieder vom Stapel heruntergenommen wird. Und genau das wird in diesem Fall ja auch benötigt: jeder rcall legt seine Rücksprungadresse auf den Stack, so dass alle nachfolgenden ret jeweils in umgekehrter Reihenfolge wieder die richtigen Rücksprungadressen anspringen.

Bei allen aktuellen AVR-Controllern wird der Stack im RAM angelegt. Der Stack wächst dabei von oben nach unten: Am Anfang wird der Stackpointer (Adresse der aktuellen Stapelposition) auf das Ende des RAMs gesetzt. Wird nun ein Element hinzugefügt, wird dieses an der momentanen Stackpointerposition abgespeichert und der Stackpointer um 1 erniedrigt. Soll ein Element vom Stack heruntergenommen werden, wird zuerst der Stackpointer um 1 erhöht und dann das Byte von der vom Stackpointer angezeigten Position gelesen.

Aufruf von Unterprogrammen

Dem Prozessor dient der Stack hauptsächlich dazu, Rücksprungadressen beim Aufruf von Unterprogrammen zu speichern, damit er später noch weiß, an welche Stelle zurückgekehrt werden muss, wenn das Unterprogramm mit ret oder die Interruptroutine mit reti beendet wird.

Das folgende Beispielprogramm (AT90S4433) zeigt, wie der Stack dabei beeinflusst wird:

Download stack.asm

.include "4433def.inc"     ; bzw. 2333def.inc

.def temp = r16

         ldi temp, RAMEND  ; Stackpointer initialisieren
         out SP, temp

         rcall sub1        ; sub1 aufrufen

loop:    rjmp loop


sub1:
                           ; Hier könnten ein paar Befehle stehen.
         rcall sub2        ; sub2 aufrufen
                           ; Hier könnten auch ein paar Befehle stehen.
         ret               ; wieder zurück

sub2:
                           ; Hier stehen normalerweise die Befehle,
                           ; die in sub2 ausgeführt werden sollen.
         ret               ; wieder zurück

.def temp = r16 ist eine Assemblerdirektive. Diese sagt dem Assembler, dass er überall, wo er „temp“ findet, stattdessen „r16“ einsetzen soll. Das ist oft praktisch, damit man nicht mit den Registernamen durcheinander kommt. Eine Übersicht über die Assemblerdirektiven findet man bei avr-asm-tutorial.net.

Bei Controllern, die mehr als 256 Byte RAM besitzen (z. B. ATmega8), passt die Adresse nicht mehr in ein Byte. Deswegen gibt es bei diesen Controllern das Stack-Pointer-Register aufgeteilt in SPL (Low) und SPH (High), in denen das Low- und das High-Byte der Adresse gespeichert wird. Damit es funktioniert, muss das Programm dann folgendermaßen geändert werden:

Download stack-bigmem.asm

.include "m8def.inc"

.def temp = r16

         ldi temp, HIGH(RAMEND)            ; HIGH-Byte der obersten RAM-Adresse
         out SPH, temp
         ldi temp, LOW(RAMEND)             ; LOW-Byte der obersten RAM-Adresse
         out SPL, temp

         rcall sub1                        ; sub1 aufrufen

loop:    rjmp loop


sub1:
                                           ; Hier könnten ein paar Befehle stehen.
         rcall sub2                        ; sub2 aufrufen
                                           ; Hier könnten auch Befehle stehen.
         ret                               ; wieder zurück

sub2:
                                           ; Hier stehen normalerweise die Befehle,
                                           ; die in sub2 ausgeführt werden sollen.
         ret                               ; wieder zurück

Natürlich wäre es unsinnig, dieses Programm in einen Controller zu programmieren. Stattdessen sollte man es mal mit dem AVR-Studio simulieren, um die Funktion des Stacks zu verstehen.

Als erstes wird mit Project/New ein neues Projekt erstellt, zu dem man dann mit Project/Add File eine Datei mit dem oben gezeigten Programm (stack.asm) hinzufügt. Nachdem man unter Project/Project Settings das Object Format for AVR-Studio ausgewählt hat, kann man das Programm mit Strg+F7 assemblieren und den Debug-Modus starten.

Danach sollte man im Menü View die Fenster Processor und Memory öffnen und im Memory-Fenster Data auswählen.

Das Fenster Processor

  • Program Counter: Adresse im Programmspeicher (Flash), die gerade abgearbeitet wird
  • Stack Pointer: Adresse im Datenspeicher (RAM), auf die der Stackpointer gerade zeigt
  • Cycle Counter: Anzahl der Taktzyklen seit Beginn der Simulation
  • Time Elapsed: Zeit, die seit dem Beginn der Simulation vergangen ist

Im Fenster Memory wird der Inhalt des RAMs angezeigt.

Sind alle drei Fenster gut auf einmal sichtbar, kann man anfangen, das Programm (in diesem Fall „stack.asm“) mit der Taste F11 langsam Befehl für Befehl zu simulieren.

Wenn der gelbe Pfeil in der Zeile out SPL, temp vorbeikommt, kann man im Prozessor-Fenster sehen, wie der Stackpointer auf 0xDF (ATmega8: 0x45F) gesetzt wird. Wie man im Memory-Fenster sieht, ist das die letzte RAM-Adresse.

Wenn der Pfeil auf dem Befehl rcall sub1 steht, sollte man sich den Program Counter anschauen: Er steht auf 0x02 (ATmega8: 0x04).

Drückt man jetzt nochmal auf F11, springt der Pfeil zum Unterprogramm sub1. Im RAM erscheint an der Stelle, auf die der Stackpointer vorher zeigte, die Zahl 0x03 (ATmega8: 0x05). Das ist die Adresse im ROM, an der das Hauptprogramm nach dem Abarbeiten des Unterprogramms fortgesetzt wird. Doch warum wurde der Stackpointer um 2 verkleinert? Das liegt daran, dass eine Programmspeicheradresse bis zu 2 Byte breit sein kann, und somit auch 2 Byte auf dem Stack benötigt werden, um die Adresse zu speichern.

Das gleiche passiert beim Aufruf von sub2.

Zur Rückkehr aus dem mit rcall aufgerufenen Unterprogramm gibt es den Befehl ret. Dieser Befehl sorgt dafür, dass der Stackpointer wieder um 2 erhöht wird und die dabei eingelesene Adresse in den Program Counter kopiert wird, so dass das Programm dort fortgesetzt wird.

A propos Program Counter: Wer sehen will, wie so ein Programm aussieht, wenn es assembliert ist, sollte mal die Datei mit der Endung „.lst“ im Projektverzeichnis öffnen. Die Datei sollte ungefähr so aussehen:

listfile.gif

Im blau umrahmten Bereich steht die Adresse des Befehls im Programmspeicher. Das ist auch die Zahl, die im Program Counter angezeigt wird, und die beim Aufruf eines Unterprogramms auf den Stack gelegt wird. Der grüne Bereich rechts daneben ist der OP-Code des Befehls, so wie er in den Programmspeicher des Controllers programmiert wird, und im roten Kasten stehen die „mnemonics“: Das sind die Befehle, die man im Assembler eingibt. Der nicht eingerahmte Rest besteht aus Assemblerdirektiven, Labels (Sprungmarkierungen) und Kommentaren, die nicht direkt in OP-Code umgewandelt werden. Der grün eingerahmte Bereich ist das eigentliche Programm, so wie es der µC versteht. Die jeweils erste Zahl im grünen Bereich steht für einen Befehl, den sog. OP-Code (OP = Operation). Die zweite Zahl codiert Argumente für diesen Befehl.

Sichern von Registern

Eine weitere Anwendung des Stacks ist das „Sichern“ von Registern. Wenn man z. B. im Hauptprogramm die Register R16, R17 und R18 verwendet, dann ist es i.d.R. erwünscht, dass diese Register durch aufgerufene Unterprogramme nicht beeinflusst werden. Man muss also nun entweder auf die Verwendung dieser Register innerhalb von Unterprogrammen verzichten, oder man sorgt dafür, dass am Ende jedes Unterprogramms der ursprüngliche Zustand der Register wiederhergestellt wird. Wie man sich leicht vorstellen kann, ist ein „Stapelspeicher“ dafür ideal: Zu Beginn des Unterprogramms legt man die Daten aus den zu sichernden Registern oben auf den Stapel, und am Ende holt man sie wieder (in der umgekehrten Reihenfolge) in die entsprechenden Register zurück. Das Hauptprogramm bekommt also, wenn es fortgesetzt wird, überhaupt nichts davon mit, dass die Register inzwischen anderweitig verwendet wurden.

Download stack-saveregs.asm

.include "4433def.inc"            ; bzw. 2333def.inc

.def temp = R16

         ldi temp, RAMEND         ; Stackpointer initialisieren
         out SP, temp

         ldi temp, 0xFF
         out DDRB, temp           ; Port B = Ausgang

         ldi R17, 0b10101010      ; einen Wert ins Register R17 laden

         rcall sub1                ; Unterprogramm „sub1“ aufrufen
 
         out PORTB, R17           ; Wert von R17 an den Port B ausgeben

loop:    rjmp loop                ; Endlosschleife


sub1:
         push R17                 ; Inhalt von R17 auf dem Stack speichern

         ; Hier kann nach belieben mit R17 gearbeitet werden,
         ; als Beispiel wird es hier auf 0 gesetzt.

         ldi R17, 0

         pop R17                  ; R17 zurückholen
         ret                      ; wieder zurück zum Hauptprogramm

Wenn man dieses Programm assembliert und in den Controller lädt, dann wird man feststellen, dass jede zweite LED an Port B leuchtet. Der ursprüngliche Wert von R17 blieb also erhalten, obwohl dazwischen ein Unterprogramm aufgerufen wurde, das R17 geändert hat.

Auch in diesem Fall kann man bei der Simulation des Programms im AVR-Studio die Beeinflussung des Stacks durch die Befehle push und pop genau nachvollziehen.

Sprung zu beliebiger Adresse

Dieser Abschnitt ist veraltet, da nahezu alle ATmega/ATtiny-Typen IJMP/ICALL unterstützen.

Kleinere AVR besitzen keinen Befehl, um direkt zu einer Adresse zu springen, die in einem Registerpaar gespeichert ist. Man kann dies aber mit etwas Stack-Akrobatik erreichen. Dazu einfach zuerst den niederen Teil der Adresse, dann den höheren Teil der Adresse mit push auf den Stack legen und ein ret ausführen:

	ldi ZH, high(testRoutine)
	ldi ZL, low(testRoutine)
	
	push ZL
	push ZH
	ret

        ...
testRoutine:
	rjmp testRoutine

Auf diese Art und Weise kann man auch Unterprogrammaufrufe durchführen:

 
	ldi ZH, high(testRoutine)
	ldi ZL, low(testRoutine)
	rcall indirectZCall
	...


indirectZCall:
	push ZL
	push ZH
	ret

testRoutine:
	...
	ret

Größere AVR haben dafür die Befehle ijmp und icall. Bei diesen Befehlen muss das Sprungziel in ZH:ZL stehen.

Multitasking

Mit nur wenig Aufwand lässt sich kooperatives Multitasking oder Multithreading zwischen 2 Threads realisieren. Nahezu alle AVRs haben das Register SPL oder SPH:SPL zum Lesen und Verändern des Stapelzeigers. Während für den Hauptthread der Stapelzeiger in irgendeiner Form initialisiert und ans Ende des RAM verweist, muss für jeden weiteren Thread ein Stapel-Bereich angelegt werden. Sinnvollerweise im nicht initialisierten RAM.

Für das Umschalten zwischen den beiden Stapeln, mithin der Threads, braucht es noch einen Speicher für den jeweils anderen Stapelzeiger. Für ATtinys mit 8-Bit-adressierbarem RAM (also nur SPL) und genau 2 Threads sieht das so aus:

byte stacksave NOINIT;

void initThread(void(*p)()) {	// p = Funktionszeiger
 static byte stack[30] NOINIT;
 byte*stackend=stack+sizeof stack;
 stacksave=byte(unsigned(stackend-7));	// POP macht Pre-Inkrement auf AVR
// Die vier darüber liegenden Bytes werden zu W16 und Y
// für den Thread. Diese werden bei yield() thread-lokal gesichert.
 stackend[-2]=unsigned(p)>>8;	// Lo und Hi sind vertauscht auf dem Stack!
 stackend[-1]=unsigned(p)&0xFF;	// <p> kommt als Wortadresse, nicht halbieren
}

// Threadwechsel-Funktion, in Assembler implementiert
extern "C" void yield();

void worker() __attribute__((noreturn));
void worker() {
 for(;;) {
// tue irgendwas
  yield();	// typischerweise im Innern von Funktionen, die auf ein Interrupt-Ereignis warten
 }
}

// in der Initialisierungssequenz
initThread(worker);

// in der Hauptschleife
yield();		// zum Worker-Thread

Und der Assembler-Teil:

 
.global yield
// Multithreading: Da die Anzahl der Threads von vornherein bekannt und 2 ist,
// ist diese Yield-Funktion (yield = Vorfahrt gewähren) einfach ein Stack-Toggler.
// Beachte: Obwohl es so aussieht als ob yield nur W24 verändert,
// verändert diese Funktion (aus Sicht des Aufrufers) alle Register
// und rettet nur W16 und Y.
// Interessanterweise müssen keine Interrupts gesperrt werden!
// (Das ist erforderlich bei AVR-Controllern _mit_ SPH, falls man nicht
// geschickterweise beide Stacks in eine „Page“ mit gleichem SPH sperrt.)
yield:	push	YH
		push	YL
		push	r17
		push	r16
		in		r24,SPL
		lds		r25,stacksave
		sts		stacksave,r24
		out		SPL,r25
		pop		r16
		pop		r17
		pop		YL
		pop		YH
		ret			// zum anderen Thread

Für mehr als 2 Threads wird das Ganze deutlich aufwändiger und lohnt sich nur noch für ATmegas. Diese Implementierung ist kompatibel zur avr-g++-ABI und „wegreservierten“ Registern R2..R15.

Das Ganze beißt sich prinzipiell nicht mit sleep_cpu().

Preemptives Multitasking entsteht daraus, wenn die (eine) Yield-Funktion vom Timer zyklisch aufgerufen wird. Dabei müssen ALLE, wirklich ALLE Register (auch R0 und R1) (außer die wegreservierten) sowie die Flags gerettet werden. Das benötigt mehr als die o.a. 30 Byte Platz auf dem Stack.

Weitere Informationen (von Lothar Müller)

(Der in dieser Abhandlung angegebene Befehl MOV ZLow, SPL muss IN ZL, SPL heißen, da SPL und SPH I/O-Register sind. Ggf. ist auch SPH zu berücksichtigen → 2-Byte-Stack-Pointer.)