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

Zugriffe auf Memories in AVRs

  1. Zugriffe auf SRAM,
  2. Zugriffe auf Portregister,
  3. Zugriffe auf EEPROM,
  4. Zugriffe auf Flash-Speicher
    1. Flashspeicher
    2. .CSEG-Direktive
    3. LPM
    4. Fortgeschrittenes LPM

4.1 Flashspeicher

Flashspeicher-Adressierung Alle AVRs haben einen Speicher, der Flashspeicher genannt wird und der das ausführbare Programm aufnimmt. Weil der ausführbare Code in AVRs 16-bittig ist, ist dieser Speicher ebenfalls 16 Bits breit (in binären Worten, nicht in Bytes).

Die Größe des Flashspeichers kann Werte zwischen 512 und bis zu 393.216 annehmen. Die Adressen reichen daher von 0x000000 bis 0x0001FF oder 0x05FFFF. In den meisten Fällen gibt die Konstante FlashEnd aus der def.inc-Datei die letzte bzw. höchste Adresse an.
Die Adresse 0x000000 ist dabei sehr speziell: sie ist die Startadresse nach jedem Reset, nach der Bereitstellung der Betriebsspannung und beim Watchdog-Reset, hier befindet sich die allererste Instruktion, die ausgeführt wird.

4.2 Die .CSEG-Direktive

Beim Assemblieren von Quellcode landet ausführbarer Code als Programmworte sowie Tabellen im Code 16-bittig in einer Hexadezimaldatei namens .hex. Dies ist die Default-Einstellung des Assemblers. Wird mit der Direktive .ESEG (EEPROM-Segment) oder .DSEG (Daten- oder SRAM-Segment) das Ziel des Assemblers umgestellt, kehrt man mit .CSEG wieder in das Code-Segment zurück.

Der Inhalt der .hex-Datei landet mittels Brenner-Software im Flashspeicher des AVR.

Im Code-Segment landen Instruktionen wie NOP als 16-Bit-Worte in der .hex-Datei. Zahlen können als hexadezimale Worte mit der Direktive .DW 16-Bit-Werte ebenfalls in den Flashspeicher geschrieben werden, als wären sie ausführbarer Code.

Aber auch Bytes, also 8-Bit-Zahlen, können mit der Direktive .DB Ein-8-Bit-Wert eingefügt werden, allerdings ist dabei dann das höherwertige Byte Null. Werden mit der Direktive .DB 8-Bit-Wert1, 8-Bit-Wert2 zwei Byte-Werte gleichzeitig eingefügt, dann landet der 8-Bit-Wert1 im LSB des Programmspeichers, der 8-Bit-Wert2 im MSB. Werden drei oder mehr Byte-Werte in eine .DB-Zeile geschrieben, landen immer zwei davon in einem Wort. Ist die Anzahl Bytes ungeradzahlig, dann ist das MSB des letzten Wortes Null und der Assembler gibt eine Warnung aus.

Der Quellcode zum Assemblieren Assembler-Listing Der Assember-Quellcode links wird assembliert, das Ergebnis kann im erzeugten Listing rechts betrachtet werden.

4.3 Die LPM-Instruktion

Lesezugriff auf Flashadresse mit LPM Die Instruktion LPM oder "Load from Program Memory" liest ein Byte aus dem Flash aus. Dazu nimmt es die Adresse aus dem Z-Registerpaar (ZH:ZL = R31:R30), liest das Flash und schreibt das Ergebnis in das Register R0.

Aber welche Adresse? Jede Adresse des Flashspeichers zeigt auf zwei Bytes gleichzeitig: ein LSB und ein MSB. Welches der beiden nun gelesen werden soll, muss irgendwie in diese Adresse in Z herein. Der Trick ist, die Adresse des Flashspeichers in Z um Eins nach links zu schieben und von rechts her ein Bit hereinzuschmuggeln. Ist das eingeschmuggelte Bit eine Null, dann wird das LSB gelesen, ist es eine Eins dann ist das MSB dran.

Einen Nachteil hat dieser Trick: Das Bit 15 der Flashadresse kann nicht benutzt werden. Daher müssen Tabellen mit tausenden von Einzelwerten in die untere Hälfte des 64k-Flashspeichers.

