Menu

Dein erstes Verilog-Modul: Schritt für Schritt erklärt

Schreibe dein erstes vollständiges Verilog-Modul von Grund auf - Deklaration, Ports, ein Stück kombinatorische Logik und eine Testbench, die es treibt. Lauffähig im Browser.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Das Modul ist die Einheit von Verilog

Alles in Verilog lebt innerhalb eines module. Ein Modul verpackt ein Stück Schaltung: Es deklariert, welche Signale hereinkommen, welche herausgehen und welche Leitungen/Register/Logik dazwischen liegen. Jeder Chip, den du je gesehen hast, ist ein Baum von Modulen, die andere Module instanziieren, bis hinunter zu den herstellerseitigen Gatter-Primitiven.

Die Form ist immer dieselbe:

module name(port_list);
    // Deklarationen: wire, reg, parameter
    // Body: assigns, Instanziierungen, always-Blöcke
endmodule

Wir bauen uns Schritt für Schritt zu einem vollständigen Modul vor.

Das kleinste nützliche Modul: Ein UND-Gatter mit zwei Eingängen

Wir brauchen etwas Einfaches, Eigenständiges. Ein UND-Gatter mit zwei Eingängen ist perfekt: zwei Eingänge, ein Ausgang, eine Zeile Logik.

Drücke Run. Du solltest die vier Zeilen einer AND-Wahrheitstabelle sehen. Schauen wir uns jedes Stück an.

Die Modul-Deklaration lesen

module and_gate(
    input  wire a,
    input  wire b,
    output wire y
);
  • module and_gate deklariert ein Modul namens and_gate. Der Name ist die Art, wie andere Module es instanziieren werden.
  • Die geklammerte Liste ist die Port-Liste - die Signale, die von außen sichtbar sind.
  • input wire a - a ist ein Eingangsport; er ist ein wire (von außen getrieben).
  • output wire y - y ist ein Ausgangsport, der von etwas innerhalb des Moduls getrieben wird.

Wenn du es knapp halten willst, kannst du input a statt input wire a schreiben - die Richtung allein macht den Defaulttyp wire. Aber explizit zu sein, ist eine gute Angewohnheit. Modul-Ports deckt die ganze Bandbreite an Port-Formen ab.

Der Body

assign y = a & b;

Das ist eine kontinuierliche Zuweisung (continuous assignment). Sie sagt: "Wann immer a oder b sich ändert, berechne y als bitweises AND der beiden neu." Kein Takt, keine Zeitsteuerung - die Beziehung gilt immer. Das ist reine kombinatorische Logik.

endmodule schließt den Block. Das Modul ist fertig.

Die Testbench

Du kannst and_gate nicht für sich laufen lassen. Du brauchst ein zweites Modul, das seine Eingänge treibt und seine Ausgänge beobachtet. Das ist die Testbench, und per Konvention nennen wir sie test, tb oder <design>_tb.

module test;
    reg  a, b;
    wire y;

    and_gate dut(.a(a), .b(b), .y(y));
    ...
endmodule

Drei Dinge fallen auf:

  • Keine Port-Liste. Eine Testbench ist die Spitze der Simulation - außerhalb steht nichts.
  • reg a, b und wire y. Die Eingänge des Design under Test (DUT) sind in der Testbench reg, weil wir sie aus einem prozeduralen Block treiben. Der Ausgang ist ein wire, weil das DUT ihn treibt.
  • and_gate dut(.a(a), .b(b), .y(y)). Das ist die Instanziierung. Wir stempeln eine Kopie von and_gate aus und nennen sie dut (üblicher Name - "design under test"). Die .a(a)-Syntax sagt: "Verbinde den Port mit Namen a der Instanz mit dem lokalen Signal mit Namen a." Modul-Instanziierung geht tiefer.

Der Stimulus

initial begin
    a = 0; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 0; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 1; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 1; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
    $finish;
end

