Es fällt mir wirklich schwer, den Unterschied zwischen blockierenden und nicht blockierenden Zuweisungen in Verilog zu verstehen. Ich meine, ich verstehe den konzeptionellen Unterschied zwischen den beiden, aber ich bin wirklich verloren, wenn es um die Implementierung geht.
Ich habe auf eine Reihe von Quellen verwiesen, einschließlich dieser Frage , aber alle Erklärungen scheinen den Unterschied in Bezug auf den Code zu erklären (was mit der Ausführungsreihenfolge von Zeilen passiert, wenn Blockierung und Nichtblockierung verwendet werden). Meine Frage ist etwas anders.
Beim Schreiben von Verilog-Code (da ich ihn schreibe, um auf einem FPGA synthetisiert zu werden), versuche ich immer zu visualisieren, wie die synthetisierte Schaltung aussehen wird, und hier beginnt das Problem:
1) Ich kann nicht verstehen, wie der Wechsel von blockierenden zu nicht blockierenden Zuweisungen meine synthetisierte Schaltung verändern würde. Zum Beispiel :
always @* begin
number_of_incoming_data_bytes_next <= number_of_incoming_data_bytes_reg;
generate_input_fifo_push_pulse_next <= generate_input_fifo_push_pulse;
if(state_reg == idle) begin
// mealey outputs
count_next = 8'b0;
if((rx_done_tick) && (rx_data_out == START_BYTE)) begin
state_next = read_incoming_data_length;
end else begin
state_next = idle;
end
end else if(state_reg == read_incoming_data_length) begin
// mealey outputs
count_next = 8'b0;
if(rx_done_tick) begin
number_of_incoming_data_bytes_reg <= rx_data_out;
state_next = reading;
end else begin
state_next = read_incoming_data_length;
end
end else if(state_reg == reading) begin
if(count_reg == number_of_incoming_data_bytes_reg) begin
state_next = idle;
// do something to indicate that all the reading is done
// and to send all the data in the fifo
end else begin
if(rx_done_tick) begin
generate_input_fifo_push_pulse_next = ~ generate_input_fifo_push_pulse;
count_next = count_reg + 1;
end else begin
count_next = count_reg;
end
end
end else begin
count_next = 8'b0;
state_next = idle;
end
end
Wie würde sich die synthetisierte Schaltung im obigen Code ändern, wenn ich alle blockierenden Zuweisungen durch nicht blockierende ersetzen würde
2) Den Unterschied zwischen blockierenden und nicht blockierenden Anweisungen zu verstehen, wenn sie nacheinander geschrieben werden, ist etwas einfacher (und die meisten Antworten auf diese Frage konzentrieren sich auf diesen Teil), aber wie wirken sich blockierende Zuweisungen auf Verhaltensweisen aus, wenn sie in separaten bedingten Verhaltensweisen deklariert werden. Zum Beispiel :
Würde es einen Unterschied machen, wenn ich das schreibe:
if(rx_done_tick) begin
a = 10;
end else begin
a = 8;
end
oder wenn ich das schreibe:
if(rx_done_tick) begin
a <= 10;
end else begin
a <= 8;
end
Ich weiß, dass bedingte Anweisungen synthetisiert werden, um zu Multiplexern oder Prioritätsstrukturen zu werden, und daher denke ich, dass die Verwendung von entweder blockierenden oder nicht blockierenden Anweisungen keinen Unterschied machen sollte, aber ich bin mir nicht sicher.
3) Beim Schreiben von Testbenches ist das Ergebnis der Simulation sehr unterschiedlich, wenn blockierende vs. nicht blockierende Anweisungen verwendet werden. Das Verhalten ist ganz anders, wenn ich schreibe:
initial begin
#31 rx_data_out = 255;
rx_done_tick = 1;
#2 rx_done_tick = 0;
#30 rx_data_out = 3;
rx_done_tick = 1;
#2 rx_done_tick = 0;
#30 rx_data_out = 10;
rx_done_tick = 1;
#2 rx_done_tick = 0;
end
gegenüber wenn ich das schreibe:
initial begin
#31 rx_data_out <= 255;
rx_done_tick <= 1;
#2 rx_done_tick <= 0;
#30 rx_data_out <= 3;
rx_done_tick <= 1;
#2 rx_done_tick <= 0;
#30 rx_data_out <= 10;
rx_done_tick <= 1;
#2 rx_done_tick <= 0;
end
Das ist sehr verwirrend. In meiner Praxis wird das rx_done_tick-Signal von einem Flip-Flop generiert. Ich denke also, dass nicht blockierende Anweisungen verwendet werden sollten, um dieses Verhalten darzustellen. Habe ich recht ?
4) Schließlich, wann man blockierende Zuweisungen verwendet und wann nicht nicht blockierende Anweisungen? Das heißt, ist es richtig, dass blockierende Anweisungen nur in kombinatorischen Verhaltensweisen und nicht blockierende Anweisungen nur in sequentiellen Verhaltensweisen verwendet werden sollten? Wenn ja oder nein, warum?
Die Zuordnung zwischen Sperrung und Nicht-Sperrung ist ein entscheidendes Konzept, und Sie haben Schwierigkeiten, sie richtig umzusetzen, weil Sie den konzeptionellen Unterschied nicht verstanden haben.
Ich habe eine Folie des MIT OCV PowerPoint-Vortrags von 2005 beigefügt, die den Unterschied zwischen den beiden klar beschreibt
Sie müssen das Konzept der RHL-Berechnung (rechte Seite) verstehen. Verilog berechnet immer die rechte Seite und fügt sie in die linke Seite ein. Beim Blockieren erfolgt die Zuweisung genau nach Abschluss der Berechnung, während beim Nicht-Blockieren die Zuweisung von RHS zu LHS erfolgt, wenn das Ende des Blocks erreicht ist. Aus diesem Grund sind, wie 'The Photon' für einzelne Linien erwähnt hat, sowohl das Blockieren als auch das Nicht-Blockieren gleich, aber wenn Sie mehr als eine Linie haben, können sich die Dinge ändern oder nicht ändern!
1) Ich kann nicht verstehen, wie der Wechsel von blockierend zu nicht blockierend erfolgt
Der gepostete Beispielcode war für einen kombinatorischen Block, das Ändern aller blockierenden ( =
) in nicht blockierende ( <=
) kann die Simulation beeinflussen, aber nicht die Synthese. Dies führt zu einer Fehlanpassung zwischen RTL und Gate-Pegel. Es ist ein falscher Ort, um die nicht blockierende Zuweisung zu verwenden, verwenden Sie sie nicht in einem kombinatorischen Abschnitt.
Um für die andere Frage zusammenzufassen, simuliert das Nicht-Blockieren, dass sich Daten unmittelbar nach einem Ereignis wie dem Stellen einer Uhr ändern. Dies ermöglicht eine korrekte Simulation eines Flip-Flops.
dies bedeutet für Prüfstände:
initial begin
#31 rx_data_out = 255;
Zum Zeitpunkt 31 erfolgt die Zuordnung.
initial begin
#31 rx_data_out <= 255;
Kurz nach Zeit 31 erfolgt die Zuordnung. Versuchen Sie beides parallel mit a
initial begin
#31 $display(rx_data_out);
end
Für das erste Beispiel haben Sie tatsächlich eine Race-Condition, die beide gleichzeitig auftreten, Sie sollten 255 ausgedruckt bekommen. Für das zweite Beispiel werden Sie immer x
gedruckt haben, da die Zuweisung direkt nach dem Ereignis zum Zeitpunkt 31 erfolgt, nicht darauf.
Non-Blocking kann für Testbenches nützlich sein, wenn Sie Daten nachahmen möchten, die von Flip-Flops gesteuert werden, dh sie ändern sich unmittelbar nach dem Ereignis. zum Beispiel das Auslösen eines Power-on-Resets.
initial begin
@(posedge clk);
@(posedge clk);
rst_n <= 1'b0;
end
Stellen Sie sich vor, wir hätten eine Reihe von Flip-Flops (a, b, c), die eine Verzögerungsleitung erzeugen, sie haben jeweils einen d-Eingang und einen q-Ausgang.
wenn die Zuweisungen verkettet wurden mit:
c = b = a
Daten würden sofort von a nach c durcheilen. aber wenn wir haben
c <= b <= a
Wir haben eine Pipeline und jedes Flip-Flop kann seinen Wert halten.
Aktueller Code:
always @(posedge clk) begin
c = b;
b = a;
a = in;
end
gegen:
always @(posedge clk) begin
c <= b;
b <= a;
a <= in;
end
Deshalb spielt es bei Frage 2 mit nur einer Zuordnung keine Rolle. aber wenn es mehrere Zuweisungen gibt, die aufeinander angewiesen sind, spielt es wirklich eine Rolle, weil Sie steuern, ob sie von einem Flip-Flop ( ) <=
oder einem Block kombinatorischer Logik ( =
) gesteuert werden.
Meine Faustregeln: Verwenden Sie Blockierung ( =
) für kombinatorische Logik und Nicht-Blockierung ( <=
) für sequentielle (Flip-Flops)
Dies ist eher ein Nachtrag zur Antwort von pre_randomize, aber da Kommentare keine Bilder zulassen, poste ich sie als Antwort.
Als allgemeine Faustregel gilt, wie bereits erwähnt:
Verwenden Sie Blockierung (=) für kombinatorische Logik und Nicht-Blockierung (<=) für sequentielle (Flip-Flops)
Die D-Flip-Flop-Kette ist ein gutes Beispiel dafür, wie die Verwendung der falschen Zuweisung (in diesem Fall eine blockierende Zuweisung für sequentielle Prozeduren) Simulationsergebnisse erzeugt, die nicht mit der synthetisierten Logik übereinstimmen.
Die Kehrseite ist, wenn Sie eine mehrstufige kombinatorische Logik haben, wie unten gezeigt:
Simulieren Sie diese Schaltung – Mit CircuitLab erstellter Schaltplan
Wenn wir dies in diesem Fall als zwei separate Zeilen für die Ausgänge X und Y schreiben würden, würden wir schreiben:
Y = B&C;
X = A^Y;
Was Sinn macht, Y wird zuerst zu B*C und danach wird X zu A+Y. Beachten Sie, dass aufgrund der Art und Weise, wie die Gatter gezeichnet werden, eine implizite Reihenfolge vorliegt. Das UND-Gatter wird vor dem ODER-Gatter aufgelöst, da ein Draht vom UND-Gatter zum ODER-Gatter führt.
Überlegen Sie, was Sie beschreiben, wenn Sie Folgendes schreiben:
Y <= B&C;
X <= A^Y;
In diesem Fall sagen wir, dass Y gleichzeitig zu B*C wird, während X zu X+Y wird (innerhalb desselben Zeitschritts). Dies bedeutet, dass ein Simulator Y mit den vorherigen Werten (letzter Zeitschritt) von B und C (kein Problem) auswertet, aber auch X mit den vorherigen Werten von A und Y (möglicherweise falsch).
Eine sehr einfache Antwort könnte das Problem lösen. Wenn Sie eine Synthese durchführen, verwenden Sie eine Teilmenge der vollständigen Ausdrucksfähigkeiten der Verilog-Sprache. In synthetisierter Logik können Sie Flops, Latches und kombinatorische Logik haben. Insbesondere Flops und Latches werden durch das Synthesetool unter Verwendung von Templates abgeleitet.
Wenn Sie eine getaktete Funktion mit einer blockierenden Zuweisung schreiben, ist die Simulation zwar zufrieden, aber sie passt nicht zu Ihrem FPGA-Verhalten. Aus Kompatibilitäts- oder Legacy-Gründen verweigert das Synthesetool die Eingabe möglicherweise nicht und beschwert sich einfach. Versuchen Sie dies einfach niemals.
Das einzige Mal, dass Sie sehen, dass eine Simulation diese unterschiedlichen Verhaltensweisen für eine getaktete Funktion in ansonsten synthetisierbarem Code aufdeckt, ist, wenn die RHS eines Ausdrucks durch andere getaktete Blöcke aktualisiert wird. Ich stelle mir dies ähnlich wie Setup-Hold-Probleme vor, die ebenso seltsam aussehen, wenn Sie Zeitdiagramme für eine Nullverzögerungssimulation zeichnen. In der Praxis ändern sich die Flop-Ausgänge immer nach der Taktflanke, aber in der Simulation wird diese Information normalerweise nicht angezeigt.
Frage 1
ist zu groß, um hier zu antworten. Wenn Sie es wirklich wissen wollen, schreiben Sie es in beide Richtungen und simulieren Sie es
Frage 2
Wenn Sie nur eine Anweisung in einem Prozedurblock haben, spielt es (zumindest praktisch) keine Rolle, ob es sich um eine Blockierung oder eine Nichtblockierung handelt.
Frage 3
Ihre Testbench wird nicht in Flip-Flops implementiert, sondern nur vom Simulationstool interpretiert. Machen Sie sich keine Gedanken darüber, wie Testbench-Code synthetisiert wird.
Frage 4
Ich habe gelernt, für Synthesecode immer Non-Blocking zu verwenden. Verwenden Sie das Blockieren nur in Sonderfällen, in denen es den Code leichter verständlich macht. Aber es gibt andere Philosophien dazu.
Ich habe eine zufriedenstellende Antwort gefunden und benötige dafür Input. Ich bin der Meinung, dass wir Nonblocking-Anweisungen sowohl für kombinatorische als auch für sequenzielle Anweisungen verwenden sollten.
Für sequenziell ist es ziemlich klar, dass wir y verwenden sollten.
Ich werde den Grund für Kombiblöcke beschreiben.
Für zB. nimm den folgenden Code
module block_nonblock(output logic x,x1,y,y1,input logic a,b,c);
always@* begin : BLOCKING
x1 = a & b;
x = x1 & c;
end
always@* begin : NONBLOCKING
y1 <= a & b;
y <= y1 & c;
end
endmodule
Hier wird für beide Schaltungen auf dieselbe Hardware geschlossen.
Wenn wir jedoch die Eingaben zusammen als (A = 1, B = 1, C = 0) angeben und sie dann zusammen nach sagen wir 10 ns als (A = 1, B = 0, C = 1) ändern, können wir das dort sehen ist ein Fehler. Dieser Fehler wird auch in der tatsächlichen Hardware vorhanden sein. Dies wird jedoch nur in der Simulation durch Nonblocking Statements Output (Y) und nicht durch Blocking Statements Output (X) angezeigt. Sobald wir einen Fehler sehen, können wir zusätzliche Maßnahmen ergreifen, um dies zu verhindern, damit dies nicht in der Hardware passiert.
Für kombinatorische Segmente verwenden wir Non-Blocking Statements, denn wenn wir Blocking- oder Non-Blocking-Statements verwenden, erhalten wir am Ende dieselbe Hardware oder RTL; Es sind die nicht blockierenden Anweisungen, die uns die Störungen in der Simulation zeigen. Diese Störungen werden auch in der Hardware vorhanden sein (aufgrund von Gate-Verzögerungen), sodass wir sie beheben können, wenn wir sie in der Simulation sehen, damit sie in einer späteren Phase des Design-/Entwicklungszyklus weniger Schaden anrichten.
Daher kann ich mit Sicherheit den Schluss ziehen, dass wir Nonblocking-Anweisungen für Kombiblöcke verwenden müssen.
user_1818839
Stanri