01signal.com

Implementierung von Single-Clock-FIFOs in Verilog

Themenbereich

Diese Seite ist als dritte in einer fünfteiligen Serie über FIFOs angelegt. Sie zeigt eine Implementierung eines Basis-Single-Clock-FIFO in Verilog. Das kann für portablen Code nützlich sein, aber der Hauptpunkt dieser Seite ist es, noch einmal zu verdeutlichen, wie ein FIFO funktioniert. Ich werde also sowohl ein „Standard-FIFO“ als auch ein FWFT-FIFO implementieren. Zuerst brauchen wir aber einen Dual-Port-RAM, den beide FIFOs verwenden.

Der Dual-Port-RAM

Das ist das Verilog-Modul, das den RAM per Inferenz implementiert. Es ist ziemlich wahrscheinlich, dass jeder Synthesizer (synthesizer) das richtig hinkriegt, aber es ist möglich, dass er die unerwünschte RAM-Art erzeugt (Block-RAM gegenüber Distributed-RAM). Möglicherweise muss man also Synthesizer-Direktiven hinzufügen. Oder man verwendet, falls das am besten passt, einen IP-Core (IP core), den der FPGA-Hersteller für einen Dual-Port-RAM anbietet.

Hier ist also das Modul:

module dualport_ram #(parameter depth = 64,
                      log2_depth = 6,
                      width = 8
                     )
   (
    input                    clk,
    input [(log2_depth-1):0] wr_addr,
    input [(log2_depth-1):0] rd_addr,
    output reg [(width-1):0] rd_data,
    input [(width-1):0]      wr_data,
    input                    rd_en,
    input                    wr_en
    );

   reg [(width-1):0] inferred_ram[0:(depth-1)];

   always @(posedge clk)
     begin
        if (wr_en)
          inferred_ram[wr_addr] <= wr_data;

        if (rd_en)
          rd_data <= inferred_ram[rd_addr];
     end
endmodule

Das „Standard-FIFO“

Und nun sind wir bereit, uns das Modul anzusehen, das ein „Standard-FIFO“ implementiert (also kein FWFT-FIFO):

module fifo
  #(parameter depth = 64, // Must equal 2^log2_depth exactly
    log2_depth = 6,
    width = 8
    )
   (
    input  clk,
    input  rst,

    input  wr_en,
    input [(width-1):0] din,

    input  rd_en,
    output [(width-1):0] dout,

    output reg full,
    output reg empty
    );

   reg [log2_depth:0]         next_words_in_ram; // Combinatorial
   reg [log2_depth:0]         words_in_ram;
   reg [(log2_depth-1):0]     rd_addr;
   reg [(log2_depth-1):0]     wr_addr;
   wire                       fetch_data, commit_data;

   assign fetch_data = rd_en && !empty;
   assign commit_data = wr_en && !full;

   always @(*)
     if (commit_data && !fetch_data)
       next_words_in_ram <= words_in_ram + 1;
     else if (!commit_data && fetch_data)
       next_words_in_ram <= words_in_ram - 1;
     else
       next_words_in_ram <= words_in_ram;

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        empty <= (next_words_in_ram == 0);

        if (fetch_data)
          rd_addr <= rd_addr + 1;

        if (commit_data)
          wr_addr <= wr_addr + 1;

        if (rst)
          begin
             empty <= 1;
             full <= 1;
             words_in_ram <= 0;
             rd_addr <= 0;
             wr_addr <= 0;
          end
     end

   dualport_ram
     #(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
       (.clk(clk), .wr_addr(wr_addr),
        .rd_addr(rd_addr),
        .wr_en(commit_data),
        .rd_en(fetch_data),
        .wr_data(din),
        .rd_data(dout)
        );
endmodule

Der einzige wichtige Hinweis zur Verwendung steht im Kommentar am Kopf des Codes und betrifft die Parameter @depth und @log2_depth: depth muss exakt 2log2_depth sein.

Der Betrieb dieses Moduls ist recht einfach: @fetch_data verhält sich wie @rd_en, berücksichtigt aber @empty (deutsch: „leer“). @fetch_data ist also eine sichere Version von @rd_en – in dem Sinne, dass nichts Schlimmes passiert, selbst wenn @rd_en und @empty (deutsch: „leer“) gleichzeitig high sind (was unzulässig ist), weil @rd_en in diesem Fall ignoriert wird.

@commit_data ist in gleicher Weise die sichere Version von @wr_en, denn es berücksichtigt @full (deutsch: „voll“).

Der nächste Wert von @words_in_ram, also @next_words_in_ram, wird als kombinatorische Funktion aus @fetch_data und @commit_data berechnet (man beachte das always @(*)). @next_words_in_ram wird verwendet, um mit dem folgenden Codeausschnitt die Werte mehrerer Register zu erzeugen:

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        empty <= (next_words_in_ram == 0);
[ ... ]

Beachten Sie, dass hier @full (deutsch: „voll“) und @empty (deutsch: „leer“) definiert werden.

Was dieses FIFO im Vergleich zu einem Dual-Clock-FIFO so einfach zu implementieren macht, ist die Möglichkeit, @words_in_ram auf diese Weise zu definieren und dasselbe Register auf beiden Seiten des FIFO zu verwenden.

