AVR-Tutorial: Speicher: Unterschied zwischen den Versionen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche
 
(48 dazwischenliegende Versionen von 25 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
== Speichertypen ==
== Speichertypen ==


Die AVR-Mikrocontroller besitzen 3 verschiedene Arten von Speichern:  
Die AVR-Mikrocontroller besitzen 3 verschiedene Arten von Speicher:  


<table border="0">
<table border="1" class="wikitable">
<tr>
<tr>
   <th align="left"></th>
   <th align="left">hat</th>
   <th align="left">Flash</th>
   <th align="left">Flash</th>
   <th align="left">EEPROM</th>
   <th align="left">EEPROM</th>
Zeile 29: Zeile 29:
</tr>
</tr>
<tr>
<tr>
   <th align="left">Größe beim AT90S2333</th>
   <th align="left">Größe beim ATtiny2313</th>
   <td>2 KiB</td>
   <td>2 KB</td>
   <td>128 Byte</td>
   <td>128 Byte</td>
   <td>128 Byte</td>
   <td>128 Byte</td>
</tr>
</tr>
<tr>
<tr>
   <th align="left">Größe beim AT90S4433</th>
   <th align="left">Größe beim ATmega8</th>
   <td>4 KiB</td>
   <td>8 KB</td>
   <td>256 Byte</td>
   <td>512 Byte</td>
   <td>128 Byte</td>
   <td>1 KB</td>
</tr>
<tr>
  <th align="left">Größe beim ATmega32</th>
  <td>32 KB</td>
  <td>1 KB</td>
  <td>2 KB</td>
</tr>
</tr>
<tr>
<tr>
   <th align="left">Größe beim ATmega8</th>
   <th align="left">Größe beim ATmega2560</th>
   <td>8 KiB</td>
   <td>256 KB</td>
   <td>512 Byte</td>
   <td>4 KB</td>
   <td>1 KiB</td>
   <td>8 KB</td>
</tr>
</tr>
</table>
</table>
Zeile 50: Zeile 56:
=== Flash-ROM ===
=== Flash-ROM ===


Das '''Flash-ROM''' der AVRs dient als Programmspeicher. Über den Programmieradapter werden die kompilierten Programme vom PC an den Controller übertragen und im Flash-ROM abgelegt. Bei der Programmausführung wird das ROM Byte für Byte ausgelesen und ausgeführt, es lässt sich aber auch zur Speicherung von Daten (z.B. Texte für eine LCD-Anzeige) nutzen. Vom laufenden Programm aus kann man das ROM normalerweise nur lesen, nicht beschreiben.
Das [[Speicher#Flash-ROM | '''Flash-ROM''']] der AVRs dient als Programmspeicher. Über den Programmieradapter werden die kompilierten Programme vom PC an den Controller übertragen und im Flash-ROM abgelegt. Bei der Programmausführung wird das ROM [[Digitaltechnik#Word | Wort]] für Wort ausgelesen und ausgeführt. Es lässt sich aber auch zur Speicherung von Daten nutzen (z.&nbsp;B. Texte für ein [[LCD]]). Vom laufenden Programm aus kann man das ROM normalerweise nur lesen, nicht beschreiben. Es kann beliebig oft ausgelesen werden, aber theoretisch nur ~10.000 mal beschrieben werden.
 
Es kann beliebig oft ausgelesen werden, aber (theoretisch) nur ~10.000 mal beschrieben werden.


=== EEPROM ===
=== EEPROM ===


Das '''EEPROM''' ist wie das Flash ein nichtflüchtiger Speicher, die Daten bleiben also auch nach dem Ausschalten der Betriebsspannung erhalten. Es kann beliebig oft gelesen und mindestens 100.000 mal beschrieben werden. Bei den AVRs kann man es z.B. als Speicher für Messwerte oder Einstellungen benutzen.  
Das [[Speicher#EEPROM |'''EEPROM''']] ist wie das Flash ein nichtflüchtiger Speicher, die Daten bleiben also auch nach dem Ausschalten der Betriebsspannung erhalten. Es kann beliebig oft gelesen und mindestens 100.000 mal beschrieben werden. Bei den AVRs kann man es z.&nbsp;B. als Speicher für Messwerte oder Einstellungen benutzen.  


=== [[RAM]] ===
=== RAM ===


Das '''RAM''' ist ein flüchtiger Speicher, d.h. die Daten gehen nach dem Ausschalten verloren. Es kann beliebig oft gelesen und beschrieben werden, weshalb es sich zur Speicherung von Variablen eignet für die die Register R0-R31 nicht ausreichen (siehe Kapitel [[AVR-Tutorial: SRAM|SRAM]]). Daneben dient es als Speicherort für den Stack, in dem z.B. bei Unterprogrammaufrufen (rcall) die Rücksprungadresse gespeichert wird (siehe Kapitel 3).
Das [[Speicher#RAM |'''RAM''']] ist ein flüchtiger Speicher, d.h. die Daten gehen nach dem Ausschalten verloren. Es kann beliebig oft gelesen und beschrieben werden, weshalb es sich zur Speicherung von Variablen eignet, für die die Register R0-R31 nicht ausreichen. Daneben dient es als Speicherort für den Stack, auf dem z.&nbsp;B. bei Unterprogrammaufrufen (rcall) die Rücksprungadresse gespeichert wird (siehe [[AVR-Tutorial:_SRAM | AVR-Tutorial: SRAM]]).


== Anwendung ==
== Anwendung ==
Zeile 68: Zeile 72:
Die erste und wichtigste Anwendung des Flash-ROMs kennen wir bereits: Das Speichern von Programmen, die wir nach dem Assemblieren dort hineingeladen haben. Nun sollen aber auch vom laufenden Programm aus Daten ausgelesen werden.  
Die erste und wichtigste Anwendung des Flash-ROMs kennen wir bereits: Das Speichern von Programmen, die wir nach dem Assemblieren dort hineingeladen haben. Nun sollen aber auch vom laufenden Programm aus Daten ausgelesen werden.  


Um die Daten wieder auszulesen, muss man die Adresse, auf die zugegriffen werden soll, in den '''Z-Pointer''' laden. Der Z-Pointer besteht aus den Registern '''R30''' (Low-Byte) und '''R31''' (High-Byte), daher kann man das Laden einer Konstante wie gewohnt mit dem Befehl '''ldi''' durchführen. Statt R30 und R31 kann man übrigens einfach '''ZL''' und '''ZH''' schreiben, da diese Synonyme bereits in der include-Datei 4433def.inc definiert sind.  
Um die Daten wieder auszulesen, muss man die Adresse, auf die zugegriffen werden soll, in den '''Z-Pointer''' laden. Der Z-Pointer besteht aus den Registern '''R30''' (Low-Byte) und '''R31''' (High-Byte), daher kann man das Laden einer Konstante wie gewohnt mit dem Befehl '''ldi''' durchführen. Statt R30 und R31 kann man übrigens einfach '''ZL''' und '''ZH''' schreiben, da diese Synonyme bereits in der include-Datei m8def.inc definiert sind.  


Wenn die richtige Adresse erstmal im Z-Pointer steht, geht das eigentliche Laden der Daten ganz einfach mit dem Befehl '''lpm'''. Dieser Befehl, der im Gegensatz zu out, ldi usw. keine Operanden hat, veranlasst dass das durch den Z-Pointer addressierte Byte aus dem Programmspeicher in das Register '''R0''' geladen wird, von wo aus man es weiterverarbeiten kann.  
Wenn die richtige Adresse erstmal im Z-Pointer steht, geht das eigentliche Laden der Daten ganz einfach mit dem Befehl '''lpm'''. Dieser Befehl, der im Gegensatz zu out, ldi usw. keine Operanden hat, veranlasst das Laden des durch den Z-Pointer adressierten Bytes aus dem Programmspeicher in das Register '''R0''', von wo aus man es weiterverarbeiten kann.  


Jetzt muss man nur noch wissen, wie man dem Assembler überhaupt beibringt, dass er die von uns festgelegte Daten im ROM plazieren soll, und wie man dann an die Adresse kommt an der sich diese Daten befinden. Um den Programmspeicher mit Daten zu füllen, gibt es die [http://www.mikrocontroller.net/studiohelp/Assembler/directives.html Direktiven] .db und .dw. In der Regel benötigt man nur .db, was folgendermaßen funktioniert:  
Jetzt muss man nur noch wissen, wie man dem Assembler überhaupt beibringt, dass er die von uns festgelegten Daten im ROM platzieren soll, und wie man dann an die Adresse kommt, an der sich diese Daten befinden. Um den Programmspeicher mit Daten zu füllen, gibt es die Direktiven .db und .dw. In der Regel benötigt man nur .db, was folgendermaßen funktioniert:  


<avrasm>
<syntaxhighlight lang="asm">  
daten:
daten:
          .db 12, 20, 255, 0xFF, 0b10010000
    .db 12, 20, 255, 0xFF, 0b10010000
</avrasm>
</syntaxhighlight>


Dieser Ausschnitt sagt dem Assembler, dass er die angegebenen Bytes nacheinander im Speicher platzieren soll; wenn man die Zeile also assembliert, erhält man eine Hex-Datei, die nur diese Daten enthält.
Direktiven wie .db sind Anweisungen an den Assembler, keine Prozessorbefehle. Von denen kann man sie durch den vorangestellten Punkt unterscheiden. In diesem Fall sagen wir dem Assembler, dass er die angegebenen Bytes nacheinander im Speicher platzieren soll; wenn man die Zeile also assembliert, erhält man eine Hex-Datei, die nur diese Daten enthält.


Aber was soll das '''daten:''' am Anfang der Zeile? Bis jetzt haben wir Labels nur als Sprungmarken verwendet, um den Befehlen '''rcall''' und '''rjmp''' zu sagen, an welche Stelle im Programm gesprungen werden soll. Würden wir in diesem Fall '''rjmp daten''' im Programm stehen haben, dann würde die Programmausführung zur Stelle '''daten:''' springen, und versuchen die sinnlosen Daten als Befehle zu interpretieren - was mit Sicherheit dazu führt, dass der Controller Amok läuft.  
Aber was soll das '''daten:''' am Anfang der Zeile? Bis jetzt haben wir Labels nur als Sprungmarken verwendet, um den Befehlen '''rcall''' und '''rjmp''' zu sagen, an welche Stelle im Programm gesprungen werden soll. Würden wir in diesem Fall '''rjmp daten''' im Programm stehen haben, dann würde die Programmausführung zur Stelle '''daten:''' springen und versuchen, die sinnlosen Daten als Befehle zu interpretieren - was mit Sicherheit dazu führt, dass der Controller Amok läuft.  


Statt nach '''daten:''' zu springen, sollten wir die Adresse besser in den Z-Pointer laden. Da der Z-Pointer aus zwei Bytes besteht, brauchen wir dazu zweimal den Befehl '''ldi''':  
Statt nach '''daten:''' zu springen, sollten wir die Adresse besser in den Z-Pointer laden. Da der Z-Pointer aus zwei Bytes besteht, brauchen wir dazu zweimal den Befehl '''ldi''':  


<avrasm>
<syntaxhighlight lang="asm">  
          ldi ZL, LOW(daten*2)                     ; Low-Byte der Adresse in Z-Pointer
    ldi ZL, LOW(daten*2)   ; Low-Byte der Adresse in Z-Pointer
          ldi ZH, HIGH(daten*2)                     ; High-Byte der Adresse in Z-Pointer
    ldi ZH, HIGH(daten*2)   ; High-Byte der Adresse in Z-Pointer
</avrasm>
</syntaxhighlight>


Wie man sieht, ist das Ganze sehr einfach: Man kann die Labels im Assembler direkt wie Konstanten verwenden. Über die Multiplikation der Adresse mit 2 sollte man sich erst mal keine Gedanken machen: "Das ist einfach so."
Wie man sieht, ist das Ganze sehr einfach: Man kann die Labels im Assembler direkt wie Konstanten verwenden. Über die Multiplikation der Adresse mit zwei sollte man sich erst mal keine Gedanken machen: "Das ist einfach so." Wer es genauer wissen will schaut [[AVR-Tutorial:_Mehrfachverzweigung#Z-Pointer_leicht_verst.C3.A4ndlich | hier]] nach.


Um zu zeigen wie das alles konkret funktioniert, ist das folgende Beispiel nützlich:
Um zu zeigen, wie das alles konkret funktioniert, ist das folgende Beispiel nützlich:


[http://www.mikrocontroller.net/sourcecode/tutorial/lpm1.asm Download lpm1.asm]
<syntaxhighlight lang="asm">
 
.include "m8def.inc"
<avrasm>  
.include "4433def.inc"


        ldi R16, 0xFF
    ldi     R16, 0xFF
        out DDRB, R16                     ; Port B: Ausgang
    out     DDRB, R16               ; Port B: Ausgang


        ldi ZL, LOW(daten*2)             ; Low-Byte der Adresse in Z-Pointer
    ldi     ZL, LOW(daten*2)       ; Low-Byte der Adresse in Z-Pointer
        ldi ZH, HIGH(daten*2)             ; High-Byte der Adresse in Z-Pointer
    ldi     ZH, HIGH(daten*2)       ; High-Byte der Adresse in Z-Pointer


        lpm                               ; durch Z-Pointer adressiertes Byte nach R0 lesen
    lpm                             ; durch Z-Pointer adressiertes Byte
        mov R16, R0                       ; nach R16 kopieren
                                    ; in R0 laden
        out PORTB, R16                    ; an PORTB ausgeben
    out     PORTB, R0              ; an PORTB ausgeben


ende:  rjmp ende                         ; Endlosschleife
ende:   
    rjmp ende                       ; Endlosschleife


daten:
daten:
        .db 0b10101010
    .db 0b10101010
</avrasm>
</syntaxhighlight>


Wenn man dieses Programm assembliert und in den Controller überträgt, dann kann man auf den an Port B angeschlossenen LEDs das mit '''.db 0b10101010''' im Programmspeicher abgelegte Bitmuster sehen.  
Wenn man dieses Programm assembliert und in den Controller überträgt, dann kann man auf den an Port B angeschlossenen LEDs das mit '''.db 0b10101010''' im Programmspeicher abgelegte Bitmuster sehen.  


==== Fallbeispiel: Zeichenketten ====
Eine häufige Anwendung von '''lpm''' ist das Auslesen von Zeichenketten ("Strings") aus dem Flash-ROM und die Ausgabe an den seriellen Port oder ein LCD. Das folgende Programm gibt in einer Endlosschleife den Text "AVR-Assembler ist ganz einfach", gefolgt von einem Zeilenumbruch, an den UART aus.  
Eine häufige Anwendung von '''lpm''' ist das Auslesen von Zeichenketten ("Strings") aus dem Flash-ROM und die Ausgabe an den seriellen Port oder ein LCD. Das folgende Programm gibt in einer Endlosschleife den Text "AVR-Assembler ist ganz einfach", gefolgt von einem Zeilenumbruch, an den UART aus.  


[http://www.mikrocontroller.net/sourcecode/tutorial/lpm-print.asm Download lpm-print.asm]
<syntaxhighlight lang="asm">
.include "m8def.inc"


<avrasm>
.def temp = r16
.include "4433def.inc"
.def temp1 = r17


.def temp = R16
.equ CLOCK = 4000000                ; Frequenz des Quarzes
.equ BAUD = 9600                    ; Baudrate
.equ UBRRVAL = CLOCK/(BAUD*16)-1    ; Baudratenteiler
; hier geht das Programmsegment los


        ldi R16, RAMEND
.CSEG
        out SPL, R16                      ; Stackpointer initialisieren
.org 0
    ldi     temp, low(RAMEND)            ; Stackpointer initialisieren
    out     SPL, temp                   
    ldi    temp, high(RAMEND)
    out    SPH, temp                   


        sbi UCSRB,TXEN                    ; UART TX aktivieren
    ldi    temp, LOW(UBRRVAL)          ; Baudrate einstellen
        ldi temp,4000000/(9600*16)-1     ; Baudrate 9600 einstellen
    out    UBRRL, temp
        out UBRR,temp
    ldi     temp, HIGH(UBRRVAL)
    out    UBRRH, temp
                                   
    ldi    temp, (1<<URSEL)|(3<<UCSZ0) ; Frame-Format: 8 Bit
    out     UCSRC, temp
    sbi    UCSRB, TXEN                ; TX (Senden) aktivieren


start:
loop:
        ldi ZL, LOW(text*2)               ; Adresse des Strings in den
    ldi     ZL, LOW(text*2)             ; Adresse des Strings in den
        ldi ZH, HIGH(text*2)             ; Z-Pointer laden
    ldi     ZH, HIGH(text*2)           ; Z-Pointer laden
        rcall print                      ; Unterfunktion print aufrufen
    rcall   print                      ; Funktion print aufrufen
    rcall  wait                        ; kleine Pause
    rjmp    loop                        ; das Ganze wiederholen


        ldi R16, 10                      ; die Bytes 10 und 13 senden
; kleine Pause
        rcall sendbyte                    ; (Zeilenumbruch im Terminal)
wait:
        ldi R16, 13
    ldi     temp,0
        rcall sendbyte
wait_1:
 
    ldi     temp1,0
        rjmp start                        ; das Ganze wiederholen
wait_2:
    dec    temp1
    brne    wait_2
    dec    temp
    brne    wait_1
    ret


; print: sendet die durch den Z-Pointer adressierte Zeichenkette


; print: sendet die durch den Z-Pointer adressierte Zeichenkette
print:
print:
        lpm                               ; Erstes Byte des Strings nach R0 lesen
    lpm                                 ; Erstes Byte des Strings nach R0 lesen
        tst R0                           ; R0 auf 0 testen
    tst     R0                         ; R0 auf 0 testen
        breq print_end                   ; wenn 0, dann zu print_end
    breq   print_end                   ; wenn 0, dann zu print_end
        mov r16, r0                       ; Inhalt von R0 nach R16 kopieren
    mov     temp, r0                     ; Inhalt von R0 nach R16 kopieren
        rcall sendbyte                    ; UART-Sendefunktion aufrufen
    rcall   sendbyte                    ; UART-Sendefunktion aufrufen
        adiw ZL, 1                       ; Adresse des Z-Pointers um 1 erhöhen
    adiw   ZL, 1                       ; Adresse des Z-Pointers um 1 erhöhen
        rjmp print                       ; wieder zum Anfang springen
    rjmp   print                       ; wieder zum Anfang springen
print_end:
print_end:
        ret
    ret
 
; sendbyte: sendet das Byte aus temp über das UART


; sendbyte: sendet das Byte aus R16 über das UART
sendbyte:
sendbyte:
        sbis UCSRA, UDRE                 ; warten bis das UART bereit ist
    sbis   UCSRA, UDRE                 ; warten bis das UART bereit ist
        rjmp sendbyte
    rjmp   sendbyte
        out UDR, R16
    out     UDR, temp
        ret
    ret
 
; Konstanten werden hier im Flash abgelegt


text:
text:
        .db "AVR-Assembler ist ganz einfach",0   ; Stringkonstante, durch eine 0 abgeschlossen
    .db "AVR-Assembler ist ganz einfach",10,13,0  
</avrasm>
    ; Stringkonstante, durch eine 0 abgeschlossen
    ; die 10 bzw. 13 sind Steuerzeichen für Wagenrücklauf und neue Zeile
</syntaxhighlight>


Neuere AVR-Controller besitzen einen erweiterten Befehlssatz. Darunter befindet sich auch der folgende Befehl:
Neuere AVR-Controller besitzen einen erweiterten Befehlssatz. Darunter befindet sich auch der folgende Befehl:


<avrasm>
<syntaxhighlight lang="asm">  
lpm r16, Z+
    lpm     r16, Z+
</avrasm>
</syntaxhighlight>
 
Dieser Befehl liest ein Byte aus dem Flash und speichert es in einem beliebigen Register, hier r16. Danach wird der Zeiger Z um eins erhöht.


Für die neuen Controller, wie ATmegas kann das Codebeispiel also so abgeändert werden:
Dieser Befehl liest ein Byte aus dem Flash und speichert es in einem beliebigen Register, hier r16. Danach wird der Zeiger Z um eins erhöht. Für die neuen Controller wie ATmegas kann das Codebeispiel also so abgeändert werden:


<avrasm>
<syntaxhighlight lang="asm">  
; print: sendet die durch den Z-Pointer adressierte Zeichenkette
; print: sendet die durch den Z-Pointer adressierte Zeichenkette
print:
print:
        lpm r16, z+                       ; Erstes Byte des Strings nach r16 lesen
    lpm     temp, Z+         ; Erstes Byte des Strings nach temp lesen
        tst r16                          ; r16 auf 0 testen
    tst     temp            ; temp auf 0 testen
        breq print_end                   ; wenn 0, dann zu print_end
    breq   print_end       ; wenn 0, dann zu print_end
        rcall sendbyte                   ; UART-Sendefunktion aufrufen
    rcall   sendbyte       ; UART-Sendefunktion aufrufen
        rjmp print                       ; wieder zum Anfang springen
    rjmp   print           ; wieder zum Anfang springen
print_end:
print_end:
        ret
    ret
</avrasm>
</syntaxhighlight>
 
Wenn man bei .db einen Text in doppelten Anführungszeichen angibt, werden die Zeichen automatisch in die entsprechenden ASCII-Codes umgerechnet:
 
<avrasm>
        .db "Test", 0
</avrasm>


ist äquivalent zu
Wenn man bei .db einen Text in doppelten Anführungszeichen angibt, werden die Zeichen automatisch in die entsprechenden ASCII-Codes umgerechnet:


<avrasm>
<syntaxhighlight lang="asm">  
        .db 84, 101, 115, 116, 0
    .db     "Test", 0             
</avrasm>
    ; ist äquivalent zu
    .db    84, 101, 115, 116, 0
</syntaxhighlight>


Damit das Programm das Ende der Zeichenkette erkennen kann, wird eine 0 an den Text angehängt.  
Damit das Programm das Ende der Zeichenkette erkennen kann, wird eine 0 an den Text angehängt.  
Zeile 209: Zeile 233:
==== Neue Assemblerbefehle ====
==== Neue Assemblerbefehle ====


<avrasm>
<syntaxhighlight lang="asm">  
        lpm                           ; Liest das durch den Z-Pointer addressierte Byte
    lpm                             ; Liest das durch den Z-Pointer
                                      ; aus dem Flash-ROM in das Register R0 ein.  
                                    ; addressierte Byte aus dem Flash-ROM
        lpm [Register], Z             ; Macht das gleiche wie lpm, jedoch in ein beliebiges Register
                                    ; in das Register R0 ein.  
        lpm [Register], Z+             ; Erhöht zusätzlich den Z-Zeiger
 
    lpm     [Register], Z           ; Macht das gleiche wie lpm, jedoch in
                                    ; ein beliebiges Register
 
    lpm     [Register], Z+         ; Erhöht zusätzlich den Z-Zeiger


        tst [Register]                 ; Prüft, ob der Inhalt eines Registers 0 ist.  
    tst     [Register]             ; Prüft, ob Inhalt eines Registers
                                    ; gleich 0 ist.


        breq [Label]                   ; Springt zu [Label], wenn der vorhergehende Vergleich wahr ist.  
    breq   [Label]                 ; Springt zu [Label], wenn der
                                    ; vorhergehende Vergleich wahr ist.  


        adiw [Register], [Konstante]   ; Addiert eine Konstante zu einem Registerpaar.
    adiw   [Register], [Konstante] ; Addiert eine Konstante zu einem
                                      ; [Register] bezeichnet das untere der beiden Register.
                                    ; Registerpaar. [Register] bezeichnet das
                                      ; Kann nur auf die Registerpaare R25:R24, R27:R26,
                                    ; untere der beiden Register.
                                      ; R29:R28 und R31:R30 angewendet werden.  
                                    ; Kann nur auf die Registerpaare
</avrasm>
                                    ; R25:R24, R27:R26, R29:R28 und R31:R30
                                    ; angewendet werden.  
</syntaxhighlight>


=== EEPROM ===
=== EEPROM ===
Den Flash Speicher kann man also benutzen, um dort Daten abzulegen, die sich während eines Programmlaufs nicht verändern. Irgendwelche Tabellen oder konstante Texte. Aber des öfteren möchte man auch die Möglichkeit haben, Daten zu speichern und wieder zu lesen, die sich während des Programmlaufs ändern. Ganz besonders möchte man eine Speicherfläche zur Verfügung haben, die ihren Inhalt auch dann behält, wenn dem µC die Versorgungsspannung abgedreht wird. Man denke z.B. an irgendwelche Konfigurationsdaten oder an Informationen, wie weit der µC in der Bearbeitung von Daten vorangekommen ist oder irgendwelche Statistikdaten, die auch nach einem Stromausfall noch verfügbar sein sollen. In solchen Fällen kommt das EEPROM zum Einsatz


==== Lesen ====
==== Lesen ====


Zuerst wird die EEPROM-Adresse, von der gelesen werden soll, in das IO-Register '''EEAR''' (EEPROM Address Register) geladen. Dann löst man den Lesevorgang durch das Setzen des Bits '''EERE''' (EEPROM Read Enable) im IO-Register '''EECR''' (EEPROM Control Register) aus. Da das EEPROM eine größere Zugriffszeit als das Flash besitzt, wird anschließend eine Schleife eingefügt, die solange wartet, bis das EEPROM signalisiert, dass die Daten jetzt verfügbar sind. Das gelesene Byte kann nun aus dem IO-Register '''EEDR''' (EEPROM Data Register) in ein normales Arbeitsregister kopiert und von dort weiterverarbeitet werden.
Als erstes muss geprüft werden, ob ein vorheriger Schreibzugriff schon abgeschlossen ist. Danach wird die EEPROM-Adresse von der gelesen werden soll in das IO-Registerpaar '''EEARH/EEARL''' (EEPROM Address Register) geladen. Da der ATmega8 mehr als 256 Byte EEPROM hat, passt die Adresse nicht in ein einziges 8-Bit-Register, sondern muss in zwei Register aufgeteilt werden: EEARH bekommt das obere Byte der Adresse, EEARL das untere Byte. Dann löst man den Lesevorgang durch das Setzen des Bits '''EERE''' (EEPROM Read Enable) im IO-Register '''EECR''' (EEPROM Control Register) aus. Das gelesene Byte kann sofort aus dem IO-Register '''EEDR''' (EEPROM Data Register) in ein normales CPU-Register kopiert und dort weiterverarbeitet werden.
 
Wie auch das Flash-ROM kann man das EEPROM über den ISP-Programmer programmieren. Die Daten, die im EEPROM abgelegt werden sollen, werden wie gewohnt mit .db angegeben; allerdings muss man dem Assembler natürlich sagen, dass es sich hier um Daten für das EEPROM handelt. Das macht man durch die Direktive '''.eseg''', woran der Assembler erkennt, dass alle nun folgenden Daten für das EEPROM bestimmt sind.  


Doch um etwas aus dem EEPROM lesen zu können, muss man natürlich erst mal Daten hineinbekommen.
Damit man die Bytes nicht von Hand abzählen muss um die Adresse herauszufinden, kann man auch im EEPROM-Segment wieder Labels einsetzen und diese im Assemblerprogramm wie Konstanten verwenden.  


Wie auch das Flash-ROM kann man das EEPROM direkt über den ISP-Programmer programmieren. Die Daten, die im EEPROM abgelegt werden sollen, werden wie gewohnt mit .db angegeben; allerdings muss man dem Assembler natürlich sagen, dass es sich hier um Daten für das EEPROM handelt. Das macht man durch die Direktive '''.eseg''', woran der Assembler erkennt, dass alle nun folgenden Daten für das EEPROM bestimmt sind.  
<syntaxhighlight lang="asm">
.include "m8def.inc"


Damit man die Bytes nicht von Hand abzählen muss um die Adresse herauszufinden, kann man auch im EEPROM-Segment wieder Labels einsetzen und diese im Assemblerprogramm wie Konstanten verwenden.  
; hier geht die Programmsektion los
.cseg


[http://www.mikrocontroller.net/sourcecode/tutorial/eeprom1.asm Download eeprom1.asm]
    ldi    r16, low(RAMEND)            ; Stackpointer initialisieren
    out    SPL, r16                   
    ldi    r16, high(RAMEND)
    out    SPH, r16                   


<avrasm>
    ldi    r16, 0xFF
.include "4433def.inc"
    out    DDRB, r16                  ; Port B Ausgang


        ldi R16, 0xFF
    ldi     ZL,low(daten)              ; Z-Zeiger laden
        out DDRB, R16                    ; Port B: Ausgang
    ldi    ZH,high(daten)
    rcall  EEPROM_read                ; Daten aus EEPROM lesen
    out     PORTB, r16


        ldi  R16, daten
loop: 
        out  EEAR, R16                    ; Adresse laden
    rjmp loop
       
        sbi  EECR, EERE                  ; Lesevorgang aktivieren


        in R16, EEDR                      ; gelesenes Byte nach R16 kopieren
EEPROM_read:
       
    sbic    EECR,EEWE                  ; prüfe ob der vorherige Schreibzugriff
        out PORTB, R16                    ; ... und an PORTB ausgeben
                                        ; beendet ist
    rjmp    EEPROM_read                ; nein, nochmal prüfen


loop:  rjmp loop
    out    EEARH, ZH                  ; Adresse laden
    out    EEARL, ZL   
    sbi    EECR, EERE                  ; Lesevorgang aktivieren
    in      r16, EEDR                  ; Daten in CPU Register kopieren
    ret


.eseg                                     ; EEPROM-Segment aktivieren
; Daten im EEPROM definieren
.eseg
daten:
daten:
        .db 0b10101010
    .db     0b10101010
</avrasm>
</syntaxhighlight>


Wenn man dieses Programm assembliert, erhält man außer der .hex-Datei noch eine Datei mit der Endung '''.eep'''. Diese Datei enthält die Daten aus dem EEPROM-Segment (.eseg), und muss zusätzlich zu der hex-Datei in den Controller programmiert werden.  
Wenn man dieses Programm assembliert, erhält man außer der .hex-Datei noch eine Datei mit der Endung '''.eep'''. Diese Datei enthält die Daten aus dem EEPROM-Segment (.eseg), und muss zusätzlich zu der hex-Datei in den Controller programmiert werden.  


Bei Controllern mit mehr als 256 Byte EEPROM, z.B. dem ATmega8, passt die EEPROM-Adresse nicht mehr in ein Byte alleine. Statt einem Adressregister ('''EEAR''') gibt es bei diesen Controllern deshalb 2 Register, eines für das Low-Byte ('''EEARL''') und eines für das High-Byte ('''EEARH'''). Für den ATmega8 muss das oben stehende Programm also folgendermaßen abgeändert werden:
Das Programm gibt die Binärzahl 0b10101010 an den Port B aus, das heißt jetzt sollte jede zweite LED leuchten.


[http://www.mikrocontroller.net/sourcecode/tutorial/eeprom1-m8.asm Download eeprom1-m8.asm]
Natürlich kann man auch aus dem EEPROM Strings lesen und an den UART senden:  


<avrasm>
<syntaxhighlight lang="asm">  
.include "m8def.inc"
.include "m8def.inc"


        ldi R16, 0xFF
.def temp = r16
        out DDRB, R16                    ; Port B: Ausgang
 
        ldi r16, HIGH(daten)              ; Adresse laden
        out EEARH, r16
        ldi r16, LOW(daten)
        out EEARL, r16
       
        sbi EECR, EERE                    ; Lesevorgang aktivieren


        in R16, EEDR
.equ CLOCK = 4000000                ; Frequenz des Quarzes
       
        out PORTB, R16


loop:  rjmp loop
.equ BAUD = 9600                    ; Baudrate
.equ UBRRVAL = CLOCK/(BAUD*16)-1    ; Baudratenteiler
; hier geht das Programmsegment los


.eseg
.CSEG
daten:
        .db 0b10101010
; Hauptprogramm
</avrasm>
main:
 
    ldi    temp, LOW(RAMEND)          ; Stackpointer initialisieren
Natürlich kann man auch aus dem EEPROM Strings lesen und an den UART senden:
    out    SPL, temp
 
    ldi    temp, HIGH(RAMEND)
[http://www.mikrocontroller.net/sourcecode/tutorial/eeprom-print.asm Download eeprom-print.asm]
    out    SPH, temp
 
                         
<avrasm>
    ldi    temp, LOW(UBRRVAL)          ; Baudrate einstellen
.include "4433def.inc"
    out    UBRRL, temp
 
    ldi    temp, HIGH(UBRRVAL)
.def temp            = r16
    out    UBRRH, temp
.def address          = r17
                                   
.def data             = r18
    ldi    temp, (1<<URSEL)|(3<<UCSZ0) ; Frame-Format: 8 Bit
 
    out    UCSRC, temp
        ldi temp, RAMEND
    sbi    UCSRB, TXEN                ; TX (Senden) aktivieren
        out SPL, temp
    ldi    ZL, low(text1)              ; ersten String senden
    ldi    ZH, high(text1)             ; Z-Pointer laden
    rcall  EEPROM_print
   
    ldi     ZL, low(text2)              ; zweiten String senden
    ldi    ZH, high(text2)            ; Z-Pointer laden
    rcall  EEPROM_print


        sbi UCSRB,TXEN                    ; UART TX aktivieren
loop: 
        ldi temp,4000000/(9600*16)-1      ; Baudrate 9600 einstellen
    rjmp    loop                        ; Endlosschleife
        out UBRR,temp


        ldi address, text1                ; ersten String senden
        rcall eep_print
       
        ldi address, text2                ; zweiten String senden
        rcall eep_print


        ldi data, 10                      ; die Bytes 10 und 13 senden
; EEPROM Lesezugriff auf Strings + UART Ausgabe
        rcall sendbyte                    ; (Zeilenumbruch im Terminal)
        ldi data, 13
        rcall sendbyte


EEPROM_print:
    sbic    EECR,EEWE          ; prüf ob der vorherige Schreibzugriff
                                ; beendet ist
    rjmp    EEPROM_print        ; nein, nochmal prüfen


loop:  rjmp loop
    out     EEARH, ZH          ; Adresse laden
       
    out    EEARL, ZL
eep_print:
   
        out EEAR, address                ; EEPROM-Adresse
    sbi     EECR, EERE         ; Lesevorgang aktivieren
        sbi EECR, EERE                   ; Lesevorgang starten
    in     temp, EEDR         ; Daten in CPU Register kopieren
        in data, EEDR                     ; gelesenes Byte nach "data"
    tst     temp                ; auf 0 testen (=Stringende)
        tst data                          ; auf 0 (Stringende testen)
    breq   eep_print_end       ; falls 0, Funktion beenden
        breq eep_print_end               ; falls 0, Funktion beenden
    rcall   sendbyte           ; ansonsten Byte senden...
        rcall sendbyte                   ; ansonsten Byte senden...
    adiw    ZL,1                ; Adresse um 1 erhöhen...
        inc address                      ; ... Adresse um 1 erhöhen...
    rjmp   EEPROM_print        ; und zum Anfang der Funktion
        rjmp eep_print                    ; ... und zum Anfang der Funktion
eep_print_end:
eep_print_end:
        ret
    ret


; sendbyte: sendet das Byte aus "data" über den UART
; sendbyte: sendet das Byte aus "data" über den UART
sendbyte:
sendbyte:
        sbis UCSRA, UDRE                 ; warten bis das UART bereit ist
    sbis   UCSRA, UDRE         ; warten bis das UART bereit ist
        rjmp sendbyte
    rjmp   sendbyte
        out UDR, data
    out     UDR, temp
        ret
    ret
 
; hier wird der EEPROM-Inhalt definiert


.ESEG


.eseg
text1:
text1:
        .db "Strings funktionieren auch ", 0
    .db     "Strings funktionieren auch ", 0
text2:
text2:
        .db "im EEPROM", 0
    .db     "im EEPROM",10,13, 0
</avrasm>
</syntaxhighlight>
 


==== Schreiben ====
==== Schreiben ====


Hier wird kurz beschrieben, wie man Daten in den EEPROM schreibt.
Als erstes muss geprüft werden, ob ein vorheriger Schreibzugriff schon abgeschlossen ist. Danach wird die EEPROM-Adresse, auf die geschrieben wird, in das IO-Register '''EEAR''' ('''EE'''PROM '''A'''ddress '''R'''egister) geladen. Dann schreibt man die Daten, welche man auf der im Adressregister abgespeicherten Position ablegen will ins Register '''EEDR''' ('''EE'''PROM '''D'''ata '''R'''egister). Als nächstes setzt man das '''EEMWE''' Bit im EEPROM-Kontrollregister '''EECR''' ('''EE'''PROM '''C'''ontrol '''R'''egister) um den Schreibvorgang vorzubereiten. Nun wird es zeitkritisch - es darf nun keinesfalls ein Interrupt dazwischenfahren - denn man muss innerhalb von 4 Taktzyklen das '''EEWE''' Bit setzen um den Schreibvorgang auszulösen. Um das unter allen Bedingungen sicherzustellen werden die Interrupts kurz gesperrt. Danach startet der Schreibvorgang und läuft automatisch ab. Wenn er beendet ist, wird von der Hardware das '''EEWE''' Bit im Register '''EECR''' wieder gelöscht.
Zuerst wird die EEPROM-Adresse, auf die geschrieben wird, in das IO-Register '''EEAR''' (EEPROM Address Register) geladen. Dann schreibt man die Daten, die man auf der im Adressregister abgespeicherten Position ablegen will ins Register '''EEDR''' (EEPROM Data Register). Als nächstes setzt man das '''EEMWE''' Bit im EEPROM-Kontrollregister '''EECR''' (EEPROM Control Register) um den Schreibvorgang vorzubereiten. Nun wird es zeitkritisch - es darf nun keinesfalls ein Interrupt dazwischenfahren - denn man muss innerhalb von 4 Taktzyklen das '''EEWE''' Bit setzen um den Schreibvorgang auszulösen.
Danach ist der Schreibvorgang beendet.


Hier ein Code für das Schreiben in den EEPROM (durch UART-Interrupt ausgelöst) dazu:
In diesem Beispiel werden Zeichen per UART und Interrupt empfangen und nacheinander im EEPROM gespeichert. Per Terminalprogramm kann man nun bis zu 512 Zeichen in den EEPROM schreiben. Per Programmieradapter kann man denn EEPROM wieder auslesen und seine gespeicherten Daten anschauen.


<avrasm>
<syntaxhighlight lang="asm">  
include "m8def.inc"
.include "m8def.inc"
   
   
def temp = R16
.def temp     = r16
.def sreg_save = r17
 
.equ CLOCK = 4000000
.equ CLOCK = 4000000
.equ BAUD = 9600
.equ BAUD = 9600
.equ UBRRVAL = CLOCK/(BAUD*16)-1
.equ UBRRVAL = CLOCK/(BAUD*16)-1
   
   
; hier geht das Programmsegment los
.CSEG
.org 0x00
.org 0x00
        rjmp main
    rjmp   main
   
   
.org URXCaddr
.org URXCaddr
        rjmp int_rxc
    rjmp   int_rxc
   
   
; Hauptprogramm
; Hauptprogramm
main:
main:
        ldi temp, LOW(RAMEND)           ; Stackpointer initialisieren
    ldi     temp, LOW(RAMEND)          ; Stackpointer initialisieren
        out SPL, temp
    out     SPL, temp
        ldi temp, HIGH(RAMEND)
    ldi     temp, HIGH(RAMEND)
        out SPH, temp
    out     SPH, temp
 
                         
                             
    ldi     temp, LOW(UBRRVAL)         ; Baudrate einstellen
        ldi temp, LOW(UBRRVAL)           ; Baudrate einstellen
    out     UBRRL, temp
        out UBRRL, temp
    ldi     temp, HIGH(UBRRVAL)
        ldi temp, HIGH(UBRRVAL)
    out     UBRRH, temp
        out UBRRH, temp
   
   
                                     
                                 
        ldi temp, (1<<URSEL)|(3<<UCSZ0)   ; Frame-Format: 8 Bit
    ldi     temp, (1<<URSEL)|(3<<UCSZ0) ; Frame-Format: 8 Bit
        out UCSRC, temp
    out     UCSRC, temp
   
   
        sbi UCSRB, RXCIE                 ; Interrupt bei Empfang
    sbi     UCSRB, RXCIE               ; Interrupt bei Empfang
        sbi UCSRB, RXEN                   ; RX (Empfang) aktivieren
    sbi     UCSRB, RXEN                 ; RX (Empfang) aktivieren
 
    ldi    ZL,low(daten)              ; der Z-Zeiger wird hier exclusiv
    ldi    ZH,high(daten)              ; für die Datenadressierung verwendet
   
    sei                                ; Interrupts global aktivieren
   
loop: 
    rjmp    loop                        ; Endlosschleife (ABER Interrupts!)
          
          
        sei                              ; Interrupts global aktivieren
; Interruptroutine wird ausgeführt,
; sobald ein Byte über den UART empfangen wurde


loop:  rjmp loop                        ; Endlosschleife(ABER Interrupts!)
       
; Interruptroutine: wird ausgeführt sobald ein Byte über das UART empfangen wurde
int_rxc:
int_rxc:
        push temp                         ; temp auf dem Stack sichern
    push   temp                       ; temp auf dem Stack sichern
       
    in      temp,sreg                  ; SREG sicher, muss praktisch in jeder
        in temp, UDR
                                        ; Interruptroutine gemacht werden
    push    temp
        rcall EEPROM_write                ; Byte im EEPROM speichern
   
    in     temp, UDR                   ; empfangenes Byte lesen
    rcall   EEPROM_write                ; Byte im EEPROM speichern
    adiw    ZL,1                        ; Zeiger erhöhen
    cpi    ZL,low(EEPROMEND+1)        ; Vergleiche den Z Zeiger
    ldi    temp,high(EEPROMEND+1)      ; mit der maximalen EEPROM Adresse +1
    cpc    ZH,temp
    brne    int_rxc_1                  ; wenn ungleich, springen
    ldi    ZL,low(Daten)              ; wenn gleich, Zeiger zurücksetzen
    ldi    ZH,high(Daten)
int_rxc_1:


        pop temp                         ; temp wiederherstellen
    pop     temp
reti
    out    sreg,temp
    pop    temp                        ; temp wiederherstellen
    reti


; der eigentliche EEPROM Schreibzugriff
; Adresse in ZL/ZH
; Daten in temp


EEPROM_write:
EEPROM_write:
    sbic    EECR, EEWE                  ; prüfe ob der letzte Schreibvorgang beendet ist
    rjmp    EEPROM_write                ; wenn nein, nochmal prüfen


        ldi r18, HIGH(Daten)              ; High-Adresse im EEPROM laden
    out     EEARH, ZH                  ; Adresse schreiben
        out EEARH, r18                    ; und ins EEARH schreiben  
    out    EEARL, ZL                  ;  
        ldi r17, LOW(Daten)              ; Low-Adresse im EEPROM laden
    out     EEDR,temp                  ; Daten  schreiben
        out EEARL, r17                    ; und ins EEARL schreiben
    in      sreg_save,sreg              ; SREG sichern
                                     
    cli                                ; Interrupts sperren, die nächsten
sbic EECR,EEWE                    ; Vorherigen Schreibvorgang abwarten               
                                        ; zwei Befehle dürfen NICHT
rjmp EEPROM_write
                                        ; unterbrochen werden
                                 
    sbi     EECR,EEMWE                 ; Schreiben vorbereiten
out EEDR,temp                    ; Daten ins EEPROM-Datenregister
    sbi     EECR,EEWE                   ; Und los !
             
    out    sreg, sreg_save            ; SREG wieder herstellen
sbi EECR,EEMWE                   ; Schreiben vorbereiten
    ret
sbi EECR,EEWE                     ; Und los !
 
ret


</avrasm>
; hier wird der EEPROM-Inhalt definiert
.ESEG


Das ist jetzt sehr einfach gehalten, aber für Anfänger sicher leicht auf ihr Projekt zu übertragen.
Daten: 
    .db    0
</syntaxhighlight>


=== SRAM ===
=== SRAM ===


Die Verwendung des SRAM wird in einem eigenen Kapitel erklärt: [[AVR-Tutorial: SRAM]]
Die Verwendung des SRAM wird in einem anderen Kapitel erklärt: [[AVR-Tutorial: SRAM]]


== Siehe auch ==
== Siehe auch ==
* [[Adressierung]]
* [[Adressierung]]
----


{{Navigation_zurückhochvor|
{{Navigation_zurückhochvor|
Zeile 446: Zeile 517:
vorlink=AVR-Tutorial: Timer}}
vorlink=AVR-Tutorial: Timer}}


[[Category:AVR]][[Category:AVR-Tutorial]]
[[Category:AVR-Tutorial|Speicher]]
[[Kategorie:Speicher und Dateisysteme]]

Aktuelle Version vom 22. November 2020, 19:08 Uhr

Speichertypen

Die AVR-Mikrocontroller besitzen 3 verschiedene Arten von Speicher:

hat Flash EEPROM RAM
Schreibzyklen >10.000 >100.000 unbegrenzt
Lesezyklen unbegrenzt unbegrenzt unbegrenzt
flüchtig nein nein ja
Größe beim ATtiny2313 2 KB 128 Byte 128 Byte
Größe beim ATmega8 8 KB 512 Byte 1 KB
Größe beim ATmega32 32 KB 1 KB 2 KB
Größe beim ATmega2560 256 KB 4 KB 8 KB

Flash-ROM

Das Flash-ROM der AVRs dient als Programmspeicher. Über den Programmieradapter werden die kompilierten Programme vom PC an den Controller übertragen und im Flash-ROM abgelegt. Bei der Programmausführung wird das ROM Wort für Wort ausgelesen und ausgeführt. Es lässt sich aber auch zur Speicherung von Daten nutzen (z. B. Texte für ein LCD). Vom laufenden Programm aus kann man das ROM normalerweise nur lesen, nicht beschreiben. Es kann beliebig oft ausgelesen werden, aber theoretisch nur ~10.000 mal beschrieben werden.

EEPROM

Das EEPROM ist wie das Flash ein nichtflüchtiger Speicher, die Daten bleiben also auch nach dem Ausschalten der Betriebsspannung erhalten. Es kann beliebig oft gelesen und mindestens 100.000 mal beschrieben werden. Bei den AVRs kann man es z. B. als Speicher für Messwerte oder Einstellungen benutzen.

RAM

Das RAM ist ein flüchtiger Speicher, d.h. die Daten gehen nach dem Ausschalten verloren. Es kann beliebig oft gelesen und beschrieben werden, weshalb es sich zur Speicherung von Variablen eignet, für die die Register R0-R31 nicht ausreichen. Daneben dient es als Speicherort für den Stack, auf dem z. B. bei Unterprogrammaufrufen (rcall) die Rücksprungadresse gespeichert wird (siehe AVR-Tutorial: SRAM).

Anwendung

Flash-ROM

Die erste und wichtigste Anwendung des Flash-ROMs kennen wir bereits: Das Speichern von Programmen, die wir nach dem Assemblieren dort hineingeladen haben. Nun sollen aber auch vom laufenden Programm aus Daten ausgelesen werden.

Um die Daten wieder auszulesen, muss man die Adresse, auf die zugegriffen werden soll, in den Z-Pointer laden. Der Z-Pointer besteht aus den Registern R30 (Low-Byte) und R31 (High-Byte), daher kann man das Laden einer Konstante wie gewohnt mit dem Befehl ldi durchführen. Statt R30 und R31 kann man übrigens einfach ZL und ZH schreiben, da diese Synonyme bereits in der include-Datei m8def.inc definiert sind.

Wenn die richtige Adresse erstmal im Z-Pointer steht, geht das eigentliche Laden der Daten ganz einfach mit dem Befehl lpm. Dieser Befehl, der im Gegensatz zu out, ldi usw. keine Operanden hat, veranlasst das Laden des durch den Z-Pointer adressierten Bytes aus dem Programmspeicher in das Register R0, von wo aus man es weiterverarbeiten kann.

Jetzt muss man nur noch wissen, wie man dem Assembler überhaupt beibringt, dass er die von uns festgelegten Daten im ROM platzieren soll, und wie man dann an die Adresse kommt, an der sich diese Daten befinden. Um den Programmspeicher mit Daten zu füllen, gibt es die Direktiven .db und .dw. In der Regel benötigt man nur .db, was folgendermaßen funktioniert:

 
daten:
    .db 12, 20, 255, 0xFF, 0b10010000

Direktiven wie .db sind Anweisungen an den Assembler, keine Prozessorbefehle. Von denen kann man sie durch den vorangestellten Punkt unterscheiden. In diesem Fall sagen wir dem Assembler, dass er die angegebenen Bytes nacheinander im Speicher platzieren soll; wenn man die Zeile also assembliert, erhält man eine Hex-Datei, die nur diese Daten enthält.

Aber was soll das daten: am Anfang der Zeile? Bis jetzt haben wir Labels nur als Sprungmarken verwendet, um den Befehlen rcall und rjmp zu sagen, an welche Stelle im Programm gesprungen werden soll. Würden wir in diesem Fall rjmp daten im Programm stehen haben, dann würde die Programmausführung zur Stelle daten: springen und versuchen, die sinnlosen Daten als Befehle zu interpretieren - was mit Sicherheit dazu führt, dass der Controller Amok läuft.

Statt nach daten: zu springen, sollten wir die Adresse besser in den Z-Pointer laden. Da der Z-Pointer aus zwei Bytes besteht, brauchen wir dazu zweimal den Befehl ldi:

 
    ldi ZL, LOW(daten*2)    ; Low-Byte der Adresse in Z-Pointer
    ldi ZH, HIGH(daten*2)   ; High-Byte der Adresse in Z-Pointer

Wie man sieht, ist das Ganze sehr einfach: Man kann die Labels im Assembler direkt wie Konstanten verwenden. Über die Multiplikation der Adresse mit zwei sollte man sich erst mal keine Gedanken machen: "Das ist einfach so." Wer es genauer wissen will schaut hier nach.

Um zu zeigen, wie das alles konkret funktioniert, ist das folgende Beispiel nützlich:

  
.include "m8def.inc"

    ldi     R16, 0xFF
    out     DDRB, R16               ; Port B: Ausgang

    ldi     ZL, LOW(daten*2)        ; Low-Byte der Adresse in Z-Pointer
    ldi     ZH, HIGH(daten*2)       ; High-Byte der Adresse in Z-Pointer

    lpm                             ; durch Z-Pointer adressiertes Byte
                                    ; in R0 laden
    out     PORTB, R0               ; an PORTB ausgeben

ende:   
    rjmp ende                       ; Endlosschleife

daten:
    .db 0b10101010

Wenn man dieses Programm assembliert und in den Controller überträgt, dann kann man auf den an Port B angeschlossenen LEDs das mit .db 0b10101010 im Programmspeicher abgelegte Bitmuster sehen.

Fallbeispiel: Zeichenketten

Eine häufige Anwendung von lpm ist das Auslesen von Zeichenketten ("Strings") aus dem Flash-ROM und die Ausgabe an den seriellen Port oder ein LCD. Das folgende Programm gibt in einer Endlosschleife den Text "AVR-Assembler ist ganz einfach", gefolgt von einem Zeilenumbruch, an den UART aus.

 
.include "m8def.inc"

.def temp = r16
.def temp1 = r17

.equ CLOCK = 4000000                ; Frequenz des Quarzes
.equ BAUD = 9600                    ; Baudrate
.equ UBRRVAL = CLOCK/(BAUD*16)-1    ; Baudratenteiler
 
; hier geht das Programmsegment los

.CSEG 
.org 0
    ldi     temp, low(RAMEND)            ; Stackpointer initialisieren
    out     SPL, temp                    
    ldi     temp, high(RAMEND)
    out     SPH, temp                    

    ldi     temp, LOW(UBRRVAL)          ; Baudrate einstellen
    out     UBRRL, temp
    ldi     temp, HIGH(UBRRVAL)
    out     UBRRH, temp
                                    
    ldi     temp, (1<<URSEL)|(3<<UCSZ0) ; Frame-Format: 8 Bit
    out     UCSRC, temp
    sbi     UCSRB, TXEN                 ; TX (Senden) aktivieren

loop:
    ldi     ZL, LOW(text*2)             ; Adresse des Strings in den
    ldi     ZH, HIGH(text*2)            ; Z-Pointer laden
    rcall   print                       ; Funktion print aufrufen
    rcall   wait                        ; kleine Pause
    rjmp    loop                        ; das Ganze wiederholen

; kleine Pause
wait:
    ldi     temp,0
wait_1:
    ldi     temp1,0
wait_2:
    dec     temp1
    brne    wait_2
    dec     temp
    brne    wait_1
    ret

; print: sendet die durch den Z-Pointer adressierte Zeichenkette

print:
    lpm                                 ; Erstes Byte des Strings nach R0 lesen
    tst     R0                          ; R0 auf 0 testen
    breq    print_end                   ; wenn 0, dann zu print_end
    mov     temp, r0                     ; Inhalt von R0 nach R16 kopieren
    rcall   sendbyte                    ; UART-Sendefunktion aufrufen
    adiw    ZL, 1                       ; Adresse des Z-Pointers um 1 erhöhen
    rjmp    print                       ; wieder zum Anfang springen
print_end:
    ret

; sendbyte: sendet das Byte aus temp über das UART

sendbyte:
    sbis    UCSRA, UDRE                 ; warten bis das UART bereit ist
    rjmp    sendbyte
    out     UDR, temp
    ret

; Konstanten werden hier im Flash abgelegt

text:
    .db "AVR-Assembler ist ganz einfach",10,13,0 
    ; Stringkonstante, durch eine 0 abgeschlossen
    ; die 10 bzw. 13 sind Steuerzeichen für Wagenrücklauf und neue Zeile

Neuere AVR-Controller besitzen einen erweiterten Befehlssatz. Darunter befindet sich auch der folgende Befehl:

 
    lpm     r16, Z+

Dieser Befehl liest ein Byte aus dem Flash und speichert es in einem beliebigen Register, hier r16. Danach wird der Zeiger Z um eins erhöht. Für die neuen Controller wie ATmegas kann das Codebeispiel also so abgeändert werden:

 
; print: sendet die durch den Z-Pointer adressierte Zeichenkette
print:
    lpm     temp, Z+         ; Erstes Byte des Strings nach temp lesen
    tst     temp             ; temp auf 0 testen
    breq    print_end       ; wenn 0, dann zu print_end
    rcall   sendbyte        ; UART-Sendefunktion aufrufen
    rjmp    print           ; wieder zum Anfang springen
print_end:
    ret

Wenn man bei .db einen Text in doppelten Anführungszeichen angibt, werden die Zeichen automatisch in die entsprechenden ASCII-Codes umgerechnet:

 
    .db     "Test", 0               
    ; ist äquivalent zu
    .db     84, 101, 115, 116, 0

Damit das Programm das Ende der Zeichenkette erkennen kann, wird eine 0 an den Text angehängt.

Das ist doch schonmal sehr viel praktischer, als jeden Buchstaben einzeln in ein Register zu laden und abzuschicken. Und wenn man statt sendbyte einfach die Routine lcd_data aus dem 4. Teil des Tutorials aufruft, dann funktioniert das gleiche sogar mit dem LCD!

Neue Assemblerbefehle

 
    lpm                             ; Liest das durch den Z-Pointer
                                    ; addressierte Byte aus dem Flash-ROM
                                    ; in das Register R0 ein. 

    lpm     [Register], Z           ; Macht das gleiche wie lpm, jedoch in
                                    ; ein beliebiges Register

    lpm     [Register], Z+          ; Erhöht zusätzlich den Z-Zeiger

    tst     [Register]              ; Prüft, ob Inhalt eines Registers
                                    ; gleich 0 ist.

    breq    [Label]                 ; Springt zu [Label], wenn der
                                    ; vorhergehende Vergleich wahr ist. 

    adiw    [Register], [Konstante] ; Addiert eine Konstante zu einem
                                    ; Registerpaar. [Register] bezeichnet das
                                    ; untere der beiden Register.
                                    ; Kann nur auf die Registerpaare
                                    ; R25:R24, R27:R26, R29:R28 und R31:R30
                                    ; angewendet werden.

EEPROM

Den Flash Speicher kann man also benutzen, um dort Daten abzulegen, die sich während eines Programmlaufs nicht verändern. Irgendwelche Tabellen oder konstante Texte. Aber des öfteren möchte man auch die Möglichkeit haben, Daten zu speichern und wieder zu lesen, die sich während des Programmlaufs ändern. Ganz besonders möchte man eine Speicherfläche zur Verfügung haben, die ihren Inhalt auch dann behält, wenn dem µC die Versorgungsspannung abgedreht wird. Man denke z.B. an irgendwelche Konfigurationsdaten oder an Informationen, wie weit der µC in der Bearbeitung von Daten vorangekommen ist oder irgendwelche Statistikdaten, die auch nach einem Stromausfall noch verfügbar sein sollen. In solchen Fällen kommt das EEPROM zum Einsatz

Lesen

Als erstes muss geprüft werden, ob ein vorheriger Schreibzugriff schon abgeschlossen ist. Danach wird die EEPROM-Adresse von der gelesen werden soll in das IO-Registerpaar EEARH/EEARL (EEPROM Address Register) geladen. Da der ATmega8 mehr als 256 Byte EEPROM hat, passt die Adresse nicht in ein einziges 8-Bit-Register, sondern muss in zwei Register aufgeteilt werden: EEARH bekommt das obere Byte der Adresse, EEARL das untere Byte. Dann löst man den Lesevorgang durch das Setzen des Bits EERE (EEPROM Read Enable) im IO-Register EECR (EEPROM Control Register) aus. Das gelesene Byte kann sofort aus dem IO-Register EEDR (EEPROM Data Register) in ein normales CPU-Register kopiert und dort weiterverarbeitet werden.

Wie auch das Flash-ROM kann man das EEPROM über den ISP-Programmer programmieren. Die Daten, die im EEPROM abgelegt werden sollen, werden wie gewohnt mit .db angegeben; allerdings muss man dem Assembler natürlich sagen, dass es sich hier um Daten für das EEPROM handelt. Das macht man durch die Direktive .eseg, woran der Assembler erkennt, dass alle nun folgenden Daten für das EEPROM bestimmt sind.

Damit man die Bytes nicht von Hand abzählen muss um die Adresse herauszufinden, kann man auch im EEPROM-Segment wieder Labels einsetzen und diese im Assemblerprogramm wie Konstanten verwenden.

 
.include "m8def.inc"

; hier geht die Programmsektion los
.cseg

    ldi     r16, low(RAMEND)            ; Stackpointer initialisieren
    out     SPL, r16                    
    ldi     r16, high(RAMEND)
    out     SPH, r16                    

    ldi     r16, 0xFF
    out     DDRB, r16                   ; Port B Ausgang

    ldi     ZL,low(daten)               ; Z-Zeiger laden
    ldi     ZH,high(daten)
    rcall   EEPROM_read                 ; Daten aus EEPROM lesen
    out     PORTB, r16

loop:   
    rjmp loop

EEPROM_read:
    sbic    EECR,EEWE                   ; prüfe ob der vorherige Schreibzugriff
                                        ; beendet ist
    rjmp    EEPROM_read                 ; nein, nochmal prüfen

    out     EEARH, ZH                   ; Adresse laden
    out     EEARL, ZL    
    sbi     EECR, EERE                  ; Lesevorgang aktivieren
    in      r16, EEDR                   ; Daten in CPU Register kopieren
    ret

; Daten im EEPROM definieren
.eseg
daten:
    .db     0b10101010

Wenn man dieses Programm assembliert, erhält man außer der .hex-Datei noch eine Datei mit der Endung .eep. Diese Datei enthält die Daten aus dem EEPROM-Segment (.eseg), und muss zusätzlich zu der hex-Datei in den Controller programmiert werden.

Das Programm gibt die Binärzahl 0b10101010 an den Port B aus, das heißt jetzt sollte jede zweite LED leuchten.

Natürlich kann man auch aus dem EEPROM Strings lesen und an den UART senden:

 
.include "m8def.inc"

.def temp = r16

.equ CLOCK = 4000000                ; Frequenz des Quarzes

.equ BAUD = 9600                    ; Baudrate
.equ UBRRVAL = CLOCK/(BAUD*16)-1    ; Baudratenteiler
 
; hier geht das Programmsegment los

.CSEG 
 
; Hauptprogramm
main:
    ldi     temp, LOW(RAMEND)           ; Stackpointer initialisieren
    out     SPL, temp
    ldi     temp, HIGH(RAMEND)
    out     SPH, temp
                          
    ldi     temp, LOW(UBRRVAL)          ; Baudrate einstellen
    out     UBRRL, temp
    ldi     temp, HIGH(UBRRVAL)
    out     UBRRH, temp
                                    
    ldi     temp, (1<<URSEL)|(3<<UCSZ0) ; Frame-Format: 8 Bit
    out     UCSRC, temp
    sbi     UCSRB, TXEN                 ; TX (Senden) aktivieren
 
    ldi     ZL, low(text1)              ; ersten String senden
    ldi     ZH, high(text1)             ; Z-Pointer laden
    rcall   EEPROM_print
    
    ldi     ZL, low(text2)              ; zweiten String senden
    ldi     ZH, high(text2)             ; Z-Pointer laden
    rcall   EEPROM_print

loop:   
    rjmp    loop                        ; Endlosschleife


; EEPROM Lesezugriff auf Strings + UART Ausgabe

EEPROM_print:
    sbic    EECR,EEWE           ; prüf ob der vorherige Schreibzugriff
                                ; beendet ist
    rjmp    EEPROM_print        ; nein, nochmal prüfen

    out     EEARH, ZH           ; Adresse laden
    out     EEARL, ZL
    
    sbi     EECR, EERE          ; Lesevorgang aktivieren
    in      temp, EEDR          ; Daten in CPU Register kopieren
    tst     temp                ; auf 0 testen (=Stringende)
    breq    eep_print_end       ; falls 0, Funktion beenden
    rcall   sendbyte            ; ansonsten Byte senden...
    adiw    ZL,1                ; Adresse um 1 erhöhen...
    rjmp    EEPROM_print        ; und zum Anfang der Funktion
eep_print_end:
    ret

; sendbyte: sendet das Byte aus "data" über den UART

sendbyte:
    sbis    UCSRA, UDRE         ; warten bis das UART bereit ist
    rjmp    sendbyte
    out     UDR, temp
    ret

; hier wird der EEPROM-Inhalt definiert

.ESEG

text1:
    .db     "Strings funktionieren auch ", 0
text2:
    .db     "im EEPROM",10,13, 0

Schreiben

Als erstes muss geprüft werden, ob ein vorheriger Schreibzugriff schon abgeschlossen ist. Danach wird die EEPROM-Adresse, auf die geschrieben wird, in das IO-Register EEAR (EEPROM Address Register) geladen. Dann schreibt man die Daten, welche man auf der im Adressregister abgespeicherten Position ablegen will ins Register EEDR (EEPROM Data Register). Als nächstes setzt man das EEMWE Bit im EEPROM-Kontrollregister EECR (EEPROM Control Register) um den Schreibvorgang vorzubereiten. Nun wird es zeitkritisch - es darf nun keinesfalls ein Interrupt dazwischenfahren - denn man muss innerhalb von 4 Taktzyklen das EEWE Bit setzen um den Schreibvorgang auszulösen. Um das unter allen Bedingungen sicherzustellen werden die Interrupts kurz gesperrt. Danach startet der Schreibvorgang und läuft automatisch ab. Wenn er beendet ist, wird von der Hardware das EEWE Bit im Register EECR wieder gelöscht.

In diesem Beispiel werden Zeichen per UART und Interrupt empfangen und nacheinander im EEPROM gespeichert. Per Terminalprogramm kann man nun bis zu 512 Zeichen in den EEPROM schreiben. Per Programmieradapter kann man denn EEPROM wieder auslesen und seine gespeicherten Daten anschauen.

 
.include "m8def.inc"
 
.def temp      = r16
.def sreg_save = r17

.equ CLOCK = 4000000

.equ BAUD = 9600
.equ UBRRVAL = CLOCK/(BAUD*16)-1
 
; hier geht das Programmsegment los

.CSEG
.org 0x00
    rjmp    main
 
.org URXCaddr
    rjmp    int_rxc
 
; Hauptprogramm
main:
    ldi     temp, LOW(RAMEND)           ; Stackpointer initialisieren
    out     SPL, temp
    ldi     temp, HIGH(RAMEND)
    out     SPH, temp
                          
    ldi     temp, LOW(UBRRVAL)          ; Baudrate einstellen
    out     UBRRL, temp
    ldi     temp, HIGH(UBRRVAL)
    out     UBRRH, temp
 
                                   
    ldi     temp, (1<<URSEL)|(3<<UCSZ0) ; Frame-Format: 8 Bit
    out     UCSRC, temp
 
    sbi     UCSRB, RXCIE                ; Interrupt bei Empfang
    sbi     UCSRB, RXEN                 ; RX (Empfang) aktivieren

    ldi     ZL,low(daten)               ; der Z-Zeiger wird hier exclusiv
    ldi     ZH,high(daten)              ; für die Datenadressierung verwendet
    
    sei                                 ; Interrupts global aktivieren
    
loop:   
    rjmp    loop                        ; Endlosschleife (ABER Interrupts!)
        
; Interruptroutine wird ausgeführt,
; sobald ein Byte über den UART empfangen wurde

int_rxc:
    push    temp                        ; temp auf dem Stack sichern
    in      temp,sreg                   ; SREG sicher, muss praktisch in jeder
                                        ; Interruptroutine gemacht werden
    push    temp
    
    in      temp, UDR                   ; empfangenes Byte lesen
    rcall   EEPROM_write                ; Byte im EEPROM speichern
    adiw    ZL,1                        ; Zeiger erhöhen
    cpi     ZL,low(EEPROMEND+1)         ; Vergleiche den Z Zeiger
    ldi     temp,high(EEPROMEND+1)      ; mit der maximalen EEPROM Adresse +1
    cpc     ZH,temp
    brne    int_rxc_1                   ; wenn ungleich, springen
    ldi     ZL,low(Daten)               ; wenn gleich, Zeiger zurücksetzen
    ldi     ZH,high(Daten)
int_rxc_1:

    pop     temp
    out     sreg,temp
    pop     temp                        ; temp wiederherstellen
    reti

; der eigentliche EEPROM Schreibzugriff
; Adresse in ZL/ZH
; Daten in temp

EEPROM_write:
    sbic    EECR, EEWE                  ; prüfe ob der letzte Schreibvorgang beendet ist
    rjmp    EEPROM_write                ; wenn nein, nochmal prüfen

    out     EEARH, ZH                   ; Adresse schreiben
    out     EEARL, ZL                   ; 
    out     EEDR,temp                   ; Daten  schreiben
    in      sreg_save,sreg              ; SREG sichern
    cli                                 ; Interrupts sperren, die nächsten
                                        ; zwei Befehle dürfen NICHT
                                        ; unterbrochen werden
    sbi     EECR,EEMWE                  ; Schreiben vorbereiten
    sbi     EECR,EEWE                   ; Und los !
    out     sreg, sreg_save             ; SREG wieder herstellen
    ret

; hier wird der EEPROM-Inhalt definiert
.ESEG

Daten:  
    .db     0

SRAM

Die Verwendung des SRAM wird in einem anderen Kapitel erklärt: AVR-Tutorial: SRAM

Siehe auch