Menu

Verilog Modul-Instanziierung: Submodule verbinden

Wie du ein Modul in einem anderen instanziierst, der Unterschied zwischen benannten und positionsbasierten Port-Verbindungen und die Mehrfach-Instanz-Muster, mit denen du echte Designs baust.

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

Module in Modulen

Ein Verilog-Design ist ein Baum aus Modulen. Das Top-Level-Modul (deine Testbench oder das Top-Level-Wrapper eines Chips) instanziiert tiefere Module, die wiederum noch tiefere Module instanziieren, bis hinunter zu herstellerseitigen Gatter-Primitiven. Instanziierung ist die Syntax für dieses Verschachteln.

Die Form hast du schon gesehen - wir haben sie in Dein erstes Modul verwendet:

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

Diese Zeile stanzt eine einzelne Instanz von and_gate aus, nennt sie dut und verbindet ihre Ports mit lokalen Signalen. Zerlegen wir die einzelnen Teile.

Die Form einer Instanziierung

module_name instance_name (port_connections);
  • module_name muss zu einem Namen aus einer module-Deklaration irgendwo in deinem Projekt passen. Verilog ist case-sensitive.
  • instance_name ist ein Label, das du wählst - üblicherweise beschreibend für die Rolle, die diese Instanz spielt. Du wirst ihn in hierarchischen Pfaden und Waveform-Ansichten verwenden.
  • port_connections verdrahten die Ports der Instanz mit lokalen Signalen. Dafür gibt es zwei Schreibweisen.

Benannte Port-Verbindungen (nimm diese)

Die benannte Form sieht so aus:

my_module instance_name(
    .clk    (clk),
    .reset  (reset_n),
    .data_in(in_bus),
    .data_out(out_bus),
    .valid  (out_valid)
);

Jedes .port(signal)-Paar sagt: "Verbinde den Port der Instanz mit Namen port mit dem lokalen Signal mit Namen signal." Die Reihenfolge ist egal. Wenn du der Modul-Deklaration einen neuen Port hinzufügst, brechen bestehende Instanziierungen nicht, solange du dem neuen Port einen Default gibst oder jede Stelle aktualisierst.

Zwei praktische Hinweise:

  • Der Port-Name (links der Klammern) muss exakt mit der Modul-Deklaration übereinstimmen.
  • Der Signalname (innerhalb der Klammern) ist lokal zu der Stelle, an der die Instanziierung steht - üblicherweise das Eltern-Modul.

Wenn ein Port unverbunden bleibt, lass die Innenseite leer: .optional_port(). Das Signal floatet (z) in der Instanz. Manche Synthese-Tools warnen; die meisten akzeptieren es.

Positionsbasierte Port-Verbindungen (vermeide diese)

Die knappere Form listet Signale in Port-Listen-Reihenfolge:

my_module instance_name(clk, reset_n, in_bus, out_bus, out_valid);

Das ist kürzer, aber fragil. Sortiere die Port-Liste des Moduls um (ein echtes Refactor, das vorkommt) und jede positionsbasierte Instanziierung verdrahtet sich leise falsch. Greife nicht zu positionsbasiert, außer die Port-Liste hat genau ein oder zwei Mitglieder und wird sich vermutlich nie ändern.

Wo sie noch akzeptabel ist: winzige Utility-Module, deren Port-Reihenfolge Teil der API ist. Ein Zwei-Eingang-Gatter ist positionsbasiert in Ordnung. Ein Memory-Controller mit 30 Ports ist eine Einladung zum Bug.

Ein vollständiges hierarchisches Beispiel

Das ist eine echte dreistufige Hierarchie: testfull_adder → zwei half_adder-Instanzen. Jede Instanz hat ihre eigene Kopie der Gatter in half_adder; das Synthese-Tool gibt pro Instanziierung eine Schaltung aus.

Mehrere Instanzen desselben Moduls

Wenn du dasselbe Modul mehrfach instanziierst, ist jede Instanz unabhängige Hardware. Sie teilen sich keinen Zustand. Sie teilen sich keine Gatter. Stell dir jede Instanz als frische Kopie vor, die aus dem Design ausgestanzt wird.

adder add0(.a(a0), .b(b0), .sum(s0));
adder add1(.a(a1), .b(b1), .sum(s1));
adder add2(.a(a2), .b(b2), .sum(s2));
adder add3(.a(a3), .b(b3), .sum(s3));

Das sind vier separate Addierer, die parallel laufen. Würde adder ein Register enthalten, hätte jede Instanz ihre eigene Kopie davon mit eigenem Zustand.

generate-Schleifen: Wiederholte Hardware ausstanzen

Vier Instanzen von Hand zu schreiben, ist okay. 64 zu schreiben, ist nervig. Der generate-Block lässt den Elaborator die Tipparbeit für dich machen:

