1.Entwurfsparadigmen und Allgemeines Wie bei jeder anderen Programmiersprache auch, macht es viel Sinn, das gesamte Design in funktionelle Blöcke (Komponenten) aufzuteilen, die dann in der Hauptkomponente nur noch instanziert werden. Instanziert bedeutet so ähnlich wie der Aufruf einer Funktion mit Parametern in anderen Programmiersprachen. Die Parameter sind dann in diesem Fall Signale und stellen so die Verbindungen zwischen den einzelnen Komponenten dar. Signale sind auch der einzigste Weg, mit denen ein Prozess nach aussen "kommunizieren" kann. Benutzt man vhdl2cpld, so reicht es aus, die benötigten Modulbeschreibungs-Dateien mit in des Verzeichnis zu kopieren, da sie automatisch mit in die Projekt-Datei eingetragen werden. Um die Reihenfolge und Dateinamen braucht man sich keine Sorgen zu machen (außer die Endung ".vhd", die ist zwingend), XST aus dem WebPack(tm) sortiert die Daten automatisch in die richtige Reihenfolge. Auch ungenutzte Komponenten werden erkannt und nicht mit in das Design übernommen. 2.Das Grundgerüst 2.1.Kommentare Eine VHDL-Datei hat im Normalfall die Endung ".vhd" und besteht aus mehreren Teilen. Vor dem eigentlichen Programmcode hat sich ein Kommentarfeld eigebürgert, welches Autor, Modulname etc. beinhaltet. Zwingend notwendig ist es nicht, hilft aber bei der Modularisierung zur Spezifizierung des Moduls. Kommentare beginnen mit zwei Minuszeichen und gehen bis zum Ende der Zeile.
2.2.Bibliothekseinbindung Die folgende Auswahl hat sich bis jetzt als ausreichend erwiesen. Wenn Projektdateien aus mehreren Komponenten bestehen, ist es wichtig ist, dass die Bibliotheken vor jeder Komponente neu definiert werden. Für die Übersichtlichkeit und Wiederverwendbarkeit von Komponenten ist es sinnvoll, für jede Komponente eine eigene vhd-Datei anzulegen. Lässt man die Bibliotheksdefinition weg, so gibt es fast garantiert (außer man benutzt nur den eingebauten Datentyp "Bit") eine Fehlermeldung wegen unbekanntem Datentyp.
Anstelle des Datentyps "Bit" sind die in der Library definierten Datentypen "std_logic" und "std_logic_vector" zu bevorzugen, da sie zusätzliche Zustände wie z.B. Tristate haben. 2.3.Entity Jede Komponentenbeschreibung besteht aus zwei Teilen "entity" und "architecture". Ersteres beschreibt die Schnittstelle nach aussen, das sind normalerweise nur die "Ports":
Unsere Komponente hat den Namen "ioreg4", danach werden die Ports definiert:
Ein Port muss immer die Richtung INOUT bekommen, wenn das Ergebnis wieder gelesen werden muss. Auch wenn es erstmal nicht so augenscheinlich ist, müssen zum Beispiel die Ausgänge von Zählern als INOUT definiert werden. Denn damit ein Bit "kippen" kann, muss sein vorheriger Zustand bekannt sein, und dazu muss es halt erstmal gelesen werden! Zusätzlich zu IN, OUT und INOUT gibt es noch BUFFER, was für die Realisierung von internen Bussen benötigt wird. Bei den Vektoren gibt es zwei Möglichkeiten, wie MSB und LSB angeordnet sind, wobei die Schreibweise "(y downto x)" der von "(x to y)" vorzuziehen ist. Und zwar deswegen, dass bei Konstanten wie z.B. "1000" das MSB wie gewohnt links steht. 2.4.Architecture In der "architecture" wird das Verhalten der Komponente beschrieben. Dazu gibt es mehrere Möglichkeiten. Am (meiner Meinung nach) einfachsten ist es, das Verhalten ähnlich einer Programmiersprache zu beschreiben, aber natürlich kann man auch Logikgleichungen etc. verwenden, darauf möchte ich aber jetzt nicht eingehen.
Die hier realisierte architecture trägt den Namen "version1". Diese Bezeichnung ist für die Konfiguration wichtig, da es zu einem Entity auch mehrere Architekturen geben kann, von denen dann letztendlich eine ausgewählt wird. Innerhalb der architecture verwendete Signale gelten nur innerhalb derselben, deshalb fehlt auch eine Richtungsangabe. 2.5.Prozesse und konkurrente Beschreibungen Jede architecture besteht aus sogenannten "konkurrenten Beschreibungen" und "Prozessen", die letztendlich auch als komplexe konkurrente Beschreibungen anzusehen sind. Im Gegensatz zu Programmanweisungen, die z.B. ein Prozessor ausführt, finden alle Prozesse, die ja letztendlich in Hardware realisiert werden sollen,GLEICHZEITIG statt. Jeder Prozess hat eine "sensitivity list", das ist eine Liste von Signalen und/oder Ports (die ja auch Signale darstellen), die Komma-separiert in Klammern zwischen "process" und "is" stehen. Die Liste gibt an, bei welchen Signalen Änderungen bei anderen Signalen hervorrufen. Am einfachsten ist es, alle Signale aufzulisten, die im Prozess als Quelle von Zuweisungen oder in Bedingungen vorkommen. Einen Prozess (und natürlich auch jede andere konkurrente Beschreibung) ist wie eine Endlosschleifezu betrachten, die nur dadurch beendet werden kann, wenn der Strom ausgeschaltet wird oder sich unser CPLD mit kleinen Rauchwölkchen "verabschiedet".
Als Alternative zur sensitivity-list ist auch eine wait-Anweisung am Ende des Prozesses möglich, an der Funktion (oder Nichtfunktion) des Designs ändert das aber nichts, es ist letztendlich Ansichtssache. Unser Beispiel würde dann so aussehen:
2.6.Sequenzielle Beschreibungen Auch wenn es sich hier um sogenannte "sequentielle Beschreibungen" handelt, werden sie trotzdem gleichzeitig ausgeführt. Ähnlich wie in anderen Programmiersprachen muss jede Anweisung mit einem Semikolon abgeschlossen werden. Zuweisungen Die Zuweisung ist zentraler Bestandteil ser meisten Programmiersprachen und natürlich auch von VHDL. Dabei gibt es mehrere Möglichkeiten, die sich in der Schreibweise voneinander unterscheiden. Den größten Unterschied gibt es zwischen Signalen und Variablen, sowie zwischen einzelnen Bits und Vektoren. Einzelne Bits werden in einfachen Anführungszeichen dargestellt (z.B. '0' oder 'Z'), Vektoren in doppelten Anführungszeichen (Gänsefüßchen) "0101". Bei Vektoren ist noch zu beachten, dass die Bitbreite auf beiden Seiten der Zuweisung gleich ist, es können auch Teilvektoren angegeben werden:
Bedingte Anweisung Ein wichtiges Sprachkonstrukt ist die bedingte Anweisung. Also wie "if-then-else". Und anders funktioniert es auch hier nicht. Wenn ein Signal von mehreren Signalen oder Variablen abhängig ist, kommt man um eine Schachtelung oder "elsif" nicht herum. Mehrfache Zuweisungen, auch wenn sie logisch nicht unrichtig sind, mag XST aus den WebPack(tm) leider gar nicht, zumindest wenn Flanken mit ins Spiel kommen. Apropos Flanken: Anstelle mit events zu hantieren ist es einfacher, "rising_edge(signal)" oder "falling_edge(signal)" zu verwenden.
Die elsif- und else-Zweige müssen nicht existieren, elsif kann auch mehrfach vorkommen. Anstelle vieler "elsif" kann auch die im nächsten Abschnitt beschriebene case-Anweisung verwendet werden, die in vielen Fällen dann übersichtlicher sein wird. Aber jetzt noch einmal zu unserem Beispiel:
Das ist eine typische Register-Anweisung. Mit der steigenden Flanke von str wird der Inhalt von iv nach tempout transferiert. Natürlich nur dann, wenn ste gesetzt ist. Da für die Pins Open-Kollektor-Eigenschaften realisiert werden sollen, können wir nicht einfach iv den Pin-Signalen zuweisen, sondern nutzen die architekturweiten Signale tempout. Fallunterscheidung Ein weiteres Sprachkonstrukt ist die Fallnterscheidung. Anstelle vieler elsif-Statements lässt sich so das Verhalten einer Komponente übersichtlich in einer Art Tabelle darstellen. Da im Beispielprojekt erst im Hauptteil eine Fallunterscheidung vorkommt, habe ich einen Dekoder als willkürliches Beispiel gewählt. Dabei sollen "insig(3 downto 0)" und "outsig(5 downto 0)" als Signale bereits definiert sein:
Im Beispiel werden nicht alle Variationen der Eingangssignale auch wirklich genutzt, mit der Variante "others" werden bei allen übrigen Eingangskonstellationen alle Ausgänge auf '0' gesetzt. Sind in verschiedenen Fällen die Ausgangssignale egal, dann kann auch ein Minuszeichen als Zustand angegeben werden:
Beim Umsetzen in CPLD-Logik können so eventuell Produktterme eingespart werden. Schleifen Mit Schleifen kann man sich jede Menge Schreibarbeit sparen, besonders wenn Vektoren verarbeitet werden:
L1 ist ein Label und dient als Sprungmarke für die Schleife. Im vorliegenden Fall wird für alle Elemente des Vektors "tempout" der korrespondierende Pin "extp" auf '0' gesetzt oder hochohmig geschaltet. Dazu dient dann wieder eine bedingte Anweisung. Instanziierung Als vorletzten Punkt des VHDL-Kurses möchte ich noch zeigen, wie man die Komponente ioreg4 in "übergeordneten" Designs verwenden kann. Dabei können Komponenten mehrfach eingesetzt werden, obwohl sie nur einmal definiert wurden. Das nennt man Instanziieren, für jede Stelle, an der die Komponente eingesetzt werden soll, wird eine sogenannte Instanz der Komponente eingesetzt. Über die "port map" Angaben werden die Ports der Komponente mit den Signalen des übergeordneten Designs verbunden. Dabei ist darauf zu achten, dass Bitbreiten und Richtungen der Signale zueinander equivalent sind. Im Beispiel wird die Komponente ioreg4 sechsmal instanziert (I1-I6).
Konfiguration Wie schon weiter oben erwähnt, lassen sich zu einem Entity mehrere Architekturen anlegen. Auch wenn dies nicht genutzt wird, muss eine minimale Konfiguration angegeben werden. Und zwar wird nur die Konfiguration des Top-Level-Elements angegeben. Ohne diese "null-Konfiguration" lässt sich das Design nicht übersetzen! Die Konfiguration sollte sich am Ende des Top-Level-Elements befinden.
Weiterführende Quellen
|