AVR-Tutorial: SRAM
SRAM – Der Speicher des Controllers
Nachdem in einem der vorangegangenen Kapitel eine Software-PWM vorgestellt und in einem weiteren Kapitel darüber gesprochen wurde, wie man mit Schieberegistern die Anzahl an I/O-Pins erhöhen kann, wäre es naheliegend, beides zu kombinieren und den ATmega8 mal 20 oder 30 LEDs ansteuern zu lassen. Wenn es da nicht ein Problem gäbe: die Software-PWM hält ihre Daten in Registern, so wie das praktisch alle Programme bisher machten. Während allerdings 6 PWM-Kanäle noch problemlos in den Registern untergebracht werden konnten, ist dies mit 30 oder noch mehr PWM-Kanälen nicht mehr möglich. Es gibt schlicht und ergreifend nicht genug Register.
Es gibt aber einen Ausweg. Der ATmega8 verfügt über 1 KByte SRAM (statisches RAM). Dieses RAM wurde bereits indirekt durch den Stack benutzt. Bei jedem Aufruf eines Unterprogrammes, sei es über einen expliziten call
(bzw. rcall
) oder einen Interrupt, wird die Rücksprungadresse irgendwo gespeichert. Dies geschieht genau in diesem SRAM. Auch push
und pop
operieren in diesem Speicher.
Ein Programm darf Speicherzellen im SRAM direkt benutzen und dort Werte speichern bzw. von dort Werte einlesen. Es muss nur darauf geachtet werden, dass es zu keiner Kollision mit dem Stack kommt, in dem z. B. die erwähnten Rücksprungadressen für Unterprogramme gespeichert werden. Da viele Programme aber lediglich ein paar Byte SRAM brauchen, der Rücksprungstack von der oberen Grenze des SRAM nach unten wächst und der ATmega8 immerhin über 1 KByte (= 1.024 Byte) SRAM verfügt, ist dies in der Praxis kein allzu großes Problem.
Das .DSEG und .BYTE
Um dem Assembler mitzuteilen, dass sich der folgende Abschnitt auf das SRAM bezieht, gibt es die Direktive .DSEG
(Data Segment). Alle nach einer .DSEG
-Direktive folgenden Speicherreservierungen werden vom Assembler im SRAM durchgeführt.
Die Direktive .BYTE
stellt dabei eine derartige Speicherreservierung dar. Sie ermöglicht, der Speicherreservierung einen Namen zu geben und erlaubt auch, nicht nur 1 Byte, sondern eine ganze Reihe von Bytes unter einem Namen zu reservieren.
.DSEG ; Umschalten auf das SRAM-Datensegment
Counter: .BYTE 1 ; 1 Byte unter dem Namen 'Counter' reservieren
Test: .BYTE 20 ; 20 Byte unter dem Namen 'Test' reservieren
Spezielle Befehle
Für den Zugriff auf den SRAM-Speicher gibt es spezielle Befehle. Diese holen entweder den momentanen Inhalt einer Speicherzelle und legen ihn in einem Register ab oder legen den Inhalt eines Registers in einer SRAM-Speicherzelle ab.
LDS
Liest die angegebene SRAM-Speicherzelle und legt den gelesenen Wert in einem Register ab.
LDS r17, Counter ; Liest die Speicherzelle mit dem Namen 'Counter'
; und legt den gelesenen Wert im Register r17 ab.
STS
Legt den in einem Register gespeicherten Wert in einer SRAM-Speicherzelle ab.
STS Counter, r17 ; Speichert den Inhalt von r17 in der
; Speicherzelle 'Counter'.
Beispiel
Eine mögliche Implementierung der Software-PWM, die den PWM-Zähler sowie die einzelnen OCR-Grenzwerte im SRAM anstelle von Registern speichert, könnte z. B. so aussehen:
.include "m8def.inc"
.def temp = r16
.def temp1 = r17
.def temp2 = r18
.org 0x0000
rjmp main ; Reset Handler
.org OVF0addr
rjmp timer0_overflow ; Timer Overflow Handler
main:
ldi temp, HIGH(RAMEND) ; Stackpointer initialisieren
out SPH, temp
ldi temp, LOW(RAMEND)
out SPL, temp
ldi temp, 0xFF ; Port B auf Ausgang
out DDRB, temp
ldi temp2, 0
sts OCR_1, temp2
ldi temp2, 1
sts OCR_2, temp2
ldi temp2, 10
sts OCR_3, temp2
ldi temp2, 20
sts OCR_4, temp2
ldi temp2, 80
sts OCR_5, temp2
ldi temp2, 127
sts OCR_6, temp2
ldi temp, (1<<CS00) ; CS00 setzen: Teiler 1
out TCCR0, temp
ldi temp, (1<<TOIE0) ; TOIE0: Interrupt bei Timer Overflow
out TIMSK, temp
sei
loop: rjmp loop
; *************************************************************************
; Behandlung des Timer-Overflows
;
; realisiert die PWM auf 6 Kanälen
;
; veränderte Register: keine
;
timer0_overflow: ; Timer 0 Overflow Handler
push temp ; Alle verwendeten Register sichern
push temp1
push temp2
in temp, SREG
push temp
lds temp1, PWMCount ; den PWM-Zähler aus dem Speicher holen
inc temp1 ; Zähler erhöhen
cpi temp1, 128 ; Wurde 128 erreicht?
brne WorkPWM ; Nein
clr temp1 ; Ja: PWM-Zähler wieder auf 0
WorkPWM:
sts PWMCount, temp1 ; den PWM-Zähler wieder speichern
ldi temp, 0b11000000 ; 0 .. LED an, 1 .. LED aus
lds temp2, OCR_1
cp temp1, temp2 ; Ist der Grenzwert für LED 1 erreicht
brlt OneOn
ori temp, $01
OneOn: lds temp2, OCR_2
cp temp1, temp2 ; Ist der Grenzwert für LED 2 erreicht
brlt TwoOn
ori temp, $02
TwoOn: lds temp2, OCR_3
cp temp1, temp2 ; Ist der Grenzwert für LED 3 erreicht
brlt ThreeOn
ori temp, $04
ThreeOn:lds temp2, OCR_4
cp temp1, temp2 ; Ist der Grenzwert für LED 4 erreicht
brlt FourOn
ori temp, $08
FourOn: lds temp2, OCR_5
cp temp1, temp2 ; Ist der Grenzwert für LED 5 erreicht
brlt FiveOn
ori temp, $10
FiveOn: lds temp2, OCR_6
cp temp1, temp2 ; Ist der Grenzwert für LED 6 erreicht
brlt SetBits
ori temp, $20
SetBits: ; Die neue Bitbelegung am Port ausgeben
out PORTB, temp
pop temp ; die gesicherten Register wiederherstellen
out SREG, temp
pop temp2
pop temp1
pop temp
reti
;
; **********************************************
;
.DSEG ; das Folgende kommt ins SRAM
PWMCount: .BYTE 1 ; Der PWM-Counter (0 bis 127)
OCR_1: .BYTE 1 ; 6 Bytes für die OCR-Register
OCR_2: .BYTE 1
OCR_3: .BYTE 1
OCR_4: .BYTE 1
OCR_5: .BYTE 1
OCR_6: .BYTE 1
Die ISR sichert alle verwendeten Register und stellt sie am Ende der ISR wieder her. Dies ist zwar streng genommen in diesem Beispiel nicht notwendig, da das eigentliche Programm in der Hauptschleife ja nichts tut, aber außer einem bisschen Zeit kostet das nichts und es erspart das Kopfkratzen, wenn dann irgendwann in der Hauptschleife Ergänzungen und anderer Code dazu kommen. Man kann natürlich einige Register speziell für die Verwendung ausschließlich in der ISR reservieren und sich so das Sichern/Wiederherstellen ersparen, aber das SREG muss im Normalfall innerhalb einer ISR auf jeden Fall gesichert und wiederhergestellt werden.
Spezielle Register
Der Z-Pointer (R30 und R31)
Das Registerpärchen R30 und R31 kann zu einem einzigen logischen Register zusammengefasst werden und heißt dann Z-Pointer. Diesem kann eine spezielle Aufgabe zukommen, indem er als Adressangabe fungieren kann, von welcher Speicherzelle im SRAM ein Ladevorgang (bzw. Speichervorgang) durchgeführt werden soll. Anstatt die Speicheradresse wie beim lds
bzw. sts
direkt im Programmcode anzugeben, kann diese Speicheradresse zunächst in den Z-Pointer geladen werden und der Lesevorgang (Schreibvorgang) über diesen Z-Pointer abgewickelt werden. Dadurch wird aber die SRAM-Speicheradresse berechenbar, denn natürlich kann mit den Registern R30 und R31, wie mit den anderen Registern auch, Arithmetik betrieben werden. Besonders komfortabel ist dies, da im Ladebefehl noch zusätzliche Manipulationen angegeben werden können, die oft benötigte arithmetische Operationen implementieren.
LD
LD rxx, Z
LD rxx, Z+
LD rxx, -Z
Lädt das Register rxx
mit dem Inhalt der Speicherzelle, deren Adresse im Z-Pointer angegeben ist. Bei den Varianten mit Z+
bzw. -Z
wird zusätzlich der Z-Pointer nach der Operation um 1 erhöht bzw. vor der Operation um 1 vermindert.
LDD
LDD rxx, Z+q
Hier erfolgt der Zugriff wieder über den Z-Pointer, wobei vor dem Zugriff zur Adressangabe im Z-Pointer noch das Displacement q
addiert wird.
Enthält also der Z-Pointer die Adresse $1000 und sei q
der Wert $28, so wird mit einer Ladeanweisung
LDD r18, Z + $28
der Inhalt der Speicherzelle $1000 + $28 = $1028 in das Register r18 geladen.
Der Wertebereich für q
erstreckt sich von 0 bis 63.
ST
ST Z, rxx
ST Z+, rxx
ST -Z, rxx
Speichert den Inhalt des Registers rxx
in der Speicherzelle, deren Adresse im Z-Pointer angegeben ist. Bei den Varianten mit Z+
bzw. -Z
wird zusätzlich der Z-Pointer nach der Operation um 1 erhöht bzw. vor der Operation um 1 vermindert.
STD
STD Z+q, rxx
Hier erfolgt der Zugriff wieder über den Z-Pointer, wobei vor dem Zugriff zur Adressangabe im Z-Pointer noch das Displacement q
addiert wird.
Enthält also der Z-Pointer die Adresse $1000 und sei q
der Wert $28, so wird mit einer Speicheranweisung
STD Z + $28, r18
der Inhalt des Registers r18 in der Speicherzelle $1000 + $28 = $1028 gespeichert.
Der Wertebereich für q
erstreckt sich von 0 bis 63.
Beispiel
Durch Verwendung des Z-Pointers ist es möglich, die Interrupt-Funktion wesentlich kürzer und vor allem ohne ständige Wiederholung von im Prinzip immer gleichem Code zu formulieren. Man stelle sich nur mal vor, wie dieser Code aussehen würde, wenn anstelle von 6 PWM-Stufen derer 40 gebraucht würden. Mit dem Z-Pointer ist es möglich, diesen auf das erste der OCR-Bytes zu setzen und dann in einer Schleife eines nach dem anderen abzuarbeiten. Nach dem Laden des jeweiligen OCR-Wertes wird der Z-Pointer automatisch durch den ld
-Befehl auf das nächste zu verarbeitende OCR-Byte weitergezählt.
.include "m8def.inc"
.def temp = r16
.def temp1 = r17
.def temp2 = r18
.def temp3 = r19
.org 0x0000
rjmp main ; Reset Handler
.org OVF0addr
rjmp timer0_overflow ; Timer Overflow Handler
main:
ldi temp, HIGH(RAMEND) ; Stackpointer initialisieren
out SPH, temp
ldi temp, LOW(RAMEND)
out SPL, temp
ldi temp, 0xFF ; Port B auf Ausgang
out DDRB, temp
ldi r30, LOW(OCR) ; den Z-Pointer mit dem Start der OCR-Bytes laden
ldi r31, HIGH(OCR)
ldi temp2, 0
st Z+, temp2
ldi temp2, 1
st Z+, temp2
ldi temp2, 10
st Z+, temp2
ldi temp2, 20
st Z+, temp2
ldi temp2, 80
st Z+, temp2
ldi temp2, 127
st Z+, temp2
ldi temp2, 0 ; den PWM-Counter auf 0 setzen
sts PWMCount, temp2
ldi temp, (1<<CS00) ; CS00 setzen: Teiler 1
out TCCR0, temp
ldi temp, (1<<TOIE0) ; TOIE0: Interrupt bei Timer Overflow
out TIMSK, temp
sei
loop: rjmp loop
; *************************************************************************
; Behandlung des Timer-Overflows
;
; realisiert die PWM auf 6 Kanälen
;
; veränderte Register: keine
;
timer0_overflow: ; Timer 0 Overflow Handler
push temp ; Alle verwendeten Register sichern
push temp1
push temp2
push R30
push R31
in temp, SREG
push temp
lds temp1, PWMCount ; den PWM-Zähler aus dem Speicher holen
inc temp1 ; Zähler erhöhen
cpi temp1, 128 ; Wurde 128 erreicht?
brne WorkPWM ; Nein
clr temp1 ; Ja: PWM-Zähler auf 0 setzen
WorkPWM:
sts PWMCount, temp1 ; den PWM-Zähler wieder speichern
ldi r30, LOW(OCR) ; den Z-Pointer mit dem Start der OCR-Bytes laden
ldi r31, HIGH(OCR)
ldi temp3, $01 ; das Bitmuster für PWM Nr. i
ldi temp, 0b11000000 ; 0 .. Led an, 1 .. Led aus
pwmloop:
ld temp2, Z+ ; den OCR-Wert für PWM Nr. i holen und Z-Pointer erhöhen
cp temp1, temp2 ; Ist der Grenzwert für PWM Nr. i erreicht?
brlo LedOn
or temp, temp3
LedOn:
lsl temp3 ; das Bitmuster schieben
cpi temp3, $40 ; Alle Bits behandelt?
brne pwmloop ; nächster Schleifendurchlauf
out PORTB, temp ; Die neue Bitbelegung am Port ausgeben
pop temp ; die gesicherten Register wiederherstellen
out SREG, temp
pop R31
pop R30
pop temp2
pop temp1
pop temp
reti
;
; *********************************************************************
;
.DSEG ; das Folgende kommt ins SRAM
PWMCount: .BYTE 1 ; der PWM-Zähler (0 bis 127)
OCR: .BYTE 6 ; 6 Bytes für die OCR-Register
X-Pointer, Y-Pointer
Neben dem Z-Pointer gibt es noch den X-Pointer und den Y-Pointer. Sie werden gebildet von den Registerpärchen
- X-Pointer: r26, r27
- Y-Pointer: r28, r29
- Z-Pointer: r30, r31
Alles über den Z-Pointer gesagte gilt sinngemäß auch für den X- bzw. Y-Pointer mit einer Ausnahme: Mit dem X-Pointer ist kein Zugriff über ldd
oder std
mit einem Displacement möglich.