Verilog

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Einleitung

Verilog ist eine Hardwarebeschreibungssprache (Hardware Description Language: HDL), die es ermöglicht, ein digitales System in einem recht weiten Spektrum an Abstraktion zu spezifizieren.

Geschichte

Verilog wurde im Winter 1983/84 ("The Verilog Hardware Description Language", Thomas & Moorby) als ein proprietäres Produkt zur Verifikation und Simulation von digitaler Logik entwickelt.

Verilog wurde erstmals vom IEEE 1995 standardisiert in IEEE 1364-1995. Eine Erweiterung wurde dann 2001 durchgeführt mit IEEE 1364-2001 und später IEEE 1364-2005.

Mit Erscheinen des letzten Standards hat die IEEE P1364 Arbeitsgruppe ihre Arbeit eingestellt und die Pflege des Standards der IEEE P1800 Arbeitsgruppe übergeben, die sich mit der Standardisierung von System Verilog befasst.

Entwicklung der Hardwarebeschreibungssprache

Das Besondere an einer Hardwarebeschreibungssprache sind die zwei unterschiedlichen Ansätze, mit der die Sprache eingesetzt wird. Bei einer Programmiersprache gibt es die Sprachsyntax, und in Abhängigkeit der Sprache gibt es eine Philosophie, wie diese Sprache und alle verfügbaren Konstrukte eingesetzt werden. So gibt es z. B. prozedurale Programmiersprachen, wie C, bei denen man mit Hilfe von Funktionsaufrufen große Probleme in viele kleine Probleme teilt. Bei objektorientierten Sprachen, wie z. B. C++, nimmt man Objekte und unterteilt ein Problem in die verschieden Objekte und wie diese miteinander kommunizieren.

Hardwarebeschreibungssprachen wurden ursprünglich zur Spezifikation und Verifikation mit einem Simulator entwickelt. In diesem Bereich kann man auch die gesamte Syntax der Sprache einsetzen. Unterschieden werden die beiden Modellierungsarten

  • Modellierung des Verhaltens (engl. "behavioural modeling")
  • Modellierung auf Register-Transfer-Ebene (engl. "Register-Transfer-Level (RTL) modeling")

Bei der Modellierung des Verhaltens werden oft noch keine zeitlichen Aspekte verwendet. Es geht darum, in einer abstrakten Form die Funktion des zu entwickelten digitalen Schaltkreises zu simulieren.

Mit der RTL-Modellierung wird dann das Design taktgenau simuliert.

Später dann kamen einige Electronic Design Automation (EDA)-Firmen auf die Idee, eine Hardwarebeschreibungssprache nicht nur für die Simulation, sondern auch für die Synthese der eigentlichen Hardware zu benutzen. Für diesen Ansatz waren aber die Hardwarebeschreibungssprachen viel zu mächtig, und so wurden nur einfache Sprachkonstrukte genommen, die das Synthesetool unterstützt.

Aus dieser Entwicklung heraus haben sich die beiden Sichtweisen einer Hardwarebeschreibungssprache für Simulation und Synthese entwickelt. Ein Benutzer muss also immer im Hinterkopf haben, welchen Teil der Sprache er für seine Entwicklung nutzen kann. Für die Simulation sind in der Regel keine Grenzen gesetzt. Für die Entwicklung synthetisierbarer Logik dagegen sind nur einige Sprachkonstrukte erlaubt. Der Verilog-Standard IEEE 1394 hat dazu einen Zusatz, in dem ein einheitlicher synthetisierbarer Syntax spezifiziert wird. Für eine spezielle Synthesesoftware bietet natürlich auch das entsprechende Benutzerhandbuch Aufschluss. Für Xilinx ist das z. B. das XST User's Guide.

Gliederung

Dieser Artikel gibt eine Einführung in die Hardwarebeschreibungssprache Verilog, mit der Zielsetzung, ein Verständnis für die unterschiedliche Nutzung der Sprache für Simulation und Synthese zu geben.

Der Artikel gliedert sich in die drei Teile:

  • Sprachübersicht
  • Simulation/Verifikation
  • Synthetisierbare Konstrukte

In der Sprachübersicht werden grundlegende Sprachelemente vermittelt. Dieser Teil ist bewusst klein gehalten und versucht nur die Teile der Sprache zu besprechen, die für Neueinsteiger(innen) erst mal wichtig sind.

Daran schließt sich ein Teil mit dem Schwerpunkt der Simulation synthetisierbarer Logik. Zwar wissen wir an der Stelle noch nicht viel von synthetisierbarer Logik, dieser Teil wurde aber bewusst vorgezogen, da es hier keine Beschränkungen in der Nutzung der Sprache und der Art der Modellierung gibt. Auch ist es der Teil, bei dem erste praktische Erfolge mit dem Simulator erzielt werden.

Darauf folgt dann der Teil in dem die synthetisierbaren Konstrukte der Sprache erläutert werden.

Sprachübersicht

module

Die grundlegende Gruppierung wird in Verilog durch module durchgeführt.

<verilog> // Einzeiliger Kommentar, wie bei C++

module MeinModulName ( input a, output b, inout data);

/* Mehrzeiliger Kommentar

  Wie von C bekannt
*/

endmodule </verilog>

Signale werden durch Ports dem Modul übergeben. Für Signale in das Modul gibt es den input-Port. Ausgänge werden durch output spezifiziert und ein bidirektionaler Bus durch inout.

Wie oben gezeigt sind ein- oder mehrzeilige Kommentare wie bei C++ möglich.

Datentypen: wire, reg

Die grundlegenden Datentypen, mit denen in Verilog modelliert wird, sind wire und reg. Analog zum elektrischen Draht können mit dem wire Verbindungen durchgeführt werden. Wie ein Draht kann aber ein wire keinen Signalzustand speichern. Für die Modellierung digitaler Logik ist es aber auch nötig, Signaltreiber zu haben. Hier kommt der Datentyp reg ins Spiel. Mit ihm können Signalzustände gespeichert werden, und er findet damit als Signaltreiber Verwendung.

Verilog unterstützt die Signalzustände:

  • 0 --> logisch null
  • 1 --> logisch eins
  • z --> hochohmig
  • x --> undefiniert

Wie bei einer Programmiersprache üblich, müssen Datentypen vor ihrer Benutzung deklariert werden. Das geschieht unter Verilog wie folgt:

<verilog> wire a; reg b; </verilog>

Einem reg wird in diesem Fall der Wert x zugewiesen und einem wire der Wert z. Bei der Deklaration kann einem reg auch bereits ein Anfangswert zugewiesen werden.

<verilog> reg b = 0; </verilog>

Mit dem assign-Konstrukt können die Signalzustände wie folgt einem wire zugewiesen werden:

