Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

VHDL-Tutorial: Entwurf einer sequentiellen Stoppuhr für das Altera DE1-SoC Board

Lerne, wie du mit VHDL eine Stoppuhr im 10-Millisekunden-Takt entwirfst – inklusive Zähler, Taktteiler und 7-Segment-Anzeige. Perfekt für das ECSE 222 Lab #2.

VHDL Tutorial sequentielle Schaltungen VHDL Stoppuhr VHDL Altera DE1-SoC ECSE 222 Lab 2 Zähler VHDL Taktteiler VHDL 7-Segment-Anzeige VHDL FPGA Programmierung ModelSim Simulation VHDL Testbench digitaler Timer VHDL VHDL für Anfänger Quartus VHDL Stoppuhr FPGA VHDL Counter

Einführung in sequentielle Schaltungen mit VHDL

In diesem Tutorial lernst du, wie man sequentielle Logikschaltungen in VHDL beschreibt. Sequentielle Schaltungen – im Gegensatz zu kombinatorischen – besitzen einen internen Zustand, der sich mit jedem Taktzyklus ändern kann. Ein alltägliches Beispiel ist eine Stoppuhr, wie sie etwa bei Sportevents oder im Gaming zur Zeitnahme verwendet wird. Stell dir vor, du streamst ein E-Sport-Turnier und musst die Rundenzeiten in Echtzeit anzeigen – genau solche Systeme lassen sich mit VHDL auf einem FPGA realisieren.

Das Ziel dieses Labors ist es, eine Stoppuhr zu entwerfen, die alle 10 Millisekunden zählt und die vergangene Zeit auf sechs 7-Segment-Anzeigen darstellt. Die Steuerung erfolgt über drei Taster: Start (PB0), Pause (PB1) und Reset (PB2). Der gesamte Entwurf wird auf dem Altera DE1-SoC Board implementiert. Dabei durchläufst du die Schritte: Zähler entwerfen, Taktteiler bauen, Stoppuhr zusammensetzen und simulieren.

1. Grundlagen: Zähler in VHDL

Ein Zähler ist die einfachste sequentielle Schaltung. Er zählt bei jedem Taktimpuls hoch (oder runter) und speichert den aktuellen Wert. In der Praxis begegnen uns Zähler überall: in digitalen Uhren, in der Prozesssteuerung oder in der Spieleentwicklung für Punktestände. Für unser Projekt benötigen wir einen 4-Bit-Aufwärtszähler mit asynchronem Reset (aktiv low) und einem Enable-Signal.

Entity-Deklaration des Zählers

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity gNN_counter is
    Port ( enable : in std_logic;
           reset  : in std_logic;
           clk    : in std_logic;
           count  : out std_logic_vector(3 downto 0));
end gNN_counter;

Die Architektur beschreibt das Verhalten: Bei steigender Taktflanke wird der Zähler erhöht, wenn Enable='1' ist. Bei Reset='0' wird der Zähler zurückgesetzt.

architecture Behavioral of gNN_counter is
    signal cnt : unsigned(3 downto 0) := (others => '0');
begin
    process(clk, reset)
    begin
        if reset = '0' then
            cnt <= (others => '0');
        elsif rising_edge(clk) then
            if enable = '1' then
                cnt <= cnt + 1;
            end if;
        end if;
    end process;
    count <= std_logic_vector(cnt);
end Behavioral;

Beachte: Der Reset ist asynchron, d.h. er reagiert sofort, unabhängig vom Takt. Das ist wichtig, um die Stoppuhr jederzeit zurücksetzen zu können.

2. Taktteiler: Von 50 MHz zu 10 ms

Das DE1-SoC Board liefert einen 50-MHz-Takt. Für eine Zeitauflösung von 10 ms benötigen wir ein Enable-Signal, das alle 500.000 Taktzyklen einmal aktiv wird (50 MHz × 10 ms = 500.000). Der Taktteiler zählt von T-1 bis 0 herunter und setzt den Ausgang en_out auf '1', wenn der Zähler 0 erreicht.

Berechnung von T

Bei 50 MHz und 10 ms: T = 50.000.000 Hz × 0,01 s = 500.000. Der Taktteiler verwendet also einen 19-Bit-Zähler (2^19 = 524.288 > 500.000).

Entity des Taktteilers

entity gNN_clock_divider is
    Port ( enable : in std_logic;
           reset  : in std_logic;
           clk    : in std_logic;
           en_out : out std_logic);
end gNN_clock_divider;

Die Implementierung nutzt einen Down-Counter. Bei jedem Takt wird der Zähler dekrementiert; bei Erreichen von 0 wird en_out='1' und der Zähler auf T-1 zurückgesetzt.

architecture Behavioral of gNN_clock_divider is
    constant T : integer := 500000;
    signal cnt : integer range 0 to T-1 := 0;
begin
    process(clk, reset)
    begin
        if reset = '0' then
            cnt <= 0;
            en_out <= '0';
        elsif rising_edge(clk) then
            if enable = '1' then
                if cnt = 0 then
                    cnt <= T-1;
                    en_out <= '1';
                else
                    cnt <= cnt - 1;
                    en_out <= '0';
                end if;
            end if;
        end if;
    end process;
