Unterprogramme und Stack - Gestapelte Zahlen

 

Etwas Hardware

 

Der Aufbau des Lehrgangs  Erste Befehle - Mit Assembler das Laufen lernen reicht hier vollkommen aus.

 

 

Der Stack wird eingerichtet

 

In fast allen Programmen, gibt es Routinen die öfter benötigt werden und eigentlich immer wieder das Gleiche machen. Solche Routinen lagert man in Hochsprachen in Unterroutinen aus. Aber auch in Assembler ist dies möglich.

 

Hierzu muss man in Assembler ein Konstrukt haben, womit der Prozessor weiß, wohin er nach dem Ende des Unterprogramms zurück kehren soll. Dieses Konstrukt ist der Stack. Man kann sich den Stack wie ein Stapel kleiner Zettel vorstellen, auf denen die jeweiligen Rücksprungadressen stehen. Schauen wir uns einmal an, wie so ein Unterprogrammaufruf in Assembler aussehen muss:

 

- Hauptprogramm will Unterprogramm aufrufen

- Aktuelle Programmadresse wird auf den Stack gelegt

- Unterprogramm wird aufgerufen

- Abarbeiten des Unterprogramms

- Rücksprungadresse wird vom Stack geholt

- Es wird an die geholte Adresse zurück gesprungen

- Hauptprogramm wird nach Unterprogrammaufruf fortgesetzt

 

Sollen innerhalb des Unterprogramms weitere Unterprogramme aufgerufen werden, so werden auch dessen Rücksprungadressen auf dem Stack abgelegt. So weiß der Prozessor immer wohin er nach dem Unterprogramm zurück kehren soll.

 

Um nun den Stack zu verwenden, muss dieser erst eingerichtet werden. Hierzu besitzt der AVR einen so genannten Stack-Pointer. Dies ist ein spezielles Register welches immer auf das Ende des Stacks zeigt. Soll Werte auf den Stack abgelegt werden, so wird der Wert an die Position gespeichert, welche der Stack-Pointer angibt und anschließend wird der Stack-Pointer verringert.

 

Warum verringert? Müsste dieser nicht erhöht werden? Eine Besonderheit von Stack-Pointern, und nicht nur bei der AVR-Familie ist es, dass der Stack immer nach unten 'wächst'. Daher wird der Stack in der Regel auch am Ende des Arbeitsspeichers gelegt. Da der Stackpointer im IO-Bereich des AVR liegt müssen wir diesen mit dem out-Befehl laden. Des Weiteren ist der Stack-Pointer 16 Bit breit wodurch wir 2 out-Befehle brauchen.

 

Bauen wir die Einrichtung des Stacks ein mal in unser Blinkprogramm ein:

 

.include    "m8def.inc"

Start:

    ldi     r16,LOW(RAMEND)

    out     SPL,r16

    ldi     r16,HIGH(RAMEND)

    out     SPH,r16
    ldi     r16,0xFF
    out     DDRD,r16

    ...

 

Nun ist der AVR vorbereitet und wir können den Stack z.B. durch Unterprogramme verwenden. Wie ruft man aber nun ein Unterprogramm auf?

 

Hierzu dient der rcall-Befehl. Will man von einem Unterprogramm zurück kehren, muss der ret-Befehl eingesetzt werden. Mit diesem Wissen können wir unser LED-Blinkprogramm umbauen:

 

.include    "m8def.inc"
 

Start:

    ldi     r16,LOW(RAMEND)

    out     SPL,r16

    ldi     r16,HIGH(RAMEND)

    out     SPH,r16
    ldi     r16,0xFF
    out     DDRD,r16

Schleife:
    ldi     r16,0b00000001
    out     PORTD,r16

    rcall   Warte

    ldi     r16,0b00000000

    out     PORTD,r16

    rcall   Warte

    rjmp    Schleife

 

Warte:

    ldi     r18,5

Warte1:

    ldi     r17,195

Warte2:

    ldi     r16,170

