Pfad: Home => AVR-Überblick => Programmiertechniken => Adressieren =>> SRAM-Zugriffe    (This page in English: Flag EN) Logo

Addressierung von Speicherzellen in AVRs

  1. Adressierung von SRAM-Speicher,
    1. Adressieren von SRAM/Registern/Portregistern
    2. Adressieren mit Festadresse
    3. Zeiger-Adressierung
    4. Zeigererhöhung
    5. Zeigerverminderung
    6. Ablage von Zeigern
  2. Adressierung von Portregistern,
  3. Addressierung von EEPROM-Speicherzellen, und
  4. Addressierung von Flash-Speicherzellen.

1.1 Adressieren von SRAM/Registern/Portregistern

Adressräme in AVRs Das allerwichtigste, wenn es um das Adressieren von Speicherzellen geht, ist schon mal der Unterschied in Adresstypen. Es gibt zwei davon:
  1. Physische Adressen, und
  2. Zeigeradressen.
Nur in Ausnahmefällen sind beide identisch, meistens sind sie unterschiedlich. Im Falle des statischen RAMs haben beide Adressen die folgenden Werte: Beim Zugriff auf SRAM wird immer nur die Zeigeradresse verwendet, die physische Adresse spielt hingegen keine Rolle. Das unterscheidet SRAM von Portregistern, bei denen beide Adressierungen eine Rolle spielen.

In der Konsequenz bedeutet das, dass beim ersten Umschalten des Assemblers mit der Direktive .DSEG auf das SRAM-Segment automatisch die Adresse SRAM_START als Segmentzeiger geladen wird. Woran merkt man das? Nun, man kann die def.inc-Datei nach SRAM_START durchsuchen. Man kann aber auch, etwas komfortabler, nach dem .DSEG ein Label setzen. Label sind dabei Marker, die immer eine Adresse festsetzen. Label-Symbole enden immer mit einem Doppelpunkt. Aufgabe der Umschaltung auf das Datensegment ist einzig das Setzen solcher Marker.

Wenn man jetzt mit meinem Assembler gavrasm mit gesetzter S-Option assembliert oder auch mit dem Simulator avr_sim assembliert, z. B. diesen Quellcode:

.nolist
.include "m324PBdef.inc" ; Definiere Typ ATmega324PB
.list
.dseg
DieErsteSramSpeicherzelle:
.cseg

Symboltabelle nach dem Assemblieren dann findet sich im erzeugten Assembler-Listing am Ende diese Tabelle, aus der man bequem die Label-Adresse 0x0100 dieses AVR-Typs herauslesen kann. Andere Assembler produzieren solche Tabellen nicht!

Damit man bequem auch das RAM-Ende herausfinden kann, geht man ein bisschen anders vor: mit .equ MeinRamEnde = RAMEND kopiert man die Konstante RAMEND in eine eigene Konstante und kann deren Wert dann aus der Symboltabelle (jetzt unter: Type C) herauslesen. Natürlich produziert das Zahlen, die für jeden AVR-Typ spezifisch sind.

Einfach die Direktive .NOLIST vor der def.inc-Include zu entfernen, kann nicht empfohlen werden, weil man dann mit Hunderten Einträgen vollgemüllt wird, nach denen man gar nicht suchen will.

Wer mit dem Simulator avr_sim unterwegs ist, kann das alles noch bequemer haben: einfach unter "View" und "Symbols" mit dem Suchbegriff "RAM" suchen und schon taucht unter anderem RAMEND in der kurzen Liste auf.

1.2 Adressierung von SRAM mit fester Adresse

SRAM kann eine physische Größe von bis zu 32.768 Bytes haben. Da die zu RAMEND umgeformte Zeigeradresse noch etwas höher ist, sind solche Adressen nicht nur 15 Bits lang sondern ganze 16 Bits. Zeigeradressen haben daher stets 16 Bits Länge.

Solche Zeigeradressen können direkt mit den Instruktionen STS 16-Bit-Adresse,Register oder LDS Register,16-Bit-Adresse beschrieben und gelesen werden. Die 16-Bit-Adresse folgt dabei auf das Instruktionswort, so dass beide Instruktionen aus zwei Befehlsworten bestehen. Als Register kann jedes zwischen R0 und R31 verwendet werden.

