Erläuterungen

Hier werden die neuen VHDL-Elemente noch einmal etwas detaillierter beschrieben.

Signalnamen und Nomenklatur

Signalnamen sind in VHDL nicht case sensitiv. Sie entsprechen ansonsten in etwa den Beschränkungen wie Variablennamen in C.

In meinen Designs verwende ich folgende Nomenklatur:

Hardware Generierungs-Skripte
SIGNAL_NAME IO-Signal eines FPGAs (nur das Top-Level-Modul)
SignalNameC Internes kombinatorisches Signal in einem VHDL Modul 
SignalNameR Internes Register-Signal in einem VHDL Modul 
SignalName IO-Signal eines VHDL-Moduls (nicht das Top-Level-Modul)

Warum eigentlich diese Kategorisierung?

IO-Signale des FPGAs kommen durch die IO-Zellen in das FPGA bzw. verlassen dieses durch IO-Zellen. Damit ist für diese Signale eine andere Hardware vorhanden wie für interne Signale. Meistens muss darauf Rücksicht genommen werden.

Kombinatorische Signale kommen aus Lookup-Tables und haben unter Umständen schon einiges an Zeit zu deren Erzeugung hinter sich. Wenn man kombinatorische Signale zur Erzeugung weiterer kombinatorischer Signale verwendet, erhöht das die Laufzeit und kann im schlimmsten Fall zu kombinatorischen Schleifen führen.

Register Signale kommen eben direkt aus dem Register und haben damit nicht die potentiellen Probleme der kombinatorischen Signale.

IO-Signale des VHDL-Moduls unterliegen Beschränkungen durch das Richtungs-Attribut. out-Signale können z.B. nicht auf der rechten Seite einer Signalzuweisung stehen. Auch wenn IO-Signale immer Registersignalen entsprechen (sollten), kann man sich eben nicht sicher sein.

Der Typ std_logic_vector

Hier bei handelt es sich um ein Array von std_logic Signalen. Normalerweise wird er als (upperIndex downto lowerIndex) deklariert.

Wichtige Operationen mit std_logic_vector

Logisch not, and, nand, or, xor und xnor
Diese Operationen werden bitweise ausgeführt.
Extraktion (n) Das Bit n als std_logic Signal
(m downto n) Die Bits m..n als std_logic_vector Signal
Verknüpfung & a & b hängt das Signal b hinter das Signal a
a und b können dabei vom Typ std_logic und std_logic_vector sein.
Beispiele zur
Extraktion und Verknüpfung
ShiftR(6 downto 0) & ShiftR(7) Rotiert ein Bits nach links
'0' & ShiftR(7 downto 1) 8 Bit Schieberegister (nach rechts)
Byte1 & Byte0 Verknüpft die beiden Bytes zu einem Wort
Arithmetisch + -

Prozesse

Prozesse sind ein zentrales Element von VHDL. Während alle Anweisungen und Prozesse in VHDL gleichzeitig ablaufen, sind die Anweisungen innerhalb eines Prozesses sequentiell.

Im Prinzip ist ein Prozess eine Endlosschleife, die allerdings ruht. Erst wenn sich ein Signal aus der Sensitivitätsliste ändert wird ein Durchgang durch  den Prozesscode gestartet. Anschließend ruht der Prozess wieder.

Beispiel Prozess

    p_simple : process (Clk)
    begin
        if rising_edge(Clk) then
            InitR       <= InitC;
        end if;
    end process;

Registerprozess

In diesem getakteten Prozess ist der Takteingang Clk das einzige Signal in der Sensitivitätsliste. Auf andere Ereignisse als die steigende Flanke wird nicht reagiert.

Das bedeutet, dass hier eines oder mehrere Register synthetisiert werden sollen.

Sensitivitätslisten

Die Argumente in Klammern nach dem Schlüsselwort process bestimmen worauf ein Prozess reagiert.

Der Ausgang InitR ist unabhängig von Änderungen von InitC. So lange keine steigende Clk-Flanke kommt, wird dieser Eingang ignoriert. Da der Änderungszeitpunkt von InitC keinen Einfluss auf InitR  hat, gehört InitC also nicht in die Sensitivitätsliste.


Bei kombinatorischen Prozessen dagegen tauchen alle Eingangssignale in Sensitivitätsliste auf:

    --! Multiplexer
    p_mux : process (FreqGenR, CtrlFreqR)
    begin
        case CtrlFreqR is
            when  0 =>  LedC <=  FreqGenR(0);
            when  1 =>  LedC <=  FreqGenR(1);
            when  2 =>  LedC <=  FreqGenR(2);
            when  3 =>  LedC <=  FreqGenR(3);
            when  4 =>  LedC <=  FreqGenR(4);
            when  5 =>  LedC <=  FreqGenR(5);
            when  6 =>  LedC <=  FreqGenR(6);
            when  7 =>  LedC <=  FreqGenR(7);
            when  8 =>  LedC <=  FreqGenR(8);
            when  9 =>  LedC <=  FreqGenR(9);
            when 10 =>  LedC <=  FreqGenR(10);
            when 11 =>  LedC <=  FreqGenR(11);
            when 12 =>  LedC <=  FreqGenR(12);
            when others => LedC <= 'X';
        end case;
    end process;

