VHDL Testbench

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Einleitung

Warum benötigt man beim Beschreiben von Hardware eine Testbench, wie es immer wieder empfohlen wird?

Beim Programmieren von Software kann man sein Programm meistens schnell übersetzen und ausprobieren. Wenn etwas nicht läuft, wird der Debugger angeschmissen, ein Breakpoint gesetzt und sich der Inhalt von Variablen, Speicher und Prozessorregistern angeschaut, um dem Fehler auf die Spur zu kommen.

Im FPGA geht das nicht ganz so einfach. Die Synthese kann für ein komplexes Design auf einem großen FPGA mehrere Stunden benötigen. Statt einem Debugger würde man einen Logikanalysator verwenden (extern oder als FPGA Soft Core) und könnte trotzdem nur sehr mühsam nach Fehlern suchen.

Daher wird beim Entwurf normalerweise zu jedem Modul eine Testbench erstellt. Mit dieser soll sich die Funktionalität des Modules vor der Synthese mittels Simulation prüfen lassen. Die Testbench generiert alle Eingangssignale, auch Testvektoren genannt, für das zu testende Modul (device under test) und prüft ggf. die Resultate.

Folgende Grafik zur Veranschaulichung:

Testbench DUT.png


Die Testbench wird ebenfalls in VHDL beschrieben. Da sie nicht synthesefähig sein muß, lassen sich in der Testbench viel mehr Sprachkonstrukte verwenden (z. B. Dateizugriff, Rechnen mit real-Zahlen, Timing-Anweisungen). Die Testbench und das DUT werden vom Simulator (ModelSim, ghdl) compiliert und ausgeführt.

Guter Stil ist es, erst die Testbench zu erstellen und dann das eigentliche Modul ("Test Driven Development"). Dieser Ansatz zwingt dazu, sich vor der Implementierung Gedanken über die genauen Aufgaben eines Moduls zu machen, und hilft dabei eine hohe Testabdeckung zu erzielen.

Beispiel: Mini-DDS

Für Module, die nur Signale erzeugen, kann die Testbench sehr einfach aussehen. Ein Merkmal von Testbenches ist, daß ihre entity-Beschreibung leer ist. Hier ist als Beispiel die Testbench für ein kleines DDS-Modul:

-- Testbench fuer Mini DDS

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

-- leere entity
entity mini_dds_tb is
end entity mini_dds_tb;

architecture bhv of mini_dds_tb is

  -- Moduldeklaration
  component mini_dds is
    port (
      clk   : in std_logic;
      reset : in std_logic;

      sinus     : out std_logic_vector(3 downto 0);
      saegezahn : out std_logic_vector(3 downto 0);
      rechteck  : out std_logic_vector(3 downto 0)
    );
  end component;

  -- input
  signal clk   : std_logic := '0';
  signal reset : std_logic;

  -- output
  signal sinus, saegezahn, rechteck : std_logic_vector(3 downto 0);

begin
  clk   <= not clk  after 20 ns;  -- 25 MHz Taktfrequenz
  reset <= '1', '0' after 100 ns; -- erzeugt Resetsignal: --__

  -- Modulinstatziierung
  dut : mini_dds
    port map (
      clk       => clk,
      reset     => reset,

      sinus     => sinus,
      saegezahn => saegezahn,
      rechteck  => rechteck
      );

end architecture;

Die Komponente mini_dds wird instanziiert und die Testbench erzeugt nur ein Taktsignal (clk) und ein Resetsignal. Die Ausgänge (sinus, saegezahn, rechteck) von mini_dds werden auf entsprechend benannte Signale geführt.

Nun das Modul, welches die eigentliche Arbeit verrichtet:

--
-- Mini-DDS zum Demonstrieren der Testbench
--

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity mini_dds is
  port
    (
      clk   : in std_logic;
      reset : in std_logic;

      sinus     : out std_logic_vector(3 downto 0);
      saegezahn : out std_logic_vector(3 downto 0);
      rechteck  : out std_logic_vector(3 downto 0)
      );
end mini_dds;

architecture bhv of mini_dds is

  type sinus_array_t is array (0 to 15) of integer range 0 to 15;

  constant sinus_array_c : sinus_array_t :=
    (8, 10, 13, 14, 15, 14, 13, 10, 8, 5, 2, 1, 0, 1, 2, 5);

  signal index : integer range 0 to 15;

  signal phase : unsigned(3 downto 0);

  signal sinus_u     : unsigned(3 downto 0);
  signal saegezahn_u : unsigned(3 downto 0);
  signal rechteck_u  : unsigned(3 downto 0);

begin
  phase_accumulator : process (clk, reset)
  begin
    if reset = '1' then                 -- asynchroner Reset
      phase <= (others => '0');
    elsif rising_edge(clk) then
      phase <= phase + 1;
    end if;
  end process;

  index <= to_integer(phase);

  sinus_u     <= to_unsigned( sinus_array_c (index), sinus_u'length);
  saegezahn_u <= phase;
  rechteck_u  <= (others => phase(3));

  -- convert output values
  sinus     <= std_logic_vector( sinus_u );
  saegezahn <= std_logic_vector( saegezahn_u );
  rechteck  <= std_logic_vector( rechteck_u);

end bhv;

Hier wird ein asynchroner Reset zum Initialisieren verwendet. "phase" stellt einen Zähler dar. Aus diesem werden die weiteren Signale generiert. Das Sinussignal wird aus einer Tabelle erzeugt, der Zähler selbst ist ein Sägezahn und das oberste Bit des Zählers wird als Rechtecksignal verwendet. Bei diesem Modul wurde bewußt ieee.numeric_std.all zum Rechnen verwendet. Die Ein- und Ausgänge sind dagegen als std_logic definiert um Probleme mit einigen Synthesetools zu umgehen.

Die ausführlichen Typkonvertierungen im unteren Teil mögen zwar nicht so schnell hinzuschreiben sein, aber sie schränken logische Fehler stark ein, weil man doch etwas mehr nachdenken muß.

Das Resultat der Simulation ist im folgenden Bild zu erkennen:

Testbench Waveform.png

Ob die vom Modul erzeugten Signale richtig sind muß in diesem Beispiel der Betrachter entscheiden. In vielen Fällen, gerade im Hobbybereich, ist das sicher ausreichend.

to be continued...