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:
LM32 bietet drei Möglichkeiten, die Registerdatei zu implementieren:
In der Praxis habe ich selbst mit verteilter RAM-Inferenz sowohl Xilinx gesehen ise
als auch yosys
einen 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 yosys
effektiv 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, ise
die yosys
erforderliche zusätzliche Glue-Logik abzuleiten.
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.
Dies ist nicht einfach in Worten zu beschreiben, daher ist hier ein Zeitdiagramm dieses möglichen Szenarios:
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.
cr1901