Pfad:
Home =>
AVR-Überblick =>
Programmiertechniken => Ringpuffer
(This page in English:
)
Programmiertechnik für Anfänger in AVR Assemblersprache
Aufbau und Programmierung eines Ringpuffers in Assembler
Wenn in einem Programm Daten oder Meldungen gespeichert werden und diese
asynchron in einem anderen Programmteil entnommen und z. B. an einer
seriellen Schnittstelle ausgesendet werden sollen, braucht man zum
Zwischenspeichern dieser Daten einen Datenpuffer. Der Puffer liegt
natürlich im SRAM des AVRs.
Der Vorteil der Zwischenspeicherung im SRAM ist, dass zwischen dem
Generieren der Daten oder Texte und ihrer Ausgabe nahezu beliebige
Zeiträume liegen können. Jedenfalls aich beide Vorgänge
nicht mehr zeitlich voneinander abhängig: das Generieren kann
separat zeitlich gestaltet werden und muss sich nicht mehr nach der
Ausgabegeschwindigkeit richten.
Sehr unterschiedliche Geschwindigkeiten bei der Ein- und der Ausgabe
in und aus dem Puffer sind möglich. Dabei muss die
Ausgabegeschwindigkeit nur immer etwas höher sein als die
Erzeugungsgeschwindigkeit, weil es sonst zu Pufferüberläufen
käme: Teile der Daten gehen dann verloren.
An einem Beispiel: der AD-Wandler soll alle 10 ms lang eine
Messung machen und das Ergebnis als vierstellige Dezimalzahl mit
einem Wagenrücklauf- und einem Zeilenvorschubzeichen an einer
seriellen Schnittstelle ausgeben. Zu senden sind also sechs
ASCII-Zeichen zu je 8 Bits macht 48 Bits. Sendet die serielle
Schnittstelle asynchron, dann kommen noch ein Start- und zwei
Stopbits pro Zeichen hinzu und insgesamt sind dann 18 + 48 =
66 Bits zu senden. Die Baudrate beim Senden muss dann mindestens
66 Bits pro 10 ms oder 6.600 Baud schnell sein. 9k2 wäre
dann schnell genug.
Natürlich ließe sich das auch ohne Ringpufferung erledigen,
wenn der AVR immer nur das zu machen hätte. Soll er aber auch
noch zusätzlich einen weiteren Pin überwachen und die Zeit
zwischen zwei Impulsen in µs auch noch dazwischen streuen,
wäre das ohne Ringpuffer nicht zu bewältigen. Jedenfalls
kämen sich beide Vorgänge in die zeitliche Quere.
Kein Problem mit einem Ringpuffer: Alle 10 ms legt dort die
ADC-Auswerteroutine ihre Daten ab. Und die Zeitmessung legt ihre
Daten einfach dann ab, wenn sie denn eintreten (gar nicht oder
irgendwann, wenn halt eine Taste gedrückt wird).
Natütürlich muss man trotzdem sicherstellen, dass der
Ringpuffer nicht überläuft (z. B. wenn die Taste
arg prellt und jede Menge Einzelimpulse mit kurzer Dauer auslöst).
Aufbau eines Ringpuffers
Das wäre mal so ein Ringpuffer mit n Bytes Kapazität.
Anfang und Ende des Ringpuffers sind mit der .DSEG-Sequenz
.dseg
.org SRAM_START
sRingpuffer:
.byte RAMEND - sRingpuffer - 15
sRingpufferEnde:
mal eben schnell festgelegt. Die 16 freien Bytes sind für
den Stapel, ansonsten umfasst der Ringpuffer alles an SRAM, was
der betreffende AVR-Typ so zu bieten hat.
In dem Bild steht die Eingabeadresse auf der Position des achten
Bytes, es sind daher schon sieben Bytes in den Puffer abgelegt
worden.
Wie der Name schon sagt, ist ein Ringpuffer kreisförmig:
ist sein Ende in "sRingpufferEnde" erreicht, geht es
einfach wieder von vorne wieder los. Das macht ein wenig
Code-Aufwand, funktioniert aber lückenlos und endlos.
Zwei Zeiger werden benötigt:
- die aktuelle Eingabeadresse, und
- die aktuelle Ausgabeadresse.
Da beide Adressen mit jeder Ein- und Ausgabe wechseln und damit
das Ganze auch mit mehr als 256 Bytes SRAM funktioniert, sind
die Adressen 16-bittig auszulegen. Sie können entweder in
zwei Doppelregistern wie X und Y oder auch - registersparend -
im SRAM angelegt sein, wie hier:
.dseg
.org SRAM_START
sEin:
.byte 2 ; Eingabeadresse
sAus:
.byte 2 ; Ausgabeadresse
;
sRingpuffer:
.byte RAMEND - sRingpuffer - 15
sRingpufferEnde:
Eingabe in den Ringpuffer
Die Eingabe in den Ringpuffer erfolgt so, dass das einzugebende
Byte an der aktuellen Position abgelegt wird. Dabei wird die
Eingabeadresse um Eins erhöht. Ist dann das Ende des Puffers
erreicht wird die Adresse auf den Pufferanfang gelegt.
Nun wird geprüft, ob die neue Eingabeadresse nun auf die
aktuelle Ausgabeadresse zeigt. Wenn das der Fall ist, liegt ein
Pufferüberlauf vor. Dann
- wird die erhöhte Eingabeadresse nicht abgelegt, und
- die Carry-Flagge gesetzt.
Falls kein Pufferüberlauf erfolgte, wird die neue
Eingabeadresse geschrieben und die Carry-Flagge gelöscht.
Die Assemblerformulierung lautet dann:
InDenPuffer:
lds ZL,sEin ; Lese Eingabeadresse in den Puffer
lds ZH,sEin+1
st Z+,R16 ; Schreibe in den Puffer und erhoehe Adresse
cpi ZL,Low(sRingpufferEnde)
brne InDenPuffer1
cpi ZH,High(sRingpufferEnde)
brne InDenPuffer1
ldi ZH,High(sRingpuffer)
ldi ZL,Low(sRingpuffer)
InDenPuffer1:
lds R16,sAus ; Lese Ausgabeadresse
cp R16,ZL ; Gleichheit?
brne InDenPuffer2
lds R16,sAus+1
cp R16,ZH
brne InDenPuffer2
sec ; Pufferueberlauf
ret
InDenPuffer2:
sts sEin,ZL ; Neue Eingabeadresse ablegen
sts sEin+1,ZH
clc ; Kein Pufferueberlauf
ret
An der Ursprungsadresse kann in beiden Fällen nun noch
geprüft werden, ob der Sendevorgang schon läuft
oder, wenn nicht, jetzt angestossen werden soll.
Ausgabe aus dem Ringpuffer
Zuerst ist hierbei zu prüfen, ob die Ausgabeadresse identisch
mit der Eingabeadresse ist. Wenn das der Fall ist, liegen keine
Zeichen im Puffer vor und die Carry-Flagge wird gesetzt. In der
Senderoutine kann dann der Sendevorgang beendet werden.
Lagen Zeichen vor, dann wird das Zeichen gelesen und die
Ausgabeadresse um Eins erhöht. Erreicht sie dabei das Ende
des Puffers, beginnt der Zeiger wieder von vorne. Schließlich
wird noch die Carry-Flagge gelöscht.
So sieht das Ganze in Assembler aus:
AusDemPuffer:
lds ZL,sAus ; Lese Ausgabeadresse in den Puffer
lds ZH,sAus+1
lds R16,sEin ; Ausgabeadresse = Eingabeadresse
cp R16,ZL
brne AusdemPuffer1
lds R16,sEin+1
cp R16,ZH
brne AusdemPuffer1
sec ; Keine Zeichen im Puffer
ret
AusDemPuffer1:
ld R16,Z+ ; Lese Zeichen aus dem Puffer und erhoehe Adresse
cpi ZL,Low(sRingpufferEnde)
brne AusDemPuffer2
cpi ZH,High(sRingpufferEnde)
brne AusDemPuffer2
ldi ZH,High(sRingpuffer)
ldi ZL,Low(sRingpuffer)
AusDemPuffer2:
sts sAus,ZL ; Schreibe Ausgabeadresse
sts sAus+1,ZH
clc ; Kein Pufferueberlauf, Byte in R16 gueltig
ret
Simulation des Ringpuffers
Um die Ein- und Ausgabe in den und aus dem Ringpuffer zu testen,
habe ich diese Routinen in ein Assemblerprogramm gepackt, das
hier heruntergeladen werden kann.
Das Programm vollführt die nachfolgenden Schritte.
- PufferLeeren: Es richtet den Puffer ein, leert alle
SRAM-Zellen des Puffers und setzt die beiden Zeiger auf den
Pufferanfang.
- EingabepufferBisCarry: Der Puffer wird so lange mit
aufsteigenden Zahlen gefüllt, bis ein Carry eintritt
(Pufferüberlauf).
- AusgabepufferBisCarry: Es werden nacheinander die
gespeicherten Zahlen aus dem Puffer ausgelesen, bis ein
Carry eintritt (Puffer ist leer).
Nachfolgend werden diese Schritte mit dem Simulator
avr_sim gezeigt.
Hier ist das SRAM eines ATtiny24 zu sehen. Die in grün
umrandeten beiden Zeigeradressen für Pufferein- und
ausgaben zeigen auf den Anfang des Puffers (0x0064). Alle
Bytes im blau umrandeten Puffer sind auf Null gesetzt. Für
die Stapelverwendung sind die hellgrün umrandeten 16
Bytes am Ende des SRAM reserviert.
Das Nullsetzen erfolgt hier nur der Sichtbarkeit halber. Aus
dem Puffer gelesen werden können nur solche Bytes, die
auch vorher dorthin geschrieben wurden, deshalb ist das Leeren
eigentlich überflüssig.
Hier wurde ein erstes Byte in den Ringpuffer geschrieben: es
ist die blau umrandete Eins an der Adresse 0x0064. Der
Eingangszeiger zeigt nun auf 0x0065, die nächste zu
beschreibende Zelle,
Hier sind nun alle Bytes des Ringpuffers gefüllt. Das
letzte Byte (0x6C) wurde zwar schon geschrieben, aber beim
Vortrieb der Eingabeadresse wurde bemerkt, dass der Zeiger
nun auf die Ausgabeadresse zeigen würde. Daher wurde
der erhöhte Zeiger gar nicht abgelegt, er steht nach
wie vor auf 0x00CF (grün umrandet). Nachfolgende
Schreiboperationen würden daher in die gleiche Zelle
erfolgen, wären aber erfolglos, weil kein Platz mehr
im Puffer ist.
Die Carry-Flagge beim Schreiben des letzten Bytes zeigt den
Pufferüberlauf an.
Die Ausgabeadresse sowie der nicht zum Puffer gehörende
geschützte Bereich für den Stapel werden beim
Schreiben in den Puffer übrigens nicht angetastet.
Beim Leseaufruf gibt das Register R16 den ausgelesenen Wert
zurück. Hier war das das Byte an der Adresse 0x0064.
Die gelöschte Carry-Flagge signalisiert, dass der Wert
gültig ist. Die Ausgabeadresse zeigt nun auf 0x0065
und damit auf den nächsten auszulesenden Wert.
Hier wurde nun solange gelesen, bis die Carry-Flagge anzeigt,
dass der Puffer leer ist. Der letzte gültige Wert wurde
in Register R17 abgelegt, es war die 0x6B. Die beiden Zeiger
sind nun identisch, der Puffer ist leer und kann wieder
befüllt werden. Wo die Zeiger dabei stehen, ist
völlig uninteressant, es funktioniert an jeder Position
im Puffer in gleicher Weise.
Ringpuffer in Assembler: einfacher als in Hochsprachen
Versuche das hier Vollführte mal in einer Hochsprache zu
programmieren. Das geht zwar, ist aber recht holprig. Zuerst
steht die Entscheidung an, welcher Datentyp hier gespeichert
werden soll: Bytes, Character, Integerwerte, oder was denn nun.
In Assembler: speicher doch was Du willst. Wenn es denn
64-Bit-Fließkommazahlen sein sollen, dann braucht halt
jede Zahl acht Schreibvorgänge. Mach das mal mit einem
Compiler: der dreht dann durch, wenn mitten in der Zahl ein
Pufferüberlauf auftreten würde.
Beim Lesen aus dem Puffer werden zwei Informationen zurück
gegeben: das Carry zeigt an, ob noch Werte gespeichert waren,
das Register gibt den Wert als Byte zurück, wenn ja.
Wird das in Hochsprache dann eine Funktion, muss man die mit
zwei Rückgaben unterschiedlichen Typs ausstatten: einem
Boolean und einem Byte. Geht, ist aber alles andere als elegant.
Probleme, die Assembler gar nicht kennt, weil der alles auf
einmal zurückgeben kann, was er denn so hat. Zur Not
halt auch das ganze SRAM auf einmal.
Was in Assembler einfach und mit wenigen Instruktionen erledigt
werden kann, kann in Hochsprachen ein Zeiger-Albtraum werden.
Da sage noch einer, Assembler sei kompliziert. Manches ist damit
sogar einfacher und eleganter formulierbar.
Zum Seitenanfang
©2022 by http://www.avr-asm-tutorial.net