<verilog> module;

 wire a, b, c, d;
 assign a = 0;
 assign b = 1;
 assign c = x;
 assign d = z;

endmodule </verilog>

Die assign-Anweisung kann als eine Art Verdrahtungsregel angesehen werden, mit der die Verbindung des wire beschrieben wird. Im obigen Beispiel werden die vier Drähte jeweils mit konstante Werte verdrahtet. Diese Konstanten sind die Signaltreiber für die Drähte.

Mit dem assign Konstrukt können nur einem wire Signale zugewiesen werden. Ein reg muss innerhalb eines always oder initial Blocks seine Werte zugewiesen bekommen. Mehr dazu in dem Abschnitt über initial und always Blöcke.

Ereignissteuerung mit @

Im vorherigen Abschnitt haben wir schon mit den Signalzuständen x und z eine Eigenart von Hardwarebeschreibungssprachen kennengelernt. Mit der Ereignissteuerung, die durch das @-Zeichen beschrieben wird, lernen wir jetzt eine weitere kennen.

Mit der Ereignissteuerung ist es möglich, den Zustand von einem Signal überwachen zu lassen und dann in Abhängigkeit davon eine Zuweisung auszulösen.

Nehmen wir als Beispiel einen Schalter, bei dessen Betätigung eine LED eingeschaltet wird.

<verilog> module led_steuerung( input schalter );

reg  led       = 0;   // ??? Widerspruch zu "Ein reg muss innerhalb eines always- oder initial-Blocks seine Werte zugewiesen bekommen"
                      // Das ist richtig, aber der Einfachheit halber wurde das weggelassen.
                      // Es wäre auch möglich, eine "assign"-Anweisung zu verwenden.
...
@(schalter) led = 1;
...

endmodule </verilog>

Wie bereits erwähnt wird die Ereignissteuerung mit dem Klammeraffen beschrieben. Nach dem Klammeraffen folgt die Ereignisliste. Sie ist eingebettet in Klammer, eine Liste von ein oder mehreren Signalen, die bestimmen, wann die folgende Operation ausgeführt wird. Im obigen Beispiel ist der wire-schalter in der Ereignisliste, und bei einem Signalwechsel von diesem wird dem reg led der Wert 1 zugewiesen.

Neben Signaländerungen kann der Konstrukt auch genutzt werden, um auf steigenden oder fallende Flanken zu reagieren. Hierzu werden die Schlüsselwörter posedge für steigende Flanke und negedge für fallende Flanke benutzt.

Im obigen Beispiel wird led der Wert 1 zugewiesen, wenn schalter einen Signalwechsel von 0 nach 1 oder 1 auf 0 macht. Soll nun in diesem Beispiel die Zuweisung nur beim Wechsel von 0 auf 1 stattfinden, kann es wie folgt geändert werden:

<verilog> module led_steuerung( input schalter );

reg  led       = 0;
...
@(posedge schalter) led = 1;
...

endmodule </verilog>

Jetzt wird die Zuweisung erst bei einer steigenden Flanke von schalter ausgeführt.

Die Ereignissteuerung ist einer der grundlegenden Sprachkonstrukte in der Hardwarebeschreibung, um die parallele Natur der Hardware zu beschreiben. Im nächsten Abschnitt werden wir sie im Zusammenhang mit always Blöcken kennenlernen. Diese Blöcke erlauben es, parallel ablaufende Prozesse zu beschreiben und mit Hilfe der Ereignissteuerung deren Abläufe zu steuern.

always, initial

Die always- und initial-Blöcke werden verwendet, um die parallele Natur von Hardware zu beschreiben. Jeder dieser Blöcke wird parallel ausgeführt, wobei ein initial-Block nur einmal durchlaufen wird und ein always-Block, wie eine Endlosschleife, ständig wiederholt wird.

Nehmen wir mal einen initial Block als Beispiel und kommen auf das Problem zurück, dass einem reg nur in einem always- oder initial-Block Werte zugewiesen werden können:

<verilog> module;

 reg a, b, c, d;
 initial begin
   a = 0;
   b = 1;
   c = z;
   d = x;
 end

endmodule </verilog>

Was in dem Beispiel neu dazu gekommen ist, ist die Gruppierung durch begin und end. Vergleichbar mit den geschweiften Klammern in der Programmierung mit C/C++, können Segmente mit begin und end zusammengefasst werden.

Im obigen Beispiel wird der initial-Block einmal durchlaufen, und die Ausführung endet dann nach dem Abarbeiten der letzten Operation.

Das initial-Konstrukt wird vorwiegend in der Simulation für Testbenches verwendet. In der Beschreibung synthetisierbarer Logik findet es weniger Verwendung.

Das always Konstrukt kann sowohl in Testbenches als auch in synthetisierbarer Logik verwendet werden. Hier erst mal ein Beispiel aus einer Testbench zum Generieren eines Taktsignals:

<verilog> module;

reg clk = 0;

always

 #10 clk = ~clk;

endmodule </verilog>

Als neuer Sprachkonstrukt kommt hier das Verzögerungszeichen #, das die Ausführung in diesen Fall für 10 Zeiteinheiten verzögert und dann mit der Ausführung des ihm folgenden Konstruktes fortfährt. Was also in dem always Block geschieht, ist, dass die Zuweisung clk = ~clk; im Simulator vom momentanen Zeitpunkt um 10 Zeiteinheiten verzögert ausgeführt wird. Bei der Zuweisung selbst wird dann dem clk-Signal das invertierte clk-Signal zugewiesen. Der always-Block toggelt also das clk Signal alle 10 Zeiteinheiten.

Die anfängliche Initialisierung von clk auf 0 ist sehr wichtig, da das Togglen nur von 0 auf 1, bzw. 1 auf 0 funktioniert. Wird sie weggelassen, ist clk undefiniert, also x, und das Togglen würde nicht funktionieren.

Vielleicht ist es ganz sinnvoll, an dieser Stelle nochmal zu erwähnen, dass das Verzögerungszeichen nicht synthetisierbar ist.

Im vorherigen Abschnitt haben wir die Ereignissteuerung kennengelernt. Jetzt werden wir diese mit einem always kombinieren und damit steuern, wann der always Block durchlaufen werden soll.

<verilog> module;

 reg y, a, b, c;
 always @(a, b, c)
   y = a & b & c;

endmodule </verilog>

Die logische UND-Verknüpfung von a, b und c wird y zugewiesen. Durch die Ereignissteuerung wird der always-Block jedesmal durchlaufen, wenn sich der Signalzustand von a, b oder c geändert hat. D.h. wenn sich einer der Signalzustände ändert, wird die Zuweisung ausgeführt und y auf den neusten Stand gebracht. Danach wartet die Ausführung wieder, bis sie durch die Ereignissteuerung erneut ausgelöst wird. Soviel sei vorweggenommen, dieses Konstrukt nennt man kombinatorische Logik, und in dem Abschnitt über synthetisierbare Logik wird nochmal näher darauf eingegangen.

