Diese Seite ist der erste Teil einer fünfteiligen Artikelserie über FIFOs.
Überblick
Ein FPGA-FIFO ist ein Speicherelement mit einem einfachen Konzept: Ein Teil der Anwendungslogik schreibt Datenwörter auf der einen Seite in den FIFO. Auf der anderen Seite liest ein anderer Teil der Anwendungslogik diese Wörter wieder aus – in derselben Reihenfolge (FIFO = First In First Out).
Diese Daten werden innerhalb des FIFO gespeichert. Die Tiefe (engl. depth) des FIFO gibt an, wie viele Datenwörter er speichern kann. Die Breite (also die Anzahl der Bits pro Wort) und die Tiefe sind Parameter, die der Anwender für jeden FIFO konfigurieren kann.
FIFOs sind vermutlich der am häufigsten verwendete IP-Core (engl. IP core) in FPGA-Designs. Immer wenn ein Teil der Logik Daten erzeugt und ein anderer Teil sie verarbeitet, ist die naheliegendste Lösung, einen FIFO zwischen die beiden zu schalten (das soll natürlich nicht heißen, dass ein FIFO immer die richtige Lösung ist ...).
Wer mit der Kommandozeile vertraut ist (insbesondere mit UNIX/Linux), kann sich den Einsatz von FIFOs wie Pipes zwischen Kommandos vorstellen: Die Ausgabe des einen Programms wird zur Eingabe des anderen, und eine magische Mechanik dazwischen kümmert sich um den Rest.
Wegen ihrer allgegenwärtigen Verwendung gibt es eine Art De-facto-Übereinkunft darüber, wie sich ein FPGA-FIFO verhalten sollte. Jede FPGA-Entwicklungssoftware bietet eine Möglichkeit, ein FIFO-IP-Modul für das Anwendungsdesign zu erzeugen. Und nicht nur das: Dieses FIFO-Modul wird mit hoher Wahrscheinlichkeit über bestimmte Ports verfügen, die sich genauso verhalten wie bei jedem anderen FPGA-FIFO.
Die vom FPGA-Hersteller mitgelieferte Software ermöglicht es, FIFOs zu erzeugen, die genau auf Ihre Bedürfnisse zugeschnitten sind. Sie müssen nur deren Attribute in einem GUI-Werkzeug konfigurieren (Breite, Tiefe und weitere Attribute, auf die ich noch eingehe); den Rest erledigen die Werkzeuge. Alles, was Ihnen bleibt, ist die Instanziierung (engl. instantiation) des Moduls in Ihrem Design. Anders als viele andere Aufgaben in der FPGA-Welt ist das wirklich so einfach.
Da jeder FPGA-Hersteller sein eigenes FIFO-IP anbietet, ist es natürlich wichtig, die Dokumentation bis ins Kleingedruckte zu lesen. Die Hersteller verwenden eine leicht unterschiedliche Terminologie zur Beschreibung ihrer FIFOs. Auch die Namen der Modul-Ports unterscheiden sich ein wenig. Darüber hinaus bietet jeder Hersteller eine etwas andere Auswahl an Zusatzfunktionen und Konfigurationsoptionen an.
Trotzdem entsprechen die Standardeinstellungen mit hoher Wahrscheinlichkeit dem, was ich als „Basis-FIFO“ (baseline FIFO) bezeichnen werde. Darüber hinaus gibt es auf jeden Fall eine Reihe von Zusatzfunktionen, die nahezu immer verfügbar sind.
Die Implementierung des FIFO in der Logikarchitektur (logic fabric) unterscheidet sich jedoch von Hersteller zu Hersteller. Deshalb ist es wichtig, die Bedeutung der FIFO-Attribute zu verstehen, um die Ressourcen des FPGA gut auszunutzen.
Insgesamt gilt: FPGA-FIFOs kennen und verstehen zu lernen ist eine einmalige Angelegenheit. Wenn man erst einmal gelernt hat, mit ihnen auf einem FPGA zu arbeiten, ist es recht einfach, das auf einem anderen FPGA ebenso zu tun. Das allein ist schon ein Grund für ihre Allgegenwart.
Das Basis-FIFO
Es gibt zwar keinen geschriebenen Standard für FPGA-FIFOs, aber dennoch eine weitgehende Übereinstimmung darüber, wie sie sich verhalten sollten.
Alle FIFOs haben zwei Schnittstellen: eine zum Schreiben und eine zum Lesen von Wörtern. Schauen wir uns also die Instanziierung (instantiation) dessen an, was ich als „Basis-FIFO“ bezeichnen werde. Es gibt wichtige Varianten, auf die ich später eingehe.
myfifo myfifo_ins
(
.rst(rst), // Asynchronous reset input
// Write interface ports
.wr_clk(wr_clk), // Write clock input
.wr_en(wr_en), // Write Enable input
.din(din), // Write word input
.full(full), // Full output
// Read interface ports
.rd_clk(rd_clk), // Read Clock input
.rd_en(rd_en), // Read Enable input
.dout(dout), // Read word output
.empty(empty) // Empty output
);
Die Portnamen entsprechen denen, die in den Xilinx-Werkzeugen verwendet werden; andere FPGA-Hersteller verwenden ähnliche Namen.
Die Ports des FIFO-Moduls sind in drei Gruppen unterteilt: ein Reset-Signal (@rst), auf das ich später zurückkomme, und – wie zu erwarten – eine Schreib- und eine Leseschnittstelle, die jeweils aus vier Ports bestehen.
Was @din und @dout betrifft: Das sind zwei Vektor-Ports, über die die Datenwörter in den FIFO hinein- bzw. aus ihm heraus transportiert werden. Wie breit diese Wörter sind, legen Sie bei der Einrichtung des FIFO mit dem entsprechenden Software-Werkzeug fest. Außerdem stellen Sie die Tiefe des FIFO ein, also wie viele Wörter er aufnehmen kann. Diese beiden Parameter beeinflussen, wie viele Speicherressourcen des FPGA der FIFO belegt.
Taktsignale
Es ist erwähnenswert, dass jede dieser beiden Schnittstellen einen eigenen Takt besitzt: @wr_clk und @rd_clk. Die übrigen Ports jeder Schnittstelle sind mit dem jeweiligen Takt synchron.
FIFOs werden häufig eingesetzt, um Daten von einer Taktdomäne (engl. clock domain) in eine andere zu verschieben, denn sie haben zwei Takte: Wenn ein Teil Ihrer Logik synchron zu clk_A arbeitet und ein anderer Teil synchron zu clk_B – wie bringt man die beiden dazu, miteinander zu arbeiten? Der erste Gedanke eines jeden FPGA-Entwicklers ist, einen FIFO dazwischenzuschalten. Das liegt vor allem daran, dass der Wechsel zwischen Taktdomänen (clock domain crossing) einiges Kopfzerbrechen bereitet, und ein FIFO löst dieses Problem einfach und zuverlässig.
Die Schreibschnittstelle
Die Schreibschnittstelle ist einfach: @wr_clk, @wr_en und @din sind Eingänge des FIFO, @full (deutsch: „voll“) ist ein Ausgang.
Wenn @wr_en bei einer steigenden Flanke von @wr_clk auf high liegt, werden die an @din anliegenden Daten in den FIFO geschrieben. Der Ausgang @full (deutsch: „voll“) liegt auf high, wenn der FIFO voll ist.
So sieht beispielsweise eine Wellenform aus, bei der fünf Wörter in den FIFO geschrieben werden:
In dieser Wellenform schreibt die Anwendungslogik zuerst die Wörter D0 und D1. Der FIFO setzt daraufhin seinen Ausgang @full („voll“) auf high, um anzuzeigen, dass er nach dem erfolgreichen Schreiben von D1 voll geworden ist. Die Anwendungslogik reagiert darauf, indem sie @wr_en im selben Taktzyklus auf low setzt. Nach einigen Taktzyklen setzt der FIFO @full („voll“) wieder auf low und signalisiert damit, dass wieder geschrieben werden darf. Das liegt höchstwahrscheinlich an Aktivität auf der anderen Seite – es wurden also Daten aus dem FIFO gelesen.
Die Anwendungslogik hätte schon in dem Taktzyklus mit dem Schreiben beginnen können, in dem @full („voll“) auf low ging, doch in diesem konkreten Beispiel beginnt sie etwas später. Wie die Wellenform zeigt, werden drei weitere Wörter geschrieben.
In der Wellenform bedeutet die Markierung „Dx“ an @din, dass dieser Wert ignoriert wird und es daher keine Rolle spielt, welcher Wert dort anliegt. Im Abschnitt mit „Dx“ zwischen D1 und D2 hätte @din zum Beispiel auf D1 bleiben, früher als dargestellt auf D2 wechseln oder einen ganz anderen Wert haben können. Das Ergebnis wäre dasselbe gewesen.
Als einfaches Codebeispiel nehmen wir an, ich möchte den FIFO möglichst oft mit hochzählenden Wörtern füllen:
assign wr_en = !full;
always @(posedge wr_clk)
if (wr_en)
din <= din + 1;
Das zeigt den richtigen Zusammenhang zwischen @full („voll“) und @wr_en: Ist @full („voll“) high, muss @wr_en im selben Taktzyklus low sein. Und was, wenn man das nicht tut? Was, wenn man das Signal @full („voll“) einfach ignoriert? Der FIFO ignoriert @wr_en in diesem Fall mit hoher Wahrscheinlichkeit. Er verhält sich dann also so, als ob sein wr_en-Port mit @the_real_wr_en verbunden wäre, das wie folgt definiert ist:
assign the_real_wr_en = wr_en && !full;
Manche FPGA-Werkzeuge erlauben es allerdings, den FIFO ohne diesen Sicherheitsmechanismus zu konfigurieren. In dem Fall kann praktisch alles passieren, wenn versucht wird, Daten in einen vollen FIFO zu schreiben.
So oder so sollte @full („voll“) beachtet werden, sonst sieht es so aus, als wären Daten verloren gegangen. Betrachten wir das obige Beispiel: Wäre @wr_en die ganze Zeit high gewesen, hätte @din einfach weiter hochgezählt, völlig unabhängig davon, ob die Daten tatsächlich in den FIFO geschrieben wurden. Beim Auslesen der Daten auf der anderen Seite wäre der Zählvorgang also lückenhaft gewesen.
Beachten Sie, dass @full („voll“) nur als Folge eines Schreibzyklus von low auf high wechseln kann, also unmittelbar nach einer steigenden Taktflanke, bei der @wr_en high war. Eine Ausnahme ist der Fall, dass der FIFO zurückgesetzt wird, wie weiter unten erläutert.
Die Leseschnittstelle
Die Leseschnittstelle ist der Schreibschnittstelle ähnlich, aber nicht ganz gleich. @rd_clk und @rd_en sind Eingänge des FIFO, @dout und @empty (deutsch: „leer“) sind Ausgänge.
Wenn @rd_en bei der steigenden Flanke von @rd_clk high ist, wird ein neues Wort aus dem Speicher des FIFO gelesen, und @dout wird nach dieser steigenden Flanke, also im nächsten Taktzyklus, mit dem Wert dieses Wortes aktualisiert. Der Ausgang @empty (deutsch: „leer“) ist high, wenn der FIFO leer ist.
In dieser Beispielwellenform werden fünf Wörter aus dem FIFO gelesen:
In dieser Wellenform beginnt die Anwendungslogik damit, drei Wörter zu lesen. Als Reaktion darauf, dass @empty („leer“) high wird (zusammen mit dem Erscheinen von D2 an @dout), setzt sie @rd_en im selben Taktzyklus auf low. Wie zuvor wäre es kein Problem gewesen, @rd_en bereits in dem Taktzyklus wieder auf high zu setzen, in dem @empty („leer“) auf low ging (weil auf der anderen Seite Daten in den FIFO geschrieben wurden). Stattdessen wartet sie einige Taktzyklen und liest dann zwei weitere Wörter.
Und nun zu einer kleinen Randbemerkung: Wenn Sie diese Wellenform mit der vorherigen vergleichen, fällt Ihnen vielleicht auf, dass fünf Wörter geschrieben und fünf Wörter gelesen wurden. Warum ist @empty („leer“) dann nicht zusammen mit D4 high gegangen? Weil ich zeigen wollte, dass man durchaus mit dem Lesen aufhören darf, auch wenn der FIFO nicht leer ist. Für dieses gedachte Beispiel wurden dem FIFO zusätzliche Wörter zugeführt, sodass er nach dem Lesen von D4 nicht leer geworden ist.
Beachten Sie, dass @dout seinen Wert behält, wenn @rd_en low ist. Die Anwendungslogik kann sich darauf verlassen: @dout enthält immer den Wert des zuletzt aus dem FIFO gelesenen Wortes (außer nach einem Reset).
Und noch wichtiger: Der neue Wert von @dout erscheint nach der steigenden Flanke, wenn @rd_en high ist. Der FIFO verhält sich also wie der folgende Verilog-Code:
always @(posedge rd_clk)
if (rd_en && !empty)
dout <= next_word_to_show;
Dieser Pseudo-Verilog-Code zeigt auch, dass die meisten FIFOs @rd_en ignorieren, wenn @empty („leer“) im selben Taktzyklus high ist. Wie bei der Schreibschnittstelle darf @rd_en nicht high sein, wenn @empty („leer“) in diesem Taktzyklus high ist. Und noch einmal: Manche FIFOs lassen sich ohne diesen Schutzmechanismus konfigurieren; brechen Sie diese Regel also nicht.
@empty („leer“) kann nur nach einem Lesezyklus von low auf high wechseln, also nachdem @rd_en bei einer steigenden Taktflanke high war. Die einzige Ausnahme ist ein Reset des FIFO.
Als Beispiel dient ein vereinfachter Verilog-Code-Ausschnitt (ohne Reset), der Wörter aus dem FIFO liest und die laufende Summe berechnet.
assign rd_en = want_to_read_now && !empty;
always @(posedge rd_clk)
begin
rd_en_d <= rd_en;
if (rd_en_d)
sum <= sum + dout; // Don't try this at home: @sum is never reset.
end
Zur Veranschaulichung habe ich ein Signal @want_to_read_now ergänzt, das anzeigt, dass die Logik lesen möchte. @rd_en ist dennoch nur high, wenn der FIFO nicht leer ist.
Achten Sie auf @rd_en_d: Es enthält den Wert von @rd_en mit einer Verzögerung von einem Taktzyklus. Dadurch ist @rd_en_d genau dann high, wenn ein neuer, gültiger Wert an @dout anliegt. Genau deshalb wird @rd_en_d als Bedingung dafür verwendet, den Wert von @dout zu übernehmen. Die Verzögerung zwischen @rd_en und @dout macht die Sache etwas kompliziert, wie dieses Beispiel zeigt.
Synchronisierung und Latenz
Da ich die obigen Beispielwellenformen getrennt für das Schreiben und das Lesen gezeichnet habe, fehlt dort ein wichtiger Punkt: Es dauert einige Taktzyklen vom Schreiben des ersten Worts in einen leeren FIFO, bis der Ausgang @empty (deutsch: „leer“) auf low wechselt. Ebenso dauert es einige Taktzyklen vom Lesen des ersten Worts aus einem vollen FIFO, bis der Ausgang @full (deutsch: „voll“) auf low wechselt.
Das liegt daran, dass sich die Information über das Schreiben in den FIFO erst über die beiden Taktdomänen ausbreiten muss, bevor sie die andere Seite des FIFO erreicht. Die Logik, die für den Taktdomänenwechsel erforderlich ist, verursacht eine Verzögerung von einigen Taktzyklen. Deshalb spricht der Ausgang @empty („leer“) erst etwas später an. Beim Ausgang @full („voll“) verhält es sich genauso.
Wie viele Taktzyklen beträgt diese Verzögerung? Das hängt von vielen Dingen ab, unter anderem vom zeitlichen Verhältnis der Flanken der beiden Takte im jeweiligen Moment. Kurz gesagt: Das lässt sich nur schwer sagen.
Einer der Faktoren, die diese Verzögerung beeinflussen, ist die Anzahl der Synchronisationsstufen (engl. synchronization stages), die bei vielen FIFOs als Parameter eingestellt werden kann. Zwei Stufen sind eine übliche Wahl, aber auch eine größere Anzahl ist möglich. Das kann die Zuverlässigkeit des FIFO erhöhen, kostet aber etwas mehr Logikressourcen. Außerdem erhöht es die Latenz der Ausgänge @empty („leer“) und @full („voll“), wie bereits beschrieben.
Wenn Sie Ihren FIFO also richtig verwöhnen möchten, stellen Sie ruhig drei Synchronisationsstufen ein – dann fühlen Sie sich bestimmt ganz besonders sicher.
Der Reset-Eingang
Alle FPGA-FIFOs besitzen ein Reset-Signal. Da der FIFO mit zwei Takten arbeitet, wird von diesem Reset-Signal nicht erwartet, dass es synchron zu einem der beiden Takte ist; es ist also ein asynchroner Reset (asynchronous reset). Die interne Logik des FIFO sorgt dafür, dass der Reset in jeder der beiden Taktdomänen intern synchronisiert wird.
Was bewirkt der Reset also? Zunächst leert er den FIFO und setzt @empty (deutsch: „leer“) auf high. Waren Daten im FIFO, gehen diese verloren.
Was den Ausgang @full (deutsch: „voll“) betrifft, ist es üblich (und empfehlenswert), dass FIFOs diesen Ausgang nach einem Reset so lange auf high halten, bis der FIFO wieder bereit ist, Daten entgegenzunehmen (also bis Schreibzyklen stattfinden). Dieses Verhalten kann jedoch optional sein; es ist daher ratsam, das in der Dokumentation des FIFO zu prüfen. Schließlich ist der FIFO nach einem Reset nicht wirklich voll. Außerdem würde ein durch den Reset verursachtes Anheben von @full („voll“) gegen die oben genannte Regel verstoßen: @full („voll“) sollte nur als Folge eines Schreibvorgangs auf high gehen.
Man sollte sich darüber im Klaren sein, dass vom Moment des aktiven Reset-Signals bis zum Wechsel der Ausgänge @empty (deutsch: „leer“) und @full (deutsch: „voll“) auf high einige Taktzyklen vergehen. Das liegt an der Synchronisationslogik des FIFO. In den wenigen Taktzyklen um die Aktivierung des Resets herum sind die Verhältnisse also etwas unklar. Stellen Sie sicher, dass die Anwendungslogik in dieser Zeit nicht versucht, in den FIFO zu schreiben oder aus ihm zu lesen.
Auch wenn das Reset-Signal asynchron ist, sollte es vom Ausgang eines Registers (Flip-Flop) des FPGA kommen. Es sollte nicht das Ergebnis kombinatorischer Logik (combinatorial logic) sein, weil der FIFO sonst durch Glitches ungewollte Resets erhalten kann.
Tatsächlich gehen viele FPGA-Entwickler fälschlich davon aus, dass man praktisch jedes Signal an den Reset-Port anschließen kann. Der FPGA-Hersteller kann jedoch unerwartete Anforderungen an das Reset-Signal stellen. Zum Beispiel stammt das Folgende aus dem Product Guide für den Xilinx-FIFO (PG057):
Wenn der asynchrone Reset nur einen einzigen Taktzyklus des langsamsten Takts breit ist und seine Aktivierung sehr nahe an der steigenden Flanke des langsamsten Takts erfolgt, kann die Reset-Erkennung unter Umständen nicht ordnungsgemäß ablaufen, was zu unerwartetem Verhalten führt. Um solche Situationen zu vermeiden, wird immer empfohlen, den asynchronen Reset für mindestens 3 [ ... ] Zyklen des langsamsten Takts aktiviert zu lassen ...
(Kapitel 3, „Resets“)
Xilinx empfiehlt also, den Reset mindestens drei Taktzyklen lang aktiv zu lassen. Ich bin nicht sicher, wie vielen diese Empfehlung bewusst ist. Lesen Sie auf jeden Fall im Benutzerhandbuch (User Guide) zum FIFO Ihres FPGA-Herstellers nach, wie man dieses Reset-Signal ordnungsgemäß erzeugt.
Wie der FIFO implementiert wird
Auch wenn die Software-Werkzeuge des Herstellers dafür sorgen, dass der FIFO ordnungsgemäß funktioniert, sollte man wissen, welche Ressourcen des FPGA dabei belegt werden – insbesondere, um Engpässe bei der einen oder anderen Ressourcenart zu vermeiden.
Die konkreten Optionen hängen vom FPGA ab; ich möchte hier kurz ein paar gängige Varianten nennen:
- Vollständig in Hardware implementiert. Das bedeutet normalerweise, dass ein Block-RAM als Speicher dient und die Steuerlogik des FIFO direkt im Silizium implementiert ist statt in der Logikarchitektur. Das spart nicht besonders viel Logik, aber die Umsetzung in Silizium ist vermutlich besser für hohe Taktraten geeignet. Der Hauptnachteil ist, dass der Funktionsumfang eines solchen FIFO auf das beschränkt ist, was im Silizium vorhanden ist: Die Größe des FIFO kann also relativ begrenzt sein, und triviale Funktionen können fehlen.
- Block-RAM-FIFOs. Das ist die häufigste Variante. Der FIFO besteht aus so vielen Block-RAMs, wie zur Realisierung der gewünschten Breite und Tiefe nötig sind. Die Steuerlogik des FIFO ist in der Logikarchitektur implementiert.
- Distributed-RAM-FIFOs. Diese funktionieren ähnlich wie Block-RAM-FIFOs, aber anstelle von Block-RAMs werden Logik-Slices (engl. slices) als RAM genutzt. Zur Erinnerung: Bei den meisten FPGAs können die LUTs in den Slices als RAM verwendet werden. Das ist besonders dann eine gute Option, wenn der FIFO nur eine geringe Tiefe haben muss. Meist eignet sich diese Option also bei Tiefen von 32 Wörtern oder weniger; der Kompromiss hängt jedoch vom Design und von der FPGA-Familie ab.
- FIFOs auf der Basis von Schieberegistern. Das ist die exotische Variante der Distributed-RAM-FIFOs: Da sich Slices auch als Schieberegister verhalten können, lässt sich das ausnutzen, um ein wenig Logik zu sparen.
Damit ist die erste Seite dieser Serie über FIFOs abgeschlossen. Die nächste Seite behandelt gängige Varianten von FIFOs und zusätzliche Funktionen.