Register

Register sind das zentrale Element getakteter Schaltungen und damit natürlich auch von FPGAs. Die Register sind das Gedächtniss eines FPGAs. Ohne sie könnten keine komplexen Funktionen realisiert werden.

Register sind die Grundlage für Zähler, Zustandsmaschinen, einfache Speicher etc.

Einfache Register

Die einfachste Form eines Registers (D-Flipflop) übernimmt zur steigenden Taktflanke die Eingangsdaten. Dies wird in VHDL folgendermaßen codiert:

p_dff : process (Clk)
begin
   if rising_edge(Clk) then
      Ausgang <= Eingang; 
   end if;
end process;
D Flip Flop

Dieser Prozess hat einen Namen p_dff und eine Sensitivitätsliste (Clk).

Wenn sich ein Signal in der Sensitivitätsliste ändert (und nur dann), wird der zugehörige Prozess ausgeführt.

Falls die Änderung eine steigende Flanke war, wird die Anweisung Ausgang <= Eingang; ausgeführt (der Wert von Eingang wird im Signal Ausgang gespeichert). Also exakt das, was ein D-Flipflop macht.

Natürlich muss nicht für jedes einzelne Register so ein Prozess erstellt werden. Alle Zuweisungen innerhalb dieses if-Konstrukts führen zur Synthese von D-Flipflops.

Register mit synchronem Reset

Zu einem Register gehört natürlich normalerweise auch ein definiertes Verhalten für den Reset. Dadurch wird sichergestellt dass der Zustand dieses Registers nach Power Up oder nach Reset definiert ist.

Die Reset kann synchron oder asynchron erfolgen, wobei der synchrone Reset zu bevorzugen ist, denn dadurch wird sichergestellt, dass der Reset alle betreffenden Register gleichzeitig erreicht und auch wieder freigibt.

Asynchrone Reset-Bedingungen können an einer einzigen Stelle einsynchronisiert werden (durch ein D-Flip-Flop) und stehen anschließend als synchrone Resets zur Verfügung.

Ein Register mit synchronem Reset wird in VHDL folgendermaßen codiert:

p_dff_res : process (Clk)
begin
   if rising_edge(Clk) then
      if (Init = '1') then
         Ausgang <= '0';
      else
         Ausgang <= Eingang;
      end if;
   end if;
end process;
D Flip Flop mit synchronem Reset

Auch dieses Register ändert nur bei der steigenden Flanke von Clk seinen Zustand. Bevor aber das Datum übernommen wird, wird erst auf die Reset-Bedingung (Init) getestet und dementsprechend bei aktivem Init das Register zurückgesetzt.

Ein Register mit synchronem Preset wäre identisch, nur würde Ausgang <= '1'; gesetzt werden

Register mit asynchronem Reset

Ist der Reset asynchron, wirkt er sich sofort auf das Register aus. Dies ist vor allem dann notwendig, wenn noch kein Takt anliegt mit dem man den Reset einsynchronisieren könnte. Da sich dadurch aber Register-Werte nicht nur zur Taktflanke ändern können, führt das beim Timing zu Problemen. Z.B. wirkt sich der Reset nicht garantiert zur gleichen Taktflanke auf alle angeschlossenen Register aus.

Deswegen sollte man asynchrone Resets so wenig wie möglich verwenden. Gerade aber für den Reset der Taktaufbereitung ist er aber durchaus sinnvoll.

Ein Register mit asynchronem Reset wird in VHDL folgendermaßen codiert:

p_dff_ares : process (Clk, Reset)
begin
   if (Init = '1') then
      Ausgang <= '0';
   elsif rising_edge(Clk) then
      Ausgang <= Eingang; 
   end if;
end process;
D Flip Flop mit asynchronem Reset

In der Sensitivitätsliste des Prozesses ist nun auch Reset aufgeführt, da sich ja eine Zustandsänderung von Reset direkt auf Q auswirkt. Wie man erkennen kann, hat jetzt Reset Priorität gegenüber Clk (da es ja zuerst ausgewertet wird).

Das Symbol für das Flip Flop ist übrigens das gleiche. Im FPGA wird durch Konfigurationsbits festgelegt, ob der Reset synchron oder asynchron erfolgt und ob er den Ausgang setzt oder zurücksetzt.

Register mit synchronem Reset und Clock Enable

Register in FPGAs besitzen auch einen dedizierten Clock-Enable-Eingang. Nur falls der Clock Enable aktiv (oder nicht angeschlossen) ist, wird der Eingangswert übernommen.

Der Clock Enable macht oft Sinn, da ein Register oft nur unter gewissen Umständen seinen Inhalt wechseln soll (z.B. wenn es von der CPU beschrieben werden soll).

Ein Register mit synchronem Reset und Clock Enable wird in VHDL folgendermaßen codiert:

p_dff_ce : process (Clk)
begin
   if rising_edge(Clk) then
      if (Init = '1') then
         Ausgang <= '0';
      elsif (CE = '1') then
         Ausgang <= Eingang; 
      end if;
   end if;
