AVR-Tutorial: Speicher

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Version vom 23. Dezember 2007, 13:25 Uhr von FalkB (Diskussion | Beiträge) (→‎Schreiben: EEPROM Schreibzugriff korrigiert)
Wechseln zu: Navigation, Suche

Speichertypen

Die AVR-Mikrocontroller besitzen 3 verschiedene Arten von Speichern:

Flash EEPROM RAM
Schreibzyklen >10.000 >100.000 unbegrenzt
Lesezyklen unbegrenzt unbegrenzt unbegrenzt
flüchtig nein nein ja
Größe beim AT90S2333 2 KB 128 Byte 128 Byte
Größe beim AT90S4433 4 KB 256 Byte 128 Byte
Größe beim ATmega8 8 KB 512 Byte 1 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 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.

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 (siehe Kapitel 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).

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 dass das durch den Z-Pointer addressierte Byte aus dem Programmspeicher in das Register R0 geladen wird, 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 Direktiven .db und .dw. In der Regel benötigt man nur .db, was folgendermaßen funktioniert:

<avrasm> daten:

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

</avrasm>

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.

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:

<avrasm>

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

</avrasm>

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."

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

<avrasm> .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 nach R0 lesen
       mov R16, R0                       ; nach R16 kopieren
       out PORTB, R16                    ; an PORTB ausgeben

ende: rjmp ende  ; Endlosschleife

daten:

       .db 0b10101010

</avrasm>

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.

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.

<avrasm> .include "m8def.inc"

.def temp = R16

       ldi R16, RAMEND
       out SPL, R16                      ; Stackpointer initialisieren
       sbi UCSRB,TXEN                    ; UART TX aktivieren
       ldi temp,4000000/(9600*16)-1      ; Baudrate 9600 einstellen
       out UBRR,temp

start:

       ldi ZL, LOW(text*2)               ; Adresse des Strings in den
       ldi ZH, HIGH(text*2)              ; Z-Pointer laden
       rcall print                       ; Unterfunktion print aufrufen
       ldi R16, 10                       ; die Bytes 10 und 13 senden
       rcall sendbyte                    ; (Zeilenumbruch im Terminal)
       ldi R16, 13
       rcall sendbyte
       rjmp start                        ; das Ganze wiederholen


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 r16, 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 R16 über das UART

sendbyte:

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

text:

       .db "AVR-Assembler ist ganz einfach",0    ; Stringkonstante, durch eine 0 abgeschlossen

</avrasm>

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

<avrasm>

lpm r16, Z+

</avrasm>

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>

print
sendet die durch den Z-Pointer adressierte Zeichenkette

print:

       lpm r16, z+                       ; Erstes Byte des Strings nach r16 lesen
       tst r16                           ; r16 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

</avrasm>

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

<avrasm>

       .db 84, 101, 115, 116, 0

</avrasm>

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

<avrasm>

       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 der Inhalt eines Registers 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. 

</avrasm>

EEPROM

Lesen

Zuerst 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 RAM 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. 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 dann aus dem IO-Register EEDR (EEPROM Data Register) in ein normales Arbeitsregister kopiert und von dort weiterverarbeitet werden.

Doch um etwas aus dem EEPROM lesen zu können, muss man natürlich erst mal Daten hineinbekommen.

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.

<avrasm> .include "m8def.inc"

       ldi R16, 0xFF
       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
       
       out PORTB, R16

loop: rjmp loop

.eseg daten:

       .db 0b10101010

</avrasm>

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:

Hinweis: Dieses Beispiel wurde noch nicht für den ATmega8 angepasst.

<avrasm> .include "4433def.inc"

.def temp = r16 .def address = r17 .def data = r18

       ldi temp, RAMEND
       out SPL, temp
       sbi UCSRB,TXEN                    ; UART TX aktivieren
       ldi temp,4000000/(9600*16)-1      ; Baudrate 9600 einstellen
       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
       rcall sendbyte                    ; (Zeilenumbruch im Terminal)
       ldi data, 13
       rcall sendbyte


loop: rjmp loop

eep_print:

       out EEAR, address                 ; EEPROM-Adresse
       sbi EECR, EERE                    ; Lesevorgang starten
       in data, EEDR                     ; gelesenes Byte nach "data"
       tst data                          ; auf 0 (Stringende testen)
       breq eep_print_end                ; falls 0, Funktion beenden
       rcall sendbyte                    ; ansonsten Byte senden...
       inc address                       ; ... Adresse um 1 erhöhen...
       rjmp eep_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, data
       ret


.eseg text1:

       .db "Strings funktionieren auch ", 0

text2:

       .db "im EEPROM", 0

</avrasm>

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 sicherzustellen werden die Interrupts kurz gesperrt. Danach startet der Schreibvorgang und läuft automatisch ab. Wenn er beendet ist, wird von der Hardware das EEPE Bit im Register EECR wieder gelöscht.

Hier ein Code für das Schreiben in den EEPROM (durch UART-Interrupt ausgelöst) dazu:

<avrasm> 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
       
       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
       pop     temp
       out     sreg,temp
       pop     temp                            ; temp wiederherstellen

reti

der eigentliche EEPROM Schreibzugriff

EEPROM_write:

       sbic    EECR, EEPE                  ; prüfe ob der letzte Schreibvorgang beendet ist
       rjmp    EEPROM_write                ; wenn nein, nochmal prüfen
       ldi     temp, HIGH(Daten)           ; High-Adresse im EEPROM laden
       out     EEARH, temp                 ; und ins EEARH schreiben
       ldi     temp, LOW(Daten)            ; Low-Adresse im EEPROM laden
       out     EEARL, temp                 ; und ins EEARL schreiben
       out     EEDR,temp                   ; Daten ins EEPROM-Datenregister
       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,0,0,0

</avrasm>

Das ist jetzt sehr einfach gehalten, aber für Anfänger leicht auf ihr Projekt zu übertragen.

SRAM

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

Siehe auch