Pfad: Home => AVR-Überblick => Programmiertechniken => Ringpuffer    (This page in English: Flag EN) Logo

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

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:
  1. die aktuelle Eingabeadresse, und
  2. 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 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.
  1. PufferLeeren: Es richtet den Puffer ein, leert alle SRAM-Zellen des Puffers und setzt die beiden Zeiger auf den Pufferanfang.
  2. EingabepufferBisCarry: Der Puffer wird so lange mit aufsteigenden Zahlen gefüllt, bis ein Carry eintritt (Pufferüberlauf).
  3. 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.

Der gelehrte Ringpuffer im SRAM 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.

Ein Byte wird im Ringpuffer abgelegt 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,

Befüllen des Ringpuffers bis Carry 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 Pufferüberlauf 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.

Ein Byte aus dem Ringpuffer lesen 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.

Alle Bytes aus dem Ringpuffer lesen 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