Der folgende Quellcode schreibt 0xAA (oder binär 0b10101010) in die erste und die 16-te Speicherzelle des SRAM. Um die Sinnhaftigkeit der Zeigeradressierung zu demonstrieren, wird dann dieses Byte auch noch in die Register R0 und R15 geschrieben.

.dseg
ErsteSramZelle: ; Ein label mit dieser Adresse
;
.cseg
  ldi R16,0xAA
  sts ErsteSramZelle,R16 ; Schreibe in die erste SRAM-Zelle
  sts ErsteSramZelle+15,R16 ; Und auch in die 16-te Zelle
  sts 0,R16 ; Und in das Register R0
  sts 15,R16 ; Sowie in das Register R15

Schreiben in das SRAM mit STS Schreiben in die Register mit STS Links sehen wir die beiden in das SRAM geschriebenen Bytes, rechts die in die Register geschriebenen Bytes.

Wenn wir nicht schreiben, sondern lesen wollen, geht das so:

  lds R16,ErsteSramZelle ; Lesen aus der ersten SRAM-Zelle
  lds R17,ErsteSramZelle+15 ; Lesen aus der 16-ten SRAM-Zelle
  lds R18,0 ; Lesen aus dem Register R0
  lds R19,15 ; Lesen aus dem Register R15

Wenn Du Deinen Controller gerne mit Unsinn beschäftigen willst: lds R16,16 oder sts 16,R16 sind dafür wunderschöne Beispiele: der Controller macht jeden Unsinn mit, kein Fensterchen warnt vor unnötigen und unsinnigen Instruktionen.

Assembler-Listing der vier LDS-Instruktionen Alle vier Lese-Instruktionen verwenden feste Adressen, die den LDS-Instruktionen einfach als weiteres Wort angefügt sind.

1.3 Zugriff auf SRAM-Zellen mit Zeigern

Adressieren von SRAM-Zellen mit X-, Y- und Z-Zeiger Um nicht nur auf einzelne Zellen des SRAM's zugreifen zu können, sondern auf ganze, mehr oder weniger große Bereiche des SRAM's zugreifen zu können, braucht es 16-Bit-Zeiger. Die AVR's haben gleich drei solcher Zeiger, nämlich Doppelregister, im Angebot: Um das Doppelregister X auf die erste SRAM-Zelle zeigen zu lassen, brauchen wir die zwei folgenden Instruktionen:

.dseg
ErsteSramZelle: ; Platzieren eines Labels an diese Adresse
.cseg
  ldi XH,High(ErsteSramZelle) ; Setze das MSB der Adresse in Register XH
  ldi XL,Low(ErsteSramZelle) ; Setze das LSB der Adresse in Register XL

X zeigt nun als Zeiger auf diese Adresse. Um 0xAA an diese Adresse zu schreiben, fügen wir folgende Instruktionen hinzu:

  ldi R16,0xAA ; Schreibe AA in Register R16
  st X,R16 ; und in die erste SRAM-Zelle

Selbiges könnten wir in gleicher Weise auch mit Y=YH:YL und Z=ZH:ZL veranstalten.

Das ist nun noch nicht so sehr fortgeschritten, da wir hier mit vier Instruktionen nur das machen, was wir mit zwei Instruktionen schon oben mit festen Adressen gemacht haben. Aber gemach, Zeiger können noch viel mehr.

1.4 Adressierung von SRAM-Zellen mit erhöhendem Zeiger

Nun könnten wir die ersten 16 Bytes des SRAMs mit 0xAA befüllen, indem wir die Zeigeradresse immerzu um Eins erhöhen:


.dseg
ErsteSramZelle: ; Platzieren eines Labels an diese Adresse
.cseg
  ldi XH,High(ErsteSramZelle) ; Setze das MSB der Adresse
  ldi XL,Low(ErsteSramZelle) ; Setze das LSB der Adresse
  ldi R16,0xAA ; Schreibe AA in das Register R16
Fuellschleife:
  st X+,R16 ; und in die SRAM-Zelle und erhoehe dann die Zeigeradresse
  cpi XL,Low(ErsteSramZelle+16) ; Pruefe ob das Ende des Bereichs erreicht ist
  brne Fuellschleife ; Nein, befuelle weiterhin

Der Status zu Beginn Dies hier zeigt die Bedingungen beim Start:

Der erste Schritt ist gemacht Der erste Schritt, ein Durchlauf durch die Schleife, ist gemacht. Das ST hat den Inhalt von R16 in die erste Zelle geschrieben. Das Plus-Zeichen hinter X hat bewirkt, dass der Zeiger X danach um Eins erhöht wurde. Das Plus bewirkt, dass aus eigentlich zwei Instruktionen
  1. ST X,R16, und
  2. ADIW XL,1,
eine einzige wird. Das Plus nennt sich auch Auto-Inkrement. Anstelle der vier Takte, die diese beiden Instruktionen benötigen würden, braucht ST X+ aber nur zwei: plus oder nicht plus braucht diesselben Takte.

Im letzten Schritt wurde geprüft, ob der LSB des Zeigers schon auf jenseits des Füllbereichs zeigt. Die letzte Zelle des Füllbereichs ist dann überschritten, wenn das LSB des Zeigers auf LetzteSramZellePlus1: zeigt. Dann wären wir fertig. Weil wir nur das untere Byte überprüfen, können wir damit nicht mehr als 256 Speicherzellen vollschreiben. Wer mehr als 256 Bytes schreiben will, muss auch das MSB überprüfen, wie hier:

.dseg
ErsteSramZelle: ; Platzieren Label
  .byte 512 ; Definiere die Laenge des Bereichs
LetzeSramZellePlus1:
;
.cseg
  ldi XH,High(ErsteSramZelle) ; Setze das MSB
  ldi XL,Low(ErsteSramZelle) ; Setze das LSB
  ldi R16,0xAA ; Schreibe AA in Register R16
Fuellschleife:
  st X+,R16 ; in die SRAM-Zelle und Zeiger erhoehen
  cpi XH,High(LetzteSramZellePlus1) ; Pruefe MSB
  brne Fuellschleife
  cpi XL,Low(LetzteSramZellePlus1) ; Pruefe LSB
  brne Fuellschleife

Nun kann unser Bereich so lange sein wie er will, er kriegt in Gänze seinen Festwert.

1.5 Adressieren von SRAM-Zellen mit Rückwärtszeiger

Automatisches Erhöhen kennen wir jetzt, aber kann man auch von hinten nach vorne adressieren?

Als ein etwas eigenwilliges, aber anschauliches Beispiel dafür kopieren wir einen Text rückwärts, also spiegelverkehrt, von einem SRAM-Bereich in einen anderen.

Text im SRAM Zuallererst machen wir einen Text in das SRAM. Das geht so:

.dseg
  Textbereich:
  .byte 16
  TextbereichEnde:
.cseg
  ldi XH,High(Textbereich)
  ldi XL,Low(Textbereich)
  ldi R16,'a'
Fuellschleife:
  st X+,R16
  inc R16
  cpi XL,Low(TextbereichEnde)
  brne Fuellschleife

Das produziert einen schönen Text. mit ldi R16,'a' haben wir den ersten Buchstaben gesetzt und diesen in der Schleife um Eins erhöht (ja, ihr lieben C-Sklaven: das geht, man kann in Assembler auch mit Buchstaben rechnen und den Buchstaben mit SUBI R16,-1 um Eins erhöhen, kein Problem! In C darfst Du Typsklave das nicht, da darfst Du nicht mit ASCII-Zeichen einfach rechnen!).

Nun soll dieser Text, der auch weniger regelmäßig sein könnte, von hinten nach vorne in einen anderen SRAM-Bereich kopiert werden. Dazu brauchen wir einen weiteren Zeiger, also Y oder Z, und entweder der erste oder der zweite Zeiger muss abwärts gezählt werden, während der andere aufwärts zählt. Wenn du nun mutmaßen solltest, dass Du nur Y- statt Y+ schreiben müsstest, um den zweiten Zeiger Y rückwqärts laufen zu lassen, hast Du das Prinzip zwar verstanden. Aber der Assembler gavrasm quittiert das mit dieser Fehlermeldung:

Fehlermeldung des Assemblers mit ST Y- Nun, aus der Fehlermeldung geht hervor, dass das Minuszeichen vor dem X, Y oder Z stehen muss. Aber das ist nicht nur Schreibstil, sondern hat auch noch weitere Konsequenzen: das Auto-Dekrement passiert zuerst, danach erfolgt dann erst der Zugriff auf die SRAM-Zelle. Also muss der Y-Zeiger zu Beginn auf die Zelle nach dem Ziel für das erste zu kopierende Zeichen stehen, damit beim vorausgehenden Auto-Dekrement die korrekte Zelle angesteuert wird.

