(:requiretuid:)
Hardware-Beschreibungssprachen(engl. Hardware Description Languages, HDL) erlauben die textuelle Beschreibung von Schaltungen.
Die Beschreibung kann auf verschiedenen Abstraktionsebenen erfolgen:
Entwurfswerkzeuge erzeugen aus der textuelle Beschreibung automatisch eine Schaltungsstruktur. Der Einsatz solcher Entwurfswerkzeuge wird als Computer-Aided Design (CAD) oder Electronic Design Automation (EDA) bezeichnet. Die Schaltungssynthese ist somit grob verlgeichbar mit der Übersetzung (Compilieren) von konventionellen Programmiersprachen.
Fast alle kommerziellen Hardware-Entwürfe werden mit HDLs realisiert. Dabei haben sich zwei HDLs durchgesetzt, die im Folgenden kurz vorgestellt werden.
VHDL steht für Very High-Speed Integrated Circuit Hardware Description Language.
Verilog ist weit verbreitet in zivilen US-Firmen. Wohingegen VHDL eher bei US-Rüstungsfirmen und vielen europäischen Firmen zum Einsatz kommt. Die Sprachen unterscheiden sich nur in der Syntax. Dabei ist die VHDL-Beschreibung in der Regel länger als die Verilog-Beschreibung. Die gezeigten Grundkonzepte sind in beiden Sprachen identisch. Moderne Entwurfswerkzeuge können im Normalfall beide Sprachen.
SystemVerilog ist im Rahmen der Veranstaltung sehr ähnlich zu Verilog. Teilweise ist SystemVerilog einfacher, da z.B. nur ein Datentyp logic
statt separaten wire
und reg
Typen verwendet wird. Teilweise aber auch aufwendiger, da getrennte Anweisungen für
always_ff
always_latch
always_comb
existieren. Diese werden in Verilog alle mit always
beschrieben.
Wichtig: Beim Verfassen von HDL-Beschreibungen ist es essentiell wichtig, immer die vom Programm beschriebene Hardware im Auge zu behalten!
In Verilog werden Schaltungen in Modulen beschrieben. Wir leiten ein Modul durch das Schlüsselwort module
ein. Danach folgt der Name des Moduls sowie die Liste der Ein- und Ausgänge des Moduls.
Im Beispiel oben trägt unser Modul den Namen example
. Die Eingänge sind a
,b
und c
. Der Ausgang ist y
. Mit endmodule
signalisieren wir das Ende der Beschreibung des Moduls. Die Tatsache, dass y
durch assign
als Wert eine boolesche Gleichung zugewiesen wird, lässt erkennen, dass es sich hierbei um eine Verhaltensbeschreibung handelt.
Die Simulation des Moduls example
ergibt Folgendes Signalverlaufsdiagramm (waves).
Die Synthese erzeugt den Schaltplan unten.
Verilog unterscheidet Groß- und Kleinschreibung, d.h. reset
und Reset
sind nicht das gleiche Signal.
Namen dürfen nicht mit Ziffern anfangen. Z.B. ist 2mux
ein ungültiger Name
Die Anzahl der Leerzeichen, Leerzeilen und Tabulatoren ist irrelevant.
Einzeilige Kommentare werden mit //
eingeleitet. Kommentare über mehrere Zeilen werden mit /*
eingeleitet und mit */
beendet.
Die Syntax von Verilog ist sehr ähnlich zu C und Java.
Bei struktureller Beschreibung verwenden wir eine Modulhierarchie, um unsere Schaltung aufzubauen. D.h. wir beschreiben Module, die Teilaufgaben unserer Schaltung übernehmen, und setzen aus diesen unsere Schaltung zusammen.
Nehmen wir als Beispiel an, dass wir ein NAND-Gatter mit drei Eingängen strukturell beschreiben wollen. Wir beginnen damit ein AND-Gatter mit drei Eingängen zu beschreiben.
Außerdem benötigen wir noch einen Inverter.
Aus diesen beiden Modulen können wir nun unser NAND-Gatter zusammenbauen.
Im Code unten werden Ein- und Ausgänge verwendet, die mehrere Bit breit sind; sog. Busse. Um ein Signal zu beschreiben, das mehrere Bit breit ist schreiben wir [msb:lsb]
vor den Variablennamen, d.h. [3:0] beschreibt ein Signal, das 4 Bit breit ist.
Die Operatoren &
, |
, ^
und ~
sind bitweise Operatoren. D.h. sie werden bitweise auf ihre Operanden angewendet. Nehmen wir z.B. an a = 0110 und b = 1010, so wäre y1 = a & b = 0010. Also nur die Bits, die in beiden Operanden 1 sind, werden im Ausgang auf 1 gesetzt.
Wir können, wie wir im Code oben sehen, mit Hilfe von Reduktionsoperatoren einen schon bekannten Operator(im Beispiel oben AND) auf alle Signale eines Busses anwenden. Es existieren noch weitere Reduktionsoperatoren:
Der Code oben würde folgende Schaltung erzeugen.
Obiger Code beschreibt einen Multiplexer mittels des ternären(verknüpft drei Operanden) Operators ? :. In diesem Fall kann man die bedingte Zuweisung folgendermaßen lesen:
Die Synthese erzeugt die Schaltung unten.
Im Beispiel oben wollen wir das Ergebnis von a ^ b
mit cin
kombinieren. Um das Signal weiterzuleiten, verwenden wir einen internen Verbindungsknoten wire p
.
Aus der Tabelle oben lässt sie die Präzedenz der Operatoren ablesen. Sie gibt an, in welcher Reihenfolge die Operatoren ausgewertet werden. Z.B. wird ~ (NOT) immer vor & (AND) ausgewertet. Man sagt auch: "~ bindet stärker als &".
Um in Verilog Zahlen zu schreiben, verwenden wir folgende Syntax:
N'Bwert
Dabei gilt: N = Breite in Bits, B = Basis
N'B ist optional, sollte der Konsistenz halber aber immer geschrieben werden. Wird es weggelassen, wird der Wert im Dezimalsystem interpretiert.
Mittels { und } können wir verschiedene Signale zu einem Bus kombinieren. Unterstriche (_) in numerischen Konstanten dienen nur der besseren Lesbarkeit und werden von Verilog ignoriert.
Im Beispiel oben verwenden wir zwei 4 Bit Multiplexer mit 2 Eingängen, um daraus einen 8 Bit Multiplexer mit 2 Eingängen zu bauen. Dabei ist der lsbmux
für die unteren 4 Bit und der msbmux
für die oberen 4 Bit zuständig.
Wir wissen, dass der Ausgang eines Tristate-Buffers, wenn der Eingang enable
nicht gesetzt ist, einen hochohmigen Wert annimmt.
Dieses Verhalten erreichen wir durch 4'bz
in der bedingten Zuweisung an y
. Die Zahl hängt natürlich von der Breite des Signals ab.
Die Synthese erzeugt Folgendes.
Für das Schreiben von Testbenches, die zu Simulationszwecken gebraucht werden, ist es oft hilfreich z.B. Zuweisungen, um eine gweissen Zeit zu verzögern.
Durch #n
erreichen wir eine Verzögerung von n
Zeiteinheiten. Die Größe der Zeiteinheiten wird mittels der timescale Direktive festgelegt.
Obiger Code erzeugt das Wellendiagramm unten.
Achtung! Nur für Simulation verwenden. #n werden für die Synthese ignoriert!
Die Beschreibung von sequentiellen Schaltungen basiert auf der Verwendung fester "Redewendungen" (Idiome).
Es gibt feststehende Idiome für:
Beim Abweichen von Idiomen ist Vorsicht geboten. Die Beschreibung wird möglicherweise richtig simuliert, könnte aber fehlerhaft synthetisiert werden. Es ist daher sinnvoll sich an die Konventionen zu halten.
always
-AnweisungenWenn sich in der sensitivity list
aufgezählte Werte ändern, wird die Anweisung statement
ausgeführt.
Werte sind in der Regel Signale. Dies wird manchmal jedoch noch erweitert.
Obiger Code beschreibt ein D Flip-Flop. Das posedge
vor clk
in der sensitivity list des always
-Blocks bedeutet, dass der Block immer zu einer positiven Taktflanke von clk
ausgeführt wird. Des Weiteren verwenden wir die nicht-blockende Zusweisung <=
in always
-Blöcken. Außerdem muss jedes Signal, an das innerhalb einer always
-Anweisung zugewiesen wird, als reg
deklariert sein (im Beispiel: q
).
Wichtig: Ein als reg deklariertes Signal wird bei der Synthese nicht zwangsläufig in ein Hardware-Register abgebildet!
Rücksetzbare D Flip-Flops können einen synchronen oder einen asynchronen Reset haben. Ein synchroner Reset kann nur zur steigenden Taktflanke von clk stattfinden. Ein asynchroner auch zu anderen Zeitpunkten.
Dadurch, dass der always
-Block nur zur steigenden Taktflanke von clk
ausgeführt wird, beschreibt obiger Code ein D Flip-Flop mit synchronem Reset.
Durch Aufnahme von reset
in die sensitivity list des always
-Blocks wird dieser auch zu steigenden Taktflanke von reset
ausgeführt und Reset kann auch außerhalb von steigenden Taktflanken von clk
stattfinden.
Im obigen Code wird der Wert d
nur dann nach q
übernommen, wenn en
= 1 ist. Er beschreibt also ein Flip-Flop mit asynchronem Reset und Taktfreigabe (engl. clock enable).
Es fällt auf, dass in der sensitivity list des always
-Blocks kein posedge
steht. Dadurch wird der always
-Block pegelgesteuert und nicht flankengesteuert, wie bei einem Flip-Flop.
Achtung: In TGdI werden Latches nur selten (wenn überhaupt) gebraucht. Sollten sie dennoch in einem Syntheseergebnis auftauchen, ist das in der Regel auf Fehler in der HDL-Beschreibung zurückzuführen! Zum Beispiel durch das Abweichen von Idiomen.
Diese Anweisungen dürfen nur innerhalb von always
-Anweisungen benutzt werden:
Erinnerung: Alle Zuweisungsziele einer always
-Anweisung müssen als reg deklariert werden! Selbst, wenn sie keine echten Hardware-Register beschreiben.
always
-BlockWir können always
-Anweisungen auch für kombinatorische Logik nutzen. Wir schreiben ein *
in die sensitivity list. Dies bewirkt, dass der always
-Block bei jeder Änderung eines Signals, das innerhalb des always
-Blocks in einer Zuweisung verwendet wird, ausgeführt wird.
Die Beschreibung oben hätte durch fünf assign
-Anweisungen einfacher beschrieben werden können.
case
Die case
-Anweisung enthält in den runden Klammern den Namen eines Signals. Danach wird vor jedem Doppelpunkt der Wert des Signals angegeben, für den der Code nach dem Doppelpunkt ausgeführt werden soll. Zum Beispiel: Ist data
= 7, so wird segments = 7'b111_0000;
ausgeführt.
Der Code oben ist so einfach nicht als assign
formulierbar.
Um kombinatorische Logik zu beschreiben, muss ein case
-Block alle Möglichkeiten abdecken. Dazu können wir diese entweder alle explizit angeben oder einen default
-Fall am Ende festlegen, der ausgeführt wird, falls keiner der vorangehenden Fälle eingetreten ist. Im Beispiel haben wir uns für den default
entschieden.
casez
Die casez
-Anweisung funkioniert sehr ähnlich wie die case
-Anweisung. Bei casez
ist es jedoch möglich mittels ?
don't cares in den Fällen anzugeben. Zum Beispiel: Wenn das msb des Signals a
= 1 ist, wird unabhängig von der Belegung der übrigen Bits der erste Fall ausgeführt.
<=
steht für eine nicht-blockende Zuweisung.
Eine nicht-blockende Zuweisung wird parallel mit allen anderen nicht-blockenden Zuweisungen ausgeführt:
=
steht für eine blockende Zuweisung.
Eine blockende Zuweisung wird hintereinander (seriell) in Reihenfolge im Programmtext ausgeführt. Solange eine blockende Zuweisung abläuft, werden andere Anweisungen blockiert. Jede Anweisung berechnet für sich die "rechte Seite" und weist an die "linke Seite" zu.
Um synchrone sequentielle Logik zu beschreiben, verwenden wir immer nicht-blockende Zuweisungen
Um einfache kombinatorische Logik zu beschreiben, verwenden wir immer ständige Zuweisungen (continuous assignment)
Um komplexere kombinatorische Logik (always@(*)
) zu beschreiben, verwenden wir immer blockende Zuweisungen.
An ein Signal sollte nicht
always
-Blöcken zugewiesen werden
always
-Block gemischt mit =
und <=
zugewiesen werden
Ein endlicher Zustandsautomat lässt sich in drei Blöcke unterteilen:
Wir betrachten als Beispiel einen FSM zum Dritteln der Taktfrequenz. Dieser erhält als Eingabe explizit kein Signal, implizit jedoch den Schaltungstakt mit Frequenz f. Die Ausgabe ist das Signal q
mit Frequenz f/3.
Um diesen FSM in Verilog zu beschreiben Kodieren wir zunächst die Zustände binär. Danach folgt unser Zustandsregister, das den aktuellen Zustand des Automaten speichert. Nun legen wir die Zustandsübergangslogik fest. Diese beschreibt, wann aus einem bestimmten Zustand in einen anderen gewechselt wird. Zuletzt folgt die Beschreibung der Ausgangslogik.
Parametrisierte Module sind Module deren genaue Beschreibung von einem oder mehreren Parametern abhängt. Sie erlauben es ein Modul mit verschiedenen Parametern zu instanziieren und dadurch an einen bestimmten Anwendungsfall anzupassen. Häufig wird die Busbreite parametrisiert.
Als Beispiel hier ein parametrisierter 2:1 Multiplexer.
Wir können das Modul mux2
nun verschieden instanziieren. Zunächst als Instanz mit 8-bit Busbreite (verwendet Standardwert).
Um mux2
mit 12-bit Busbreite zu instanziieren, schreiben wir #(12)
zwischen den Modultyp und den Namen der Instanz.
Zur besseren Lesbarkeit, vorallem wenn mehrere Parameter auftreten, ist es sinnvoll den Namen des Parameters explizit anzugeben.
Ein Testrahmen (engl. test bench) ist ein HDL-Programm zum Testen eines anderen HDL-Moduls. Im Hardware-Entwurf ist dies schon lange üblich und findet seit einigen Jahren auch im Software-Bereich Anwendung (z.B. JUnit). Das getestete Modul wird dabei als device under test (DUT) oder unit under test (UUT) bezeichnet. Der Testrahmen wird nicht synthetisiert, sondern nur für die Simulation benutzt.
Es gibt verschiedene Arten von Testrahmen:
Als Beispiel wollen wir nun einen Testrahmen für die folgende Funktion schreiben.
Wir beschreiben zunächst mit Verilog Hardware, die diese Funktion berechnet:
Danach erstellen wir einen einfachen Testrahmen, der unser Modul mit verschiedenen Eingangswerten testet.
Wir legen zuerst reg
's für die Eingangswerte des DUT an und ein wire
für den Ausgangswert. Danach Instanziieren wir das zu testende Modul. Innerhalb einer initial
-Anweisung belegen wir die Eingänge das Moduls nun mit verschiedenen Werten und warten danach immer etwas, damit die Änderungen propagieren können.
Um unser Modul mit einem selbstprüfenden Testrahmen zu testen, müssen wir nach jeder Belegung der Eingänge und einer angemessenen Wartezeit mittels if
prüfen, ob der erwartete Wert ausgegeben wurde und lassen uns, falls nicht, eine entsprechende Meldung anzeigen.
Um einen selbstprüfenden Testrahmen mit Testvektoren zu schreiben, ist zunächst wichtig das HDL-Programm und die Testdaten zu trennen. Wir organisieren die Eingaben und erwarteten Ausgaben als Vektor von zusammenhängenden Signalen/Werten. Diese Vektoren speichern wir in einer eigenen Datei.
Dann verfassen wir ein HDL-Programm für einen universellen Testrahmen:
Der im Testrahmen erzeugte Takt legt den zeitlichen Ablauf fest:
Der Takt kann auch als Takt für sequentielle synchrone Schaltungen verwendet werden.
Die Beispieldatei example.tv
enthält Testvektoren in einem einfachen Textformat:
Dabei enhält jede Zeile zunächst die Eingangsdaten. Nach dem "_" folgen die jeweils erwarteten Ausgangsdaten.
Im Folgenden wenden wir oben vorgestelltes Vorgehen auf unser Beispielmodul sillyfunction
an.
Hinweis: Falls hexadezimale Testvektoren verwendet werden sollen, statt $readmemb
den Aufruf $readmemh
verwenden.
a
, b
und c
sind Eingänge des DUT
yexpected
ist eine Hilfsvariable, die nun den erwarteten Ausgangswert dieses Vektors enthält.
Hinweis: Um Werte hexadezimal auszugeben, verwenden wir die Formatkennung %h
. Zum Beispiel:
Hinweis: Zum Vergleichen auf X und Z müssen die Operatoren ===
und !==
benutzt werden.