end Behavioral;

3. Stoppuhr-Architektur

Die Stoppuhr besteht aus sechs Zählern (für Zehntelsekunden, Sekunden und Minuten) und einem Taktteiler. Die Taster steuern einen Zustandsautomaten, der die Modi Running, Paused und Reset umschaltet. Die 7-Segment-Decoder aus Lab #1 wandeln den 4-Bit-Zählerstand in die Ansteuerung der LED-Segmente um.

Top-Level-Entity

entity gNN_stopwatch is
    Port ( start : in std_logic;
           stop  : in std_logic;
           reset : in std_logic;
           clk   : in std_logic;
           HEX0  : out std_logic_vector(6 downto 0);
           HEX1  : out std_logic_vector(6 downto 0);
           HEX2  : out std_logic_vector(6 downto 0);
           HEX3  : out std_logic_vector(6 downto 0);
           HEX4  : out std_logic_vector(6 downto 0);
           HEX5  : out std_logic_vector(6 downto 0));
end gNN_stopwatch;

Die Architektur instanziiert die Komponenten und verbindet sie. Die Taster sind aktiv low (gedrückt = '0'). Ein Zustandsregister speichert den aktuellen Modus.

architecture Structural of gNN_stopwatch is
    signal en_10ms : std_logic;
    signal cnt_centi0, cnt_centi1, cnt_sec0, cnt_sec1, cnt_min0, cnt_min1 : std_logic_vector(3 downto 0);
    signal enable_chain : std_logic_vector(5 downto 0);
    type state_type is (IDLE, RUNNING, PAUSED);
    signal state : state_type := IDLE;
begin
    -- Taktteiler
    clk_div: entity work.gNN_clock_divider
        port map (enable => '1', reset => reset, clk => clk, en_out => en_10ms);

    -- Zustandsautomat
    process(clk, reset)
    begin
        if reset = '0' then
            state <= IDLE;
        elsif rising_edge(clk) then
            case state is
                when IDLE =>
                    if start = '0' then state <= RUNNING; end if;
                when RUNNING =>
                    if stop = '0' then state <= PAUSED; end if;
                    if reset = '0' then state <= IDLE; end if;
                when PAUSED =>
                    if start = '0' then state <= RUNNING; end if;
                    if reset = '0' then state <= IDLE; end if;
            end case;
        end if;
    end process;

    -- Enable für Zählerkette
    enable_chain(0) <= en_10ms when state = RUNNING else '0';

    -- Zähler und Decoder (vereinfacht dargestellt)
    -- ... (Instanzen für cnt_centi0, cnt_centi1, etc.)
end Structural;

Die Zähler sind kaskadiert: Der erste Zähler (Zehntelsekunden) zählt bei jedem en_10ms-Impuls. Bei Überlauf von 9 wird der nächste Zähler erhöht, usw. Die 7-Segment-Decoder wandeln die 4-Bit-Werte in die Segmentmuster um.

4. Simulation mit ModelSim

Bevor du die Schaltung auf dem Board testest, solltest du eine funktionale Simulation durchführen. Schreibe eine Testbench, die die Taster simuliert und die Ausgänge überprüft. Achte darauf, dass der asynchrone Reset korrekt funktioniert und die Stoppuhr im Pause-Modus anhält.

Testbench-Beispiel für den Zähler

entity tb_counter is
end tb_counter;

architecture sim of tb_counter is
    signal clk, enable, reset : std_logic := '0';
    signal count : std_logic_vector(3 downto 0);
begin
    uut: entity work.gNN_counter
        port map (enable => enable, reset => reset, clk => clk, count => count);

    clk_process: process
    begin
        clk <= '0'; wait for 10 ns;
        clk <= '1'; wait for 10 ns;
    end process;

    stim: process
    begin
        reset <= '0'; wait for 20 ns;
        reset <= '1'; enable <= '1';
        wait for 200 ns;
        enable <= '0';
        wait for 100 ns;
        enable <= '1';
        wait;
    end process;
end sim;

Simuliere auch den Taktteiler und die gesamte Stoppuhr. In der Wellenform solltest du sehen, dass en_out alle 500.000 Taktzyklen für einen Takt high wird.

5. Implementierung auf dem DE1-SoC Board

Nach erfolgreicher Simulation kompilierst du das Projekt in Quartus und weist die Pins zu. Die Taster sind an GPIO-Pins angeschlossen, die 7-Segment-Anzeigen an dedizierten Ausgängen. Beachte, dass die Taster invertiert sind (gedrückt = '0'). Lade das Bitstream auf das Board und teste die Funktion: Drücke PB0 zum Starten, PB1 zum Pausieren, PB2 zum Zurücksetzen.

Fazit

Du hast erfolgreich eine sequentielle Schaltung in VHDL entworfen und auf einem FPGA implementiert. Dieses Wissen ist grundlegend für das Verständnis von digitalen Systemen und eingebetteten Anwendungen. Mit den gleichen Techniken lassen sich auch komplexere Systeme wie digitale Timer, Frequenzzähler oder Zustandsautomaten für AI-Anwendungen realisieren. Viel Erfolg bei deinem ECSE 222 Lab!