3. Schritt: Optimierungen

Bisher funktioniert zwar alles (bis hin zur Power-On-Bedingung) aber der Code ist durchaus verbesserungswürdig.

Folgende Probleme sind im aktuellen Code:

  1. Der Code ist nur für das ZYBO und nicht für das ARTY geeignet.
  2. Die Umschaltung zwischen Simulation und Hardware muss durch Auskommentieren der nicht zutreffenden Codezeilen erfolgen.
  3. Der Taktgeber hat eine vorgegebene Hardware (n Bit Zähler), obwohl er durch unterschiedliche Parameter (Board, Simulation) spezifiziert wird.
  4. Die Rücksetztbedingung für den Taktgeber und das Modifizieren des Frequenzgenerators sind 2 mal kodiert.

Für die Punkte 1 und 2 gibt es Generics. Für die Punkte 3 und 4 sollte man anders programmieren.

Generics einbauen

Es wird ein Generic für das verwendete Board und ein Generic für Simulation/Synthese benötigt.

Die Generics werden wie die Ports angegeben. Für Generics wird normalerweise ein Default-Wert angegeben.

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;

Die Komponentendefinition der UUT muss natürlich auch um die generics erweitert werden.

Wenn man das Modul BlinkingLed nun einbindet wie zuvor wird durch den Default-Wert eine Version für ZYBO-Synthese erzeugt.

Das Testprogramm wird aber auch angepasst:

entity TestBlinkingLed is
    generic (
        BoardIdx : integer  := 0 -- 0: ZYBO, 1: ARTY
    );
end TestBlinkingLed;
    uut: BlinkingLed
        generic map (
            BoardIdx    => BoardIdx,
            Simulation  => true
        )
        port map (
            EXT_CLK     => Ext_Clk,
            BTN         => Button,
            LED         => Led
        );

Die Testbench reicht also das eigene Generic BoardIdx an die UUT durch und setzt das Simulations-Flag.

Somit kann man jetzt beide Boards simulieren, und wenn das FPGA erzeugt wird, ist das Simulations-Flag Default mäßig auf false, es wird als immer automatisch die Synthese-Version erzeugt.

Jetzt stellt sich nur noch das Problem, wie man Vivado dazu bringt, die Testbench und die FPGA-Synthese mit dem gewünschten BoardIdx zu starten.

Das geht zum einen über die GUI:

Generics für das Top Level Modul angeben
  1. Project Settings aufrufen
  2. Generic/Parameter editieren
  3. Neuen Wert anlegen
  4. Wert eingeben
  5. und bestätigen
  6. Das Gleiche für die Simulation durchführen
Natürlich geht es auch über Tcl-Kommandos:
set_property generic BoardIdx=1 [get_filesets sources_1]
set_property generic BoardIdx=1 [get_filesets sim_1]

Generics verwenden

In der Simulation muss entsprechend des Boards die Taktperiode zwischen 8 ns für ZYBO und 10 ns für ARTY umgestellt werden:

    type     tCycle is array(0 to 1) of time;
    constant cycleTimes     : tCycle := (0 => 8 ns, 1 => 10 ns);
    constant Clk_period     : time  := cycleTimes(BoardIdx); -- clock period of selected board

Ansonsten ändert sich nichts.

In der UUT haben die Generics auch nur Auswirkungen auf die Konstanten. Um den Teiler richtig einzustellen muss zum einen mal die Taktfrequenz bekannt sein und der Faktor 10000 für die Simulation berücksichtigt werden.

Dazu wird der folgende Code vor die Signaldeklaration eingefügt:

-- constant declarations
-- ===================
type     tCycle is array(0 to 1) of time;
constant cycleTimes     : tCycle := (0 => 8 ns, 1 => 10 ns);
constant cycleTime      : time  := cycleTimes(BoardIdx);
type     tMultiplier is array (false to true) of integer;
constant mutlipliers    : tMultiplier := (false => 10000, true  => 1);
constant mutliplier     : integer  := mutlipliers(Simulation);

constant simCycles      : integer := 200 ns / cycleTime;
constant toggleCycles   : integer := simCycles * mutliplier;
constant maxToggleCycle : integer := toggleCycles - 1;

