courses:system_design:vhdl_language_and_syntax:notizen

Deutsche Notizen

VHDL ist allgemein unabhängig von Klein- oder Grossschreibung. Dies kann man ausnützen, in dem man hauseigene Formatierungsrichtlinien festlegt. Z.B. könnte man alle VHDL Schlüssewörter klein schreiben und alle selbstgewählten Bezeichner groß, wie dies auch im folgenden der Fall sein wird.

Anweisungen werden in VHDL mit einem Semikolon (;) beendet. D.h. man kann beliebig viel Zeilenumbrüche, Kommentare oder andere Konstrukte einfügen.

Listen werden mit einem Komma getrennt. Signalzuweisungen erfolgen mit dem zusammengesetzten Zuweisungsoperator (⇐).

Selbstgewählte Bezeichner dürfen nach dem VHDL 87 Standard nur aus Buchstaben, Ziffern und Unterstrichen bestehen und außerdem mit einem buchstaben beginnen. Der VHDL93 Standard läßt hier mehr Freiheiten, wie dies in der nächsten Folie gezeigt wird.

Einfache Bezeichner, wie sie in VHDL'87 definiert sind, dürfen Buchstaben, Ziffern und den Unterstrich beinhalten. Z.B. ist 'my_Signal_23' ein gültiger Bezeichner. Desweiteren unterscheidet man in VHDL nicht zwischen Groß- und Kleinschreibung, das heißt 'rdy', 'RDY' und 'Rdy' sind gleiche Bezeichner. Speziell hat ein einfacher Bezeichner mit einem Buchstaben zu beginnen. '24th_signal' ist somit kein gültiger Bezeichner. Desweiteren sind Sonderzeichen, Leerzeichen, aufeinanderfolgende Unterstriche und VHDL Schlüßelwörter nicht als einfache Bezeichner verwendbar.

In VHDL'93 wurden zusätzlich sogenannte extended identifiers, also erweiterte Bezeichner definiert. Diese werden mit Schrägstrichen gekennzeichnet. Nun können auch Sonderzeichen, Leerzeichen, aufeinanderfolgende Unterstriche und VHDL Schlüßelwörter verwendet werden. Man muß nun allerdings beachten, das bei extended identifiers zwischen Groß- und Kleinschreibung unterschieden wird. '/rdy/', '/RDY/' und '/Rdy/' sind somit unterschiedliche Bezeichner.

In diesem Tutorial werden VHDL Schlüßelwörter in Kleinbuchstaben und selbstgewählte Bezeichner in Großbuchstaben geschrieben. Hervorhebungen werden durch Fettdruck gekennzeichnet.

Es werde einige selbsterklärende Icons verwendet. Diese kennzeichnen spezielle Gesichtspunkte bezüglich der VHDL'93 Syntax (verglichen zu der von VHDL'87), Synthese, Kodierungsstil und spezielle Tips.

Die Tabelle zeigt die erlaubten Deklarationsbereiche verschiedener Objekte:

  • Ein Unterprogramm ist eine Funktion in C ähnlich und kann in VHDL mehrfach aufgerufen werden. Ein Unterprogramm kann im deklarativen Bereich einer Entity, Architektur, Prozeß oder auch jedem anderen Unterprogramm und in Packages. Da ein Unterprogramm an mehreren Stellen benutzt werden soll (Architekturen), ist es sinnvoll, diese immer in einem Package zu deklarieren.
  • Komponenten benötigt man zum Einbinden von Entity/Architektur Paaren in einer Architektur der nächst höheren Ebene. Diese Komponenten können nur im Deklarationsbereich einer Architektur oder in einem Package deklariert werden. Letzteres ist sinnvoll, falls ein Entity/Architektur Paar öfters verwendet wird, da hier dann nur eine Deklaration notwendig ist.
  • Konfigurationen sind eigene Design Units. Es ist aber möglich Konfigurationsanweisungen im deklarativen Bereich einer Architektur zu schreiben. Allerdings wird von dieser Möglichkeit selten Gebrauch gemacht, da es sich allgemein empfiehlt eine eigenständige Konfiguration für den gesamten Entwurf zu erstellen.
  • Konstanten und Datentypen können in allen verfügbaren Objekten deklariert werden.
  • Port Deklarationen sind nur in Entities erlaubt. Hier werden alle Signale aufgelistet, die nach außen sichtbar und verfügbar sein sollen. Zusätzlicher interne Architekursignale werden in dieser selbst deklariert oder aber in Prozessen, Unterprogrammen und Packages. Bitte beachten Sie, daß Signale nicht in Funktionen, ein bestimmter Typ von Unterprogrammen, deklariert werden können.
  • Variablen können generell nur in Prozessen und Unterprogrammen deklariert werden. In VHDL'93 gibt es auch globale Variablen, welche in Entities, Architekturen und Packages deklariert werden können.

Auf den folgenden Seiten wird schrittweise ein Volladdierer bestehend aus zwei Halbaddierern und einem Oder-Gatter rein strukturell aufgebaut, d.h. wir befinden uns eigentlich schon auf der Gatterebene und brauchen keinerlei Synthesewerkzeuge. Dieses Beispiel verdeutlich aber sehr schön das Zusammenspiel der verschiedenen VHDL-Objekte.

In der Entity Deklaration wird die Schnittstelle des Moduls mit der Außenwelt bzw. mit anderen Modulen beschrieben und die Entity mit einem Namen versehen. Der benutzerdefineirte Name dieser Entity ist HALFADDER.

Mittels der Port Anweisung werden die Eingangs- bzw. Ausgangssignale und deren Datentypen festgelegt. Dabei wird zuerst der Port-Name (oder mehrere Port Signal Namen durch Komma getrennt) angegeben, nach einem Doppelpunkt folgen der Port-Mode (in - kann nur gelesen werden; out - kann nur beschrieben werden; inout - kann gelesen und beschrieben werden; buffer - wie out, kann aber auch gelesen werden) und der Datentyp des Port Signals (der Datentyp bit kann nur die Werte `0` und `1` annehmen).

Der Datentyp eines Signals gibt immer die Zustände wieder, welche das Signal während der Simulation einnehmen kann.

Im neuen VHDL`93 Standard kann optional zur Verdeutlichung das Schlüsselwort “entity” nach dem Schlüsselwort “end” eingefügt werden.

Die Architecture enthält die Implementierung für eine Entity, d.h. eine Verhaltensbeschreibung (Behavioral-Level), eine blockorientierte, synthesegerechte Beschreibung (RT-Level) oder gleich eine strukturelle Netzliste (Gate-Level).

Eine Architecture gehört immer zu einer bestimmten Entity, eine Entity kann aber mehrere Architectures besitzen, die dann unterschiedliche Namen benötigen (z.B. verschiedene Implementationen des selben Algorithmus oder auf unterschiedlichen Ebenen modelliert).

Innerhalb der Architecture werden die nebenläufigen Prozesse beschrieben oder aber, wie in diesem Beispiel nebenläufige Signalzuweisungen (innerhalb der Architecure ist alles nebenläufig).

Eine Signalzuweisung wird durch das Zeichen “⇐” dargestellt. Auf der linken Seite der Zuweisung ist das Zielsignal, auf der rechten Seite steht ein auszuwertender Ausdruck. Die Datentypen auf linker und rechter Seite müssen gleich sein!

Die Namensgebung dieser Architecure deutet auf den RT-Level hin, aber bei dieser einfachen Funktion ist der Unterschied zur reinen Verhaltensebene (algorithmische Ebene) gering oder nicht vorhanden. Im weiteren Verlauf dieses Tutorials werden wir immer zwischen den einzelnen Abstraktionsebenen unterscheiden und die jeweiligen Architectures angelehnt an eine der drei Abstraktionsebenen benennen (BEHAVE oder TEST, RTL, STRUCT oder GATE).

In VHDL`93 ist es wieder optional möglich das Schlüsselwort “architecture” nach dem “end” einzufügen.

