Adressierung

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Mikrocontroller und -prozessoren bieten in der Regel mehrere Möglichkeiten an, um auf Daten zuzugreifen. An dieser Stelle sollen die grundlegenden Adressierungsarten der AVR-Controller mit internem SRAM behandelt werden.

Immediate-Werte

Eigentlich keine Adressierungsart, aber dennoch sehr wichtig ist die Möglichkeit, direkt konstante Werte in ein Register zu schreiben. Dabei ist schon zur Entwicklungszeit bekannt, welcher Wert in welches Register geladen werden soll.

    ldi     r16, 0xA0           ; Schreibt den Wert 0xA0 in das Register r16

ldi steht hierbei für load immediate. Bei AVR-Mikrocontrollern ist das direkte Laden von Werten nur mit den Registern r16 bis r31 möglich.

Direkte Adressierung

Um auf Daten im Speicher zuzugreifen, muss man selbstverständlich wissen, wo sich diese Daten befinden. Will man z. B. den Inhalt eines Registers in eine Speicherzelle schreiben, so muss das Mikroprogramm die Adresse der gewünschten Speicherzelle kennen. Eine einfache Möglichkeit der Adressierung ist es, dem Befehl die Adresse direkt mitzuteilen.

.dseg
variable:  .byte 1              ; Ein Byte im SRAM reservieren.
                                ; Da davor das Label "variable" steht, wird jedes Vorkommen von
                                ; "variable" durch die eigentliche Adresse der reservierten
                                ; Speicherzelle ersetzt.
.cseg
    ldi     r16, 25             ; Den direkten Wert 25 in das Register r16 schreiben (immediate)
    sts     variable, r16       ; Den Inhalt von Register r16 (also 25) in die Speicherzelle
                                ; mit der Adresse "variable" schreiben. Wie oben beschrieben
                                ; ersetzt der Assembler "variable" mit der eigentlichen Adresse.

Die Adresse der Speicherzelle wird also schon zur Entwicklungszeit im Assembler-Befehl eingetragen, was nach sich zieht, dass so ein Befehl nur auf Speicherzellen zugreifen kann, deren Adressen schon im Vorhinein bekannt sind. Da variable in obigem Beispiel eine Adresse und somit nur eine Zahl darstellt, kann man zur Entwicklungszeit auch Konstanten addieren:

.dseg
variable2: .byte 2              ; Zwei Bytes im SRAM reservieren. Dabei ist variable2 die Adresse
                                ; der ERSTEN Speicherzelle von den beiden reservierten.

.cseg
    ldi     r16, 17             ; Den direkten Wert 17 in das Register r16 schreiben (immediate)
    sts     variable2, r16      ; Diesen Wert schreiben wir nun an die Speicheradresse variable2 (1. Byte).
    inc     r16                 ; Register r16 inkrementieren, also um 1 erhöhen.
    sts     variable2+1, r16    ; Hier schreiben wir das zweite Byte von variable2.

Nun steht in diesem Beispiel im ersten Byte die Zahl 17 und im zweiten Byte die Zahl 18. Dabei muss man beachten, dass die Addition im sts-Befehl bereits während der Assemblierung und nicht vom Mikrocontroller durchgeführt wird. Das ist der Fall, weil die Adresse der reservierten Speicherzelle schon zu dieser Zeit berechnet worden ist. Somit ist natürlich auch die Adresse + 1 bekannt.

Indirekte Adressierung

Wenn wir nur die direkte Adressierung zur Verfügung haben, stoßen wir schnell an Grenzen. Betrachten wir folgendes Beispiel:

Wir sollen Code schreiben, welcher eine variable Anzahl an Zahlen addieren soll. Die Zahlen stehen bereits hintereinander im Speicher, beginnend mit der Adresse zahlen_start, und im Register r16 steht, wie viele Zahlen es sind. Man merkt leicht, dass dies mit direkter Adressierung nur schwer möglich ist, denn es ist zur Entwicklungszeit noch nicht bekannt, wie viele Zahlen es sind.

Wir lösen diese Aufgabe, indem wir eine Schleife programmieren, die die Zahlen liest und aufaddiert, und das ganze so oft, wie im Register r16 steht. Da wir hier von einer Schleife reden, müssen wir bei jedem Lesen aus dem Speicher mit demselben Befehl auf eine andere Speicherzelle zugreifen. Wir brauchen also die Möglichkeit, die Adresse dynamisch im Programmablauf zu ändern. Dieses bietet uns die indirekte Adressierung, bei der die Adresse der gewünschten Speicherstelle in einem Register steht.