Simulieren der LPM-Instruktion Um das Linksschieben der Flash-Speicheradresse zu bewerkstelligen, gibt es drei Möglichkeiten, die alle im folgenden Quellcode demonstriert sind.

.equ FlashAddr = ByteTabelle ; Setze die Flashadresse auf die Tabelle
  ; Formulierung 1
  ldi ZH,High(FlashAddr+FlashAddr+0) ; Zugriff aus das LSB, MSB von Z
  ldi ZL,Low(FlashAddr+FlashAddr+0) ; dto., LSB von Z
  lpm
  ldi ZH,High(FlashAddr+FlashAddr+1) ; Zugriff auf das MSB, MSB von Z
  ldi ZL,Low(FlashAddr+FlashAddr+1) ; dto., LSB von Z
  lpm
  ; 
  ; Formulation 2
  ldi ZH,High(2*FlashAddr+0) ; Zugriff auf das LSB, MSB von Z
  ldi ZL,Low(2*FlashAddr+0) ; dto., LSB von Z
  lpm
  ldi ZH,High(2*FlashAddr+1) ; Zugriff auf das MSB, MSB von Z
  ldi ZL,Low(2*FlashAddr+1) ; dto., LSB von Z
  lpm
  ;
  ; Formulation 3
  ldi ZH,High((FlashAddr<<1)|0) ; Zugriff auf das LSB, MSB von Z
  ldi ZL,Low((FlashAddr<<1)|0) ; dto., LSB von Z
  lpm
  ldi ZH,High((FlashAddr<<1)|1) ; Zugriff auf das MSB, MSB von Z
  ldi ZL,Low((FlashAddr<<1)|1) ; dto., LSB von Z
  lpm
Loop:
  rjmp Loop
;
ByteTabelle:
  .db 1
  .db 1,2
  .db 1,2,3
  .db "Dies ist eine Textzeile"

Natürlich muss man nicht unbedingt eine Konstante mit dem Namen FlashAddr, man kann auch direkt die Marke ByteTable: als Adresse in die LDI-Instruktionen schreiben. Und alle +0 und |0 in den formulierungen sind auch überflüssig, weil sie an dem eingentlichen Wert nichts mehr ändern.

Welche der angebotenen Formulierungen auch immer Du bevorzugst, es kommt immer dasselbe heraus. Wie die Simulation mit avr_sim zeigt, landet das Ergebnis in allen Fällen im Register R0. Die Simulation zeigt auch, dass so ein Zugriff mit LPM drei Taktzyklen braucht, zusammen mit den beiden LDIs fünf Mikrosekunden. Flashzugriffe sind daher etwas langsamer als SRAM-Zugriffe.

4.4 Fortgeschrittene LPM-Instruktionen

ATMEL hat der LPM-Instruktion später noch die Möglichkeit hingefügt, mehr mit LPM zu machen. Die erste Möglichkeit ist es, der LPM-Instruktion mit lpm Register,Z ein anderes Ziel zuzuordnen, eines von den 32 Registern.

Noch ein wenig später kam das Auto-Inkrementieren der Speicheradresse, im Anschluss an den Lesezugriff, hinzu. Das vereinfacht die beiden Instruktionen lpm und adiw ZL,1 zu einer, mit lpm register,Z+. Das zusätzliche Inkrementieren verlängert nicht die Ausführungsdauer.

Das Gegenteil, das Auto-Dekrementieren, um Tabellen auch von hinten nach vorne lesen zu können, ist wie beim SRAM auch umgekehrt ausgeführt: zuerst wird dekrementiert, dann erst gelesen. Die Formulierung lpm Register,-Z bringt das zum Ausdruck und ersetzt sbiw ZL,1 und lpm Register,Z.

4.5 Anwendungsbeispiele für LPM

Der Text fuer das SRAM Das erste Anwendungsbeispiel verwendet LPM um einen null-terminierten Text aus dem Flashspeicher in das SRAM zu kopieren.

; Datensegment vorbereiten
.dseg
sText:
;
.cseg
  ; Z auf Flashspeicher-Text
  ldi ZH,High(2*Text)
  ldi ZL,Low(2*Text)
  ; X auf Zieladresse im SRAM
  ldi XH,High(sText)
  ldi XL,Low(sText)