Jede Architektur ist in einen Definitionsbereich und einen optionalen Deklarationsbereich aufgeteilt.

Der deklarative Bereich befindet sich zwischen den Schlüßelwörtern ' is ' und ' begin '. Neue Objekte, die nur innerhalb der Architektur benötigt werden (Knostanten, Daten Typen, Signale, Unterprogramme, etc.), können hier deklariert werden.

Der Definitionsbereich folgt nach dem Schlüßelwort ' begin ' und beinhaltet nebenläufige Anweisungen. Das können einfache Signalzuweisungen, Prozeßanweisungen, welche sequentielle Anweisungen zusammenfassen, und Komponenteninstantitierungen sein. Nebenläufig meint hierbei, daß die Reihenfolge der Anweisungen innerhalb der Architektur keine Rolle spielen. Zum Beispiel bekommt das Signal SUM immer den Wert von (3 + 7), unabhängig von der Reihenfolge der Zuweisungen an die Signal DIGIT_A, DIGIT_B und SUM.

Signalzuweisungen werden durch den Operator '⇐' gekennzeichnet. Dabei symbolisiert dieser Operator den Signalfluß, d.h. das Ziel, dessen Wert aktualisiert werden soll, steht auf der linken Seite. Auf der rechten Seite steht ein Ausdruck, nach dem der den neuen Wert berechnet wird. Die Datentypen der linken und rechten Seite müssen identisch sein. Zu beachten ist, daß die hier verwendeten Signal implizit in der Entitydeklaration deklariert werden.

Der mode eines Entity-Ports gibt die Datenflußrichtung vor. Der Port mode 'in' wird für rein lesbare Signale verwendet. Signale deren Ports diesen mode habe, können in der untergeordneten Architektur keine Werte zugewiesen werden.

Entsprechend bezeichnet der mode 'out' Signale, denen ein Wert zugewiesen werden kann, aber die nicht gelesen werden können. Wenn der aktuelle Wert eines Signals in der gleichen Architektur aktualisiert und gelesen werden, z.B. um eine Zähler zu implementieren, sollte es als Zwischensignal innerhalb der Archtitektur deklariert werden. Mit Zwischensignalen ist keine Signalflußrichtung assoziiert.

Alternativ kann man den Port mode 'buffer' vergeben. Somit müsste man kein Zwischensignal verwenden, da Ports mit diesem mode intern zurückgelesen werden können.

Um Busse zu modellieren, bei dem mehrere Module Zugriff auf die gleichen Datenleitungen haben, benötigt man den Port mode 'out', wenn das Modul nur treibend zugreifen soll, und 'inout', wenn es treibend und lesend zugreifen soll.

Man muß beachten, daß die Port modes übereinstimmen müssen, wenn ein Ausgangsport direkt mit einem Ausgangsport der nächsthöheren Ebene verbunden werden soll. Notfalls müssen Zwischensignale eingefügt werden, um den mode wechseln zu können.

VHDL erlaubt einen hierarchischen Modellaufbau, d.h. ein Modul kann aus mehreren Submodulen aufgebaut werden. Diese Submodule werden in einem Top-Modul verbunden. Im Bild wurde ein Volladdierer aus zwei Halbaddierern und einem Odergatter aufgebaut.

Die Entity unseres Volladdierers läßt sich sofort aus dem Blockschaltbild ableiten. Wir benötigen die Eingänge A und B, sowie einen Carry Eingang. Als Ausgänge werden die Summe und der Carry Ausgang benötigt.

Da der Volladdierer aus mehreren Submodulen besteht, müssen diese und deren Verwendung “bekanntgemacht” werden. Zuerst müssen alle Typen der zu verwendenden Module deklariert werden. Dies erfolgt in einer sog. Component-Declaration. An anderer Stelle wird dann deklariert, wieviele dieser Module in dieser Architecture verwendet werden.

Die Component-Declaration ist auch mit einer Definition des Sockels vergleichbar, der dann ein oder mehrmals verwendet und in den dann später die entsprechende Entity eingesetzt wird.

Die Port-Listen Elemente der Component sind sogenannte lokale Elemente, d.h. es handelt sich nicht um SIgnale! Diese Bestimmung des Sockeltyps erfolgt also in der Component-Declaration, welche vor dem Schlüsselwort `begin` der Architecture stehen muß.

In unserem Fall benötigen wir nur zwei unterschiedliche Sockel, nämlich einen Sockel HALFADDER und einen Sockel ORGATE. Diese Component Deklarationen sind (und müssen für die Simulation mit einer Default Configuration und für die Synthese!!) identisch zu den jeweiligen Entity-Declarations der Module sein, welche später in den Sockel “gesteckt” werden (die Pins müssen stimmen!!!, am besten copy-paste der Entity und austauschen der Schlüsselworte “component” und “entity”).

Ist eine Component deklariert, d.h. der Sockeltyp festgelegt, so kann sie mittels der Component Instantiierung beliebig oft verwendet werden (Erzeugung der Sockel). Dies ist vergleichbar mit dem Befestigen der Sockel auf der Leiterplatte.

In den Sockel können später entsprechende Entities eingesetzt und mit Signalen der übergeordneten Entity oder Architecture verbunden werden.

Zur Instantiierung gehört auch immer ein vorangestellter benutzerdefinierter Name der Instanz (label), der Name der zugehörigen Component und die Port Map, d.h. die Verdrahtungsanweisungen. Die Signale zur Verdrahtung der instantiierten Sockel müssen entweder in der zur Architecture gehörenden Entity (als Port-Signale) oder aber in der Architecture selbst deklariert worden sein.

In diesem Beispiel erfolgt die Verdrahtung automatisch über die Position der Elemente in der Component Schnittstelle (positional associatin), d.h. es wird die gleiche Reihenfolge der Elemente wie in der Component Port-Liste gewählt.

Die Instantiierung erfolgt nach dem Schlüsselwort “begin” der Architecture.

Man kann auch direkt über die Namen der Signale verdrahten (named association). Die Reihenfolge der Elemente bei der “named association” ist frei wählbar (vgl. im obigen Bild: … port map(A ⇒ N_SUM, SUM ⇒ SUM, B ⇒ CIN, CARRY ⇒ N_CARRY2); … SUM kommt in der Component-Declaration eigentlich erst an dritter Stelle).

