Diese Seite ist die vierte in einer fünfteiligen Serie über FIFOs. Diese Seite schlägt eine Methode vor, um mit der Möglichkeit umzugehen, dass ein Überlauf (overflow) auftreten kann.
Einführung
In vielen Anwendungen, die einen FIFO einsetzen, gibt es keine Möglichkeit, den Zustrom der ankommenden Daten zu steuern. In einer Datenerfassungsanwendung (data acquisition) beispielsweise schreibt die Logik die erfassten Daten direkt in den FIFO. Wenn der FIFO voll (full) ist, gehen diese Daten verloren. Bei Anwendungen dieser Art muss der Leser auf der anderen Seite des FIFO dafür sorgen, dass die Daten im FIFO schnell genug verbraucht werden, damit der FIFO niemals voll (full) wird.
Wird der FIFO jedoch voll (full), bricht die Lückenlosigkeit der Daten wegen des Datenverlusts ab: Der FIFO ignoriert die Schreibversuche. Der Inhalt des FIFO ist dann nicht mehr korrekt. Diese Situation nennt man einen Überlauf.
Ein Überlauf ist die Folge einer Fehlfunktion und sollte vermieden werden. Wenn ein Überlauf allerdings auftritt, ist es dennoch wichtig, dieses Ereignis zu erkennen und den Schaden zu begrenzen. Im Folgenden wird eine Strategie dazu vorgeschlagen.
Ein modifizierter FIFO
Das Hauptaugenmerk sollte immer darauf liegen, einen Überlauf zu verhindern. Schlägt das fehl, bleibt nur noch sicherzustellen, dass keine fehlerhaften Daten aus dem FIFO gelesen werden. Anders gesagt: Alle Daten, die aus dem FIFO gelesen werden, müssen lückenlos und korrekt sein.
Der Lösungsvorschlag ist ein modifizierter FIFO, der sich in zweierlei Hinsicht unterscheidet:
- Wenn ein Überlauf auftritt (d. h. der FIFO voll (full) wird), können bis zu einem Reset keine weiteren Daten mehr in den FIFO geschrieben werden.
- Wenn der FIFO nach einem Überlauf leer (empty) wird, meldet er, dass er so lange leer (empty) bleibt, bis ein Reset erfolgt. Diese Situation bezeichnen wir als EOF.
Die erste Eigenschaft stellt sicher, dass die aus dem modifizierten FIFO gelesenen Daten immer lückenlos und korrekt sind: Der FIFO verweigert das Schreiben weiterer Daten, nachdem die Lückenlosigkeit gebrochen ist. Die zweite Eigenschaft (EOF) übermittelt der Logik, die den FIFO verwendet, die Nachricht, dass etwas schiefgegangen ist. Das gibt der Logik die Möglichkeit, den Datenfluss neu zu starten.
Die Bezeichnung EOF steht für End of File (Dateiende): Dieser modifizierte FIFO ist für Datenerfassungsanwendungen (data acquisition) nützlich, die auf dem IP-Core (IP core) von Xillybus basieren. Bei einer solchen Anwendung transportiert der IP-Core von Xillybus die Daten zu einem Computer. Ein einfaches Computerprogramm verwendet die übliche Datei-I/O-API, um die Daten aus dem FIFO zu lesen. Mit anderen Worten öffnet das Programm eine Datei und liest auf die gewohnte Weise Daten daraus. Durch das EOF des FIFO verhält sich diese Datei ähnlich wie bei Erreichen des Endes einer regulären Datei. Das ist die natürliche Reaktion darauf, dass keine weiteren Daten zu lesen sind.
Eine Implementierung in Verilog
Hier ist ein Beispiel für ein Verilog-Modul, das den oben vorgeschlagenen modifizierten FIFO implementiert:
module eof_fifo
(
input rst,
input wr_clk,
input rd_clk,
input [31:0] din,
input wr_en,
input rd_en,
output [31:0] dout,
output full,
output empty,
output eof
);
reg rst_sync;
reg rst_cross;
reg fifo_has_been_full;
reg fifo_has_been_nonfull;
reg has_been_full_cross;
reg has_been_full;
assign ok_to_write = !rst_sync && !full && !fifo_has_been_full;
assign eof = empty && has_been_full;
always @(posedge wr_clk)
begin
if (!full)
fifo_has_been_nonfull <= 1;
else if (rst_sync)
fifo_has_been_nonfull <= 0;
if (full && fifo_has_been_nonfull)
fifo_has_been_full <= 1;
else if (rst_sync)
fifo_has_been_full <= 0;
end
// Clock domain crossing logic: asynchronous -> wr_clk
always @(posedge wr_clk)
begin
rst_cross <= rst;
rst_sync <= rst_cross;
end
// Clock domain crossing logic: wr_clk -> rd_clk
always @(posedge rd_clk)
begin
has_been_full_cross <= fifo_has_been_full;
has_been_full <= has_been_full_cross;
end
fifo fifo_ins
(
.rst(rst),
.wr_clk(wr_clk),
.rd_clk(rd_clk),
.din(din),
.wr_en(wr_en && ok_to_write),
.rd_en(rd_en),
.dout(dout),
.full(full),
.empty(empty)
);
endmodule
Man sieht deutlich, dass dieses Modul aus einer Instanziierung (instantiation) eines Standard-FIFO plus etwas Zusatzlogik besteht. Beachten Sie, dass die Ports dieses Moduls fast genau denen eines Standard-FIFO entsprechen. Es gibt nur einen Unterschied: Der modifizierte FIFO besitzt einen Port namens @eof.
Beachten Sie außerdem, dass @din und @dout eine Breite von 32 Bit haben. Wenn ein FIFO mit anderer Breite gewünscht ist, muss lediglich die Deklaration dieser Ports am Anfang des Moduls geändert werden.
Der modifizierte FIFO besitzt einen Port @full (deutsch: „voll“), der nicht unbedingt nützlich ist. Die Logik, die in den FIFO schreibt, könnte diesen Port ebenso gut ignorieren: Wenn der FIFO voll (full) wird, lässt sich ohnehin nichts mehr dagegen tun. So oder so wird der FIFO spätere Schreibversuche ignorieren. In den meisten Anwendungen hilft das Wissen, dass der FIFO voll (full) geworden ist, nicht viel weiter: Am besten wartet man, bis @eof high wird, und startet dann den gesamten Mechanismus neu.
Verhindern von Schreibvorgängen nach einem Überlauf
Wie bereits erwähnt, basiert dieses Modul auf einem Standard-FIFO. Alle Ports dieses FIFO sind direkt mit den Ports von eof_fifo verbunden – mit einer Ausnahme: @wr_en. Dieser Port ist stattdessen mit wr_en && ok_to_write verbunden. Es ist also offensichtlich, dass @ok_to_write dazu dient, die Schreibvorgänge anzuhalten, nachdem der FIFO voll (full) gewesen ist. Die Definition dieses Signals lautet:
assign ok_to_write = !rst_sync && !full && !fifo_has_been_full;
Aus diesem Ausdruck ergeben sich drei Situationen, die Schreibvorgänge verhindern:
- Wenn der FIFO zurückgesetzt wird.
- Wenn der FIFO voll (full) ist.
- Wenn der FIFO in der Vergangenheit voll (full) gewesen ist.
Die ersten beiden Bedingungen sind trivial. Konzentrieren wir uns auf die dritte Bedingung, die durch @fifo_has_been_full repräsentiert wird:
always @(posedge wr_clk)
begin
if (!full)
fifo_has_been_nonfull <= 1;
else if (rst_sync)
fifo_has_been_nonfull <= 0;
if (full && fifo_has_been_nonfull)
fifo_has_been_full <= 1;
else if (rst_sync)
fifo_has_been_full <= 0;
end
@fifo_has_been_full ist high, wenn der FIFO voll (full) gewesen ist. Dieses Register geht auf high, wenn sowohl @full (deutsch: „voll“) als auch @fifo_has_been_nonfull high sind.
Der erste Teil überrascht nicht: @full (deutsch: „voll“) ist mit dem full-Ausgang des FIFO verbunden. Warum ist @fifo_has_been_nonfull aber notwendig? Der Grund ist, dass ein FIFO seinen Ausgang full (deutsch: „voll“) oft so lange auf high hält, wie sich der FIFO im Reset-Zustand befindet. Dieser Ausgang soll der Anwendungslogik mitteilen, dass der FIFO noch nicht bereit ist, Daten aufzunehmen. @fifo_has_been_nonfull verhindert, dass @fifo_has_been_full in diesem Szenario fälschlich auf high geht.
@fifo_has_been_full geht auf low, wenn der FIFO zurückgesetzt wird. Mit anderen Worten: Ein Reset des FIFO ist der einzige Weg, nach einem Überlauf den normalen Betrieb mit dem modifizierten FIFO wieder aufzunehmen. Beachten Sie, dass @rst ein asynchroner Reset (asynchronous reset) ist. Deshalb muss eine Logik hinzugefügt werden, die ein Reset-Signal erzeugt, das zur richtigen Taktdomäne (clock domain) gehört. Dieses Signal ist @rst_sync, eine Kopie von @rst.
Erzeugung von EOF
Der Ausgang @eof ist high, wenn die beiden folgenden Bedingungen erfüllt sind:
- Alle Daten im FIFO sind verbraucht: Der FIFO ist empty (leer).
- Es werden keine weiteren Daten mehr in den FIFO geschrieben: @fifo_has_been_full ist high, weil der FIFO in der Vergangenheit voll (full) gewesen ist. Daher bleibt @ok_to_write so lange low, bis der FIFO zurückgesetzt wird.
Der Ausdruck im Verilog-Code für diese Bedingungen lautet:
assign eof = empty && has_been_full;
Beachten Sie, dass dieser Ausdruck auf @has_been_full basiert und nicht auf @fifo_has_been_nonfull: Sowohl @eof als auch @empty (deutsch: „leer“) gehören zur Taktdomäne von @rd_clk. @fifo_has_been_nonfull dagegen gehört zur Taktdomäne von @wr_clk. @fifo_has_been_nonfull wird daher mit einem Taktdomänenwechsel (clock domain crossing) in die Taktdomäne von @rd_clk kopiert. Diese Kopie ist @has_been_full.
@eof bedeutet also: „Der FIFO ist nicht nur empty (leer), sondern er wird auch nicht mehr gefüllt, bis man ihn zurücksetzt.“
Fazit
In vielen Anwendungen lässt sich nicht garantieren, dass kein Überlauf auftritt. Wenn dieses Ereignis nicht tolerierbar ist, lässt sich der Schaden bei seinem Eintreten begrenzen: Die Strategie besteht darin, die bis zum Überlauf geschriebenen Daten noch durchzulassen und anschließend keine weiteren Schreibvorgänge mehr zuzulassen. Irgendwann wird der FIFO dann empty (leer). In diesem Augenblick meldet der FIFO über den Ausgang @eof, dass keine weiteren Daten mehr ankommen werden.
Diese Methode garantiert die Lückenlosigkeit der aus dem FIFO gelesenen Daten: In der Mitte gehen keine Daten verloren, weil der FIFO voll (full) geworden ist. Das macht diese Methode für Datenerfassungsanwendungen (data acquisition) nützlich.
Diese Seite hat gezeigt, wie ein auf dieser Strategie basierender modifizierter FIFO implementiert wird. Dieser modifizierte FIFO kann ohne weitere Änderungen anstelle eines Standard-FIFO eingesetzt werden. Der einzige wichtige Unterschied ist, dass die den FIFO verwendende Logik auf den Port @eof achten muss: Sobald dieser Port high wird, muss die Logik den FIFO zurücksetzen und den Datenfluss neu starten.
Damit ist die vierte Seite in dieser Serie über FIFOs abgeschlossen. Die nächste Seite zeigt, wie man ein „Standard-FIFO“ in ein FWFT-FIFO umwandelt und umgekehrt, und wie sich das Timing verbessern lässt.