Was ist der Zweck dieses Verilog-Codes zum Implementieren von 3-Port-Block-RAM?

LatticeMico32 (LM32) ist eine lizenzfreie CPU, die ich verwende, um zu untersuchen, wie eine Pipeline-In-Order-CPU implementiert werden kann.

Ein besonders problematischer Punkt, mit dem ich Probleme habe, ist die Implementierung der Registerdatei. Auf einer Pipeline-CPU haben Sie normalerweise mindestens drei Speicherzugriffe auf die Registerdatei in einem bestimmten Taktzyklus:

  • 2 Lesevorgänge für beide Operanden für die Ausführungseinheiten.
  • 1 Schreibvorgang aus der Rückschreibphase

LM32 bietet drei Möglichkeiten, die Registerdatei zu implementieren:

  • Block-RAM-Inferenz, bei der Lese-/Schreibvorgänge eine zusätzliche Logik aufweisen, um parallele Lese-/Schreibvorgänge zu vermeiden.
  • Blockieren Sie die RAM-Inferenz mit phasenverschobenen Takten, die keine zusätzliche Logik erfordern.
  • Verteilte RAM-Inferenz.

In der Praxis habe ich selbst mit verteilter RAM-Inferenz sowohl Xilinx gesehen iseals auch yosyseinen Block-RAM mit phasengleichen Lese- und Schreibtakten abgeleitet. Darüber hinaus habe ich gesehen, dass beide Synthesizer folgern und zumindest einen Teil der zusätzlichen Logik, die der lm32 explizit für eine Block-RAM-Registerdatei mit positiver Flanke enthält.

Die abgeleitete zusätzliche Logik ermöglicht transparente Lesevorgänge . Ich habe den Code hier für die explizite Implementierung von lm32 eingefügt, aber ich weiß aus Experimenten, die yosyseffektiv denselben Code generieren, um die Registerdatei im Block-RAM auf iCE40 zu platzieren:

// Register file
`ifdef CFG_EBR_POSEDGE_REGISTER_FILE
   /*----------------------------------------------------------------------
    Register File is implemented using EBRs. There can be three accesses to
    the register file in each cycle: two reads and one write. On-chip block
    RAM has two read/write ports. To accomodate three accesses, two on-chip
    block RAMs are used (each register file "write" is made to both block
    RAMs).
    One limitation of the on-chip block RAMs is that one cannot perform a
    read and write to same location in a cycle (if this is done, then the
    data read out is indeterminate).
    ----------------------------------------------------------------------*/
   wire [31:0] regfile_data_0, regfile_data_1;
   reg [31:0]  w_result_d;
   reg         regfile_raw_0, regfile_raw_0_nxt;
   reg         regfile_raw_1, regfile_raw_1_nxt;

   /*----------------------------------------------------------------------
    Check if read and write is being performed to same register in current
    cycle? This is done by comparing the read and write IDXs.
    ----------------------------------------------------------------------*/
   always @(reg_write_enable_q_w or write_idx_w or instruction_f)
     begin
        if (reg_write_enable_q_w
            && (write_idx_w == instruction_f[25:21]))
          regfile_raw_0_nxt = 1'b1;
        else
          regfile_raw_0_nxt = 1'b0;

        if (reg_write_enable_q_w
            && (write_idx_w == instruction_f[20:16]))
          regfile_raw_1_nxt = 1'b1;
        else
          regfile_raw_1_nxt = 1'b0;
     end

   /*----------------------------------------------------------------------
    Select latched (delayed) write value or data from register file. If
    read in previous cycle was performed to register written to in same
    cycle, then latched (delayed) write value is selected.
    ----------------------------------------------------------------------*/
   always @(regfile_raw_0 or w_result_d or regfile_data_0)
     if (regfile_raw_0)
       reg_data_live_0 = w_result_d;
     else
       reg_data_live_0 = regfile_data_0;

   /*----------------------------------------------------------------------
    Select latched (delayed) write value or data from register file. If
    read in previous cycle was performed to register written to in same
    cycle, then latched (delayed) write value is selected.
    ----------------------------------------------------------------------*/
   always @(regfile_raw_1 or w_result_d or regfile_data_1)
     if (regfile_raw_1)
       reg_data_live_1 = w_result_d;
     else
       reg_data_live_1 = regfile_data_1;

   /*----------------------------------------------------------------------
    Latch value written to register file
    ----------------------------------------------------------------------*/
   always @(posedge clk_i `CFG_RESET_SENSITIVITY)
     if (rst_i == `TRUE)
       begin
          regfile_raw_0 <= 1'b0;
          regfile_raw_1 <= 1'b0;
          w_result_d <= 32'b0;
       end
     else
       begin
          regfile_raw_0 <= regfile_raw_0_nxt;
          regfile_raw_1 <= regfile_raw_1_nxt;
          w_result_d <= w_result;
       end

// Two Block RAM instantiations follow to get 2 read/1 write port.

Transparente Lesevorgänge stellen sicher, dass Schreibvorgänge an dieselbe Adresse wie ein Lesevorgang von einem anderen Port auch am Leseport an derselben Taktflanke erscheinen (vorausgesetzt, die Lese- und Schreibtakte sind synchron). Die lm32-Pipeline verlässt sich darauf, dass die Leseports den zurückgeschriebenen Registerwert sofort widerspiegeln.

Es gibt jedoch eine zusätzliche Glue-Logik für den Umgang mit einem Stillstand der Pipeline, und ich bin mir nicht sicher, was dieser Code bewirkt, selbst nachdem ich die CPU-Implementierung im Detail untersucht habe. Ich habe den folgenden Code der Einfachheit halber kommentiert:

 ifdef CFG_EBR_POSEDGE_REGISTER_FILE
 // Buffer data read from register file, in case a stall occurs, and watch for
 // any writes to the modified registers
 always @(posedge clk_i `CFG_RESET_SENSITIVITY)
 begin
    if (rst_i == `TRUE)
    begin
        use_buf <= `FALSE;
        reg_data_buf_0 <= {`LM32_WORD_WIDTH{1'b0}};
        reg_data_buf_1 <= {`LM32_WORD_WIDTH{1'b0}};
    end
    else
    begin
        if (stall_d == `FALSE)
            use_buf <= `FALSE;
        else if (use_buf == `FALSE)
        begin
            // If we stall in the decode stage, unconditionally
            // buffer the register file values from the read ports.
            // They will be used instead when the stall ends.
            reg_data_buf_0 <= reg_data_live_0;
            reg_data_buf_1 <= reg_data_live_1;
            use_buf <= `TRUE;
        end
        if (reg_write_enable_q_w == `TRUE)
        // If either register's address matches the register
        // to be written back, replace the buffered read values.
        begin
            if (write_idx_w == read_idx_0_d)
                reg_data_buf_0 <= w_result;
            if (write_idx_w == read_idx_1_d)
                reg_data_buf_1 <= w_result;
        end
    end
end
endif

Warum ist diese Logik erforderlich, und zwar nur für phasengleiche Lese-/Schreibtakte? Ist dieser Code anderen gebräuchlichen Redewendungen für das Lesen der korrekten Daten aus dem Block-RAM ähnlich, wie sie auf FPGAs implementiert sind (dh ähnlich wie Synthesizer auf transparenten Lese-/Schreibcode schließen)?

Ich hätte gedacht, dass während eines Stillstands der Dekodierungsphase einer RISC-CPU eine Logik, die transparente Lesevorgänge sicherstellt, ausreichen würde, um sicherzustellen, dass die Leseports die richtige Datenausgabe haben, wenn der Stillstand endet. Bis ein vollständiger Taktzyklus verstrichen ist, nachdem ein gleichzeitiges Lesen/Schreiben an derselben Adresse an verschiedenen Ports stattgefunden hat, sollten sich die Datenausgabe(n) der Leseports nicht auf den neuen Wert eingestellt haben, sodass wir nur puffern müssen die unmittelbarsten Daten, die an den Schreibport geschrieben werden?

Ich habe diese CPU viele Male nur mithilfe der verteilten RAM-Inferenz (als Block-RAM abgeleitet) synthetisiert, sodass diese Logik entweder nicht erforderlich ist oder in der Lage ist, isedie yosyserforderliche zusätzliche Glue-Logik abzuleiten.

Antworten (1)

Dies wurde einen Tag lang nicht beantwortet und ich glaube, ich weiß warum. Wenn der Verilog-Code etwas größer und komplexer wird, ist es sehr schwierig, alle zeitlichen Beziehungen zu sehen. Selbst wenn der Benutzer viele Kommentare eingibt (Sie sagten, Sie hätten die Kommentare hinzugefügt, also nehme ich an, dass dies hier nicht der Fall war), stellen Sie fest, dass Sie die Simulation ausführen müssen, um zu sehen, wie alles zusammenhängt.
Um herauszufinden, warum dieser Code benötigt wird, entfernen Sie ihn und sehen Sie, wo etwas schief geht.

Trotzdem kann ich mir ein mögliches Szenario vorstellen.

  • Wenn die Registerdatei ein synchroner Speicher ist, verzögert sich die Datenausgabe um einen Zyklus.
  • Die Adressen an die Registerdatei werden nicht sofort in einem Decoderstillstand gestoppt.
  • Die herauskommenden Daten gehen während des Stalls verloren und müssen erfasst werden.

Dies ist nicht einfach in Worten zu beschreiben, daher ist hier ein Zeitdiagramm dieses möglichen Szenarios:

Geben Sie hier die Bildbeschreibung ein

In Zyklus 2 wird die Notwendigkeit eines Stalls erkannt. Aus irgendeinem Grund können die Adressen nicht gestoppt werden.
Zyklus 3 ist unser zusätzlicher Stallzyklus. Jetzt hat der Stall die Adresslogik erreicht, also wird er aufhören.
In Zyklus 4 wollen wir fortfahren, aber die Daten „M1“ gehen verloren. Wenn wir es nicht während des Stalls lagern, verwenden Sie es in Zyklus 4 und in Zyklus 5 ist alles wieder in Ordnung.

Beachten Sie, dass das Problem bei einer asynchronen Registerdatei nicht auftritt.


Als Randbemerkung: Ich stimme Ihrem Kommentar "unbedingt die Registerdateiwerte puffern" nicht zu. Es ist nicht "unbedingt", weil der folgende Code "if (reg_write_enable_q_w ..." Vorrang hat. Das heißt, es gibt ein implizites "if es findet kein Schreibvorgang statt"-Bedingung.

Ich hatte keine Zeit, dies genauer zu untersuchen oder einen Testfall mit äquivalentem Verhalten und abgeleitetem Block-RAM zu erstellen, aber Ihre Antwort ist in diesem Fall richtig (außer dass M0 während des Stillstands beibehalten werden muss; es ist nicht möglich, Adrs zu stoppen nicht auf A1 aktualisiert werden, selbst wenn das Stall-Signal empfangen wird). Ich akzeptiere und werde meine eigene Antwort hinzufügen, wenn ich die Gelegenheit dazu bekomme.