www.mikrocontroller.net

AVR-Tutorial: 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.

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.

Inhaltsverzeichnis

[Bearbeiten] 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 hier.

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 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.

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 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.

Das Fenster Processor

  • Program Counter: Adresse im Programmspeicher (ROM), 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 3 Fenster gut auf einmal sichtbar, kann man anfangen, das Programm 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.

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. 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.

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:

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.

[Bearbeiten] 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.

[Bearbeiten] Sprung zu beliebiger Adresse

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.

[Bearbeiten] Weitere Informationen (von Lothar Müller):

(Der in dieser Abhandlung angegebene Befehl MOV ZLow, SPL muss für einen ATmega8 IN ZL, SPL heißen, da hier SPL und SPH ein I/O-Register sind. Ggf ist auch SPH zu berücksichtigen --> 2byte Stack-Pointer)


webmaster@mikrocontroller.netImpressumNutzungsbedingungenWerbung auf Mikrocontroller.net