Seit Verilog 2001 kann die Ereignisliste auch vereinfacht werden und einfach durch (*) ersetzen. Der Konstrukt heißt dann also always @(*).

Wenn wir bisher von Signaländerungen im Zusammenhang mit der Ereignissteuerung gesprochen haben, dann sind wir immer davon ausgegangen, dass diese Änderung irgendwie von außen initiiert wurde. Gehen wir jetzt mal etwas mehr auf die parallele Beschreibung von Hardware ein und erweitern das vorherige Beispiel, um die Signaländerung mit in dem module auszuführen.

<verilog> module;

 reg y;
 reg a = 1;
 reg b = 1;
 reg c = 0;
 always
   #10 c = ~c;


 always @(a, b, c)
   y = a & b & c;

endmodule </verilog>

Hier haben wir jetzt zwei always-Blöcke, die parallel ausgeführt werden. Der erste always-Block basiert auf dem Taktgenerator von zuvor. Eine kleine Abänderung ist, dass jetzt hier das Signal c an Stelle von clk alle 10 Zeiteinheiten in seinem Zustand geändert wird. Der Block hat keine Ereignissteuerung, er wird also ständig durchlaufen. Nur der #10-Konstrukt hält ihn jeweils für 10 Zeiteinheiten an.

Der zweite always-Block wird wie vorher von der Ereignissteuerung immer dann ausgelöst, wenn sich eines der Signale in seiner Ereignisliste ändert. Hier haben wir a und b einen festen Wert zugewiesen. Das Signal c hingegen ändert sich ständig und löst die Ereignissteuerung und damit den Durchlauf des always-Blockes aus. Die Zuweisung von y wird also immer dann auf den neusten Stand gebracht, wenn sich c ändert.

Skalar, Vektor, Bit-Splitting, Verkettung, Wiederholung

Bisher haben wir nur einfache Signale benutzt, d.h. Signale, die ein Bit breit sind. Ein Bus wird von dem wire-Statement abgeleitet. Er wird in der Form wire [msb:lsb] <wire_name> spezifiziert und in Verilog Vektor genannt. Entsprechend kann auch ein reg-Vektor spezifiziert werden.

<verilog>

 wire [15:0] data;
 ...
 data[0]   = 1;          // Setze nur Bit 0
 data[15:13] = 3'b110;   // Setze Bits 15, 14 = 1, Bit 13 = 0, Wert ist binär
 data[13:11] = 3'd2;     // Setze Bits 13-11, Wert ist dezimal
 data[10:1]  = 9'h0a;    // Setze Bits 10-1, Wert ist hexadezimal

</verilog>

Das Beispiel zeigt, wie ein 16 Bit breiter Vektor spezifiziert wird und anschließend einzelne Bits davon gesetzt werden.

Bei der Zuweisung wird hier erstmals bei Werten die Bitbreite festgelegt und verschiedene Zahlenrepräsentationen verwendet. Die Bitbreite wird in der Form <Zahl><Hochkomma> beschrieben. Für die verschiedenen Zahlenformate gibt es die Buchstaben b, d, h, die für binär, dezimal, bzw. hexadezimal stehen.

Aufmerksamen Lesern ist vielleicht aufgefallen, dass die Art der Zuweisung eigentlich in einen initial- oder always-Block hätte gepackt werden müssen- um so zu funktionieren. Eine andere Möglichkeit wäre- ein assign-Konstrukt daraus zu machen. Aus Übersichtlichkeit wurde hierbei darauf verzichtet.

In diesen Zusammenhang passt es auch, die Verkettung von Signalen zu erklären. Sie wird mit geschweiften Klammern erreicht:

<verilog>

 wire [7:0] data;
 wire [3:0] nibble;
 ...
 data = {4'b0000, nibble}; 

</verilog>

Dem wire data wird ein 8 Bit breites Wort zugewiesen, bei dem die oberen 4 Bit auf Null gesetzt sind und die unteren 4 Bit den Signalzustand von nibble erhalten.

Die Wiederholung wird auch mit geschweiften Klammern beschrieben und hat die Form {r{num}}. Hierbei wird num, r mal wiederholt.

Um bei dem letzten Beispiel zu bleiben, eine Abänderung bei der die oberen 4 Bit nicht auf Null gesetzt werden, sondern auch den Signalzustand von nibble erhalten, kann folgendermaßen erreicht werden:

<verilog>

 ...
 data = {2{nibble}};

</verilog>

Hier wird nibble zweimal wiederholt und damit auf 8 Bit Länge gebracht.

Speichermodellierung durch Felder

Aus der Programmiersprache C ist das Konzept der Felder bekannt, um Elemente gleichen Datentyps aufzuzählen. In Verilog wird das gleiche Konstrukt z. B. zur Modellierung von Speicher genutzt.

Mit dem folgenden Konstrukt wird ein 8-Bit-breiter Speicher mit 64 Speicherzellen und der Bezeichnung meinMem definiert:

<verilog>

 reg [7:0] meinMem [0:63];

</verilog>

Diese Syntax ist nur eine Verschmelzung der Syntaxen von Vektordefinitionen (die Bitbreite steht vor dem Bezeichner) mit der der Definition von Feldern (die Anzahl der Elemente steht nach dem Bezeichner).

Auf ein Element kann jetzt mit der bekannten Indizierung von Feldern zugegriffen werden:

<verilog>

 meinMem[0] = 8'h7f;

</verilog>

Hier wird in die erste Speicherstelle der hexadezimale Wert 7f geschrieben.

Wird versucht, von einem Speicherelement außerhalb des spezifizierten Indexbereiches zu lesen, ist das Ergebnis x. Ein Schreibversuch auf ein Element außerhalb des spezifizierten Indexbereichs hat keine Auswirkung.

Neben eindimensionalen ist es auch möglich, mehrdimensionale Speicher zu definieren, wie z. B. mit:

<verilog>

 reg [7:0] zweiDmem [0:15][0:63];

</verilog>

Analog zum eindimensionalen Feld erfolgt der Zugriff mit:

<verilog>

 zweiDmem[0][7] = 8'h2a;

</verilog>

=, <=; blockierende und nicht-blockierende Zuweisung

In Verilog gibt es die zwei Zuweisungsarten:

  • =
  • <=

Erstere wird als blockierende Zuweisung (blocking assignment) bezeichnet. Letztere als nicht-blockierend (nonblocking assignment).

Im Standard werden die folgenden zwei Richtlinien für deren Verwendung gegeben:

  • (=) Benutze blockierende Zuweisungen in always Blöcken die kombinatorische Logik modellieren.
  • (<=) Benutze nicht-blockierende Zuweisungen in always Blöcken die sequentielle Logik modellieren.

Nähere Details über den Hintergrund können in dem Artikel "Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!", Cliff Cummings, SNUG 2000 nachgelesen werden.

An dieser Stelle möchte ich auf einen Kommentar von Jan Decaluwe, dem Entwickler von MyHDL hinweisen, der die Regelung als nicht notwendig erachtet und für die Mischung der beiden Zuweisungsarten eintritt:

http://article.gmane.org/gmane.comp.python.myhdl/827

Logische Bit-Operationen

Die grundlegenden logischen Bit-Operation sind:

  • | --> OR
  • & --> AND
  • ~ --> NOT
  • ^ --> XOR

Sie werden erweitert durch:

  • ~& --> NAND
  • ~| --> NOR
  • ^~ oder ~^ --> XNOR

Verilog unterstützt einen sogenannten Reduktions-Operator, bei dem mit einem Operator mehrere Signale zusammen geführt werden. Damit kann z. B. eine logische AND-Verknüpfung von 8 Signalen wie folgt durchgeführt werden:

<verilog> module MeinUnd8 ( input [7:0] a,

                  output        y);
 assign y = &a;

endmodule </verilog>

Arithmetische Operationen, signed, $signed

Bevor wir kurz über die grundlegenden arithmetischen Operationen sprechen, kehren wir noch einmal zu den Datentypen wire und reg zurück. Diese werden in Verilog als vorzeichenlos, also unsigned, angesehen.

Entsprechend folgen arithmetische Operationen mit wire und reg dem Modulo 2^n, wobei n die Bitbreite des Vektors ist.

Verilog unterstützt die bekannten arithmetischen Operationen:

  • +
  • -
  • *
  • /

mit der entsprechend bekannten Präzedenz, dass Multiplikation und Division vor Addition und Subtraktion geht. Zur besseren Lesbarkeit können, wie so oft bei Programmiersprachen, Operationen mit Klammern gruppiert werden.

Leider unterstützt Verilog nicht eine Vereinfachung, die z. B. von der Programmiersprache C bekannt ist, bei der die Zuweisung und der Operator zusammengeschrieben werden und damit ein Operand eingespart werden kann (etwa: a++ für a=a+1 oder auch: a+=b für a=a+b)

Die Schreibweise:

<verilog>

 a=a+12;

</verilog>

kann also nicht abgekürzt werden. Auch ist kein Inkrement- oder Dekrement-Operator bekannt.

In for Schleifen ist es also immer nötig zu schreiben:

<verilog>

 i=i+1;

</verilog>

Um jetzt auch mit 2er Komplementzahlen rechnen zu können, wird das Konstrukt signed benutzt:

<verilog>

 reg signed [3:0] a;
 reg        [3:0] b;
 a = 2 - 4;          // a = -2
 b = 2 - 4;          // b = 14

</verilog>

Erinnern wir uns, dass es sich um Modulo 2^n Arithmetik handelt. Im dem Fall, wo a als signed spezifiziert wird, ist der Zahlenbereich von a [-8 .. 7]. Für den Fall von b ist er [0..15] und damit entsteht für 2-4 ein Unterlauf und die Berechnung ist 16-2.

In diesem Zusammenhang ist es wichtig zu betrachten was passiert, wenn bei arithmetischen Operationen gemischte Operanden (d.h. einer ist unsigned und ein anderer signed) genommen werden. Eine Operation zwischen einem signed und einem unsigned Datentyp resultiert in einen unsigned Datentyp. Da dieses Verhalten nicht immer gewünscht ist, gibt es die $signed() System Funktion. Auf System Funktionen und System Tasks wird später noch näher eingegangen. Der $signed() System Task wandelt einen unsigned Datentypen in einen signed um.

Um jetzt eine arithmetische Operation mit a und b aus dem vorherigen Beispiel durchzuführen und das Ergebnis auch als signed zu haben, muss der unsigned Operand erst mit $signed() in einen signed gewandelt werden.

<verilog>

 reg signed [3:0] a;
 reg        [3:0] b;
 reg signed [4:0] c;
 a = 2 - 4;          // a = -2
 b = 2 - 4;          // b = 14
 c = a + $signed(b);

</verilog>

Vergleichsoperationen; ==, ===, !=, !==, <, >

Analog zu anderen Programmiersprachen stellt Verilog die Vergleichsoperatoren ==, !=, <, > zur Verfügung. Erinnern wir uns aber daran das ein Signal neben 0 und 1 auch noch die Zustände x oder z annehmen kann.

Eine Vergleichsoperation bei der ein Bit den Zustand x oder z hat, ist im Ergebnis x und das wird als FALSCH angesehen.

Zum Beispiel:

<verilog>

 4'b1100 == 4'b1100;    // = 1 --> WAHR
 4'b1100 == 4'b11xx;    // = x --> FALSCH
 4'b11xx == 4'b11xx;    // = x --> FALSCH
 4'b110z == 4'b110z;    // = x --> FALSCH

</verilog>

Im gleichen Sinn ergeben größer- und kleiner-Operationen immer FALSCH wenn ein x- oder z-Zustand vorhanden ist:

<verilog>

 4'b1100 > 4'b11xx;      // = x --> FALSCH
 4'b110z > 4'b1100;      // = x --> FALSCH

</verilog>


Um einen exakten Vergleich durchzuführen, gibt es den sogenannten case equality operator (===). Hier werden auch die Zustände x und z mit in den Vergleich einbezogen:

<verilog>

 4'b1100 === 4'b1100;    // = 1 --> WAHR
 4'b1100 === 4'b11xx;    // = 0 --> FALSCH
 4'b11xx === 4'b11xx;    // = 1 --> WAHR
 4'b110z === 4'b110z;    // = 1 --> WAHR
 4'b110x === 4'b110z;    // = 0 --> FALSCH

</verilog>

Logische Operationen

Die im letzten Abschnitt beschriebenen Vergleichsoperationen können mit logischen Operationen verknüpft werden. Wie von den gängigen Programmiersprachen bekannt, unterstützt Verilog die logischen Operationen:

  • && --> AND
  • || --> OR
  • ! --> NOT

Entscheidungsoperation mit if-else und ? :

Bisher haben wir nur die sequentielle Abarbeitung von Operationen besprochen. Oft ist es nötig, basierend auf Signalzustände unterschiedliche Blöcke abzuarbeiten. Der von Programmiersprachen bekannte if- und else-Konstrukt ist hierzu hilfreich und wird von Verilog unterstützt.

Im folgenden Beispiel wird in Abhängigkeit von einem reset Signal entweder der eine oder der andere Anweisungsblock ausgeführt.

<verilog>

 ...
 if(reset)
   dout <= 0;
 else
   dout <= din;
 ...

</verilog>

Wenn in dem obigen Beispiel reset 1 ist, dann wird dout der Wert 0 zugewiesen. Ist reset 0, wird dout der Wert von din zugewiesen.

Ein weiteres Sprachkonstrukt der Form if-else ist der Entscheidungsoperator ?:, der auch von der Programmiersprache C bekannt ist. Er wird benutzt in der Form Test ? Wert_WAHR : Wert_FALSCH. Test kann hier eine logische Operation sein, und wenn diese WAHR ist, dann wird Wert_WAHR zurückgegeben, andernfalls wird Wert_FALSCH zurückgegeben.

Die Nutzung soll hier am Beispiel eines Multiplexers erläutert werden:

<verilog>

wire dout;
wire d0, d1, sel;

...
assign dout = sel ? d0 : d1;

</verilog>

Mit wire nehmen wir einen assign-Konstrukt, und die Zuweisung an dout hängt jetzt von dem sel-Signal ab. Ist es 1, wird dout der Wert von d0 zugewiesen. Ist sel 0, wird dout der Wert von d1 zugewiesen.

Mehrwegeentscheidung mit case

Die Erweiterung der Entscheidungsoperation ist die Mehrwegeentscheidung, die mit case durchgeführt wird.

Um das vorherige Beispiel mit dem Multiplexer zu erweitern:

<verilog>

 wire [1:0] sel;
 reg        dout;
 wire       d0, d1, d2, d3;
 always@(*)
  case (sel)
    2'b00: dout = d0;
    2'b01: dout = d1;
    2'b10: dout = d2;
    2'b11: dout = d3;
  endcase

</verilog>

Hier werden jetzt vier Eingangssignale in Abhängigkeit von sel auf den Ausgang gemultiplext.

Manchmal ist die Entscheidungsliste größer und es müssen nicht immer alle Fälle behandelt werden. Dafür kann dann der default Konstrukt verwendet werden.

<verilog>

 wire [1:0] sel;
 reg        dout;
 wire       d0, d1, d2, d3;
 always@(*)
  case (sel)
    2'b00:   dout = d0;
    2'b01:   dout = d1;
    2'b10:   dout = d2;
    default: dout = d3;
  endcase

</verilog>

Für alle nicht gelisteten Ereignisse wird jetzt die Zuweisung für den default Fall durchgeführt.

Schleifen mit for, while, und repeat

Für die wiederholte Ausführung von Codesegmenten, bei denen eine Zählervariable nötig ist, stellte Verilog die for-Schleife zur Verfügung. Die einfache Anwendung erfolgt folgendermaßen

<verilog>

 integer i;
 reg [3:0] mem[0:63];
 ...
 for(i=0; i<64; i=i+1)
   mem[i] = 4'd0;
 ...

</verilog>

In dem Beispiel wird allen Speicherplätzen von mem der Wert 0 zugewiesen. Neu ist hier der Datentype integer der für die Zähler variable genutzt wird und laut Definition 32 Bit groß ist.

Für die wiederholte Ausführung von Codesegmenten bei denen keine Zählervariable nötig ist kann der repeat Konstrukt genutzt werden.

<verilog>

 reg reset = 0;
 ...
 repeat(2) begin
   #10 reset = 1;
   #10 reset = 0;
 end
   

</verilog>

Hier wird zehn Zeiteinheiten gewartet und dann reset auf 1 gesetzt. Anschließend wieder 10 Zeiteinheiten gewartet und reset auf 0 gesetzt. Das ganze zwei mal wiederholt, so das ein doppelter reset Impuls entsteht.

Eine bedingte Schleife kann mit while ausgeführt werden.

<verilog>

 integer i;
 while(i < 10) begin
   ...
   i = i + 1;
 end

</verilog>

Der Schleifenkörper wird solange ausgeführt so lange die Bedingung WAHR ist.

Parametrisierung durch Konstanten mit parameter

Bisher haben wir konstante Werte immer direkt in den Code eingeführt. Um konstante Werte einheitlich über ein größeres Design zu verändern, ist es sinnvoll, für den Parameter einen einheitlichen Namen zu wählen und den Wert nur an einer Stelle im Design zuzuweisen.

<verilog>

 parameter DW = 8;
 ...
 reg [DW-1:0] data_bus;

</verilog>

Hierarchie

Um ein komplexeres Design zu erstellen, ist die Instantiierung von Modulen sehr hilfreich. Nehmen wir das Modul module mux(input a, input b, input sel, ouput y), ohne genauer auf dessen Innenleben einzugehen, und instantiieren es mehrmals in dem module top(...):

<verilog> module top (input a,

           input b, 
           input c, 
           input d,
           input s1,
           input s2,
           input s3,
           output y);
 wire w1, w2;
 mux m1 ( .a(a),  .b(b),  .sel(s1), .y(w1) );
 mux m2 ( .a(c),  .b(d),  .sel(s2), .y(w2) );
 mux m3 ( .a(w1), .b(w2), .sel(s3), .y(y)  );

endmodule </verilog>

Die Verknüpfung findet statt durch die explizite Nennung der Ports. Der Port von dem instantiierten Modul wird in der Form .PortName() geschrieben. Das Signal, mit dem er verknüpft wird, schreibt man in die Klammern des Ports. Mit der Verknüpfung .y(w1) wird also der Ausgangsport y von der Instanz m1 des Modules mux mit dem wire w1 im Modul top verknüpft.

Eine andere Variante der Verknüpfung wird durch die Position der Ports in der Definition des Modules erreicht. In unserem Fall ist Port a der erste Port, also wird der wire der an erste Stelle bei der Instantiierung geschrieben wird, mit Port a des Moduls verbunden. Das vorherige Beispiel wird in diesem Fall dann so aussehen:

<verilog>

 ...
 mux m1 ( a,  b,  s1, w1 );
 mux m2 ( c,  d,  s2, w2 );
 mux m3 ( w1, w2, s3, y  );
 ...

</verilog>

Der Vorteil ist, dass hier nicht noch explizit der Port des Modules genannte werden muss. Das wird jedoch schnell unübersichtlich mit Modulen, die eine lange Portliste haben.

Die Instantiierung kann jetzt noch durch die Überladung von parameter erweitert werden. Nehmen wir das vorherige Beispiel und schauen uns das module mux etwas näher an.

<verilog>

module mux #(parameter DW=8)

           ( input [DW-1:0]  a, 
             input [DW-1:0]  b, 
             input           sel, 
             output [DW-1:0] y);
 assign y = sel ? a : b;

endmodule </verilog>

In der module-Definition wird jetzt noch eine Parameterliste vor die Portdefinition eingefügt, die mit dem Lattenzaun # gekennzeichnet ist. Dadurch ist es möglich, die Parameter in der Portliste zu nutzen.

Soll das Modul nun instantiiert werden, kann die Parameterliste verändert und damit das instantiierte Modul angepasst werden.

<verilog> ...

 parameter NeueDW=16;
 wire [NeueDW-1:0] w1;
 wire [NeueDW-1:0] w2;
 mux m1 #(.DW(NeueDW))( .a(a),  .b(b),  .sel(s1), .y(w1) );
 mux m2 #(.DW(NeueDW))( .a(c),  .b(d),  .sel(s2), .y(w2) );
 mux m3 #(.DW(NeueDW))( .a(w1), .b(w2), .sel(s3), .y(y)  );

... </verilog>

Die Parameterüberladung erfolgt wie bei der expliziten Portverknüpfung in der Form .ZielParameter(NeuerParameter). Im obigen Beispiel werden nun also drei Multiplexer mit der neuen Datenbreite von 16 Bit instantiiert.

Verilog für Simulation

Soweit haben wir die Sprache in Anlehnung an eine Programmiersprache beschrieben. Dabei sind schon einige Sonderheiten wie die Vierwertigkeit von wire und reg oder die Ereignissteuerung durch @ im Zusammenhang mit der parallelen Abarbeitung von always-, bzw. initial-Blöcken erschienen. Auch hatten wir die Verzögerungsoperation # klammheimlich eingeführt, ohne wirklich tiefer darauf einzugehen, wie das ganze im Hintergrund funktioniert.

Der grundlegende Unterschied zwischen einer Programmiersprache und einer Hardwarbeschreibungssprache ist, dass letztere ein Simulationsmodel zugrunde liegt. In der Praxis sieht der Unterschied folgendermaßen aus: Bei einer kompilierten Programmiersprache wird der Quellcode durch den Compiler in ein ausführbares Programm umgesetzt, das dann auf dem Computer ausgeführt werden kann. Bei der Hardwarebeschreibungssprache wird der Quellcode in eine für einen Simulator ausführbares Format umgewandelt. Nach dem Compilieren kann dies dann mit dem Simulator ausgeführt werden. Der Simulator arbeitet den übersetzten Quellcode in Zeitschritten ab. Dadurch kann die parallele Funktion von Hardware simuliert werden.

In diesem Abschnitt werden jetzt Sprachkonstrukte beschrieben, die nicht synthetisierbar sind und nur für die Verifikation im Zusammenhang mit einem Simulator sinnvoll sind.

System Tasks und Functions

System Tasks und Functions sind ein Weg, einen Verilog-Simulator mit Funktionen zu erweitern. In diesem Abschnitt werden einige gängige System Tasks beschrieben, die im Verilog-Standard spezifiziert sind und somit in allen bekannten Simulatoren implementiert sind.

System Tasks und Functions beginnen immer mit einem Dollarzeichen ($) und werden in der Regel von Synthesis-Tools ignoriert.

$display

Der $display-Task wird verwendet, um Informationen auf die Standard-Ausgabe stdout zu schreiben. Die Form der Benutzung ist sehr an die printf()-Funktion von 'C' angelehnt. Ein wesentlicher Unterschied ist, dass mit der $display-Funktion automatisch ein Zeilenvorschub angehängt wird und so kein extra "\n" am Ende des Strings gesetzt zu werden braucht.

<verilog>

 integer i;
 ...
 i = 10;
 $display("Hallo Welt Nr. %d", i);

</verilog>

Wenn der Task ausgeführt wird, ersetzt der Simulator %d durch den Wert von i. Die Darstellung kann unterschiedlich formatiert werden, und es gibt folgende Formatierungsbefehle:

  • %d - dezimal
  • %h - sedezimal ("hexadezimal")
  • %b - binär
  • %o - oktal
  • %s - Zeichenkette
  • %c - ASCII-Zeichen
  • %v - Signalstärke
  • %m - hierarchischer Name

Einen zusätzlichen Zeilenvorschub erhält man durch \n, Tab z. B. durch \t.

Ein weitere Unterschied zu printf ist, dass mit $display die Zahlen rechtsbündig ausgerichtet werden. In manchen Fälle mag das nicht erwünscht sein, und so besteht die Möglichkeit, den Formatierungsbefehl %d z. B. in der Form %0d zu schreiben. Jetzt wird die Zahl linksbündig ausgerichtet.

Am Beispiel eines integer-Datentyps sei das verdeutlicht: <verilog>

 integer i;
 ...
 $display("Hallo Welt Nr. %d", i);
 $display("Hallo Welt Nr. %0d", i);

</verilog>

Im ersten Fall werden so viele Leerzeichen nach dem Wort 'Nr.' eingefügt, daß die Zahl rechtsbündig dargestellt wird. Mit der %0d-Version wird die Zahl linksbündig dargestellt.

<verilog> Hallo Welt Nr. 10 Hallo Welt Nr. 10 </verilog>

Die linksbündige Darstellung wird von den Formatierungsbefehlen %d, %o, und %h unterstützt.

$monitor

Ähnlich dem $display-Task schreibt der $monitor-Task den Text auf die Standardausgabe. Einziger Unterschied ist, dass der Task automatisch überprüft, ob eines der Signal sich geändert hat, und jedesmal bei einer Änderung einen neuen Ausdruck durchführt.


Wird das folgende Beispiel simuliert:

<verilog> module monitor_tb;

reg clk    = 0;
reg reset  = 0;
initial
  $monitor("Zeit: %t Takt: %b reset: %b", $time, clk, reset);
always
  #10 clk =~clk;
initial begin
  $display("Kleiner $monitor-Test");
  #15 reset = 1;
  #22 reset = 0;
  #10 $display("#%0t fertig", $time);
  $finish;
end

</verilog>

Erhält man folgenden Ausdruck:

Kleiner $monitor-Test
Zeit:                    0 Takt: 0 reset: 0
Zeit:                   10 Takt: 1 reset: 0
Zeit:                   15 Takt: 1 reset: 1
Zeit:                   20 Takt: 0 reset: 1
Zeit:                   30 Takt: 1 reset: 1
Zeit:                   37 Takt: 1 reset: 0
Zeit:                   40 Takt: 0 reset: 0
#47 fertig

Zum Zeitpunkt 0 wird der $display-Task ausgeführt und druckt den Text "Kleiner $monitor test" aus. Alle Signale sind zu dem Zeitpunkt auf 0, und der $monitor-Task druckt zum ersten Mal den Signalzustand aus.

Der nächste Aufruf erfolgt, wenn clk nach 10 Zeiteinheiten seinen Zustand wechselt. Dieser Wechsel wird autonom in dem always-Block alle 10 Zeiteinheiten durchgeführt. Parallel dazu wird in dem initial-Block nach 15 Zeiteinheiten das reset-Signal auf 1 gesetzt. Dazwischen kommt wieder der Wechsel von clk, der um 20 auf 0 und um 30 wieder auf 1 gesetzt wird.

Im initial-Block wird reset 22 Zeiteinheiten nach dem Setzen wieder zurückgesetzt. Das Setzen fand um 15 statt, plus 22 Zeiteinheiten, somit wird reset um 37 zurückgesetzt. Dann folgt noch mal ein clk-Wechsel, bevor die Simulation um 47 beendet wird. Zu der Zeit findet kein Signalwechsel mehr statt, so daß der $monitor Task nicht mehr aufgerufen wird. So haben wir vor dem $finish Task nochmal einen $display-Task gesetzt, der die Zeit mit dem Wort "fertig" ausdruckt.

$finish

Beendet die Simulation. Jede Testbench sollte einen $finish-System-Task haben, um die Simulation zu beenden.

$dumpfile, $dumpvars

Eine "Value Change Dump" (VCD) Datei ist eine Textdatei, in der Signalzustandsänderungen von der Simulation geschrieben werden. Die Datei kann dann z. B. von einem Programm eingelesen und dargestellt werden. Ein solches Programm ist z. B. gtkwave.

Der Simulator generiert in der Regel so eine VCD-Datei nicht von selbst, sondern muss dazu instruiert werden. Eine Form, die von Simulatoren unterstützt wird, ist durch die beiden Verilog System Tasks $dumpfile und $dumvars.

<verilog> module top_tb;

 initial begin
   $dumpfile("top_tb.vcd");
   $dumpvars(0, top_tb);
 end
 ...

endmodule </verilog>


Mit $dumpfile("top_tb.vcd") wird der Dateiname der VCD-Datei spezifiziert. Gerade bei der Simulation von großen Designs ist es sinnvoll, die Anzahl der Signale, die in das "dump file" geschrieben werden, zu beschränken. Der $dumpvars System Task erlaubt, dazu die Hierarchieebene mit zu spezifizieren. Mit dem Befehl $dumpvars(0, top_tb) werden alle Signale von der spezifizierten Ebene an nach unten in das "dump file" geschrieben. Sollen z. B. nur die Signale in top_tb in das "dump file" geschrieben werden, dann kann dies mit der Ebene 1, also $dumpvars(1, top_tb) beschränkt werden.

Zeit in der Simulation

Wenn wir im Zusammenhang mit der Simulation von Zeit gesprochen haben, dann bisher nur von Zeiteinheiten. Der Simulator arbeitet mit Simulationsschritte, die erstmal dimensionslos sind. Um dem Verzögerungsoperator eine Dimension zu geben, stellt Verilog die `timescale Anweisung zur Verfügung.

<verilog> `timescale 10ns / 1ns </verilog>

Die erste Zahl bestimmt die Einheit für den Verzögerungsoperator, die zweite Zahl die Auflösung, mit der die Simulation durchgeführt wird. Beide Zahlen werden als Ganzzahl ("integer") angegeben und können die Werte 1, 10, oder 100 haben.

Als Zeiteinheiten werden die folgenden Werte unterstütz:

Zeiteinheit Abkürzung
Sekunden s
Millisekunden ms
Microsekunden us
Nanosekunden ns
Picosekunden ps
Femtosekunden fs

Ein Beispiel soll die Nutzung von `timescale verdeutlichen.

<verilog> module test;

initial begin

 $display("%t Start um 0", $time);
 #10 $display("%t Nach 10 Zeiteinheiten", $time);
 $finish();

end

endmodule </verilog>

Wenn wir den obigen Quellcode in eine Datei time.v packen und dann mit Icarus-Verilog simulieren, erhalten wir folgenden Ausdruck:

>iverilog -o time.vvp time.v
>vvp time.vvp
                   0 Start um 0
                  10 Nach 10 Zeiteinheiten

Die Verzögerungsoperation von 10 Zeiteinheiten wird direkt umgesetzt, d.h. nach einer Verzögerung von 10 sind auch 10 Simulationsschritte ausgeführt worden.

Wenn wir jetzt die Zeitskala verändern, z. B. nach `timescale 10ns/1ns, indem wir den Code wie folgt ändern:

<verilog> `timescale 10ns/1ns

module test;

initial begin

 $display("%t Start um 0", $time);
 #10 $display("%t Nach 10 Zeiteinheiten", $time);
 $finish();

end

endmodule </verilog>

So erhalten wir mit der Simulation folgende Ausgabe:

>iverilog -o time.vvp time.v
>vvp time.vvp
                   0 Start um 0
                 100 Nach 10 Zeiteinheiten

Eine Zeiteinheit entspricht jetzt 10ns, und entsprechend wartet die Simulation jetzt 100ns, bevor die zweite $display-Anweisung ausgeführt wird.

Die Präzision gibt nun an, mit welcher Genauigkeit die Verzögerungsoperation erfolgt. Bisher haben wir ganzzahlige Werte verwendet. Für die Verzögerungsoperation können aber auch reale Zahlen verwendet werden. Die Simulationszeit wird errechnet, indem der Wert des Verzögerungsoperators mit der Zeiteinheit multipliziert wird und dann, basierend auf der spezifizierten Präzision gerundet wird.

Zeiteinheit /
Präzision
Verzögerungswert Verzögerungszeit
10ns/1ns #3 30ns
10ns/1ns #3.345 33ns
10ns/100ps #3.345 33.5ns

Funktionsaufrufe mit function und task

Analog zu Funktionen und Prozeduren in der Software-Programmierung stellt Verilog function und task zur Verfügung, um komplexere Systeme in kleinere Einheiten aufzuteilen. Zu beachten ist, daß Verilog schon den module-Konstrukt für die Aufteilung hat. Aufrufe von function und task können im Rahmen von initial-, bzw. always-Blöcken in module-Blöcken eingesetzt werden.

Es gibt zwei wesentliche Unterschiede bei der Verwendung von function und task im Zusammenhang mit der Simulation:

function

  • Der Aufruf liefert einen Rückgabewert.
  • Es können keine Zeit- oder Ereignissteuerungen (#, @ und wait) eingesetzt werden.

task

  • Der Aufruf liefert keinen Rückgabewert.
  • Es können Zeit- oder Ereignissteuerungen eingesetzt werden.


Parallele Prozesse mit join/fork

Synthetisierbare Konstrukte

Kombinatorische Logik

Das folgende Beispiel zeigt einen 2:1-Multiplexer:

<verilog> module Mein2zu1Mux ( input [3:0] d0, d1,

                    input       sel,
                    output      y);
 assign y = sel ? d1 : d0;

endmodule </verilog>

Ein neuer Operator, der hier vorgestellt wird, ist der Entscheidungsoperator (?:), im Englischen als "conditional operator" bezeichnet. Für bedingte Zuweisungen kann er an Stelle von der if oder case Anweisung verwendet werden.

Dem Signal y wird in Abhängigkeit von sel entweder d1 oder d0 zugewiesen. Ist sel wahr, wird d1 zugewiesen, ist es falsch, wird d0 zugewiesen.

Natürlich kann der Operator auch verschachtelt werden, irgendwann ist das aber nicht mehr sinnvoll und wird unübersichtlich. Der Entscheidungsoperator kann dann durch eine case-Anweisung ersetzt werden.

<verilog> module mux ( input sel,

            input d0,
            input d1,
            input d2,
            input d3,
            output y);
 
 always @(*)
   case (sel)
     2'b00: y = d0;
     2'b01: y = d1;
     2'b10: y = d2;
     2'b11: y = d3;
   endcase

</verilog>

Getreu der Empfehlung für die Zuweisung kombinatorischer Logik wurde hier die blockierende Zuweisung verwendet.

In Bezug auf die Synthese ist zu beachten, dass alle Kombinationen von sel in der case-Anweisung enthalten sein müssen, sonst ist es möglich, daß durch die Synthese ein Latch entsteht. Näheres kann dazu im "Synthesis User's Guide" des jeweiligen FPGA-Herstellers gefunden werden.

Sequentielle Logik

Analog zum VHDL process wird in Verilog die always Anweisung verwendet. Ein einfaches Register synchron zur steigenden Flanke des Taktsignals wird wie folgt beschrieben:

<verilog> module Flop (input clk,

            input [3:0]      d,
            output reg [3:0] q);
 always @ (posedge clk)
   q <= d;

endmodule </verilog>

Analog zur steigenden Flanke kann auch auf die fallende Flanke mit negedge getriggert werden.

Open-Source-Tools

Simulation

Für Verilog gibt es die folgenden Simulatoren als Open-Source:

Synthese

Für ausgewählte FPGAs ist auch eine Synthese mit Open-Source-Tools möglich:

Im folgenden wird etwas näher auf die Nutzung mit Icarus Verilog eingegangen.

Icarus Verilog

Icarus Verilog ist eine Simulations- und Synthese-Software für Verilog. Bei der Software handelt es sich um kommandozeilenbasierte Applikationen. In diesem Abschnitt werden wir uns auf die Funktion als Simulator beschränken.

Die Funktion ist verteilt auf die zwei Applikationen:

  • iverilog (Compiler)
  • vvp (Simulator)

Basierend auf dem Beispiel von der Icarus Verilog Dokumentation, nehmen wir das "Hallo Welt"-Beispiel:

<verilog> module main;

 initial 
   begin
     $display("Hallo, Welt!");
     $finish;
   end

endmodule </verilog>

Zum Compilieren wird das Kommando iverilog verwendet. Mit dem "-o"-Parameter spezifiziert man den Namen der Ausgabedatei.

> iverilog -o main.vvp main.v

Der compilierte Quellcode wird dann mit der Applikation vvp simuliert.

> vvp main.vvp
Hallo Welt!

Für größere Projekte ist ein sinnvoller Einsatz den Aufruf durch ein "Makefile" auszuführen. Im Mai 2008 hat Larry Doolittle auf der "gEDA users mailing list" ein Makefile zur Verfügung gestellt, das hier folgend beschrieben wird.

Das Makefile besteht aus einem allgemeinen Teil, der sich nicht ändert, und einem projektspezifischen Teil, in dem die Abhängigkeiten für das spezielle Projekt festgelegt werden.

#
# Hier werden die Abhängigkeiten festgelegt
#
foo_tb: foo.v bar.v


#####################################################################
# Allgemeiner Teil, der sich nicht ändert
#
%_tb: %_tb.v
	iverilog -Wall -DSIMULATE ${VFLAGS_$@} $^ -o $@
#
# Generic regression test
%_check: %_tb testcode.awk
	vvp $<
#
%.vcd: %_tb
	vvp $<
#
# Useful for those testbenches that have a corresponding .sav file
%_view: %.vcd %.sav
	gtkwave $^
#
#
#
clean:
	rm -f *_tb *.vcd

Im obigen Beispiel ist eine Datei foo.v vorhanden, die ein Modul von bar.v instantiiert. Die Testbench ist in der Datei foo_tb.v.

Das Projekt wird compiliert und simuliert mit dem Kommando:

> make foo_check

Vorausgesetzt, daß gtkwave installiert ist, kann im Zusammenhang mit einer generierten foo.vcd-Dump-Datei und einer foo.sav-Datei dieses mit folgendem Kommando aufgerufen werden:

> make foo_view

Die foo.vcd-Dump-Datei erhält man durch folgenden Konstrukt in der Testbench:

<verilog>

initial begin
  $dumpfile("foo.vcd");
  $dumpvars (0, foo_tb);
end

</verilog>

Zu diesem Aufruf des Makefiles sei noch folgende Anmerkung gemacht. Der normale Aufruf von foo.vcd mit gtkwave erfolgt folgendermaßen:

> gtkwave foo.vcd

Das Kommando hätte keinen Vorteil gegenüber make foo_view. Mit gtkwave ist es nun möglich, eine sogenannte .sav-Datei zu erstellen, in der abgespeichert wird, welche Signale in der Anzeige gerade angezeigt werden. Startet man gtkwave mit dieser .sav-Datei bzw. lädt sie später nach, wird die Anzeige wieder so dargestellt wie zu dem Zeitpunkt, an dem die .sav-Datei erstellt wurde. Wird gtkwave nur mit einer .vcd-Datei gestartet, ist die Anzeige immer leer, und die Signale müssen erst in die Anzeige gebracht werden. Wenn eine .sav-Datei vorhanden ist, dann muss gtkwave wie folgt aufgerufen werden:

> gtkwave foo.vcd foo.sav

Behält man nun diese Datei als Teil des Projektes, ist es möglich, beim nächsten Mal mit make foo_view die gleiche Ansicht wieder zu erhalten.

Das ursprüngliche Makefile von Larry Doolittle hatte noch einen Test mit awk, der das Testergebnis an das Makefile zurückgibt. Den Test habe ich herausgenommen, da der Aufwand meines Erachtens größer als der Nutzen war.

Referenz

Bücher

  • "The Verilog Hardware Description Language", Thomas & Moorby, Kluwer Academic Publisher


Links