Schritt 2: Eine LED gesteuert blinken lassen

Im nächsten Schritt wollen wir etwas professioneller vorgehen. Für jedes FPGA und eigentlich für jedes darin enthaltene Modul gibt es eine Spezifikation. Diese kann dann in der Simulation in der Testbench überprüft werden.

Spezifikation

Die LED-Ansteuerung wird grundsätzlich geändert. Die LED soll nun blinken und die Blink-Frequenz durch die Taster 2 und 3 gesteuert werden. Der Taster 0 soll eine Reset-Funktion bekommen.

Nr Name Spezifikation
1 Reset Nach dem Einschalten und nach Taster 0 blinkt die LED mit 250 Hz/256(ca. 1 Hz)
2 FrqUp Nach Taster 2 blinkt die LED mit doppelter Frequenz
3 FrqUp Nach Taster 3 blinkt die LED mit halber Frequenz
4 MaxFreq Die maximale Frequenz ist 250 Hz
5 MinFreq Die minimale Frequenz ist 250 Hz/4096 (ca. 1/16 Hz)


Analyse der Aufgabenstellung

Letztendlich benötigt man um die Spezifikation zu erfüllen einen Frequenzgenerator der im Bereich 250 Hz bis 1/16 Hz Rechtecksignale ausgibt. Das erreicht man mit einem Zähler, der alle 1/500 Sekunde inkrementiert wird.

Ein weiterer Zähler, der im Bereich 0 bis 12 arbeitet, wählt über einen Multiplexer das entsprechende Bit des Frequenzgenerators aus, welches die LED zum blinken bringt. Dieser Zähler wird mit den Tastern beeinflusst.

Blockschaltbild BlinkingLed

Erweiterung der Reset-Logik:

Auf Grund von Spezifikation Punkt 1 soll durch den Taster 0 auch ein Reset ausgelöst werden. GenInitR soll also nicht nur nach PowerOn aktiv.

...

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

...

-- ============================================================================
-- constant declarations
-- ===================
type     tMaxDiv is array (false to true) of natural;
constant maxDiv    : tMaxDiv := (false => 199998, true  => 18);

...

Die Entity wurde um das generic Simulation erweitert.

Dazu wurde noch die Konstante maxDiv deklariert.

maxDiv(Simulation) ist 199998, falls nicht simuliert wird und 18 falls simuliert wird. Somit hat CLK100MHZ in der Simulation nur 10 kHz. Die Simulation läuft damit 10.000 mal schneller ab.

...

signal GenInitC 		: std_logic; 	        		--! Initializing condition
signal GenInitR 		: std_logic := '1'; 			--! Causes an init pulse after power on

...

	--! Simple Flip-Flops
	p_simple : process (Clk)
	begin
		if rising_edge(Clk) then
			GenInitR	<= GenInitC;	-- Release InitR after it was activated
			InitR		<= InitC;
		end if;
	end process;

	GenInitC   <=  '1'  when GenInitR = '1' and InitR = '0' else    -- hold signal until processed
	               '1'	when ButtonR(0) = '1' else                  -- new init condition
	               '0';                                             -- release after processed
	InitC		<=	GenInitR;				-- Reset on power on

Es wurde die Bedingung GenInitC, die von GenInitR übernommen wird hinzugefügt. Das macht die Sache etwas übersichtlicher.

Für die Erzeugung des Kombinatorischen Signals verwenden wir die when-Anweisung.

Was bedeutet die when-Anweisung?

Die when-Anweisung ist eine kurze und übersichtliche Methode, um kombinatorische Logik zu schreiben. Sie ist wie ein if-Statement, aber kürzer, und wird oft für solche Signale benutzt. Schauen wir uns die Anweisung Schritt für Schritt an:  

  • '1' when GenInitR = '1' and InitR = '0' else:  
    • Die Haltebedingung:  Falls GenInitR = '1' ist, aber noch nicht verarbeitet wurde (InitR = '0') wird GenInitC auf  '1'  gesetzt und damit bleibt uns GenInitR =  '1'  erhalten. 
  • '1' when ButtonR(0) = '1' else:  
    • Setze GenInitC auf  '1', wenn ButtonR(0) = '1' (Taster 0 ist gedrückt).  
    • ButtonR(0) ist das synchronisierte Signal von Taster 0. Das bedeutet: Solange du Taster 0 gedrückt hältst, wird GenInitC '1' , und ein Reset wird ausgelöst.
  • '0':  
    • Wenn keine der Bedingungen zutrifft, wird GenInitC auf '0' gesetzt, und es gibt keinen Reset.

Erweiterung der Tasten-Logik:

Auf Grund von Spezifikation Punkt 2 und 3, soll auf die Taster 2 und 3 reagiert werden und zwar genau einmal pro Tastendruck. Also müssen die Taster einsynchronisiert und eine Flankenerkennung implementiert werden.

...

-- input and output
signal Clk			    : std_logic;					--! Internal processing clock
signal ButtonR          : std_logic_vector(3 downto 0); --! Button inputs after input flipflops