Für die Simulation ist eine Configuration notwendig, um dem Simulator die für die Entity gültige Architecture mitzuteilen. Wenn keine Configuration vorhanden ist, wird für die Synthese die Default Configuration verwendet, d.h. es wird die zuletzt analysierte Architecture zu einer Entity verwendet. In VHDL`93 ist es möglich, die Component Deklaration im Kopf der Architecture wegzulassen und direkt zu instantiieren.

In der Configuration wird die Verbindung zwischen der Entity und der zu verwendenden Architecture zu dieser Entity hergestellt. Dadurch entsteht das simulierbare Objekt. Für die Synthese wird stets die Default Configuration verwendet, d.h. die zuletzt analysierte Architecture wird mit der Entity verbunden und für die Synthese verwendet.

Hier sind ein paar Entitie und Architekturen zur Verdeutlichung der Aufgaben einer Konfiguration gezeigt.

Es soll letztendlich ein Volladdierer simuliert werden können. Die Struktur der Volladdierers ist die gleiche, wie im vorherigen Beispiel; es werden also zwei Halbaddierer benutzt. Jeder Halbaddierer wird mit zwei 'bit'-Signalen als Ein- und Ausgänge deklariert. Die Komponentenports werden mit den Signalen der Architektur über die Position verbunden, d.h. das erste Signal in der Port-Map-Liste wird mit dem ersten Port der Komponente verbunden.

In diesem Beispiel besitzen die Entities der Halbaddierer aber den Namen A und B, mit ihren zugehörigen Architekturen RTL und GATE. Beide Entities haben zu den Komponenten passende Portdeklarationen.

Wiederum wird die Architektur STRUCT für die Entity FULLADDER ausgewählt. Innerhalb dieser FOR-Schleife werden die Entities und Architekturen für die untergeordneten Komponenten ausgewählt.

Hierfür wird eine FOR Anweisung benutzt. Der erste Name nach dem Schlüßelwort 'for' bezeichnet den Komponenteninstantiierungsnamen; es folgt ein Doppelpunkt und der eigentliche Komponentenname. Wenn alle Instantiierungen einer bestimmten Komponente angesprochen werden soll, kann das Schlüßelwort 'all' verwendet werden. Innerhalb der Schleife wird durch die Use Anweisung die passende Entity ausgewählt. Hierzu wird der Bibliotheksname, in der die übersetzte Entity steht, bezeichnet, der Entityname selbst und in ein Paar runder Klammern der Name der Architektur.

Da die Portnamen der Entity B nicht mit den Portnamen der Komponentendeklaration übereinstimmen, muß eine Port Map Anweisung angegeben werden. Hier können die Ports und Signal über die Position aber auch über explizite Zuordnungen (über den Namen) verknüpft werden. Bei dem letzteren werden die Port Namen der Entity als Formals benutzt und die Port Namen der Komponente als Actuals.

Desweiteren können alle bisher nicht berücksichtigten Komponenteninstantiierungen mit dem Schlüßelwort 'others' angesprochen werden. Im obigen Beispiel wird dies für alle Instantiierungen von HALFADDER, außer MODULE2 gemacht. Da die verwendete Entity A gleiche Portdeklarationen, wie die Komponente besitzt, ist keine Port Map Anweisung notwendig.

Allle anderen eventuell vorhanden Komponenteninstantiierungen werden hier nach den Default-Regeln behandelt.

Es ist bei dehr großen Entwürfen hilfreich, Konfigurationen für Untermodule zu erstellen und auf oberster Entwurfseben nurmehr diese untergeordneten Konfigurationen zu referenzieren.

Innerhalb eines Prozesses sind nur sequentielle Anweisungen zugelassen. Mehrere Prozesse können in einer Architecture zusammengefaßt werden, wobei diese dann alle parallel zueinander ablaufen (concurrent).

Für die Simulation ist wichtig, daß alle Signale, die gelesen werden, in der Sensitivity List aufgeführt sind. (Bei rein kombinatorischen Prozessen müssen alle Eingangssignale=gelesene Signale in die Sensitivity List, bei getakteten Prozessen sind nur das Clocksignal und alle asynchron verwendeten SIgnale nötig z.B. asynchroner Reset, Set, Enable …)

Denn der Simulator entscheidet anhand dieser Liste, ob der Prozeß zu einem Simulationszeitpunkt aktiv wird oder nicht, d.h. ein event an einem Signal, das in der Sensitivity List steht, löst in der Simulation den Prozeß aus.

In VHDL können mehrere Prozesse beschrieben werden, die parallel ausgeführt werden. Innerhalb eines jeden Prozesses laufen die Anweisungen sequentiell ab, d.h. sie werden nacheinander ausgeführt.

Sind in einem Design mehrere Entities miteinander verbunden (über deren Entity-Schnittstelle), so laufen sämtliche Prozesse aller Entities parallel ab. Die einzelnen Prozesse kommunizieren über Signale miteinander.

Es gibt eine Reihe von vordefinierten Datentypen, wie bit, bit_vector, time, integer usw.. Es ist aber auch möglich, selbst weitere Datentypen zu definieren. Dadurch können z.B. Zustandsautomaten lesbarer gemacht werden, wenn ein Zustand nicht “00101”, sondern WAITING heißt (sprechende Namen).

Ein Package ist eine Sammlung von Definitionen, Datentypen, Unterprogrammen, Konstanten usw.. In einem Team kann jeder das Package einbinden, sodaß dann z.B. alle mit denselben Datentypen arbeiten, was eine spätere Verbindung der einzelnen Module von verschiedenen Designern vereinfacht. Auch werden notwendige Änderungen sofort an alle Beteiligten weitergegeben.

man kann ein Package in einen Kopf (“HEAD”) und einen Rumpf (“BODY”) trennen. Der Kopf enthält sog. Prototypendeklarationen von Funktionen oder Prozeduren, alle nötigen Datentypen usw..Im Rumpf sind dann die kompletten Unterprogramme beschrieben. Dies erleichtert den Kompiliervorgang, da nur der package-Kopf mit den kurzen Deklarationen der Unterprogramm-Köpfe gelesen werden muß, um zu entscheiden, ob die gerade analysierte Funktion die richtigen Parameter verwendet.

In der Library (Bibliothek) sind alle analysierten Objekte (Package, Package Body, Entity, Architecture, Configuration) zu finden. In VHDL wird eine Library unter einem logischen Namen angesprochen. Die voreingestellte Library ist WORK. Diese muß auf Rechnerebene auf einen physikalischen Pfad abgebildet sein. Man kann aber auch eine Library auf andere logische Namen abbilden, von denen aber die “letzte” auf einen physikalischen Pfad abgebildet sein muß.

Gewöhnlich arbeitet jeder Entwickler mit seiner eigenen Arbeitsbibliothek (work library). Er kann aber auf vorherige Projektergebnisse (PROJEKT_1 and PROJEKT_XY) oder auf die Projekt Bibliothek (USERPACK) zurückgreifen. Soll eine andere Library als WORK benutzt werden, so muß diese dem Compiler sichtbar gemacht werden. Dies geschieht mit der Library Anweisung, die aus dem Schlüßelwort 'library' un dem logischen Namen der Library besteht. Z.B. wird die Bibliothek IEEE allgemein benutzt, da sie verschiedene Standardpackages enthält.

In obigem Design wurde ein Package P als reine Headerdatei geschrieben und ein dazugehöriger Package Body. Dieses Package wird in den Enities MONITOR, DATA_IN und DATA_OUT eingebunden. In der Configuration CFG_FASTSIM wird die Enitities MONITOR, DATA_IN und DATA_OUT jeweils mit ihrer zugehörigen Architektur Asowie die Entity CTRL mit der zugehörigen Architecture SIM verbunden (Erinnerung: eine Entity kann mehrere Architectures haben).

Die Compilierreihenfolge wird durch die Abhängigkeiten der Einzelteile vorgegeben. Es muß immer hierarchisch von oben nach unten compiliert werden (Entity vor Architecture, Package Header vor Package Body, Configuration am Ende). Grundsätzlich: Wird ein Modul referenziert, so muß dieses vorher compiliert sein.

Die gängigste Art, einen Entwurf zu überprüfen, ist die Simulation. Da VHDL ursprünglich für die Simulation entwickelt worden ist, ist diese entsprechend gut unterstützt.

Für die Simulation wird üblicherweise eine neu Topebene definiert, die zumeißt Testbench genannt wird. In dieser wird der zu überprüfende Entwurf (Design Under Test, DUT) eingebunden und die spätere Umgebung desselben nachgebildet. Die Architektur der Testbench enthält mehrere Prozess und Untermodule, die die Stimuli für den DUT liefern und eventuell dessen Reaktionen darauf analysieren.

Man muß sich darüber bewußt sein, daß der Aufwand zum Erstellen einer Testbench erheblich sein kann. Zumeißt benötigt man genau soviel Zeit, wie für den eigentlichen Entwurf selber. Der Aufwand richtet sich nach der Art derTestbench, d.h. wieviel Funktionalität (Stimuli generieren, Antworten anlysieren, Text Ein-/Ausgabe, …) in der Testbench enthalten sein sollen.

Das Beispiel zeigt den VHDL Code eines einfachen Entwurfes (ADDER) und seiner Testbench (TB_ADDER). Die Testbench enthält keine Ports. In ihrer Architektur (TEST) sind die Komponente ADDER und ein paar Zwischensignale deklariert. Die Zwischensignale (*_I) werden in der Komponenteninstantiierung mit den Ports der Komponente verknüpft. Dabei erhalten die Eingangssignale ihre Werte aus dem Prozeß STIMULUS. Die Ausgangssignale können dann in einem Waveform (Signalverlauf) Fenster verfolgt werden.

Am Ende des VHDL Codes steht die Konfiguration CFG_TB_ADDER. Es wird nur die Architektur TEST für die Entity TB_ADDER ausgewählt. Mehr ist hier nicht notwendig.

VHDL ist eine sehr exakte Sprache, in der kaum kryptischer Programmierstil möglich ist (wie dies z.B. bei der Programmiersprache C der Fall ist). Jedes Signal muß einen bestimmten Datentyp besitzen, muß an entsprechender Stelle deklariert sein und akzeptiert nur Zuweisungen vom gleichen Datentyp.

Zur Funktionsüberprüfung eines VHDL-Modells muß eine Testbench, ebenfalls in VHDL, geschrieben werden, die die Testumgebung für das Modell liefert. Hierin werden Stimuli als Eingangssignale für das Modell beschrieben und weiterhin können auch die erwarteten Modell-Antworten überprüft werden. Die Testbench stellt die oberste Hierarchieebene dar und hat deshalb weder Eingangs- noch Ausgangsports.

Jedes Signal hat einen ganz bestimmten Signaltyp. Dieser Signaltyp hat eine bestimmte Anzahl von möglichen Werten. Bereits bei der Signaldeklaration muß der zugehörige Signaltyp festgelegt werden. Das geschieht entweder in der Port-Deklaration in der Entity oder als echte Signaldeklaration in der Architecture. Bei Signalzuweisungen ist darauf zu achten, daß die Signaltypen übereinstimmen.

Jeder Signaltyp hat eine bestimmte Anzahl von möglichen Werten. In VHDL sind bereits einige Standard-Datentypen definiert (BOOLEAN, BIT, CHARACTER, INTEGER, REAL usw.). Es ist auch möglich, eigene Datentypen zu definieren. In VHDL93 gibt es neue Typen, die unter anderem auch die Benutzung von Umlauten ermöglichen.

Ein spezieller Standard-Datentyp ist der Datentyp TIME. Er wird benötigt, um Signalzuweisungen nach einer bestimmten Zeit durchzuführen, was besonders in Testbenches verwendet wird. Ebenso lassen sich mit Hilfe des Datentyps TIME Verzögerungen jeder Art modellieren. Ein Signal des Datentyps TIME kann mit Signalen der Datentypen INTEGER und REAL durch Muiltiplikation oder Division verknüpft werden. Das Ergebnis hat den Datentyp TIME und kann wie oben gezeigt eingesetzt werden.

Die zwei vordefinierten ARRAY-Datentypen sind BIT_VECTOR (ARRAY of BIT) und STRING (ARRAY of CHARACTER). Zuweisungen können sowohl an einzelne Elemente des ARRAYS wie auch an das ganze ARRAY vorgenommen werden. Dabei müssen natuerlich die Datentypen wieder genau übereinstimmen. Zwei Zuweisungsbeispiele sind im Bild gezeigt.

Integer Signale werden durch die Synthese auf die passende Anzahl von Leitungen abgebildet. Diese Leitungen können auch als Bit Vektoren modelliert werden. Allerdings besitzen Bit Vektoren nicht von Haus aus eine numerische Bedeutung. Das Syntheseergebnis ist aber für beide Beispiele gleich. Der Prozeß beschreibt einen einfachen Multiplexer, der für SEL='1' das Eingangssignal A durchschaltet und das Eingangssignal B im anderen Fall (SEL='0'). Bitte beachten Sie, daß für beide Beschreibungsvarianten der Prozeß gleich ist.

Vorsicht ist geboten bei der Signalzuweisung an ARRAYs. Sind beide Signale vom Typ BIT_VECTOR, der eine jedoch `to`, der andere `downto` deklariert, dann werden die Werte nach ihrer Position und NICHT nach ihrer Nummer zugewiesen. Es empfielt sich darum, wenn möglich nur eine Richtung `to` ODER `downto` zu verwenden.

Man muß zwischen BIT und BIT_VECTOR Typen hinsichtlich der Signalzuweisungen unterscheiden. Einem BIT-Signal wird ein Wert in einfachen Hochkommata zugewiesen, bei BIT_VECTORen hat man dagegen verschiedene Möglichkeiten. Zum einen als String mit den einzelnen Bits in doppelten Anführungszeichen, optional kann diesem auch ein b für binäres Eingabeformat vorangestellt werden. Zum anderen kann die Zuweisung auch im HEX-Format erfolgen, wobei dem HEX-String ein x vorangestellt wird. Dabei wird Groß- und Kleinschreibung nicht unterschieden. Bei großen BIT_VECTORen hat man zusätzlich die Möglichkeit, je 4 bits zusammenzufassen und durch Unterstriche zu trennen. Dies kann einer besseren Lesbarkeit dienen. In VHDL93 sind alle Zuweisungen, die für BIT erlaubt sind, auch für Character Arrays zugelassen.

Die Signalzuweisung an einen Vektor kann auch mittels Concatenation (Verknüpfung, Verkettung) durchgeführt werden. Das Verknüpfungssymbol ist “&” und die Zuweisung erfolgt nach Position. Dem BIT_VECTOR BYTE wird die Concatenation von A_BUS und B_BUS zugewiesen. BYTE besteht aus 8 BIT die `downto` deklariert sind. A_BUS und B_BUS betsteht jeweils aus 4 BIT, ebenfalls `downto` deklariert. Das bedeutet also für die Signalzuweisung: BYTE(7 downto 4) wird A_BUS und BYTE(3 downto 0) wird B_BUS zugewiesen. Bei der zweiten Signalzuweisung wird dem `downto` deklarierten BIT_VECTOR Z_BUS die Concatenation von A_BIT, B_BIT, C_BIT und D_BIT zugewiesen, d.h. Z_BUS(3) erhält A_BIT, Z_BUS(2) erhält B_BIT, Z_BUS(1) erhält C_BIT und Z_BUS(0) erhält D_BIT.

Ähnlich der Signalzuweisung mittels Concatenation ist auch eine Signalzuweisung über Aggregates möglich. Dabei werden mehrere Signale durch Klammerung zu einem Aggregat zusammengefaßt und einem Zielsignal zugewiesen. Hier ist es sogar möglich, einem Aggregat etwas zuzuweisen. Einem Aggregat aus 4 einzelnen BIT-Signalen kann z.B. “1011” zugewiesen werden oder ein anderes Signal aus 4 BIT-Signalen oder ein Bereich eines längeren BIT_VECTORs. Die Zuweisung erfolgt nach Position. Ebenso kann man die Positionen direkt ansprechen, an die die Zuweisung erfolgen soll. Im letzten Beispiel oben wird dem BIT_VECOTR BYTE an Position 7 eine `1`, an Positionen 5 bis 1 eine `1`, an Position 6 das SIgnal B_BIT und allen anderen eine `0` zugewiesen. Allen bits von BYTE kann `0` zugewiesen werden mit BYTE ⇐ (others ⇒ `0`).

Durch einen Slice kann ein bestimmter Bereich eines Vektors angesprochen werden. Es ist lediglich darauf zu achten, daß die Richtung mit der Richtung in der Deklaration des Signals übereinstimmt.

Wie werden die Prozesse der Reihe nach abgearbeitet?

Eine Architecture beinhaltet neben den Prozessen auch nebenläufige Anweisungen. Innerhalb der Prozesse werden die VHDL Befehle sequentiell abgearbeitet. Die Verbindung zwischen den einzelnen Bestandteilen der Architetcure wird über die Sensitivity List hergestellt. Nebenläufige Anweisungen können auch als Prozesse interpretiert werden, wobei all die Signale in der Sensitivity List stehen, die gelesen werden. Beispielsweise wird der Prozeß P2 aufgrund einer steigenden Taktflanke gestartet und die Befehle innerhalb des Prozesses sequentiell abgearbeitet. Am Ende des Prozesses erfolgt dann ein Signal Update. Ein Signalwechsel in P2 hat dann zur Folge, daß im nächsten Schritt der Prozeß P1 und die nebenläufige Anweisung C1 aufgerufen werden, da dieses Signal in der Sensitivity List von P1 und C1 steht. Nach der Abarbeitung von P1 und C1, werden P1 und C2 ausgeführt. Dann wird erneut P2 und C2 abgearbeitet. Usw. bis ein stabiler Zustand erreicht wird, d. h. kein Signalwechsel mehr auftritt.

Treten dieselben zwei Signalzuweisungen einmal nebenläufig in der Architecture und einmal sequentiell in einem Prozeß auf, so unterscheidet sich das Ergebnis erheblich. Im ersten Fall erfolgen tatsächlich zwei parallele Signalzuweisungen an das Signal, was nur im Fall eines resolved Typs erlaubt ist. Im zweiten Fall wird innerhalb eines Prozesses zuerst eine Zuweisung vorgemerkt, anschließend aber von einer anderen Zuweisung überschrieben, so daß immer nur die letzte Signalzuweisung in einem Prozeß beim Signal-Update ausgeführt wird.

Signale besitzen innerhalb der Signalverwaltung einen vergangenen, einen aktuellen und einen zukünftigen Wert. Die Signalzuweisungen in einem Prozeß weisen den Wert immer dem zukünftigen Wert des Signals zu. Erst am Ende des Prozesses führt ein Signal Update dazu, daß der zukünftige Wert den aktuellen Wert überschreibt. Diese Änderung des aktuellen Wertes kann weitere Prozesse auslösen, insbesondere auch denselben Prozeß (wie im Beispiel). Erfährt B eine Wertänderung, so wird der Prozeß zum ersten Mal aufgerufen. Der zukünftige Wert von X wird mit dem Wert von B beschrieben. Am Ende des Prozesses wird der zukünftige Wert von X auf den aktuellen Wert übertragen. Diese Wertänderung an X ruft den Prozeß ein zweites Mal auf. Jetzt wird der Wert von X in den zukünftigen Wert von Z geschrieben. Am Ende des Prozesses wird wieder ein Signal Update durchgeführt, wobei der zukünftige Wert von Z auf den aktuellen Wert übertragen wird. Da jetzt keine Änderung mehr an den aktuellen Werten der Signale A, B oder X aufgetreten ist, wird der Prozeß nicht mehr aufgerufen. Dieses Beispiel dient nur dazu die Signalverwaltung im Simulator zu verdeutlichen, denn normalerweise würde man auf das Zwischensignal X verzichten und dem Z sofort das Signal B zuweisen.

Ein Simulationszyklus besteht immer aus einem Signal-Update und einer Prozeßausführung. Zu einer Simulationszeit können mehrere dieser Zyklen (= Delta Zyklen) ausgeführt werden. Die Anzahl der Delta Zyklen hat keinen Einfluß auf die Simulationszeit. Zu Beginn der Simulation werden alle Signale aktualisiert (Signal-Update) und eine Abarbeitungsliste aller Signale mit Wertänderung erstellt. Diese wird dann der Reihe nach abgearbeitet. Im Delta-Zyklus 1 werden alle Prozesse angestoßen, die das erste Signal der Abarbeitungsliste in der Sensitivity List haben. Sind diese Prozesse zur Ruhe gekommen, wird ein Signal-Update durchgeführt und Signale, deren Wert sich geändert hat, werden an die Abarbeitungsliste angehängt. So wird verfahren bis die Abarbeitungsliste leer ist, d.h. es werden keine weiteren Prozesse mehr angestoßen und alle Signale sind zur Ruhe gekommen. Dann wird das Signal aktualisiert, welches eine Zeitweiterschaltung nach sich zieht (wait for x ns, clock ⇐ not clock after x ns, z.B. Anweisungen in der Testbench zur Stimulibeschreibung). Anschließend beginnen die Delta-Zyklen für die neue Simulationszeit.

Die Delta-Zyklen sind orthogonal zur Simulationszeit aufgereiht, d.h. zu einer Simulationszeit können mehrere Delta-Zyklen ausgeführt werden.

Ein Signal-Update und eine Prozeß Ausführung bilden einen Deltazyklus. Ändert sich der Wert des Signals B, so wird der Prozeß in der Architecture A aufgrund dieses Events aktiviert. Der zukünftige Wert von Y erhält den momentanen von A, der zukünftige Wert von X erhält den momantenen von B und der zukünftige Wert von Z erhält den momentanen von X. Beim Signal-Update ändert sich demzufolge nur das Signal X. Dieses Event aktiviert den Prozeß ein zweites Mal, da sich X in der Sensitivity List befindet. Der zukünftige Wert von Z erhält den momentanen Wert von X. Nach erneutem Signal-Update kommt der Prozeß zur Ruhe, da kein Event mehr vorhanden ist. Es haben sowohl X als auch Z den Wert von B erhalten. Zu beachten ist hierbei, daß bei der Ausführung der einzelnen Deltazyklen die Simulationszeit nicht voranschreitet, sondern angehalten wird.

Grundsätzlich ist ein Prozeß als Endlosschleife zu verstehen. Diese kann durch eine WAIT-ON-Anweisung unterbrochen werden. Äquivalent zu einer WAIT-ON-Anweisung ist die Verwendung einer Sensitivity List. Falls mit Sensitivity List gearbeitet wird, darf im Prozeß keine WAIT-Anweisung mehr erscheinen.

Postponed Prozesse werden zu jeder Simulationszeit im letzten Delta-Zyklus ausgeführt. Das heißt, daß dieser Prozeß bereits auf die stabilen Signale zu dieser Simulationszeit zugreifen kann. In Postponed Prozessen sind WAIT Anweisungen mit 0 ns und Signalzuweisungen ohne Verzögerung verboten. Postponed Prozesse könne nur in der Simulation verwendet werden, eine Synthese ist nicht möglich.

Die vordefinierten Standardtypen sind nur von eingeschränktem Nutzen in der Praxis. Man kann z.B. natürlich das Verhalten eines RAM oder ROM mit CASE Anweisungen modellieren; der VHDL Code wäre aber umfangreich und schwer zu lesen. Mit entsprechenden Feldtypen wäre dies einfacher zu bewerkstelligen.

Ein anderer Grund für erweiterte Datentypen ist die Lesbarkeit. VHDL Code ist auch als Dokumentation gedacht (selbstdokumentierend). Entsprechend bietet es sich an sprechend Namen zu verwenden. Bei aller Lesbarkeit des VHDL Codes kann trotzdem nicht auf ausreichende Kommentierung desselben verzichtet werden!

Ein weiterer Grund ist die Modellierung von Bussen. Das Hauptmerkmal eines Busses ist das Vorhandensein mehrerer Busteilnehmer, d.h. die gleichen Signalleitungen werden von mehreren Modulen benutzt. Die Teilnehmer die keinen Wert treiben, müssen ihre Treiber auf einen Wert setzen können, der andere Werte auf dem Bus nicht “zerstört”.

Es werden in VHDL 3 Typen unterschieden: skalare, zusammengesetzte und andere Typen. Skalare Typen enthalten genau einen Wert. Das kann ein INTEGER-, REAL-, BIT-, TIME- oder ein Wert eines Aufzählungstyps sein. Die zusammengesetzten Typen bestehen aus mindestens 2 Werten, wobei in Arrays alle Werte vom selben Typ sein müssen. Im Gegensatz dazu kann man in Records auch verschiedene Datantypen verwenden.

Zur Gruppe “andere Typen” werden FILE- und ACCESS-Typen gezählt. Um eine Datei einzulesen bzw. Daten in ein File abzuspeichern, wird der Datentyp FILE verwendet. Beim ACCESS-Datentyp handelt es sich um einen Zeiger.

Der BIT-Typ kann nur die Werte `0` und `1` annehmen. Für die Simulation sind aber weitere Zustände wünschenswert. Zum Beispiel undefiniert, wenn gleichzeitig eine `0` und `1` zugewiesen wird hochohmig, wenn in einem Bussystem ein Signaltreiber nicht treiben soll, weil ein anderer gerade auf den Bus schreibt uninitialisiert, wenn ein Signal nie eine Zuweisung erhält `don`t care`, wenn der Wert des Signals egal ist, oder unterschiedliche Signalstärken.

