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 den 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 teilt man mit Hilfe von Funktionsaufrufen große Probleme in viele kleine Probleme. 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. Behavioral 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 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 Kommentar 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

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 kennen gelernt. 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 Klammern, 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 kennen lernen. Diese Blöcke erlauben es, parallel ablaufende Prozesse zu beschreiben und mit Hilfe der Ereignissteuerung deren Abläufe zu kontrollieren.

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 weiter 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 selber 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 Toggeln nur von 0 auf 1, bzw. 1 auf 0 funktioniert. Wird sie weggelassen ist clk undefiniert, also x, und das Toggeln würde nicht funktionieren.

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

Im vorherigen Abschnitt haben wir die Ereignissteuerung kennen gelernt. 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 jedes Mal 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. So viel sei vorweg genommen, diesen Konstrukt nennt man kombinatorische Logik und in dem Abschnitt über synthetisierbare Logik wird noch mal 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ähre 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 zwei mal 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 mal 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. Für Klarheit 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.

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 2's Komplement zahlen rechnen zu können wird der 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 das 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, also einer ist unsigned der andere 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 werden größer oder 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 durch zu fü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 verschiedene Blöcke abzuarbeiten. Der von Programmiersprachen bekannte if und else Konstrukt ist hierzu hilfreich und wird von Verilog unterstütz.

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 weiterer 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ück gegeben, andernfalls wird Wert_FALSCH zurück gegeben.

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 zu zu weisen.

<verilog>

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

</verilog>

Hierarchie

Um ein komplexeres Design zu kreieren 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 [DW-1:0] w1;
 wire [DW-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 zu Grunde 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 Kompilieren 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 zum 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 werden muss.

<verilog>

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

</verilog>

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

  • %d - dezimal
  • %h - hexadezimal
  • %b - binär
  • %o - oktal
  • %s - string
  • %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 Datentyp 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, damit 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 zum stdout. Einziger Unterschied ist, dass der Task automatisch überprüft ob eines der Signal sich geändert hat und jedes mal 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:

<c> 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

  1. 47 Fertig

</c>

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 der reset 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ück gesetzt. Das Setzen fand um 15 statt, plus 22 Zeiteinheiten, somit wird reset um 37 zurück gesetzt. Dann folgt noch mal ein clk Wechsel, bevor die Simulation um 47 beendet wird. Zu der Zeit findet kein Signalwechsel mehr statt, so das der $monitor Task nicht mehr aufgerufen wird. So haben wir vor dem $finish Task noch mal 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

Ein Value Change Dump (VCD) Files ist ein text basiertes File in dem 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 ein VCD File 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 des VCD Files spezifiziert. Gerade bei der Simulation von großen Designs ist es sinnvoll die Anzahl der Signale die in das Dumpfile 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 Dumpfile geschrieben. Sollen z. B. nur die Signale in top_tb in das Dumpfile 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 erst mal 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 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:

<c> >iverilog -o time.vvp time.v >vvp time.vvp

                  0 Start um 0
                 10 Nach 10 Zeiteinheiten

</c>

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:

<c> >iverilog -o time.vvp time.v >vvp time.vvp

                  0 Start um 0
                100 Nach 10 Zeiteinheiten

</c>

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 kleiner Einheiten aufzuteilen. Zu beachten ist das 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 macht das aber keine Sinn mehr und wird unübersichtlich. Der Entscheidungsoperator kann dann durch ein 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 das 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 Simulatoren

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

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 eine Kommandozeilen-basierte 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 Kompilieren wird das Kommando iverilog verwendet. Mit dem -o Parameter spezifiziert man den Namen der Ausgabedatei.

<c> > iverilog -o main.vvp main.v </c>

Der kompilierte Quellcode wird dann mit der Applikation vvp simuliert.

<c> > vvp main.vvp Hallo Welt </c>

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 werden soll.

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.

<c>

  1. Hier werden die Abhängigkeiten festgelegt

foo_tb: foo.v bar.v


  1. Allgemeiner Teil der sich nicht ändert

%_tb: %_tb.v iverilog -Wall -DSIMULATE ${VFLAGS_$@} $^ -o $@

  1. Generic regression test

%_check: %_tb testcode.awk vvp $<

%.vcd: %_tb vvp $<

  1. Useful for those testbenches that have a corresponding .sav file

%_view: %.vcd %.sav gtkwave $^

clean: rm -f *_tb *.vcd </c>

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:

<c> > make foo_check </c>

Vorausgesetzt das gtkwave installiert ist, kann im Zusammenhang mit einem generierten foo.vcd-Dumpfile und einem foo.sav-File dieses mit folgendem Kommando aufgerufen werden:

<c> > make foo_view </c>

Das foo.vcd-Dumpfile 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:

<c> > gtkwave foo.vcd </c>

Das Kommando hätte keinen Vorteil gegenüber make foo_view. Mit gtkwave ist es nun möglich ein sogenanntes .sav-File zu kreieren, in dem abgespeichert wird, welche Signale in der Anzeige gerade angezeigt werden. Startet man gtkwave mit diesem .sav-File bzw. lädt es später nach, wird die Anzeige wieder so dargestellt wie zu dem Zeitpunkt wenn das .sav-File erstellt wurde. Wird gtkwave nur mit einem .vcd-File 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:

<c> > gtkwave foo.vcd foo.sav </c>

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ück gibt. 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