Integer Zähler

Zähler kann man nicht nur in std_logic_vector sondern auch in integer realisieren. Das hat den Vorteil, dass sich die Synthese darum kümmern muss, wie viele Bit der Zähler hat, und die Simulation ungültige Zählerstände erkennt. Dies ist im Falle des Taktgebers der Fall.

Zähler in std_logic_vector sind sinnvoll, wenn man bewusst Überläufe verwenden möchte oder auf bestimmte Bits des Vektors zugreifen muss. Dies ist im Falle des Frequenzgenerators der Fall.

Punkt 3 der Optimierung führt also zu folgenden Änderungen:

signal ToggleCntR       : integer range 0 to maxToggleCycle;    --! Generate main time base
    p_sync : process (Clk)
    begin
        if rising_edge(Clk) then
            if (InitR = '1') then
                ToggleCntR      <= 0;
            else
                if (ToggleCntR = maxToggleCycle) then
                    ToggleCntR      <= 0;
                else
                    ToggleCntR      <= ToggleCntR + 1;
                end if;
            end if;
        end if;
    end process;

Verwendung von Signalen

Wenn man in VHDL programmiert, sollte man immer die Hardware im Kopf haben, welche synthetisiert werden soll.

Wenn man also einen Aufwärts-Zähler mit Reset und Load-Funktion synthetisieren möchte gibt es sinnvollerweise Signale wie CountR für den Zähler, ResCountC, LoadCountC und IncCountC für die Funktionen.

Also in etwa:

    p_count : process (Clk)
    begin
        if rising_edge(Clk) then
            if (ResCountC = '1') then
                CountR      <= 0;
            else
                if (LoadCountC = '1') then
                    CountR          <= LoadValueR;
                elsif (IncCountC = '1') then
                    CountR          <= CountR + 1;
                end if;
            end if;
        end if;
    end process;

    ResCountC   <= Resetbedingung...;
    LoadCountC  <= Ladebedingung...;
    IncCountC   <= Zählbedingung...;

Dadurch hat man die Zählerhardware von deren Ansteuerung getrennt. Wenn sich eine Ansteuerbedingung ändert, passt man halt Bedingung an, der Zähler bleibt aber wie er ist.

Punkt 4 der Optimierung führt also zu folgenden Änderungen:

signal ResToggleCntC    : std_logic;                    --! Reset ToggleCntR
signal IncFreqGenC      : std_logic;                    --! Increment the frequency generator
    --! Flipflops with synchronous reset
    p_sync : process (Clk)
    begin
        if rising_edge(Clk) then
            if (ResToggleCntC = '1') then
                ToggleCntR      <= 0;
            else
                ToggleCntR      <= ToggleCntR + 1;
            end if;
        end if;
    end process;
    ResToggleCntC   <=  '1' when    InitR = '1'                     else
                        '1' when    ToggleCntR = maxToggleCycle     else
                        '0';

    --! Flipflops with synchronous reset and clock enable
    p_ce : process (Clk)
    begin
        if rising_edge(Clk) then
            if (InitR = '1') then
                FreqGenR        <= (others => '0');
            elsif (IncFreqGenC = '1') then
                FreqGenR        <= FreqGenR + 1;
            end if;
        end if;
    end process;
    IncFreqGenC     <= ResToggleCntC;
    LED <= FreqGenR(8);

Das Signal ResToggleCntC wird auch für das Inkrementieren des Frequenzgenerators verwendet, aber eben unter einem neuen Namen. Es könnte ja später mal eine Bedingung hinzukommen, unter der der Taktgeber zwar weiterläuft, aber der Frequenzgenerator pausieren soll.

Dazu ist noch zu sagen, dass mehrfache und unbenutzte Signale keine Ressourcen kosten. Die Synthese versucht den VHDL Code möglichst Ressourcenschonend zu synthetisieren.

Es ist viel wichtiger einen lesbaren und wartbaren Code zu schreiben, als hier irgendwelche Kompromisse einzugehen.

Diese Änderungen kann man jetzt noch verifizieren indem man den Simulator noch einmal startet. Wenn alles richtig gemacht wurde, ist das Ergebnis identisch mit der letzten Simulation.

Weiter zum 4. Schritt