Bei AVR-Mikrocontrollern gibt es dafür drei 16 Bit breite Register, die jeweils aus zwei 8-Bit-Registern bestehen. Dies rührt daher, dass ein 8-Bit-Register nur maximal 256 verschiedene Speicherzellen adressieren könnte, was für Mikrocontroller mit mehr Speicher nicht ausreicht. Die Register (r26, r27) und (r28, r29) und (r30, r31) bilden die besagten drei 16 Bit breiten Register zur indirekten Adressierung. Da diese Register auf Daten zeigen, nennt man sie logischerweise Zeigerregister (engl. Pointer). Sie tragen die Namen X, Y und Z, wobei die einzelnen 8-Bit-Register neben ihren rxx-Namen auch mit XL, XH, YL, YH, ZL und ZH angesprochen werden können. L (low) bzw. H (high) bedeutet hierbei, dass die unteren respektive die oberen 8 Bits der 16-Bit-Adresse gemeint sind.

Zeigerregister des AVR
Register alternativer Name 16-Bit Zeigerregister
r26 XL X
r27 XH
r28 YL Y
r29 YH
r30 ZL Z
r31 ZH

Wir werden beispielhalber das Z-Register für unser Problem verwenden. Dazu müssen wir zunächst die Adresse der ersten Zahl in dieses laden. Da das Z-Register 16 Bit breit ist, müssen wir ZH und ZL in zwei einzelnen ldi-Operationen beschreiben. Der AVR-Assembler bietet uns hier zwei praktische Funktionen: Mit LOW(...) und HIGH(...) bekommt man die unteren respektive die oberen 8 Bit einer Speicheradresse. Das kommt uns gerade recht, da wir gerade die unteren/oberen 8 Bit der Adresse in die Register ZL/ZH schreiben wollen.

Dann können wir mit dem ld-Befehl die Zahl von der Speicherstelle lesen, auf die das Z-Register verweist. Wir schreiben den Wert in das Register r17. Zum Aufsummieren wollen wir das Register r18 verwenden, welches ganz zu Anfang mit clr auf 0 gesetzt wird.

.dseg
zahlen_start: .byte 20              ; 20 Byte reservieren, das soll die Maximalanzahl sein

.cseg
; Irgendwo vorher werden die Zahlen geschrieben, das interessiert
; erstmal nicht weiter, wie das geschieht. Wir gehen jetzt davon aus,
; dass beginnend bei der Speicheradresse zahlen_start so viele Zahlen
; im Speicher stehen, wie im Register r16 steht.

    ldi     ZL, LOW(zahlen_start)   ; ZL mit den unteren 8 Bits der Adresse initialisieren
    ldi     ZH, HIGH(zahlen_start)  ; ZH mit den oberen 8 Bits der Adresse initialisieren
    clr     r18                     ; r18 auf 0 initialisieren
schleife:
    ld      r17, Z                  ; Inhalt der von Z adressierten Speicherstelle in r17 lesen
    adiw    ZH:ZL, 1                ; Z inkrementieren, da wir gleich die darauffolgende
                                    ; Zahl lesen wollen. adiw eignet sich für 16-Bit-Addition
    add     r18, r17                ; Aufsummieren
    dec     r16                     ; Wir erniedrigen r16 um 1
    brne    schleife                ; Solange r16 ungleich 0, zu "schleife" springen

; An dieser Stelle ist die Schleife fertig und in r18 steht das Ergebnis.

Das Programm funktioniert zwar schon, aber eine Sache ist unpraktisch: Das Z-Register muss jedes Mal manuell inkrementiert werden, um im nächsten Schleifendurchlauf die nächste Zahl zu lesen. Da das sequenzielle Lesen oder Schreiben von Daten aus dem bzw. in das SRAM sehr oft in Programmen vorkommt, gibt es folgende Möglichkeiten:

Postinkrement

Die beiden Zeilen

    ld      r17, Z                  ; Inhalt der von Z adressierten Speicherstelle in r17 lesen
    adiw    ZH:ZL, 1                ; Z inkrementieren

können durch folgende Zeile ersetzt werden:

    ld      r17, Z+                 ; Inhalt der von Z adressierten Speicherstelle in r17 lesen
                                    ; und danach Z automatisch inkrementieren

Das spart Ausführungszeit und macht den Code kürzer. Zu beachten ist, dass die Inkrementierung erst nach der Ausführung des (eigentlichen) Ladevorgangs durchgeführt wird.

Predekrement

Äquivalent zum Postinkrement gibt es auch die Möglichkeit des Dekrementierens. Hierbei wird der Wert jedoch vor der Ausführung des Ladevorgangs dekrementiert. Das Predekrement eignet sich, um rückwärts durch linear angeordnete Daten zu gehen.

    ld      r17, -Z                 ; Z dekrementieren und DANACH Inhalt der
                                    ; von Z adressierten Speicherstelle in r17 lesen