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