Projekt BlinkingLed: Einführung in getaktete Schaltungen, Register und Zähler
Was sind getaktete Schaltungen?
In der Hardware-Welt, wie bei einem FPGA (einem Chip, den wir programmieren können), arbeiten Schaltungen oft mit einem Takt – einer Art „Herzschlag“. Dieser Takt ist ein Signal, das regelmäßig zwischen „0“ und „1“ wechselt, z. B. 100 Millionen Mal pro Sekunde (100 MHz). Eine getaktete Schaltung bedeutet, dass Änderungen (z. B. das Ein- oder Ausschalten einer LED) nur passieren, wenn der Takt „tickt“ – genauer gesagt, bei einer steigenden Flanke (wenn das Takt-Signal von „0“ zu „1“ wechselt). Das sorgt dafür, dass alles in der Schaltung synchron passiert, wie bei einem Orchester, das im Takt spielt.
Was sind Register in FPGAs?
Ein Register ist ein Speicher, der einen Wert (z. B. „0“ oder „1“) festhalten kann, bis der nächste Takt kommt. In FPGAs bestehen Register aus Flip-Flops. Ein Flip-Flop speichert einen Wert und aktualisiert ihn nur, wenn der Takt tickt. Zum Beispiel:
- Ein Register kann den Zustand einer LED speichern (an oder aus).
- Ein Register kann auch eine Zahl speichern, z. B. „5“, und diese Zahl bei jedem Takt ändern (z. B. zu „6“).
In VHDL schreiben wir ein Register mit einem Prozess, der auf den Takt reagiert. Zum Beispiel:
p_simple : process(Clk)
begin
if rising_edge(Clk) then
mein_register <= neuer_wert; -- Speichere den neuen Wert
end if;
end process;
Hier ist clk der Takt, mein_register das Register (z. B. ein Signal) und neuer_wert der Wert, den wir speichern wollen.
Was ist ein process?
Ein process in VHDL ist ein Block, in dem du beschreibst, wie sich deine Schaltung verhalten soll. Stell dir vor, ein process ist wie ein kleines Programm, das in deinem FPGA läuft. Innerhalb eines process schreibst du Anweisungen, die gleichzeitig ausgeführt werden, wenn bestimmte Bedingungen erfüllt sind. Es ist ein zentraler Baustein, um Logik in VHDL zu definieren.
Die Signale in Klammern hinter dem process-Schlüsselwort heißen Sensitivitätsliste. Sie sagen dem process, wann er „aufwachen“ und seine Anweisungen ausführen soll.
Hier z. B. enthält die Sensitivitätsliste nur Clk. Das bedeutet: Der process wird jedes Mal ausgeführt, wenn sich Clk ändert – also bei jeder steigenden oder fallenden Flanke des Takts. neuer_wert wird also die meiste Zeit über ignoriert und nur bei Änderung von Clk interpretiert. Durch die weitere Logik im Prozess, wird auch nur auf die steigende Flanke reagiert, womit ein normales Daten-Register implementiert wurde. Der Prozess oben ist also ein getakteter Prozess.
Es gibt auch logische Prozesse, in unserem Beispiel einen Multiplexer:
p_mux : process (FreqGenR, FreqSelR)
begin
case FreqSelR is
when "0000" => LedC <= FreqGenR(0); -- 250 Hz
when "0001" => LedC <= FreqGenR(1); -- 125 Hz
...
when "1100" => LedC <= FreqGenR(12); -- 1/16 Hz
when others => LedC <= '0';
end case;
end process;
Hier sind ToggleCntR und FreqSelR in der Sensitivitätsliste. Das bedeutet, der process wird ausgeführt, sobald sich eines dieser Signale ändert.
Warum beide Signale? Weil LedC von ToggleCntR und FreqSelR abhängt. Wenn sich z. B. FreqSelR ändert (weil du Taster 2 gedrückt hast), muss der process neu ausgeführt werden, um LedC zu aktualisieren. Weil LedC auch nur von diesen beiden Signalen abhängt, wird dieser Prozess in kombinatorische Logik (also ohne Register) übersetzt.
Angenommen, man würde FreqSelR in der Sensitivitätsliste vergessen, würde der Prozess nur ausgeführt, wenn sich ToggleCntR ändert. Änderungen von FreqSelR alleine dürfen sich nicht auf LedC auswirken (wie zuvor Änderunen von neuer_wert alleine sich nicht auf mein_register auswirken.
Jetzt müsste ein Register (in diessem Fall Latch genannt) eingebaut werden, welches LedC auf den Wert festnagelt, der bei der Änderung von FreqGenR erzeugt wird. Das ist ganz schlecht und wird unter Gated Clocks genauer erläutert. Das Timing des FPGAs wäre ein Problem, aber die LED würde blinken.
Noch schlimmer wäre es, wenn man FreqGenR weglassen würde, also nur FreqSelR in der Liste hätte. dann würde die LED nicht mehr blinken, da sich ihr Zustand ja nur ändert, wenn ein Taster gedrückt wird. Also mit 50 % Wahrscheinlichkeit, würde dann eim Tastendruck sich die LED umschalten und ansonsten würde nichts passieren.
Registertypen
Die Register in den FPGAs haben außer dem Dateneingang aber noch weitere wichtige Eingänge. Clock Enable und Reset. Bei den Resets unterscheidet man zwischen synchron und asynchron. Da alles asynchrone nur Ärger macht, verwenden wir normalerweise den synchronen Reset, den ich Init nenne. Damit wird ein Register eben auf einen bestimmten Wert - synchron zum Takt - initialisiert.
Hier sind Codebeispiele für diese Eingänge:
--! Register with synchronous reset
p_sync : process (Clk)
begin
if rising_edge(Clk) then
if (InitR = '1') then
ToggleCntR <= (others => '0');
else
ToggleCntR <= ToggleCntR + 1;
end if;
end if;
end process;
--! Register with synchronous reset and clock enable
p_ce : process (Clk)
begin
if rising_edge(Clk) then
if (InitR = '1') then
TriggerR <= '0';
elsif (EnableC = '1') then
TriggerR <= TriggerC;
end if;
end if;
end process;
Kombinatorische und getaktete Signale:
Es ist wichtig, zwischen kombinatorischen und getakteten Signalen zu unterscheiden. In meinen VHDL-Programmen mache ich das mit einem C bzw. R am Ende des Signalnamens.
Getaktete Signale sind ab Anfang eines Taktes stabil, da sie ja durch den Takt erzeugt werden. Diese getakteten Signale müssen jetzt verarbeitet werden. Z. B. ResetCounterC <= (CounterR = 42);
Um ResetCounterC zu erzeugen müssen die einzelnen Bits des Zählers mit den Bits von 42 verglichen werden. Das geschieht in den Lookup-Tables im FPGA. Diese Signale sind aber erst etwas später gültig, also verzögert gegenüber den getakteten Signalen. ResetCounterC könnte jetzt noch in einer anderen Bedingung eingesetzt werden um dann irgendwann einmal wieder ein Ergebnis in ein Register zu schreiben. Das entsprechende Signal muss aber zwingend vor der nächsten Taktflanke stabil sein, sonst kommt das Orchester aus dem Takt.
Durch die Nomenklatur mit C und R erkennt man beim Programmieren sofort, ob man sich Gedanken über die Signallaufzeiten machen muss (C) oder nicht (R).
Bei komplexen Verarbeitungen ist es oft sinnvoll Zwischenergebnisse in Registern zu speichern und diese dann im nächsten Takt weiter zu verarbeiten. Das nennt sich Pipelining und wird von den allermeisten Prozessoren benutzt.
Wie realisiert man einen Zähler?
Ein Zähler ist eine Schaltung, die bei jedem Takt eine Zahl hochzählt (z. B. 0, 1, 2, 3, ...). Wir benutzen Zähler, um Zeit zu messen oder einen schnellen Takt in einen langsameren umzuwandeln. Zum Beispiel: Wenn unser Takt 100 MHz ist (100 Millionen Ticks pro Sekunde), können wir zählen, wie viele Ticks 1 Sekunde dauern, um eine LED mit 1 Hz (1 Mal pro Sekunde) blinken zu lassen.
Ein Zähler in VHDL sieht so aus:
signal zaehler : integer := 0; -- Unser Zähler startet bei 0
process(Clk)
begin
if rising_edge(Clk) then
zaehler <= zaehler + 1; -- Bei jedem Takt um 1 erhöhen
end if;
end process;
- zaehler ist ein Register, das die Zahl speichert.
- Bei jedem Takt wird zaehler um 1 erhöht.
Wenn wir z. B. bis 99.999.999 zählen (weil 100 MHz × 1 Sekunde = 100 Millionen Ticks), können wir sagen: „1 Sekunde ist vorbei“, und die LED toggeln (ein-/ausschalten). Danach setzen wir den Zähler zurück auf 0 und zählen wieder.
Was ist ein std_logic_vector?
Bisher haben wir mit std_logic gearbeitet, das ein einzelnes Bit repräsentiert – also entweder „0“ oder „1“. Aber was, wenn wir mehr als ein Bit speichern wollen, z. B. eine Zahl mit 26 Bits? Dafür benutzen wir std_logic_vector. Ein std_logic_vector ist wie eine Reihe von Bits – ein Vektor (oder Array) von std_logic-Werten.
Stell dir vor, ein std_logic_vector ist wie eine Kette von Schaltern, die jeweils „0“ oder „1“ sein können. Zum Beispiel:
- Ein std_logic_vector(3 downto 0) hat 4 Bits, z. B. „1011“.
- Ein std_logic_vector(25 downto 0) hat 26 Bits, z. B. „00000000000000000000000000“ (alle 0).
Warum brauchen wir das?
Wir wollen die LED jetzt blinken lassen, haben aber einen Takt von z. B. 100 MHz zur Verfügung. Das ist etwas schnell für das menschliche Auge. Deswegen müssen wir durch einen Zähler die Frequenz verringern.
- Für unseren Zähler wollen wir bis zu 50 Millionen zählen (für 1 Hz bei 100 MHz), und dafür brauchen wir 26 Bits, weil 226=67.108.8642, was größer als 50 Millionen ist. Ein std_logic_vector(25 downto 0) kann diese 26 Bits speichern.
- std_logic_vector wird auch oft für Eingänge wie Taster verwendet, wenn wir mehrere Taster haben (z. B. BTN(0) für Taster 0, später vielleicht BTN(1) für Taster 1).
Was sind Generics?
Mit einem generic erweitert man die Schnittstelle um einen Parameter.
In unserem Fall, um die Simulationszeit zu reduzieren:
--! Toggle a LED
entity BlinkingLed is
generic (
Simulation : boolean := false
);
port (
CLK100MHZ : in std_logic; --! 125 MHz system clock
BTN : in std_logic_vector(3 downto 0); --! Button inputs, high active
LED : out std_logic --! LED output, high active
);
end BlinkingLed;
Wir können jetzt das Modul BlinkingLed instantiieren und angeben, ob es gerade simuliert wird. Wenn nichts angegeben wird, wird der Defaultwert false verwendet.
Oft werden Generics für das Verhalten der Schaltung selber verwendet. Z. B. könnte man in einem generic angeben wie viele Buttons vorhanden sind:
ButtonCount : natural := 4;
und in der port-Definition dann
BTN : in std_logic_vector(ButtonCount - 1 downto 0;
Weiter zu Schritt 1.