-- button functions
signal Button1R         : std_logic_vector(3 downto 0); --! Delayed button signals for edge detection
signal ButtonPressC     : std_logic_vector(3 downto 0); --! Button was pressed
signal FreqUpR          : std_logic;                    --! Frequency up event
signal FreqDownR        : std_logic;                    --! Frequency down event

...

    --! Simple flipflops
    p_simple : process (Clk)
    begin
        if rising_edge(Clk) then
            GenInitR    <= GenInitR and not InitR;  -- Release InitR after it was activated
            InitR       <= InitC;
            ButtonR     <= BTN;  -- input FF for Button signals
            Button1R    <= ButtonR; -- Delayed button inputs
            FreqUpR     <= ButtonPressC(2); -- Frequency up event
            FreqDownR   <= ButtonPressC(3); -- Frequency down event
        end if;
    end process;

    ButtonPressC        <=  ButtonR and not Button1R;   -- rising edge detection of buttons

Es sind die Vektoren ButtonR, Button1R und ButtonPressC hinzugekommen.

BTN ist ja ein externes Signal, welches asynchron zum 100 MHz Haupttakt des FPGAs ist, es kann also zu einem beliebigen Punkt im Taktzyklus gesetzt oder zurückgesetzt werden.

ButtonR <= BTN; sorgt dafür, dass jetzt ein Signal synchron zum Takt vorliegt. BTN ist also einsynchronisiert worden.

Button1R <= ButtonR; verzögert die Taster-Eingänge um genau einen Takt. Damit kann man dann Flanken also Änderungen des Eingangs erkennen. War z.B. BTN(2) nicht gesetzt und wird jetzt '1', so wird im ersten Takt ButtonR(2) = '1' aber Button1R(2) ist noch '0'. Einen Takt später sine beide Signale '1'.

ButtonPressC <=  ButtonR and not Button1R; nutzt dies aus und würde in diesem Fall für genau einen Takt ButtonPressC(2) setzen. Dieses Signal wird in FreqUpR gespeichert, was dann wiederum einen Takt später genau für einen Takt aktiv ist. Analog natürlich mit dem Eingang 3 und FreqDownR.

Die Signale FreqUpR und FreqDownR können dann für die Manipulation des Selektor-Index verwendet werden und führen pro Tastendruck zu genau einer Aktion (außer der Taster würde prellen, dazu aber später).

Erweiterung der LED Ausgabe:

Auf Grund von Spezifikation Punkt 4 und 5, soll sich die Ausgabefrequenz der LED ändern können. Die Bedienung dazu FreqUpR und FreqDownR ist ja schon implementiert. Also müssen die Zähler und der Multiplexer aus dem Blockschaltbild implementiert werden.

Fangen wir mit dem 500 Hz Taktgeber und dem 13 Bit Zähler an:

...

-- counter
signal DividerR		    : std_logic_vector(17 downto 0);	--! Clock divider
signal ResDividerC      : std_logic;                    --! Reset DividerR
signal FreqGenR			: std_logic_vector(12 downto 0);	--! Generate blinking frequencies
signal DivCycleC    	: std_logic; 	        		--! Divider reload condition
signal DivCycleR		: std_logic; 		    	   --! Divider reload condition

...

	--! Flip-Flops with synchronous reset
	p_sync : process (Clk)
	begin
		if rising_edge(Clk) then
		    -- frequendy divider
			if (ResDividerC = '1') then
				DividerR		<= (others => '0');
			else
                DividerR		<= DividerR + 1;
            end if;

			DivCycleR       <= DivCycleC;
		end if;
	end process;
	DivCycleC      <=  '0' when InitR = '1'        else
	                   '1' when DividerR = std_logic_vector(to_unsigned(maxDiv(simulation), 18)) else 
	                   '0';
	ResDividerC    <=  '1' when InitR = '1'        else
	                   '1' when DivCycleR = '1'    else
	                   '0';
	
    --! Flip-Flops with synchronous reset and clock enable
    p_ce : process (Clk)
    begin
        if rising_edge(Clk) then
            if (InitR = '1') then
                FreqGenR        <= (others => '0');
            elsif (DivCycleR = '1') then
                FreqGenR        <= FreqGenR + 1;
            end if;
        end if;
    end process;

Der Taktgeber besteht aus dem Zähler Divider und den Signalen ResDividerC, DivCycleC und DivCycleR und wird im Prozess p_sync implementiert.

Die Idee ist, dass er munter vor sich hin zählt und 500 mal pro Sekunde wieder von vorne anfängt. Bei einem Takt von 100 MHz muss dieser Zyklus also alle 200000 Takte neu starten. Der Zähler muss also von 0 bis 199999 zählen und wieder von vorne beginnen.

Da ich hier zuerst das kombintorische Signal DivCycleC erzeuge und damit einen Takt später DivCycleR aktiv wird, muss DivCycleC dementsprechend einen Takt früher erzeugt werden. Zusätzlich möchte ich, dass in einer eventuellen Simulation die Zeit 10.000 mal schneller vergeht. Deswegen wird letztendlich mit maxDiv(simulation) verglichen um das Ende des Zyklus einzuleiten.