Um nun für die Simulation eine mehrwertige Logik zur Verfügung zu haben, ist ein eigener Datentyp notwendig. Da anfangs jeder Toolhersteller seine eigenen mehrwertigen Logiktypen mitlieferte, wurde eine 9-wertige Logik im IEEE-Standard 1164 definiert.

Der neue Datentyp des Packages 'std_logic_1164' is 'std_ulogic'. Das Package ist in der Bibliothek IEEE abgelegt. Diese Bibliothek wird also durch die Anweisung 'use IEEE.std_logic_1164.all' eingebunden. Der neue Typ ist als Aufzählungstyp definiert, bestehend aus den Zeichen '0' und '1' sowie weiteren ASCII Zeichen. Die wichtigsten hiervon sind 'u' für uninitialized und 'x' für unknown value. Das 'u' ist der erste Wert der Typendeklaration und wird somit als Anfangswert in der Simulation benutzt.

Ein unresolved Typ kann nur von einem Treiber (einem Prozeß) beschrieben werden. Bei der Modellierung von Bussen ist dies aber nicht ausreichend, weil mehrere Busteilnehmer auf den Bus schreiben können müssen. Dafür gibt es den resolved Datentyp, der sich von seinem unresolved Gegenstück nur dadurch unterscheidet, daß mehere Treiber ein Signal treiben dürfen. Dabei kann es passieren, daß 2 Treiber unterschiedliche Werte auf den Bus schreiben. Welcher Wert in diesem Fall auf dem Bus aktiv werden soll, muß die Resolution-Function bestimmen.

