Eine blinkende LED ist das "Hello World!" der Hardware.
Man kann daran erkennen, dass die Versorgung funktioniert, die Logik arbeitet und anhand der Blinkfrequenz einen Zähler beurteilen. Also schon einiges, was so eine einzelne Funzel einem sagen kann
Leider funktioniert diese einfache Variante eines Blinklichts nur in der Simulation:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity BlinkLED is
Port (led : buffer STD_LOGIC := '0');
end BlinkLED;
architecture Behavioral of BlinkLED is
begin
led <= not led after 500 ms; -- LED alle 500 ms toggeln
end Behavioral;
Warum geht das nicht? Es gibt keine "einstellbaren Verzögerungsglieder" (ähnlich wie Monoflops oder RC-Glieder...) im FPGA. Alles, was mit Zeiten zu tun hat, muss in einem FPGA als Zähler aufgebaut werden, der einen Takt mitzählen und beim Erreichen eines passenden Endwertes eine Reaktion auslöst.
Also brauchen wir einen Takt und einen Zähler. In der Regel ist am FPGA ein Quarz mit z.B. 50MHz angeschlossen. Für eine halbe Sekunde müssen also 25 Millionen Takte gezählt werden. Das ergibt eine Registerkette mit 25 Bits (mit diesen 25 Bit wären maximal 2^25=33554432 Zählschritte möglich).
Und diese 25 Register werden mit einem festen Wert 24999999 verglichen. Warum 24999999 und nicht 25000000?
Weil 0...24999999 schon 25000000 Schritte sind. Einfacher wird es vielleicht mit kleineren Zahlen: ein Zähler, der 5 Schritte abzählen muß, braucht die Zahlen 0, 1, 2, 3 und 4.
Also zusammengefasst:
Ein Takt geht auf einen Zähler, der mit einem festen Wert verglichen wird, bei dessen Erreichen zurückgesetzt wird und gleichzeitig eine Aktion auslöst: die LED wird getoggelt.
Hier ist der VHDL-Quellcode für so ein Blinklicht:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity BlinkLED is
Port ( clk : in STD_LOGIC;
led : out STD_LOGIC);
end BlinkLED;
architecture Behavioral of BlinkLED is
signal c : integer range 0 to 24999999 := 0; -- 0,5s bei 50MHz fosc
signal x : std_logic:= '0';
begin
process begin
wait until rising_edge(clk); -- warten bis zum nächsten Takt
if (c<24999999) then -- 0…24999999 = 25000000 Takte = 1/2 Sekunde bei 50MHz
c <= c+1; -- wenn kleiner: weiterzählen
else -- wenn Zählerende erreicht:
c <= 0; -- Zähler zurücksetzen
x <= not x; -- und Signal x togglen
end if;
end process;
led <= x; -- Signal x an LED ausgeben
end Behavioral;
Zur Funktionsweise:
mit einem Zähler werden 25 Millionen Takte eines 50 MHz Taktes abgezählt (also 0..24999999). Wenn der Zählerendwert erreicht wurde, wird der Zähler zurückgesetzt und gleichzeitig ein lokales Signal x getoggelt. Dieses Signal wird dann an den led Ausgang gegeben.
Das lokale Signal x brauchen wir, weil der Port led als out deklariert ist, und daher entsprechend den VHDL Richtlinien nicht lesbar ist.
Und um den Schlauen zuvorzukommen: Nein, es ist keine gute Idee, den Port led als buffer oder inout zu deklarieren, nur damit er zurückgelesen werden und so ein lokales Signal gespart werden kann. Das ist eher ein Kündigungsgrund oder sollte wenigstens einen strengen Verweis in der Personalakte nach sich ziehen...
Als kleiner Lichtblick: ab VHDL 2008 können auch out Ports wieder zurückgelesen werden, diese Ports verhalten sich dann ähnlich wie buffer. So ist es dann möglich, z.B. einen Zähler direkt auf einen out Port zu erzeugen.
Und hier die kompakte Stimuli-"Testbench":
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity tb_BlinkLED is
-- leere Entity --> Testbench
end tb_BlinkLED;
architecture behavior of tb_BlinkLED is
component BlinkLED
port( clk : IN std_logic;
led : OUT std_logic );
end component;
signal clk : std_logic := '0'; -- lokale Signale der Testbench
signal led : std_logic; -- werden an den Prüfling angeschlossen
begin
uut: BlinkLED -- der Prüfling wird verdrahtet
port map ( clk => clk,
led => led );
clk <= not clk after 10 ns; -- einen 50MHz Takt erzeugen
end;
Wer dieses Blinklicht zum ersten Mal mit ISE realisieren will, der kann sich die Schritt-für-Schritt Anleitung HIER mal ansehen.
Für alle, die noch nicht genug haben kommt hier der Binärzähler auf 8 LEDs.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity BinaryLED is
Port ( clk : in STD_LOGIC;
leds : out STD_LOGIC_VECTOR(7 downto 0));
end BinaryLED;
architecture Behavioral of BinaryLED is
signal c : integer range 0 to 12499999 := 0; -- 0,25s bei 50MHz fosc
signal x : unsigned (7 downto 0) := (others=>'0');
begin
process begin
wait until rising_edge(clk); -- warten bis zum nächsten Takt
if (c<12499999 ) then -- 0..12499999 = 12500000 Takte = 1/4 Sekunde bei 50MHz
c <= c+1; -- wenn kleiner: c weiterzählen
else -- wenn Zählerende erreicht:
c <= 0; -- Zähler c zurücksetzen
x <= x+1; -- und Zähler x hochzählen
end if;
end process;
leds <= std_logic_vector(x); -- Signal x an LEDs ausgeben
end Behavioral;
Im Großen und Ganzen ist das Beispiel gleich wie das obere. Nur wird hier statt der einzelnen LED ein unsigned-Vektor hochgezählt. Das Hochzählen von Vektoren hat den Vorteil, dass der Überlauf von 11111111bin = 255dez nach 00000000bin = 0dez automatisch passiert.
Ich hätte den Zähler x natürlich wie den Zähler c auch als integer machen können, dann hätte der Code (für eine fehlerfreie Simulation) aber so aussehen müssen:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity BinaryLED is
Port ( clk : in STD_LOGIC;
leds : out STD_LOGIC_VECTOR(7 downto 0));
end BinaryLED;
architecture Behavioral of BinaryLED is
signal c : integer range 0 to 12499999 := 0; -- 0,25s bei 50MHz fosc
signal x : integer range 0 to 255 := 0;
begin
process begin
wait until rising_edge(clk); -- warten bis zum nächsten Takt
if (c<12499999 ) then -- 0..12499999 = 12500000 Takte
c <= c+1; -- wenn kleiner: c weiterzählen
else -- wenn Zählerende erreicht:
c <= 0; -- Zähler c zurücksetzen
if (x<255) then -- ist Zähler x schon "oben" angekommen
x <= x+1; -- nein: Zähler x hochzählen
else
x <= 0; -- ja: zurücksetzen
end if;
end if;
end process;
-- Signal konvertieren und casten und an LEDs ausgeben
leds <= std_logic_vector(to_unsigned(x,8));
end Behavioral;
Der nächste logische Schritt ist dann, die Erzeugung des Fortschaltimpulses in einem eigenen Prozess zu erzeugen.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity BinaryLED is
Port ( clk : in STD_LOGIC;
leds : out STD_LOGIC_VECTOR(7 downto 0));
end BinaryLED;
architecture Behavioral of BinaryLED is
signal c : integer range 0 to 12499999 := 0; -- 0,25s bei 50MHz fosc
signal t : std_logic := '0';
signal x : integer range 0 to 255 := 0;
begin
process begin
wait until rising_edge(clk); -- warten bis zum nächsten Takt
if (c<12499999 ) then -- 1/4 Sekunde bei 50MHz
c <= c+1; -- wenn kleiner: c weiterzählen
t <= '0';
else -- wenn Zählerende erreicht:
c <= 0; -- Zähler c zurücksetzen
t <= '1'; -- Flag für Tick setzen
end if;
end process;
process begin
wait until rising_edge(clk); -- warten bis zum nächsten Takt
if (t='1') then -- sind schon wieder 0,25 sec vorbei?
if (x<255) then -- ist Zähler x schon "oben" angekommen
x <= x+1; -- nein: Zähler x hochzählen
else
x <= 0; -- ja: zurücksetzen
end if;
end if;
end process;
-- Signal konvertieren und casten und an LEDs ausgeben
leds <= std_logic_vector(to_unsigned(x,8));
end Behavioral;
Wie man klar sieht, gibt es in diesem Design nur und genau einen einzigen Takt clk. Das ist der Quarzoszillator, der aussen am FPGA angeschlossen ist. Mit diesem Takt läuft das ganze Design. Für Anfänger gilt:
Der Takt ist wie der Highlander: es kann nur einen geben.