AVR-Tutorial: SRAM
SRAM - Der Speicher des Controllers
Nachdem in einem der vorangegangenen Kapitel eine Software PWM vorgestellt wurde 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 Mega8 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 taten. Nur gibt es dann ein Problem: Während 6 PWM Kanäle noch problemlos in den Registern untergebracht werden konnten, ist dies mit 30 oder noch mehr PWM Kanälen nicht möglich. Es gibt schlicht und ergreifend nicht genug Register!
Es gibt aber einen Ausweg. Der Mega8 verfügt über 1 KByte SRAM (statisches RAM). Dieses RAM wurde bereits indirekt benutzt: Bei jedem Aufruf eines Unterprogrammes, sei es über einen expliziten CALL oder einen Interrupt, wird ja die Rücksprungsadresse irgendwo gespeichert. Dies geschieht genau in diesem SRAM. Auch PUSH und POP operieren in diesem Speicher.
Und dasselbe können auch wir tun! Ein Programm darf Speicherzellen im SRAM benutzen und dort Werte speichern bzw. von dort Werte einlesen. Es muss nur darauf geachtet werden, daß es zu keiner Kollision mit dem Stack kommt, in dem 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 Mega8 immerhin über 1 KByte SRAM verfügt, ist dies in der Praxis kein allzugroßes Problem.
Das .DSEG und .BYTE
Um dem Assembler mitzuteilen, daß 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.
Das Direktiv .BYTE stellt dabei eine derartige Speicherreservierung dar. Es ermöglicht, der Speicherreservierung einen Namen zu geben und es erlaubt auch, nicht nur 1 Byte sondern eine ganze Reihe von Bytes unter einem Namen zu reservieren.
<avrasm>
.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 </avrasm>
spezielle Befehle
Für den Zugriff auf den SRAM Speicher gibt es spezielle Befehle. Diese holen den momentanen Inhalt einer Speicherzelle und legen ihn in einem Register ab, bzw. die Umkehrung: Den Inhalt eines Registers in einer SRAM Speicherzelle ablegen.
LDS
Liest die angegebene SRAM Speicherzelle und legt den gelesenen Wert in einem Register ab.
<avrasm>
LDS r17, Counter ; liest die Speicherzelle mit dem Namen 'Counter' ; und legt den gelesenen Wert im Register r17 ab
</avrasm>
STS
Legt den in einem Register gespeicherten Wert in einer SRAM Speicherzelle ab.
<avrasm>
STS Counter, r17 ; Speichert den Inhalt von r17 in der ; Speicherzelle 'Counter'
</avrasm>
Beispiel
Eine mögliche Implementierung der Software PWM, die die einzelnen OCR Grenzwerte im SRAM anstelle von Registern speichert, könnte zb. so aussehen:
<avrasm> .include "m8def.inc"
.def temp = r16 .def temp2 = r17
.def PWMCount = r18
.org 0x0000
rjmp main ; Reset Handler
.org OVF0addr
rjmp timer0_overflow ; Timer Overflow Handler
main:
ldi temp, LOW(RAMEND) ; Stackpointer initialisieren out SPL, temp ldi temp, HIGH(RAMEND) out SPH, 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, 0b00000001 ; CS00 setzen: Teiler 1 out TCCR0, temp ldi temp, 0b00000001 ; TOIE0: Interrupt bei Timer Overflow out TIMSK, temp sei
loop: rjmp loop
timer0_overflow: ; Timer 0 Overflow Handler
inc PWMCount ; den PWM Zähler von 0 bis cpi PWMCount, 128 ; 127 zählen lassen brne WorkPWM clr PWMCount
WorkPWM:
ldi temp, 0b11000000 ; 0 .. Led an, 1 .. Led aus
lds temp2, OCR_1 cp PWMCount, temp2 ; Ist der Grenzwert für Led 1 erreicht brlt OneOn ori temp, $01
OneOn: lds temp2, OCR_2
cp PWMCount, temp2 ; Ist der Grenzwert für Led 2 erreicht brlt TwoOn ori temp, $02
TwoOn: lds temp2, OCR_3
cp PWMCount, temp2 ; Ist der Grenzwert für Led 3 erreicht brlt ThreeOn ori temp, $04
ThreeOn:lds temp2, OCR_4
cp PWMCount, temp2 ; Ist der Grenzwert für Led 4 erreicht brlt FourOn ori temp, $08
FourOn: lds temp2, OCR_5
cp PWMCount, temp2 ; Ist der Grenzwert für Led 5 erreicht brlt FiveOn ori temp, $10
FiveOn: lds temp2, OCR_6
cp PWMCount, temp2 ; Ist der Grenzwert für Led 6 erreicht brlt SetBits ori temp, $20
SetBits: ; Die neue Bitbelegung am Port ausgeben
out PORTB, temp
reti
.DSEG ; das Folgende kommt ins SRAM
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 </avrasm>
Natürlich würde es auch Sinn machen, den PWMCount ebenfalls im SRAM abzulegen.
spezielle Register
Der Z-Pointer (R30 und R31)
Das Registerpärchen R30 und R31 können zu einem einzigen logischen Register zusammengefasst werden und heissen dann der 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. Besonders komfortabel ist dies, da im Ladebefehl noch zusätzliche Manipulationen angegeben werden können.
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
ST
- ST rxx, Z
- ST rxx, Z+
- ST rxx, -Z
Speichert den Inhalt des Register 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
- ST rxx, Z+q
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, deren 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.
<avrasm> timer0_overflow: ; Timer 0 Overflow Handler
inc PWMCount ; den PWM Zähler von 0 bis cpi PWMCount, 128 ; 127 zählen lassen brne WorkPWM clr PWMCount
ldi r30,LOW(OCR) ; den Z-Pointer mit dem Start der OCR Bytes laden ldi r31,HIGH(OCR) ldi temp1, $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 PWMCount, temp2 ; ist der Grenzwert für PWM Nr. i erreicht? brne LedOn or temp, temp1
LedOn:
lsl temp1 ; das Bitmuster schieben cpi temp1, $20 ; alle Bits behandelt ? brne pwmloop ; nächster Schleifendurchlauf
out PORTB, temp ; Die neue Bitbelegung am Port ausgeben reti
.DSEG ; das Folgende kommt ins SRAM
OCR: .BYTE 6 ; 6 Bytes für die OCR Register </avrasm>
X-Pointer, Y-Pointer
Neben dem Z-Pointer gibt es noch den X-Pointer bzw. 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-Pointer bzw. Y-Pointer mit einer Ausnahme: Mit dem X-Pointer ist kein Zugriff LDD/STD mit einem Displacement möglich.