Der resolved Typ zu STD_ULOGIC heißt STD_LOGIC und ist auch im package ``Std_Logic_1164` vordefiniert inklusive der Resolution-Function. Im Umfang der Werte unterscheiden sich der Grundtyp und der resolved-Typ nicht.

Das Bild oben zeigt die Resolution Function, die aus dem unresolved Typ STD_ULOGIC den resolved Typ STD_LOGIC erzeugt. Nachdem alle Werte, die auf ein Signal geschrieben werden, gesammelt sind, wird mit Hilfe einer Tabelle entschieden, welcher Wert tatsächlich auf das Signal geschrieben wird.

Bei der VHDL Umsetzung eines Designs kann es durchaus vorkommen, daß man ungewollt mehrere Treiber an ein Signal anschließt. Dieses Problem tritt bei der Verwendung von STD_ULOGIC Datentypen erst gar nicht auf, da beim Kompilieren eine Fehlermeldung auftritt. Jedoch wird dieser Datentyp selten auf RTL Beschreibungsebene verwendet, da dort die Vorteile der STD_LOGIC Datentypen überwiegen. Viele Standards basieren auf dem STD_LOGIC Datentyp, die in den Packages der Toolhersteller abgelegt sind. Beispielsweise existieren die Grundrechnungsarten ('+', '-', '*' '/') für diesen Typ und können ohne zusätzliche Konvertierungsfunktionen aufgerufen werden. Einige Designs enthalten auch Busstrukturen, so daß ohnehin kein Weg an dem STD_LOGIC Datentyp vorbeiführt.

Das 'numeric_std' Package befindet sich in der Bibliothek IEEE und stellt zwei numerische Interpretationen eine Bitvektors zur Verfügung. Diese werden entweder als vorzeichenbehaftet oder nicht interpretiert. Weiterhin sind darin überladene Operatoren zum Verknüpfen mit Integers und anderen Vektortypen definiert.

Man beachte, daß der Signalzuweisungsoperator nicht überladen werden kann. D.h. hier muß gegebenenfalls zur Typenkonvertierung eine Funktion aufgerufen werden. In diesem Package sind z.B. die Typenkonvertierungsfunktionen 'to_integer' und 'to_(un)signed' definiert.

Es gibt entsprechend für 'bit' basierende Operationen das Package 'numeric_bit', von welchem aber wegen der Nachteile einer 2-werte Logik abgeraten wird.

In VHDL hat der Designer die Möglichkeit selber Typen zu definieren, die von ihm gewählte Werte (RESET, START, …) annehmen können. Auf diese Weise muß man nicht mit “001…” usw. arbeiten, sondern kann aussagekräftige Namen vergeben. Bei den Signalzuweisungen werden direkt die definierten Werte angegeben, so daß der VHDL Code lesbarer wird. Im Beispiel sind vier Signal-Werte/Zustände definiert. Bei einer Schaltungsumsetzung benötigt man in diesem Fall zwei Bit. Jedoch ist eine direkte Zuweisung eines 2 Bit Vektors (“00”, TWO_BIT_VEC) nicht erlaubt. Es dürfen nur die definierten Werte verwendet werden.

Das Beispiel zeigt die Verwendung von einem aufzählenden Signaltyp bei einer einfachen Finite-State-Machine, nämlich einer Ampelsteuerung. Im Deklarationsteil der Architecture wird der neue Datentyp T_STATE definiert. Die Signale STATE und NEXT_STATE erhalten genau diesen Datentyp, so daß man im LOGIC Process mit den definierten Namen arbeiten kann und auf diese Weise der VHDL Code lesbarer wird.

Der Array-Typ kann unconstrained (z.B. std_ulogic_vector) oder constrained (z.B. MY_BYTE) definiert werden. Bei der Signaldeklaration muß im unconstrained Fall noch der Indexbereich angegeben werden. Es können auch Arrays über aufzählenden Typen definiert werden, allerdings läßt sich diese Variante nicht synthetisieren.

Mehrdimensionale Arrays kann man auf 2 verschieden Arten erzeugen. Einmal, indem man ein Array von einem Array bildet, und zum anderen, indem man ein mehrdimensionales Array direkt definiert. Je nach Definition des mehrdimensionalen Arrays muß unterschiedlich referenziert werden, um auf ein Array-Element zugreifen zu können. Mehrdimensionale Arrays lassen sich meist nur bis zur Dimension 2 synthetisieren.

Mit Hilfe von Aggregaten kann man allen Elementen eines mehrdimensionalen Arrays auf übersichtliche Weise Werte zuweisen.

Zusammengesetzte Typen aus verschiedenen Einzeltypen heißen RECORDs. Eine Wertzuweisung erfolgt entweder direkt (HUMAN1.BIRTHDAY.YEAR ⇐ 1974) oder mit Aggregaten( HUMAN1 ⇐ (“Franziska”,TODAY)).

Da bei Signalzuweisungen die Signaltypen übereinstimmen müssen, werden für verschiedene Typen Konvertierungsfunktionen benötigt. Bei closely related types reicht für die Typkonvertierung das Voranstellen des Zieltyps( BYTE ⇐ MY_BYTE(VECTOR)). Closely related types sind Arrays der selben Länge, mit der selben Indexmenge und den selben Elementtypen. Andernfalls ist eine separate Konvertierungsfunktion notwendig, z.B. zur Umwandlung eines std_logic_vector in einen bit_vector.

Neben den Typedeklarationen kann der Designer auch Subtypes von einem bereits existierenden Typ definieren. Subtypes haben den selben Typ wie ihr zugrundeliegender Typ, sind also mit ihrem Grundtyp kompatibel. Ein Vorteil liegt darin, daß man sich einmal einen SUBTYPE in einem Package definiert, verwendet diesen im gesamten Design und muß bei einer Spezifikationsänderung nicht alle Dateien modifizieren. Auf diese Weise ist nur eine Änderung im Package erforderlich und man schaltet die Fehlerquelle aus, an einer Stelle die Modifikation zu vergessen. Ein neuer Datentyp kann jedoch mit SUBTYPE nicht definiert werden.

Die Alias Anweisung gibt einem bereits definierten Objekt einen neuen Namen. Komplexere Datenstrukturen können so in kleinere Teile aufgebrochen werden, was den Umgang damit vereinfachen kann. Es besteht die Möglichkeit mit dem Alias zu arbeiten oder mit dem zugrunde liegenden Signal.

In der Tabelle sind die in VHDL existierenden Operatoren nach Art und Priorität aufgelistet. Man beachte die in VHDL'93 neu hinzugekommenen Operatoren.

Logische Operationen mit Feldern können nur mit Operanden gleichen Typs und gleicher Länge durchgeführt werden. Die Operation wird elementeweise durchgeführt.

Beim Vergleich von Array-Typen ist darauf zu achten, daß der Vergleich von links nach rechts durchgeführt wird. Das heißt für Arrays unterschiedlicher Länge, daß z.B. “111” größer ist als “1001”. Ist dies nicht gewünscht, soll also rechtsbündig verglichen werden, dann müssen die Arrays von Hand auf die gleiche Länge gebracht werden, z.B. mittels Concatenation: `0`&“111” ist dann kleiner als “1001”.

Die Operatoren sind weitgehend selbsterklärend. mod und rem sind nur für Integer-Typen definiert. (a rem b) hat das Vorzeichen von a, (a mod b) hat das Vorzeichen von b und die Resultate der Operationen sind über folgende Gleichungen festgelegt:

a=(a/b)*b+(a rem b) und a= n *b+(a mod b) für einen Integer n.

Beispiele: 5 rem 3 = 2 5 mod 3 = 2 für n=1

(-5) rem 3 = -2 (-5) mod 3 = 1 für n=-2

(-5) rem (-3) = -2 (-5) mod (-3) = -2 für n=1

5 rem (-3) = 2 5 mod (-3) = -1 für n=-2.

Sequentielle Anweisungen werden in der Reihenfolge ihres Auftretens abgearbeitet.

Die IF Bedingung muß ein wahr/falsch(BOOLEAN) Ausdruck sein. Nach der ersten IF Bedingung können beliebig viele ELSIF Bedingungen folgen. Dabei dürfen auch Überlappungen in den verschiedenen Bedingungen auftreten. Optional kann als letzter Zweig eine einfache ELSE Anweisung folgen, die alle vorher nicht aufgetretenen Fälle zusammenfaßt. Beendet wird das IF Statement mit END IF. Die erste IF Bedingung ist auch die mit der höchsten Priorität, d.h. falls diese Bedingung erfüllt ist, wird die entsprechende Anweisung ausgeführt und der Rest des IF - END IF Blocks übersprungen.

Das Beispiel zeibt zwei unterschiedliche Implementierungen des gleichen Verhaltens. Die Signalzuweisung an Z in der ersten Zeile des linken Prozesses (Architektur EXAMPLE1) wird default Zuweisung genannt; diese wird nur dann wirksam, wenn sie nicht durch eine spätere Anweisung überschrieben wird. Man beachte, daß sich die Bedingungen des 'if' und 'elsif' Zweiges überschneiden. Im Fall X=“1111” wird aber aufgrund der höheren Priorität der 'if' Zweig abgearbeitet.

Während bei der IF Anweisung durch die Reihenfolge der Abfragen die Prioritäten der einzelnen Zweige festgelegt werden, sind bei der CASE Anweisung alle Zweige gleichberechtigt. Es müssen hier aber alle Möglichkeiten abgedeckt werden. Dazu steht insbesondere das Schlüsselwort “others” zur Verfügung, welches alle anderen Fälle abdeckt. Es kann wie bei der IF Anweisung nach einzelnen Werten, nach mehreren Werten oder ganzen Bereichen abgefragt werden.

Der Typ der EXPRESSION im Kopf der CASE Anweisung muß mit dem Typ der abgefragten Werte übereinstimmen.

Bei INTEGER Typen können Ranges definiert werden. Einerseits wird dann schon in der Simulation eine Bereichsüber- oder -unterschreitung mit einer Fehlermeldung quittiert, andererseits macht das Synthesetool sonst aus allen INTEGER-Typ-Signalen 32 Bit INTEGER Typen. INTEGER Werte haben eine definierte Ordnung. ARRAYS haben keine Ordnung, weshalb auch die Bereichsabfrage eines Bit_Vectors “0000” TO “0100” keine in VHDL zulässige Aussage darstellt.

Schleifen arbeiten in der gewohnten Weise. Die Schleifenvariable braucht nicht definiert zu werden. Dies geschieht implizit in der Schleifenanweisung. Eine Wertzuweisung an eine Schleifenvariable ist nicht möglich. Obiger Prozeß führt folgende Zuweisung durch: falls A=0 : Z=“0001” falls A=1 : Z=“0010” falls A=2 : Z=“0100” falls A=3 : Z=“1000”.

Schleifensyntax: Ein Schleifenname ist möglich, aber nicht unbedingt notwendig. Durch die Rangeangabe wird sowohl die Laufrichtung als auch der Bereich der Schleifenvariable festgelegt. Die Schleifenvariabel ist nur innerhalb der Schleife definiert. Soll eine Schleife synthetisiert werden, so muß der Laufbereich der Schleifenvariable fest sein und darf nicht von Signal- oder Variablenwerten abhängen. While Schleifen sind im allgemeinen nicht synthetisierbar.

Die drei Schleifenbeispiele sind funktional equivalent. Der Unterschied liegt in der Definition des Schleifenbereiches. Die Architekturen A und B benutzen die for Syntax. Statt den Bereich durch Integers vorzugeben, werden Signalattribute verwendet. Der Wert dieser Attribute hängt nur vom Signaltyp ab und liegt somit zur Ausführungszeit fest. Die Architektur C benutzt die while Syntax, zeigt aber gleiches Verhalten.

Range Attribute werden benutzt, um den selben VHDL Code mehrfach verwenden zu können. Folgende Zeilen beschreiben den gleichen Zahlenbereich, vorrausgesetzt Z ist vom Typ integer range 0 to 3.

for I in 0 to 3 loop
for I in Z'low to Z'high loop
for I in Z'range loop

Wie vorher schon erwähnt, können Prozesse in zwei verschiedenen Formen beschrieben werden. Wenn die Sensitivitätsliste weggelassen wird, muß ein anderer Mechanismus die Abarbeitung des Prozesses unterbrechen. Wait Anweisungen können dies. Sie halten die Abarbeitung so lange an, bis die Bedingung der Wait Anweisung erfüllt ist. Besitzt die Wait Anweisung kein Argument, so wird der Prozeß für den Rest der Simulation angehalten.

Wait Anweisungen dürfen nicht zusammen mit einer Sensitivitätsliste verwendet werden.

Prozess ohne Sensitivitätsliste werden bis zur ersten Wait Anweisung abgearbeitet. Im obigen Beispiel der Architektur BEH_1 wartet der Prozeß auf einen Event am Signal CLK ('wait on CLK'). Tritt dieser Event ein, wird in der folgenden If Schleife der Wert des Taktsignals überprüft und gegebenenfalls ein neuer Ausgabewert zugewiesen. Dies beschreibt das Verhalten eines Flip Flops. In der Architektur BEH_2 sind beide Bedingungen in einer Wait Anweisung enthalten ('wait until …'). Der Prozeß zeigt aber wiederum das Verhalten eines Flip Flops. Mit einer Wait For Anweisung lassen sich sehr leicht Stimuli für die Verifikation eines Entwurfes beschreiben.

Wait Anweisungen sind zur Beschreibung von Zeitverhalten geeignet. Zum Beispiel läßt sich sehr einfach ein Busprotokoll für die Simulation beschreiben. Das Zeitverhalten kann direkt in simulierbares VHDL kodiert werden. Man beachte aber, daß solche Verhaltensbeschreibungen nicht synthetisiert werden können.

Variablen existieren nur innerhalb eines Prozesses. Wertzuweisungen an Variablen werden im Gegensatz zu Wertzuweisungen an Signale sofort ausgeführt. Variablen behalten ihren letzten Wert.

Variablen zeigen ein grundsätzlich anderes Verhalten als Signale. Bei mehreren Signalzuweisungen in einem Prozeß wird lediglich die letzte durchgeführt. Bei Variablen geschieht die Wertzuweisung sofort. Deshalb zeigen die beiden obigen Prozesse auch unterschiedliches Verhalten jenachdem, ob mit Variablen oder mit Signalen gearbeitet wird.

Variablen werden zur Ausführung von Algorithmen verwendet. Dabei erhält eine Variable ihren Wert von einem Signal. mit der Variablen wird dann der Algorithmus durchgeführt und anschließend das Ergebnis wieder einem Signal zugewiesen. Variablen sind nur innerhalb ihres Prozesses bekannt. Sie besitzen beim nächsten Prozeßaufruf ihren alten Wert. Wird eine Variable gelesen, bevor ihr ein Wert zugewiesen wurde, so muß diese Variabel speicherndes Verhalten zeigen, d.h. sie wird zu einem Latch bzw. einem Flip-Flop synthetisiert.

Im obigen Beispiel wird ein weiterer Unterschied zwischen Signalen und Variablen deutlich. Während ein (skalares) Signal allgemein fest mit einer Leitungen assoziiert werden kann, gilt dies nicht für Variablen. Im obigen Beispiel wird die For Schleife vier mal durchlaufen; jedesmal bezeichnet die Variable TMP eine andere Leitung der später erzeugten Hardware, nämlich jeweils den Ausgang des entsprechenden XOR Gatters.

In VHDL 93 sind auch globale Variablen erlaubt, d.h. Variablen, die nicht nur innerhalb eines Prozesses sichtbar sind, sondern in der ganzen Architecture. Dabei kann aber das Problem auftreten, daß 2 Prozesse einer globalen Variablen einen Wert zuweisen. Dann ist nicht klar, welcher von diesen nebenläufigen Prozessen den letzten Wert an die Variable zuweist. Zu einer bestimmten Simulationszeit gibt es keine Hierarchie zwischen den Prozessen, sodaß zu einer Zeit t_1 ein anderer Prozeß die Wertzuweisung an die globale Variable durchführt und zu einer Zeit t_2 ein anderer Prozeß. Dies kann zu nicht deterministischem Verhalten führen! In synthetisierbarem VHDL-Code dürfen globale Variablen nicht verwendet werden.

Durch Überladen von den vordefinierten Operatoren ist es möglich, einem einzigen Funktionsnamen mehrere Funkionen zuzuordnen. Diese müssen sich nur in den Typen der Eingangs- oder Ausgangssignale unterscheiden, sodaß die richtige Funktion bei einem Funktionsaufruf mittels der übergebenen Signaltypen eindeutig gefunden werden kann.

Hier sind 4 Funktionen “+” definiert (überladen). Sie unterscheiden sich entweder in den Eingangssignaltypen und/oder in den Ausgangssignaltypen, sodaß beim Aufruf eine eindeutige Zuordnung möglich ist.

Innerhalb einer Architecture laufen alle Prozesse parallel (nebenläufig) ab. Das heißt insbesondere, daß die Reihenfolge, mit der die einzelnen Prozesse in der Architecture erscheinen, weder auf das Verhalten noch auf die resultierende Hardware einen Einfluß hat. Grundsätzlich bekommt man genau das Ergebnis, das man beschrieben hat.

Die IF Bedingung muß ein wahr/falsch(BOOLEAN) Ausdruck sein. Nach der ersten IF Bedingung können beliebig viele ELSIF Bedingungen folgen. Dabei dürfen auch Überlappungen in den verschiedenen Bedingungen auftreten. Optional kann als letzter Zweig eine einfache ELSE Anweisung folgen, die alle vorher nicht aufgetretenen Fälle zusammenfaßt. Beendet wird das IF Statement mit END IF. Die erste If Bedingung ist auch die mit der höchsten Priorität, d.h. falls diese Bedingung erfüllt ist, wird die entsprechende Anweisung ausgeführt und der Rest des IF - END IF Blocks übersprungen.

Im obigen Beispiel werden zwei equivalente Beschreibungen eines Multiplexers gezeigt. Man beachte, daß in dem Prozeß alle Signale der rechten Seite der Signalzuweisung in der Sensitivitätsliste auftauchen. Der unbedingte Else Zweig könnte durch eine Default Anweisung vor der If Schleife ersetzt werden. Diese Anweisung wird dann nicht wirksam, wenn einer der Zweige innerhalb der If Schleife abgearbeitet wird.

Während bei der IF Anweisung durch die Reihenfolge der Abfragen die Prioritäten der einzelnen Zweige festgelegt werden, sind bei der CASE Anweisung alle Zweige gleichberechtigt. Es müssen hier aber alle Möglichkeiten abgedeckt werden. Dazu steht insbesondere das Schlüsselwort “others” zur Verfügung, welches alle anderen Fälle abdeckt. Es kann wie bei der IF Anweisung nach einzelnen Werten, nach mehreren Werten oder ganzen Bereichen abgefragt werden. Der Typ der EXPRESSION im Kopf der CASE Anweisung muß mit dem Typ der abgefragten Werte übereinstimmen.

Alle nebenläufigen Anweisungen beschreiben das Verhalten von Multiplexerstrukturen. Es ist nicht möglich, mit nebenläufigen Anweisungen alleine speichernde Elemente, wie z.B. Flip Flops zu modellieren. Die liegt nicht zuletzt an dem unbedingten elses Zweig, der bei einer bedingten Signalzuweisung eingefügt werden muß. Allerdings kann umgekehrt jede nebenläufige Anweisung durch einen entsprechenden Prozeß beschrieben werden. Da Prozesse allgemein durch ihre sequentielle Natur einfacher für Menschen zu verstehen sind, sollten sie im Normalfall verwendet werden.


Chapters of System Design > VHDL Language and Syntax