Umgekehrte Kopie des Texts Dies hier ist der gesamte Quellcode des umgekehrten Kopierens.

.dseg
Textbereich:
.byte 16
TextbereichEnde:
.byte 16
Kopierbereich:
.byte 16
KopierbereichEnde:
;
.cseg
  ldi XH,High(Textbereich)
  ldi XL,Low(Textbereich)
  ldi R16,'a'
Fuellschleife:
  st X+,R16
  inc R16
  cpi XL,Low(TextbereichEnde)
  brne Fuellschleife
;
  ldi XH,High(Textbereich)
  ldi XL,Low(Textbereich)
  ldi YH,High(KopierbereichEnde)
  ldi YL,Low(KopierbereichEnde)
Kopierschleife:
  ld R16,X+
  st -Y,R16
  cpi XL,Low(TextbereichEnde)
  brne Kopierschleife

Der erste Teil des Programms funktioniert genauso wie das Befüllen mit einer Konstante, nur wird hier R16 bei jedem Schleifendurchlauf um Eins erhöht, so dass wir die Reihe mit 'a' bis 'p' vollkriegen.

Zeiger X beim Lesen Hier ist der Zeiger X beim Lesen dargestellt. Zu sehen ist der Beginn und darunter der erste Leseschritt mit X+ sowie der letzte Leseschritt.

Zeiger Y beim Schreiben Hier ist der Schreibvorgang zu sehen. Oben ist der Beginn, darunter der erste Schreibschritt und ganz unten der letzte Schreibschritt zu sehen.

Die beiden Schritte, Lesen und Schreiben, werden in der Kopierschleife so lange wiederholt, bis entweder Dies war ein schnelles Umdrehen des Textes, weil alle Adressmanipulationen das Auto-Inkrementieren und -Dekrementieren verwenden und keinerlei langwierige ADIW- oder SBIW-Instruktionen nötig wurden. Das sind so die Vorteile, die ein gut durchdachter Befehlssatz wie der von AVRs so bietet. Aber der kennt noch einen weiteren Befehl.

1.6 Zugriff auf SRAM-Zellen mit Verschiebung (displacement)

AVRs haben einen zusätzlichen Adressierungsmodus: er basiert auf der zeitweiligen Addition von konstanten Werten zum Zeigerwert. Zeitweilig deswegen, weil der eigentliche Zeigerwert, anderes als beim Auto-Inkrement, dabei gar nicht verändert wird, nur das Schreiben oder Lesen findet an der temporären Stelle statt.

Diese Art der Adressierung wird auch als "indirekt" bezeichnet. Es geht aber nur mit den Zeigerregistern Y und Z, nicht mit X. Die beiden Instruktionen dafür heißen STD und LDD. Beim Schreiben lautet das Mnemonic STD Y/Z+d,Register beim Lesen LDD Register,Y/Z+d. d ist dabei die Verschiebung, die zur Addresse in Y oder Z temporär addierte Konstante.

In unserer Textreihe im vorherigen Kapitel würde, wenn Z auf den Textbereichs-Beginn zeigen würde, LDD R16,Z+1 das Zeichen 'b' in das Register R16 schreiben, LDD R16,Z+2 das Zeichen 'c'. Ein STD ersetzt daher folgende drei Instruktionen:

  adiw Z,d ; Addiese Verschiebung zu Z
  st Z,R16 ; Schreibe R16 an die verschobene Adresse
  sbiw ZL,d ; Subtrahiere Verschiebung von Z  

Der Unterschied zwischen STD und dieser Ersatzformulierung ist, dass STD und LDD sind in Fällen nützlich, wenn auf benachbarte Zellen zugegriffen werden soll oder wenn mit dem Zeiger zwischen ganzen Datensätzen gewechselt werden soll. Ein Beispiel dafür.