Weiter unten im Code folgt das Aktualisieren von @rd_addr und @wr_addr und danach der Block für @rst. Beachten Sie, dass sowohl @empty (deutsch: „leer“) als auch @full (deutsch: „voll“) durch den Reset auf high gehen; @full (deutsch: „voll“) geht jedoch wieder auf low, sobald der Reset aufgehoben wird.

Ein Hinweis zum Codestil: Wenn @rst high ist, überschreiben die Zuweisungen im begin-end-Block der if-Anweisung alle eventuell weiter oben gemachten Zuweisungen. @rst setzt also tatsächlich alle Register auf ihre Anfangswerte zurück. Das ist nicht der üblichste Codestil, hat aber klare Vorteile, wenn nicht alle Register zurückgesetzt werden. Dieses konkrete Beispiel zeigt diesen Vorteil nicht – aber siehe diese Seite.

Schließlich gibt es eine Instanziierung (instantiation) des Dual-Port-RAMs.

Das FWFT-FIFO

Wie auf dieser Seite gezeigt, ist es recht einfach, ein „Standard-FIFO“ in ein FWFT-FIFO umzuwandeln. Aber die direkte Implementierung erlaubt es, einige interessante Punkte zu besprechen – hier ist sie also:

module fwft_fifo
  #(parameter depth = 64, // Must equal 2^log2_depth exactly
    log2_depth = 6,
    width = 8
    )
   (
    input  clk,
    input  rst,

    input  wr_en,
    input [(width-1):0] din,

    input  rd_en,
    output [(width-1):0] dout,

    output reg full,
    output reg empty
    );

   reg [log2_depth:0]         next_words_in_ram; // Combinatorial
   reg [log2_depth:0]         words_in_ram;
   reg [(log2_depth-1):0]     rd_addr;
   reg [(log2_depth-1):0]     wr_addr;
   reg                        has_more_words;
   wire                       fetch_data, commit_data;

   assign fetch_data = (rd_en || empty) && has_more_words;
   assign commit_data = wr_en && !full;

   always @(*)
     if (commit_data && !fetch_data)
       next_words_in_ram <= words_in_ram + 1;
     else if (!commit_data && fetch_data)
       next_words_in_ram <= words_in_ram - 1;
     else
       next_words_in_ram <= words_in_ram;

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        has_more_words <= (next_words_in_ram != 0);

        if (fetch_data)
          rd_addr <= rd_addr + 1;

        if (commit_data)
          wr_addr <= wr_addr + 1;

        if (fetch_data)
          empty <= 0;
        else if (rd_en)
          empty <= 1;

        if (rst)
          begin
             empty <= 1;
             full <= 1;
             words_in_ram <= 0;
             has_more_words <= 0;
             rd_addr <= 0;
             wr_addr <= 0;
          end
     end

   dualport_ram
     #(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
       (.clk(clk), .wr_addr(wr_addr),
        .rd_addr(rd_addr),
        .wr_en(commit_data),
        .rd_en(fetch_data),
        .wr_data(din),
        .rd_data(dout)
        );
endmodule

Zunächst ist zu beachten, dass wir ein neues Register haben: @has_more_words. Vergleichen Sie dessen Definition mit dem „Standard-FIFO“ weiter oben und überzeugen Sie sich davon, dass @has_more_words genau die logische Negation von @empty (deutsch: „leer“) ist.

Weiterhin ist die Definition von @fetch_data anders. Sie wird jetzt (wenig überraschend) durch @has_more_words abgesichert und lautet also:

assign fetch_data = (rd_en || empty) && has_more_words;

Erinnern Sie sich von weiter oben daran, dass @empty (deutsch: „leer“) bei einem FWFT-FIFO bedeutet: „@dout ist nicht gültig“. Diese Zuweisung bedeutet also, dass außer bei @rd_en auch dann gelesen wird, wenn der Ausgang nicht gültig ist, aber Daten im Speicherarray vorhanden sind. Genau das ist das Durchfallen des ersten Worts (first word fall through).

Und schließlich hat sich die Logik für die Zuweisung an @empty (deutsch: „leer“) wie folgt geändert:

   always @(posedge clk)
     if (fetch_data)
       empty <= 0;
     else if (rd_en)
       empty <= 1;

Das bedeutet einfach, dass @empty (deutsch: „leer“) im nächsten Taktzyklus auf low geht, wenn ein Wort aus dem Speicherarray gelesen wird, weil dann offensichtlich neue und gültige Daten anliegen. Wenn dies jedoch nicht geschieht und @rd_en trotzdem aktiv ist, hat die Anwendungslogik gerade das letzte verfügbare Wort übernommen; deshalb geht @empty (deutsch: „leer“) auf high. Beachten Sie, dass bei @rd_en = high und @fetch_data = low mit Sicherheit auch @has_more_words low ist (siehe die Zuweisung für @fetch_data weiter oben). Genau deshalb entspricht diese Bedingung dem Lesen des letzten Wortes im FIFO.

Damit ist die dritte Seite in dieser Serie über FIFOs abgeschlossen. Die nächste Seite zeigt, wie man ein „Standard-FIFO“ für den Einsatz in einer Datenerfassungsanwendung (data acquisition) anpasst.

Diese Seite wurde maschinell aus dem Englischen übersetzt. Im Zweifelsfall siehe den Originaltext.
Copyright © 2021-2026. All rights reserved. (dcc38493)