Die Anweisung "DivCycleC <= ..." besagt, bei der Reset Bedinung ist das Signal auf jeden Fall null, ansonsten, wenn der Zählerstand 199998 (keine Simulation) oder 18 (Simulation) erreicht ist, setze es '1' und ansonsten bleibt es '0';

Mit diesen when-Anweisungen könnnen recht komplexe Signale einfach und lesbar implementiert werden.

Des weiteren empfiehlt es sich diese Logik außerhalb des Prozesses zu machen. Im Prozess ist einfach ein Zähler mit einem Reset-Eingang. Das sieht man auf einen Blick. Unten in der Logik sieht man dann, wie dieser Reset erzeugt wird.

Im p_ce Prozess hat man jetzt einen Zähler mit Reset und Clock Enable. Man erkennt auf einen Blick, dass nur bei DivCycleR = '1' der Zähler inkrementiert wird. Zudem sieht man auch, durch die Logik oben, dass der DividerR synchron dazu ist, weil ja beide Male DivCycleR verwendet wird.

Somit hätten wir also die erste Zeile aus dem Blockschaltbild von oben implementiert.

Jetzt brauchen wir noch den Selektor und den Multiplexer:

...

signal FreqSelR			: std_logic_vector(3 downto 0);	--! Select blinking frequency
signal IncFreqSelC      : std_logic;                    --! Increment FreqSel
signal DecFreqSelC      : std_logic;                    --! Decrement FreqSel
signal ResFreqSelC      : std_logic;                    --! Reset FreqSel

...

    -- Frequency control
    p_freq : process (Clk)
    begin
        if rising_edge(Clk) then
            if ResFreqSelC = '1' then
                FreqSelR <= "1000";  -- Reset: 1 Hz (Selektor = 8)
            elsif DecFreqSelC = '1' then  -- Taster 2: Frequenz verdoppeln
                FreqSelR <= FreqSelR - 1;
            elsif IncFreqSelC = '1' then  -- Taster 3: Frequenz halbieren
                FreqSelR <= FreqSelR + 1;
            end if;
        end if;
    end process;

    ResFreqSelC        <=  '1' when InitR = '1'         else    '0';
    IncFreqSelC        <=   '0' when FreqSelR >= 12     else
                            '1' when FreqDownR = '1'    else
                            '0';
    DecFreqSelC        <=   '0' when FreqSelR = 0       else
                            '1' when FreqUpR = '1'      else
                            '0';

    -- Multiplexer: Choose LED-Signal based on FreqSelR
    p_mux : process (FreqGenR, FreqSelR)
    begin
        case FreqSelR is
            when "0000" => LedC <= FreqGenR(0);  -- 250 Hz
            when "0001" => LedC <= FreqGenR(1);  -- 125 Hz
            when "0010" => LedC <= FreqGenR(2);  -- 64 Hz
            when "0011" => LedC <= FreqGenR(3);  -- 32 Hz
            when "0100" => LedC <= FreqGenR(4);  -- 16 Hz
            when "0101" => LedC <= FreqGenR(5);  -- 8 Hz
            when "0110" => LedC <= FreqGenR(6);  -- 4 Hz
            when "0111" => LedC <= FreqGenR(7);  -- 2 Hz
            when "1000" => LedC <= FreqGenR(8);  -- 1 Hz
            when "1001" => LedC <= FreqGenR(9);  -- 1/2 Hz
            when "1010" => LedC <= FreqGenR(10);  -- 1/4 Hz
            when "1011" => LedC <= FreqGenR(11);  -- 1/8 Hz
            when "1100" => LedC <= FreqGenR(12);  -- 1/16 Hz
            when others => LedC <= '0';
        end case;
    end process;

end RTL;

Der Selektor ist in p_freq implementiert. In dem Prozess erkennt man einen Zähler mit Reset Eingang der auf und abwärts zählen kann.

Unter dem Prozess folgt die Logik.

Der Reset wird  ausschließlich durch InitR ausgelöst.

Der Zähler zählt nach oben, wenn der Selektor noch nicht den Wert 12 erreicht hat und die Frequenz halbiert werden soll (FreqDownR) da höhere Bits des Zählers niedrigere Frequenzen haben, führ der Abwärts-Taster also zu einer Erhöhung des Index.

Analog dazu führt FreqUpR zum Dekrementieren, wenn FreqSelR die 0 noch nicht erreicht hat.

Der Multiplexer ist in p_mux implementiert.

LedC hängt nur von den Signalen FreqGenR und FreqSelR ab. Beide Signale sind in der Senssitivitätsliste aufgeführt. Also wird LedC rein kombinatorisch erzeugt.

Entssprechend des Wertes im Frequenz-Selektor wird eines der 13 Bit aus dem Frequenzgenerator auf LedC geschaltet. 

Weiter zu TestBlinkingLed.