AVR-Tutorial: Speicher: Unterschied zwischen den Versionen
FalkB (Diskussion | Beiträge) (Beispiele überarbeitet und getestet!) |
FalkB (Diskussion | Beiträge) K (→Flash-ROM) |
||
Zeile 51: | Zeile 51: | ||
=== Flash-ROM === | === Flash-ROM === | ||
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 | 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.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 | |||
=== EEPROM === | === EEPROM === |
Version vom 24. Dezember 2007, 13:00 Uhr
Speichertypen
Die AVR-Mikrocontroller besitzen 3 verschiedene Arten von Speicher:
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 |
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, 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 das Laden des durch den Z-Pointer addressierte Byte 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 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 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:
<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 ; in R0 laden out PORTB, R0 ; 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 .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 r16, low(RAMEND) ; Stackpointer initialisieren out SPL, r16 ldi r16, high(RAMEND) out SPH, r16
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
- 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
- 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
</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>
- 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 ; ist äquivalent zu .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 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.
</avrasm>
EEPROM
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.
<avrasm> .include "m8def.inc"
- hier geht die Programmsektion los
.cseg .org 0
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
</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:
<avrasm> .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
</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 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.
<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
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
</avrasm>
SRAM
Die Verwendung des SRAM wird in einem anderen Kapitel erklärt: AVR-Tutorial: SRAM