end process;

DFF mit synchronem Reset und Clock Enable

Wie man hier sieht ist das Flip-Flop voll synchron, wechselt also nur zur steigenden Taktflanke den Zustand.

Der Init-Eingang ist vom Clock Enable unabhängig. Der Zustand des Ausgangs wird aber nur vom Eingang übernommen, wenn der Clock Enable aktiv ist.

Zeitverhalten über Clock Enable steuern

Durch den Clock Enable ist es zum Beispiel möglich, Teile des FPGAs zu verlangsamen indem, unabhängig vom angelegten Takt, der Clock Enable nur in den gewünschten zeitlichen Abständenden aktiviert ist.

Die Case Anweisung

case <Signal> is
    when <Wert1> => {Anweisungen 1}
    when <Wert2> => {Anweisungen 2}
    ...

    when others => {Anweisungen others}
end case;

Je nach Signalwert werden andere Anweisungen ausgeführt. Je nach Kontext werden dabei unterschiedliche Schaltungen synthetisiert (Multiplexer, Clock Enables, State Machines).

Die Anweisungen in den einzelnen Zweigen sollten stark miteinander in Beziehung stehen (siehe die entsprechenden Beispiele im Index).

When others ist der "default"-Zweig. Er sollte immer angegeben werden. Wenn keine Sonderbehandlung notwendig ist, wird ein ; als Anweisung verwendet.

Multiplexer synthetisieren

Um reine Multiplexer zu synthetisieren verwendet man einen nicht getakteten Prozess:

p_mux : process (sel, a, b, c, d)
begin
    case sel is
        when "00" => muxOutC <= a;
        when "01" => muxOutC <= b;
        when "10" => muxOutC <= c;
      --when "11" => muxOutC <= d;
        when others => muxOutC <= 'X';
      --when others => muxOutC <= '0';
    end case;
end process;

Ganz wichtig hierbei ist das der others Zweig vorhanden ist. Normalerweise wird hier ein Default-Wert oder 'X' angegeben. Im Falle von 'X' wird die Synthese den Wert erzeugen, der am wenigsten Hardware-Ressourcen kostet.

Würde der Default-Zweig fehlen darf sich die Ausgangsvariable im Falle nicht aufgeführter Kombinationen nicht ändern.

Damit hat man aber implizit einen Speicher erzeugt, und das ohne Takt. Es wird also versucht ein Latch zu synthetisieren. Der Zeitpunkt zu dem sich der Ausgangswert des Latches ändert ist nicht synchron zum Takt. Das sollte auf jeden Fall vermieden werden!

File IO mit der Testbench

Von der Testbench aus kann man auch auf Dateien zugreifen. Dies kann Verwendung finden für:

  • Testvektoren/Testdaten einlesen
  • Erwartete Ergebnisse einlesen und damit vergleichen
  • Testreports erstellen

In diesem Projekt wird der File IO zur Erstellung eines Reports verwenden.

Dazu wird eine Datei-Variable vom Typ text und eine Report Funktion verwendet

    file report_file    : text;

    --! print to output file
    procedure tb_report(constant report_text : in string) is
        variable line_out: line;
    begin
        write(line_out,report_text);
        writeline(report_file, line_out);
    end tb_report;

und im Testprozess wird diese Datei geöffnet, dorthin ausgegeben und sie wieder geschlossen.

        -- prepare reporting
        file_open(report_file,"../../../TestBlinkingLed.log",WRITE_MODE);
        tb_report("Testing BlinkingLed:");
        tb_report("====================");
        
...

        if (TestErr = '0') then
            tb_report("Test completed successfully");
        else
            tb_report("Test completed with error(s)");
        end if;
        file_close(report_file);

Somit kann man einen übersichtlichen Report erstellen.

Generics

Generics sind in etwa ein Pendant für #defines in C.

Generics werden parallel zu den IOs eines VHDL-Modul angegeben. Es handelt sich dabei um Parameter für die Synthese.

Verwendet man Generics, beeinflusst man direkt die Synthese, denn im Gegensatz zu Signalen können sich Generics für ein Design nicht ändern.

Generics definieren

entity BlinkingLed is
    generic (
        BoardIdx    : integer  := 0; -- 0: ZYBO, 1: ARTY
        Simulation  : boolean  := false
    );
    port (
        EXT_CLK     : in  std_logic; --! 125 MHz system clock
        BTN         : in  std_logic_vector(3 downto 0); --! Button inputs, high acitve
        LED         : out std_logic --! LED output, high acitve
    );
end BlinkingLed;

Ein Generic kann einen Default-Wert haben. Wenn ein VHDL-Modul instanziiert wird ohne das Generic explizit anzugeben, wird der Default-Wert verwendet.

Generics verwenden

    uut: BlinkingLed
        generic map (
            BoardIdx    => BoardIdx,
            Simulation  => true
        )
        port map (
            EXT_CLK     => Ext_Clk,
            BTN         => Button,
            LED         => Led
        );

Generics werden verbunden wie die IO-Signale. Sie können jedoch nur mit eigenen Generics und konstanten Ausdrücken verbunden werden.

Weiter zu den Übungen