Kopierschleife:
  lpm R16,Z+ ; Load from program memory
  st X+,R16 ; Speichern im SRAM
  tst R16 ; Null-termination?
  brne Kopierschleife ; Nein, weiter machen
; Nicht in die Tabelle laufen
Loop:
  rjmp Loop
; Text im Flashspeicher
Text:
  .db "Dieser Text soll in das SRAM",0x00

Die gesamte Ablauf braucht 235 µs.

Das zweite Anwendungsbeispiel ist ein wenig komplexer und auch ein bisschen akademisch: es nimmt an, dass auf einen Tastendruck hin zehn verschiedene Unterprogramme ausgeführt werden sollen. Einige sind kurz, andere länger, alle sind jedenfalls verschieden lang. Nun kann man sich natürlich mit CPI und BRNE durch die zehn Werte hangeln und das jeweilige Unterprogramm damit herausfinden. Schneller und eleganter ist es aber, Das bringt Überblick und Flexibilität gleichzeitig und ist nicht mit Verweigungsorgien gespickt.

Das ist der Quellcode.

.cseg
.equ select = 3 ; Kann zwischen Null und Fuenf sein
  .ifdef SPH
    ldi R16,High(RAMEND) ; Stapelzeiger MSB
    out SPH,R16
    .endif
  ldi R16,Low(RAMEND) ; Stapelzeiger LSB
  out SPL,R16
  ldi R16,select ; Lade Nummer der gewaehlten Routine
  lsl R16 ; Multiplikation mit zwei (Adressen haben 16 Bits)
  ldi ZH,High(2*SprungTabelle) ; Zeige mit Z auf Tabelle, MSB
  ldi ZL,Low(2*SprungTabelle) ; LSB
  add ZL,R16 ; Addiere die Auswahl zum Tabellenanfang
  ldi R16,0 ; Addiere Ueberlauf, wenn nötig
  adc ZH,R16
  lpm R16,Z+ ; Lese LSB
  lpm ZH,Z ; Lese MSB (kein +, wuerde ZH ueberschreiben)
  mov ZL,R16 ; Kopiere LSB nach ZL
  icall ; Rufe Routine in Z auf
Loop:
  rjmp Loop
;
; Routinen
Routine0:
  nop
  ret
Routine1:
  nop
  nop
  ret
Routine2:
  nop
  nop
  nop
  ret
Routine3:
  nop
  nop
  nop
  nop
  ret
Routine4:
  nop
  nop
  nop
  nop
  nop
  ret
Routine5:
  nop
  nop
  nop
  nop
  nop
  nop
  ret
;
; Sprungtabelle
SprungTabelle:
  .dw Routine0,Routine1,Routine2
  .dw Routine3,Routine4,Routine5
  ; Weitere Routinen hier

Sprungtabelle im Flash So sieht die Sprungtabelle aus, die der Assembler im Flash angelegt hat. Die erste Sprungadresse ist auf der Adresse 0x000029 zu finden, alle nachfolgenden jeweils Eins höher.

Startadresse der Sprungtabelle ist in Z eingetragen Die zwei LDI-Instruktionen haben die Startadresse der Sprungtabelle in Z eingetragen. avr_sim zeigt die Adresse an, auf die Z zeigt.

Selection mal zwei ist addiert Zu dieser Basisadresse ist nun zwei Mal die 3 aus select addiert, der Z-Zeiger zeigt nun auf 0x002C. in der Tabelle enthält diese Position die Adresse von Routine3.

Auslesen der Adresse mit zwei LPM Mit zwei LPM und einem MOV ist nunmehr die eigentliche Adresse im Z-Register zusammengesetzt worden. Z ist nun fertig für den Sprung.

Sprung mit ICALL an die Adresse aus Z Das war nun das ICALL, die weitere Ausführung erfolgt in Routine3, wie von select vorherbestimmt.

Schlussfolgerung: LPM und seine modernen Varianten bieten weitgefächerte Möglichkeiten, um umfangreiche Texte und Tabellen im großen Flashspeicher anzulegen und zu verwenden. Effektive Programmierung kann bequem auf diese Tools zugreifen.

Zum Seitenanfang

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