Ein kleines VHDL-Intro

Im Folgenden möchte ich ein bisschen erklären, wie man in VHDL programmiert, wobei ich mich auf Konstrukte beschränke, die auch in CPLD's synthetisierbar sind. Als Beispiel dien mcio24, eine Porterweiterung für Mikrocontroller. Ich habe vieles weggelassen, was für die Entwicklung von CPLD's nicht unbedingt notwendig ist, so zum Beispiel alle Dinge, die das Timing betreffen. Im Gegensatz zu FPGA's ist die Signallaufzeit in CPLD's immer gleich, egal welche Makrozellen und Register benutzt werden.

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.

--------------------------------------------------------------------------------
-- Company: 
-- Engineer:		Joerg Wolfram
--
-- Create Date:    	26.02.2006
-- Design Name:    
-- Module Name:		ioreg4_01    
-- Project Name:   
-- Target Device:  
-- Tool versions:  
-- Description:		4 bits latch register with "open collector" output 
--                      and feedback
--
-- Revision:		1.0
-- License:		GPL
--------------------------------------------------------------------------------


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.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;


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":

entity ioreg4 is
    port ( str : in std_logic;
    ste : in std_logic;
    iv : in std_logic_vector(3 downto 0);
    ov : out std_logic_vector(3 downto 0);
    extp : inout std_logic_vector(3 downto 0));    
end entity ioreg4;


Unsere Komponente hat den Namen "ioreg4", danach werden die Ports definiert:

Portsignal Bitbreite Richtung Funktion
str 1 IN Strobe-signal, übernimmt die an iv anliegenden Signale in die Speicher
ste 1 IN Freigabe für das Strobe-Signal
iv 4 IN IN-Vektor, Signale, die an den Pins ausgegeben werden sollen
ov 4 OUT OUT-Vektor, Signale, die von den Pins gelesen werden
extp 4 INOUT INOUT-Vektor, diese Signale werden später mit den Pins verbunden

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.

architecture version1 of ioreg4 is
signal tempout: std_logic_vector(3 downto 0);
begin

 ...
     
end architecture version1;



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

process (str,ste,iv,extp) is
variable count: integer;
begin

    ...

end process;



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:

process
variable count: integer;
begin

    ...

wait on str,ste,iv,extp;
end process;




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:

Zuweisungsart Beispiel
Konstantenzuweisung für ein Bit-Signal C <= 'Z'
Konstantenzuweisung für eine Bit-Variable D := '1'
Konstantenzuweisung für ein Bitvektor-Signal E <= "0010"
Partielle Konstantenzuweisung für ein Bitvektor-Signal E(3 downto 2)="11"
Konstantenzuweisung für eine Integer-Variable Var := 4

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.

if Bedingung
then
    Anweisungen;
elsif Bedingung
    Anweisungen
else
    Anweisungen
end if;            


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:

if rising_edge(str)
then
    if ste = '1'
    then 
	tempout <= iv;
    end if;
end if; 



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:

case insig is
   when "000" => outsig <= "000001";
   when "000" => outsig <= "000010";
   when "000" => outsig <= "000100";
   when "000" => outsig <= "001000";
   when "000" => outsig <= "010000";
   when "000" => outsig <= "100000";
   when others => outsig <= "000000";
end case; 


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:
 when others => outsig <="----"

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:     for count in 0 to 3 loop
            if tempout(count)='0'
            then
                extp(count) <= '0';
            else
                extp(count) <= 'Z';
            end if;
        end loop L1;


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


entity mylogik is
port (  mcl : inout std_logic_vector(3 downto 0);
        mch : in std_logic_vector(3 downto 0);
        tstrobe : in std_logic;
        tso     : out std_logic;
        myio    : inout std_logic_vector(23 downto 0));
end entity mylogik;

architecture version1 of mylogik is
signal nummer,setout : std_logic;
signal insig1 : std_logic_vector(3 downto 0);
signal insig2 : std_logic_vector(3 downto 0);
signal insig3 : std_logic_vector(3 downto 0);
signal insig4 : std_logic_vector(3 downto 0);
signal insig5 : std_logic_vector(3 downto 0);
signal insig6 : std_logic_vector(3 downto 0);
signal tmpsig : std_logic_vector(3 downto 0);
signal decoded : std_logic_vector(5 downto 0);

component ioreg4 is
    port ( str : in std_logic;
    ste : in std_logic;
    iv : in std_logic_vector(3 downto 0);
    ov : out std_logic_vector(3 downto 0);
    extp : inout std_logic_vector(3 downto 0));
end component ioreg4;

begin
    I1: ioreg4 port map (iv => mcl, ov => insig1, extp => myio(3 downto 0), 
    str => tstrobe, ste => decoded(0));
    I2: ioreg4 port map (iv => mcl, ov => insig2, extp => myio(7 downto 4),
    str => tstrobe, ste => decoded(1));
    I3: ioreg4 port map (iv => mcl, ov => insig3, extp => myio(11 downto 8),
    str => tstrobe, ste => decoded(2));
    I4: ioreg4 port map (iv => mcl, ov => insig4, extp => myio(15 downto 12),
    str => tstrobe, ste => decoded(3));
    I5: ioreg4 port map (iv => mcl, ov => insig5, extp => myio(19 downto 16),
    str => tstrobe, ste => decoded(4));
    I6: ioreg4 port map (iv => mcl, ov => insig6, extp => myio(23 downto 20), 
    str => tstrobe, ste => decoded(5));
    ...
    
end architecture version1;  


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.

configuration main of mylogik is
    for version1
    end for;
end configuration main;


Weiterführende Quellen
  1. Bei Xilinx gibt es verschiedene Application-Notes für die Gestaltung von CPLD-Designs.
  2. VHDL Kompakt, eine sehr gute deutschsprachige Referenz zu VHDL von Andreas Mäder.