Record-Datenstruktur Nehmen wir an, dein ADC hat acht Kanäle, jeder Kanal braucht einen eigenen Speicherbereich, in dem die Summe der Einzelmessungen gebildet wird, die dann mit einem spezifischen 8-Bit-Multiplikator zu multiplizieren ist, dessen Ergebnis als 16-Bit-Wert benötigt wird und der mit einem kanalspezifischen Unterprogramm mit eigener Adresse weiterverarbeitet werden muss. Normalerweise würde man die Adresse des aktiven Kanals in einen Zeiger schreiben, aber bei jeder Operation, bei der auf einen der Werte im Kanal zugegriffen werden muss, müsste der Zeiger mit ADIW diesem speziellen Wert angepasst werden. Genau das braucht man aber mit STD und LDD nicht: es genügt, die Basisadresse nach Y zu schreiben und mit LDD R16,Y+3 kann man den Multiplikator des Kanals kriegen. Auch alle anderen Werte der Datenstruktur eines Kanals sind so direkt zugänglich, ohne dass Zeigeranpassungen nötig würden. So kann dann beispielsweise der Messwert zur Summe addiert werden:

  ld R16,Y ; Lese das LSB der Summe
  add R16,R0 ; Addiere das LSB in R0
  st Y,R16 ; Speichere das LSB
  ldd R16,Y+1 ; Lese das MSB der Summe mit Verschiebung
  adc R16,R1 ; Addiere das MSB und das Carry
  std Y+1,R16 ; Speichere das MSB mit Verschiebung

So ein Record kann übrigens maximal 64 Bytes groß sein.

Ein String im SRAM Aber die zeitweilige Verschiebung von Zeigern kann auch in anderen Fällen hilfreich sein. Als Beispiel ein Textstring im SRAM wie der abgebildete. Wir wissen schon aus den vorangegangenen Kapiteln, wie man so was in das SRAM geschrieben kriegt.

Nun soll an einer beliebigen Stelle, vorzugsweise am Beginn des Strings, ein weiteres Zeichen eingefügt werden. Das erweitert unseren Textstring um ein Zeichen, der bestehende Textstring muss dabei nach rechts kopiert werden. Damit jeweils Platz ist für das vorausgehende Zeichen, muss das wieder von hinten nach vorne passieren, also mit minus X/Y/Z. Natürlich könnten wir dafür zwei Zeiger opfern: einer, der auf den Ziel-Textbereich zeigt, einer der auf den Herkunfts-Textbereich zeigt. Die Differenz zwischen beiden wäre minus 1. Zu viel Aufwand, es geht auch viel einfacher.

Fertig verschobener String So soll das fertige Ergebnis aussehen: der String ist nach rechts verschoben und von links wurde ein weiteres Zeichen hereingeschoben. Wir könnten zuerst das letzte Zeichen mit ld R16,-Y lesen und müssten es, um eine Stelle nach rechts verschoben, wieder einfügen. Das ginge dann zum Beispiel mit den drei Instruktionen

  adiw YL,1
  st Y,R16
  sbiw YL,1

Aber diese drei können wir mit dem Verschiebebefehl std Y+1,R16 ganz einfach ersetzen, denn wir können damit die Verschiebung um eine Stelle höher temporär ausführen und müssen dazu den Zeiger gar nicht erst verlegen.

Kein zweiter Zeiger, das spart zwei Register, keine unnötige Zeigeranpassung, das spart unnötige Takte, einfach nur optimaler Einsatz der eingebauten Adressierungsarten beim AVR. Das geht aber nur mit Y oder Z, nicht mit X.

Wenn der AVR das kann, der PIC oder andere Prozessoren aber nicht, um wieviel schneller sind dann AVRs? Um den Faktor 2 oder 3? Mal eben aus einem zwei oder drei MHz gemacht, und das ohne jeden erhöhten Betriebsstrom.

Schlussfolgerung:

Wenn Dein Programm in Sachen Komplexität weit über eine einfache Blinkroutine hinausgeht, dann denke über solche Optimierungsmethoden nach. Es lohnt sich, denn Du kriegst damit elegantere, schnellere und effektivere Abläufe hin. Und wenn Du den Quellcode auch noch mit aussagekräftigen Kommentaren spickst, sind die dann auch noch schneller lesbar und leichter verständlich als Spaghetticode, gespickt mit so vielen ADIW und SBIW, dass Dir das Hirn schon ganz schummrig wird.

Nur: kennen musst Du die vielen Zugriffsarten dann schon.

Zum Seitenanfang

©2021 by http://avr-asm-tutorial.net