(:requiretuid:)

Einführung in TGDI – Kapitel 4

Autoren: Anna Wenzelburger, Moritz Fischer, Patrick Meyn

Hardware-Beschreibungssprachen

Hardware-Beschreibungssprachen(engl. Hardware Description Languages, HDL) erlauben die textuelle Beschreibung von Schaltungen.

Die Beschreibung kann auf verschiedenen Abstraktionsebenen erfolgen:

  • Struktur: Wir beschreiben den strukturellen Aufbau der Schaltung (z.B. Verbindungen zwischen Gattern); Wie ist die Schaltung aus Untermodulen aufgebaut?
  • Verhalten: Wir beschreiben, wie sich die Schaltung verhalten soll(z.B. Boole'sche Gleichungen); Was tut die Schaltung?

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.

Verilog

  • 1984 von der Fa. Gateway Design Automation entwickelt
  • Seit 1995 ein IEEE Standard (1364)
  • Überarbeitet 2001 und 2005
  • Neuer Dialekt SystemVerilog (Obermenge von Verilog-2005)

VHDL

VHDL steht für Very High-Speed Integrated Circuit Hardware Description Language.

  • Entwickelt 1981 durch das US Verteidigungsministerium
  • Inspiriert durch konventionelle Programmiersprache Ada
  • Standardisiert in 1987 durch IEEE (1076)
  • Überarbeitet in 1993, 2000, 2002, 2006, 2008

Vergleich Verilog und VHDL

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

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

  • Flip-Flops: always_ff
  • Latches: always_latch
  • Kombinatorische Logik: always_comb

existieren. Diese werden in Verilog alle mit always beschrieben.

Von einer HDL zu Logikgattern

Simulation

  • Eingangswerte werden in HDL-Beschreibung eingegeben
  • Beschriebene Schaltung wird stimuliert
  • Berechnete Ausgangswerte werden auf Korrektheit geprüft
  • Fehlersuche viel einfacher und billiger als in realer Hardware

Synthese

  • Übersetzt HDL-Beschreibungen in Netzlisten
  • Logikgatter (Schaltungselemente)
  • Verbindungen (Verbindungsknoten)

Wichtig: Beim Verfassen von HDL-Beschreibungen ist es essentiell wichtig, immer die vom Programm beschriebene Hardware im Auge zu behalten!

Verilog-Module

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 Syntax

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.

Strukturelle Beschreibung

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.

Bitweise Verknüpfungsoperatoren

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.

Reduktionsoperatoren

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:

  • | für OR
  • ^ für XOR
  • ~& für NAND
  • ~| für NOR

Der Code oben würde folgende Schaltung erzeugen.

Bedingte Zuweisung

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:

  • Ist s = 1?
  • ...wenn ja, weise y den Wert von d1 zu
  • ...wenn nein, weise y den Wert von d0 zu

Die Synthese erzeugt die Schaltung unten.

Interne Verbindungsknoten oder Signale

Im Beispiel oben wollen wir das Ergebnis von a ^ b mit cin kombinieren. Um das Signal weiterzuleiten, verwenden wir einen internen Verbindungsknoten wire p.

Bindung von Operatoren(Präzedenz)

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 &".

Zahlen

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.

Operationen auf Bit-Ebene

Beispiel 1

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.

Beispiel 2

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.

Hochohmiger Ausgang: Z

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.

Verzögerungen: # Zeiteinheiten

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!

Sequentielle Schaltungen

Die Beschreibung von sequentiellen Schaltungen basiert auf der Verwendung fester "Redewendungen" (Idiome).

Es gibt feststehende Idiome für:

  • Latches
  • Flip-Flops
  • Endliche Zustandsautomaten (FSM)

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

Allgemeiner Aufbau

Interpretation

Wenn 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.

D Flip-Flops

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ücksetzbares D Flip-Flop

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.

Synchroner Reset

Dadurch, dass der always-Block nur zur steigenden Taktflanke von clk ausgeführt wird, beschreibt obiger Code ein D Flip-Flop mit synchronem Reset.

Asynchroner 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.

Rücksetzbares D Flip-Flop mit Taktfreigabe

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).