Der initial-Block läuft einmal beim Start der Simulation. Darin:

  • a = 0; b = 0; treibt die Eingänge. Das sind blocking-Zuweisungen - die Reihenfolge ist relevant, und jede passiert vor der nächsten.
  • #1 rückt die simulierte Zeit um 1 Einheit vor. Wir brauchen das, damit y Zeit hat, sich nach den Eingangsänderungen einzuschwingen. Ohne das #1 würde $display den alten Wert von y drucken.
  • $display(...) schreibt auf die Simulationskonsole. Der Format-String funktioniert wie C's printf: %b ist binär, %d ist dezimal, %h ist hex, %t ist Simulationszeit.
  • $finish beendet die Simulation. Ohne es würde der Simulator endlos die Zeit weiterticken lassen und auf ein Ereignis warten, das nie kommt.

Probier kaputtzumachen

Mache aus dem Modul ein OR-Gatter (ändere & zu |) und lass es erneut laufen. Die Wahrheitstabelle dreht sich. Jetzt probiere ein XOR:

Gleiches Skelett. Anderer Operator. Das ist das ganze Spiel für kombinatorische Logik - Ports deklarieren, assigns schreiben, Eingänge durchspielen, Ausgänge beobachten.

Ein Modul mit zwei Ausgängen

Module können mehr als einen Ausgang haben. Hier ist ein Halbaddierer - zwei Eingänge, Summe und Carry-Out:

Die beiden assign-Anweisungen stehen nebeneinander, passieren aber parallel - es gibt kein "erst Summe ausrechnen, dann Carry". Beide gelten immer. Das ist die Nebenläufigkeit, über die wir in Hardware vs Software gesprochen haben, jetzt konkret.

Was du jetzt kennst

Du hast das vollständige Skelett einer Verilog-Datei gesehen: ein Design-Modul mit deklarierten Ports und einem Body, plus ein Testbench-Modul, das es instanziiert, seine Eingänge in einem initial-Block treibt und Ergebnisse meldet. Fast jede Verilog-Quelldatei, die du je lesen wirst, passt in dieses Schema. Der Rest der Sprache füllt nur reichere Logik, mehrbittige Signale, getaktetes Verhalten und größere Testbenches darin auf.

Als Nächstes: Kommentare und Code-Stil, damit deine Module beim Wachsen lesbar bleiben.

Häufig gestellte Fragen

Was ist das einfachste Verilog-Modul?

Das kleinste zulässige Verilog-Modul ist einfach module name; endmodule - keine Ports, kein Body. Das kleinste nützliche ist ein Modul mit einem Ausgang: module and_gate(input wire a, input wire b, output wire y); assign y = a & b; endmodule. Das ist ein echtes Stück kombinatorischer Logik, das du in jedes größere Design einfügen kannst.

Wie führe ich ein Verilog-Modul aus?

Du kannst ein Modul nicht für sich allein ausführen - es ist eine Schaltungsbeschreibung, kein Programm. Du schreibst ein Testbench-Modul, das dein Design instanziiert und seine Eingänge treibt, und kompilierst dann beides mit iverilog -o sim design.v test.v und lässt vvp sim laufen. Der Browser-Editor auf dieser Seite erledigt beides für dich, wenn du Run drückst.

Was ist eine Testbench in Verilog?

Eine Testbench ist ein zweites Modul - üblicherweise ohne Ports -, dessen Aufgabe darin besteht, dein Design auszuüben. Sie instanziiert das Design, wackelt in einem initial-Block an dessen Eingängen, beobachtet die Ausgänge mit $display oder $monitor und ruft $finish auf, wenn sie fertig ist. Testbenches sind nicht synthetisierbar; sie existieren nur, um Verhalten zu verifizieren.

Warum braucht mein Verilog-Code $finish?

Weil Hardware nie aufhört. Ein Simulator tut so, als verginge Zeit, und ohne ein explizites $finish würde er ewig weiterlaufen und auf neue Ereignisse warten. $finish sagt dem Simulator: 'Wir sind fertig, sauber beenden'. In einer Testbench ist es die letzte Zeile des initial-Blocks - Test ausführen, dann finish.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S