Warte3:

    dec     r16

    brne    Warte3

    dec     r17

    brne    Warte2

    dec     r18

    brne    Warte1

    ret

 

Wie man nun sehr schön sehen kann, wurde das Programm um Einiges kleiner. Das Hauptprogramm wurde auch gleich wieder übersichtlicher da, anstelle der ganzen Warteroutine, jetzt nur noch der rcall-Befehl zur Warteroutine steht.

 

 

Homogene Unterprogramme

 

Wir haben mit unserem Unterprogramm ein kleines Problem. Es werden bei der Warteroutine die Register r16, r17 und r18 verwendet. Dies bedeutet, dass wir, bei größeren Programmen, diese Register nicht verwenden können. Noch problematischer wird dies, wenn z.B. ein Unterprogramm ein weiteres Unterprogramm aufruft und ebenfalls die gleichen Register benötigt. Da wäre Chaos vorprogrammiert.

 

Eine Lösung ist, dass wir die verwendeten Register in der Unterroutine irgendwie abspeichern und, sobald die Routine verlassen werden soll, wieder restaurieren.

 

Auch hierzu kann der Stack verwendet werden. Um Registerinhalte auf dem Stack abzulegen, gibt es den Befehl 'push'. Sollen Registerinhalte wieder restauriert werden, muss man dies mit 'pop' machen. Man muss aber tunlichst darauf achten, dass man das Register, welches wir als erstes auf dem Stack sichern, als letztes wieder restaurieren. Ansonsten kommt es zu einem Daten-Chaos.

 

Schauen wir uns nun einmal das Unterprogramm mit den gesicherten Register an:

 

Warte:

    push    r16

    push    r17

    push    r18

    ldi     r18,5

Warte1:

    ldi     r17,195

Warte2:

    ldi     r16,170

Warte3:

    dec     r16

    brne    Warte3

    dec     r17

    brne    Warte2

    dec     r18

    brne    Warte1

    pop     r18

    pop     r17

    pop     r16

    ret

 

Nun bekommt das Hauptprogramm nicht mehr viel vom Unterprogramm mit. Man kann die Register einsetzen, als würde das Unterprogramm nicht aufgerufen. Es ist nur die Wirkung, in diesem Fall 0,5s warten, zu merken.

 

 

Parameterübergabe

 

Unser kleines Blinkprogramm ist ja schon ganz nett. Was aber, wenn wir eine andere Blinkfrequenz haben wollen? Dann muss die Warteroutine entsprechend umgeschrieben werden. Und was ist, wenn wir die Routine mit unterschiedlichen Zeiten haben wollen? Z.B. 0,1s LED an und 0,4s LED aus? Muss dann eine 2. Routine geschrieben werden, nur mit einer anderen Zeit?

 

Natürlich nicht. Wir lagern nur die Registerzuweisung für r18 ins Hauptprogramm aus. Da wir den Inhalt von r18 im Hauptprogramm nicht weiter benötigen, können wir auch auf das Sichern dieses Registers in der Warteroutine verzichten.

 

Ändern wir doch auch gleich die beiden Zeiten. Unser endgültiges Programm sieht nun so aus:

 

.include    "m8def.inc"
 

Start:

    ldi     r16,LOW(RAMEND)

    out     SPL,r16

    ldi     r16,HIGH(RAMEND)

    out     SPH,r16
    ldi     r16,0xFF
    out     DDRD,r16

Schleife:
    ldi     r16,0b00000001
    out     PORTD,r16

    ldi     r18,1

    rcall   Warte

    ldi     r16,0b00000000

    out     PORTD,r16

    ldi     r18,4

    rcall   Warte

    rjmp    Schleife

 

Warte:

    push    r16

    push    r17

Warte1:

    ldi     r17,195

Warte2:

    ldi     r16,170

Warte3:

    dec     r16

    brne    Warte3

    dec     r17

    brne    Warte2

    dec     r18

    brne    Warte1

    pop     r17

    pop     r16

    ret

 

 

Zurück zur Auswahlseite            Zur Hauptseite