Blockierende vs. nicht blockierende Zuweisungen

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?

Dieses Dilemma nicht zu haben, ist einer der größten Vorteile von VHDL.
Es ist nicht wirklich ein Dilemma. Ich habe in den ersten ~3 Jahren der Codierung von Verilog keine Blockierungsanweisungen verwendet. Du benutzt sie nur nicht.

Antworten (6)

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

Blockierende vs. nicht blockierende Zuweisung

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!

Ich danke Ihnen für Ihre Erklärung. Könnten Sie mir bitte einen Link zu der obigen Präsentation geben?

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 xgedruckt 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:

schematisch

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.

#3 Meine Testbench enthält im Moment rx_done_tick als Signal, aber es wird später ein Ausgangssignal des uart-Moduls sein. Es wird also synthetisiert.
@ironstein, dann müssen Sie all diese Verzögerungselemente (# 31 usw.) loswerden, da sie nicht synthetisierbar sind. Um für die Synthese zu schreiben, stellen Sie einen Takt bereit, bauen Sie eine Zustandsmaschine und treiben Sie die Ausgangssignale gemäß dem FSM-Zustand.

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.

Simulationswellenform, die Glitch darstellt

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.

Wenn Sie nicht blockierende Anweisungen für kombinatorische Blöcke verwenden, unterscheidet sich die Ausgabe, die Sie in der Simulation beobachten, stark von der, die Sie bei der synthetisierten Schaltung beobachten würden. Es ist wahr, dass beide Schaltungen genau gleich synthetisiert werden, aber die Simulation vor der Synthese und die Simulation nach der Synthese würden sich unterscheiden (im Grunde simulieren Sie nur die falsche Schaltung und synthetisieren die richtige. Dies bedeutet nicht, dass die Ausgabe diejenige sein wird Sie sah in der Simulation). Grundsätzlich entwerfen Sie die gesamte Schaltung falsch (weil Sie sie falsch simulieren).
Hallo @ironstein, könnten Sie die obige Schaltung näher erläutern. Ich habe nicht verstanden, dass Sie es als falsch bezeichnet haben. Meine Absicht ist es, ein UND-Gatter mit 3 Eingängen unter Verwendung von 2-i / p-UND-Gattern zu erstellen und zu entscheiden, welche Aussage (Blockieren oder Nichtblockieren) am besten wäre. Macht die obige Schaltung nicht dasselbe (3 i / p AND Gate). Wie ist die Schaltung falsch ..? Was fehlt mir hier...?