Drei neue Syntaxteile:

  • genvar i deklariert eine Schleifenvariable, die in generate nutzbar ist. Sie ist kein Laufzeit-Signal - sie existiert nur zur Elaborationszeit.
  • generate ... endgenerate umschließt die Schleife. Manche Tools akzeptieren generate-Schleifen ohne das explizite generate-Schlüsselwort, aber es zu schreiben macht die Absicht klar.
  • begin : invert_loop benennt den Generate-Scope. Das Label wird Teil des hierarchischen Namens jeder erzeugten Instanz (dut.invert_loop[0].u_inv, dut.invert_loop[1].u_inv usw.).

Der Synthesizer rollt die Schleife auf und erzeugt WIDTH Kopien von bit_inverter. Jede Kopie ist unabhängige Hardware.

Parameter-Overrides bei der Instanziierung

Hat das Modul Parameter, kannst du sie mit #(.PARAM(value)) zwischen Modulname und Instanzname überschreiben:

counter #(.WIDTH(16)) c16 (.clk(clk), .count(out16));
counter #(.WIDTH(32)) c32 (.clk(clk), .count(out32));

Beide Instanzen nutzen denselben counter-Quelltext, haben aber unterschiedliche Breiten. Die Syntax haben wir in Parameter behandelt; sie fügt sich sauber in die Instanziierung ein.

Hierarchische Namen

Sobald du eine Hierarchie hast, hat jedes Signal einen hierarchischen Pfad:

test.dut.ha0.sum

Das liest sich als: im test-Modul, in der dut-Instanz, in der ha0-Instanz, das Signal mit Namen sum. Du wirst diese Pfade in Waveform-Viewern, Fehlermeldungen und gelegentlich in $display-Aufrufen sehen, die aus einer Testbench tief in ein Submodul hineinschauen:

$display("internal carry1 = %b", dut.carry1);

Solche hierarchischen Referenzen gelten nur für Testbenches und Debug - synthetisierbarer RTL greift nicht in andere Module hinein.

Häufige Fehler

Port-Namen passen nicht. .clk_in(clk) verbindet das lokale clk mit einem Port namens clk_in. Heißt der Port des Moduls eigentlich clk, wird dir der Parser das sagen (manche Tools klarer als andere).

Breiten-Mismatch an einem Port. Ein 4-Bit-Signal an einen 8-Bit-Port anzuschließen, erweitert still mit Nullen; umgekehrt schneidet es still ab. Die meisten Tools warnen; siehst du keine Warnungen, schau genauer.

Vergessenes # beim Parameter. counter (.WIDTH(8)) c(.clk(clk)) sieht aus wie ein Override, ist aber keiner - der Parser versucht, (.WIDTH(8)) als Port-Verbindung zu lesen und scheitert. Richtig: counter #(.WIDTH(8)) c(.clk(clk)).

Instanznamen wiederverwenden. Zwei Instanzen können im selben Scope nicht denselben Namen haben. Die Fehlermeldung ist meist deutlich; was dich erwischt, ist die Versuchung zu kopieren.

Wie es weitergeht

Du kannst jetzt Module zu einer echten Hierarchie verdrahten. Das nächste Doc rundet die strukturelle Seite von Verilog ab - Kontinuierliche Zuweisung - und geht tiefer auf die assign-Anweisung ein, die wir seit dem ersten Kapitel frei benutzt haben.

Häufig gestellte Fragen

Wie instanziiert man ein Modul in Verilog?

Du schreibst den Modulnamen, dann einen Instanznamen, dann eine geklammerte Liste von Port-Verbindungen: my_module instance_name(.port(signal), ...);. Der üblichste Stil verwendet benannte Verbindungen (.port(signal)), die unabhängig von der Reihenfolge nach Port-Namen verbinden. Der knappere positionsbasierte Stil (my_module instance(signal1, signal2)) hängt von der Reihenfolge der Port-Liste ab und ist wartungsgefährlich.

Was ist der Unterschied zwischen benannten und positionsbasierten Port-Verbindungen?

Positionsbasierte Verbindungen listen Signale in derselben Reihenfolge wie die Port-Liste des Moduls - das erste Signal geht an den ersten Port, das zweite an den zweiten usw. Benannte Verbindungen nutzen .port_name(signal_name) und matchen über den Namen. Benannt ist ausführlicher, aber immun gegen Port-Umsortierungen und dokumentiert sich an der Aufrufstelle selbst. Nimm benannte Verbindungen ab zwei oder drei Ports.

Kann man dasselbe Verilog-Modul mehrfach instanziieren?

Ja - genau darum geht es. Jede Instanz ist unabhängige Hardware mit eigenem Zustand. Wenn du ein adder-Modul hast, kannst du es 64-mal in einer SIMD-Einheit instanziieren, jede mit anderen Eingängen. Die generate-Schleife ist die übliche Syntax, wenn die Instanzen ähnlich und indiziert sind.

Was ist ein generate-Block in Verilog?

generate ... endgenerate ist ein Konstrukt zur Kompilierzeit, das wiederholte Hardware ausstanzt. Eine for-Schleife innerhalb von generate erzeugt N Instanzen dessen, was im Body steht. generate läuft zur Elaborationszeit, vor Start der Simulation - es ist keine Laufzeitschleife, es ist ein Codegenerator für den Synthesizer.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S