Latch

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.

Weitere Anweisungen zur Verhaltensbeschreibung

Diese Anweisungen dürfen nur innerhalb von always-Anweisungen benutzt werden:

  • if/else
  • case, casez

Erinnerung: Alle Zuweisungsziele einer always-Anweisung müssen als reg deklariert werden! Selbst, wenn sie keine echten Hardware-Register beschreiben.

Kombinatorische Logik als always-Block

Wir 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.

Kombinatorische Logik mit 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.

Kombinatorische Logik mit 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.

Nicht-blockende Zuweisung

<= steht für eine nicht-blockende Zuweisung.

Eine nicht-blockende Zuweisung wird parallel mit allen anderen nicht-blockenden Zuweisungen ausgeführt:

  1. Schritt: Alle "rechten Seiten" werden berechnet
  2. Schritt: Am Ende des Blocks werden alle Berechnungsergebnisse an "linke Seiten" zugewiesen

Blockende Zuweisung

= 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.

Regeln für die Zuweisung von Signalen

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)

Attach:regelStändige.png Δ

Um komplexere kombinatorische Logik (always@(*)) zu beschreiben, verwenden wir immer blockende Zuweisungen.

An ein Signal sollte nicht

  • ... in mehreren always-Blöcken zugewiesen werden
  • ... in einem always-Block gemischt mit = und <= zugewiesen werden

Endliche Zustandsautomaten (FSM)

Ein endlicher Zustandsautomat lässt sich in drei Blöcke unterteilen:

  • Zustandsübergangslogik (next state logic)
  • Zustandsregister (state register)
  • Ausgangslogik (output logic)

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

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.

Testrahmen

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:

  • Einfach: Legt nur feste Testdaten an und zeigt Ausgaben an
  • Selbstprüfend: Prüft auch noch, ob Ausgaben den Erwartungen entsprechen
  • Selbstprüfend mit Testvektoren: Auch noch mit variablen Testdaten

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.

Selbstprüfender Testrahmen mit Testvektoren

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:

  1. Erzeuge Takt zum Anlegen von Eingabedaten/Auswerten von Ausgabedaten
  2. Lese Vektordatei in Verilog Array
  3. Lege Eingangsdaten an
  4. Warte auf Ausgabedaten und werte Ausgabedaten aus
  5. Verlgeiche tatsächliche mit erwarteten Ausgabedaten und melde Fehler bei Differenz
  6. Noch weitere Testvektoren abzuarbeiten?

Der im Testrahmen erzeugte Takt legt den zeitlichen Ablauf fest:

  • Steigende Flanke: Eingabewerte aus Testvektor an Eingänge anlegen
  • Fallende Flanke: Aktuelle Werte an Ausgängen lesen

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.

1. Erzeuge Takt

2. Lese Testvektordatei in Array ein

Hinweis: Falls hexadezimale Testvektoren verwendet werden sollen, statt $readmemb den Aufruf $readmemh verwenden.

3. Lege Testdaten an Eingänge an

a, b und c sind Eingänge des DUT

yexpected ist eine Hilfsvariable, die nun den erwarteten Ausgangswert dieses Vektors enthält.

4. Warte auf Ausgabedaten, lese Ausgabedaten

5. Verlgeiche aktuelle Ausgabedaten mit erwarteten Werten

Hinweis: Um Werte hexadezimal auszugeben, verwenden wir die Formatkennung %h. Zum Beispiel:

6. Sind noch weitere Testvektoren abzuarbeiten?

Hinweis: Zum Vergleichen auf X und Z müssen die Operatoren === und !== benutzt werden.

  

zum Seitenanfang