TestBlinkingLed: Testbenches mit Prozeduren

In dieser Testbench verwenden wir Funktionen für das Debugging, zur Analyse und zum Abtesten:

library ieee;
	use ieee.std_logic_1164.all;
	use std.textio.all;
library std;
    use std.env.all;

entity TestBlinkingLed is
end TestBlinkingLed;

architecture behavior of TestBlinkingLed is

	-- Component Declaration for the Unit Under Test (UUT)
	component BlinkingLed
        generic (
            Simulation      : boolean  := false
        );
		port (
			CLK100MHZ 		: 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 component;

	-- Time definitions
	constant Clk_period		: time	:= 10 ns; -- clock period of selected board
	constant LED_min_per	: time := 400 ns;			--! LED period in fastest mode
	constant LED_res_per	: time := LED_min_per*256;	--! LED period after reset
	constant LED_max_per	: time := LED_res_per*16;	--! LED period in slowest mode

	--Inputs
	signal Button		: std_logic_vector(3 downto 0) := (others => '0');
	signal CLK100MHZ	: std_logic := '0';

	--Outputs
	signal Led			: std_logic;

Der Anfang schaut prinzipiell wie bei TestSimpleLed aus.

Außer der Anpassung der Port-Schnittstelle kommt der Parameter Simulation dazu.

Es werden auch Konstanten definiert, damit wir die Zeitlichen Rahmenbedingungen durch die Spezifikation einfach abtesten können.

	--Test
	file report_file	: text;
	signal TestCase 	: integer;
	signal TestErr		: std_logic := '0';
	signal LED_per		: time := 0 ns;

	--! print to output file
	procedure tb_report(constant report_text : in string) is
		variable line_out: line;
	begin
		write(line_out,report_text);
		writeline(report_file, line_out);
	end tb_report;

	--! wait a number of cycles
	procedure WaitCycles(constant cycles : in integer) is
		variable i : integer;
	begin
		for i in 1 to cycles loop
			wait until rising_edge(CLK100MHZ);
		end loop;
	end WaitCycles;

Da die eingebaute report-Funktion auch viel unnötiges ausgibt, wird in diesem Schritt gezeigt, wie man das auch etwas eleganter mit einer Dateiausgabe löst.

Dazu gibt es eine zusätzliche Test-Variable report_file die einen Zugriff auf eine Datei erlaubt, die über die Prozedur tb_report beschrieben wird. Diese Debug-Ausgabe dient auch nur zur Demonstration von einfachem Dateizugriff. Man kann so auch Testvektoren einlesen und den Test Datei-gesteuert ablaufen lassen.

Im Anschluss findet man noch eine Hilfsfunktion WaitCycles, die ich gerne verwende um eine kurze Zeit im FPGA vergehen zu lassen oder nur um Eingangssignale einzusynchronisieren.

	--! measure LED period
	procedure MeasureLedPeriod(constant maxCycles : in integer; signal Led_period : out time) is
		variable i : integer;
		variable s : integer;
		variable rise_1 : integer;
		variable rise_2 : integer;
	begin
		s := 0; -- initial state
		Led_period <= 0 ns; -- timeout return value
		for i in 1 to maxCycles loop	-- timeout condition
			wait until rising_edge(CLK100MHZ);		-- measure in clock ticks
			case s is
				when 0 =>				-- wait for initial condition
					if (Led = '0') then
						s := 1; 		-- initial condition reached
					end if;
				when 1 =>				-- wait for rising edge
					if (Led = '1') then
						s := 2; 		-- first rising edge reached
						rise_1 := i;	-- remember clock tick
					end if;
				when 2 =>				-- wait for LED being off again
					if (Led = '0') then
						s := 3; 		-- LED is off again reached
					end if;
				when 3 =>				-- wait for rising edge
					if (Led = '1') then
						s := 4; 		-- nothing more to do
						rise_2 := i;	-- remember clock tick
						Led_period <= (rise_2 - rise_1) * Clk_period;
						wait for 10 ps; -- Let variable value settle
						return; 		-- got result
					end if;
				when others =>
					return;
			end case;
		end loop;
	end MeasureLedPeriod;

	--! measure LED period
	procedure CheckLedPeriod(constant expected : in time; signal LED_period : inout time;
							signal TestErr : out std_logic) is
	begin
		MeasureLedPeriod(2*expected/Clk_period, LED_period);
		if (LED_period = expected) then
			tb_report("  Expected " & time'Image(expected) & ": passed.");
		else
			TestErr <= '1';
			tb_report("  Expected " & time'Image(expected) & "; Found " & time'Image(LED_per) & ": Failed!");
		end if;
	end CheckLedPeriod;

In der Prozedur MeasureLedPeriod wird gemessen, wie schnell die LED blinkt. Dies wird von der Prozedur CheckLedPeriod benutzt um zu testen ob die aktuelle Blinkfrequenz mit der Spezifikation übereinstimmt.

CheckLedPeriod wird später im Stimulus-Prozess aufgerufen, nachdem eine bestimmte Blinkfrequenz gewählt worden ist.

Analysieren wir MeasureLedPeriod:

Die Prozedur hat einen Eingang maxCycles und einen Ausgang Led_period. maxCycles dient dazu die Messung abzubrechen, wenn die LED z. B. gar nicht blinkt. Der Ausgang ist natürlich das Messergebnis.

In der Messschleife wird jeweils auf eine Taktflanke gewartet und mit einer kleinen Statemachine gemessen. Die States laufen dabei einfach von 0 bis 3 durch.

Da der Test ja nicht synchron zur LED gestartet werden muss, startet die Statemachine in State 0 und fängt das analysieren an:

  • State 0: Warten bis LED ausgeschaltet, dann zu State 1
  • State 1: Warten bis LED eingeschaltet (jetzt sind wir einsynchronisiert). Die aktuelle Taktnummer wird in rise_1 gespeichert und es geht zu State 2.
  • State 2: Warten bis LED wieder ausgeschaltet, dann zu State 3
  • State 3: Warten bis LED wieder eingeschaltet. Die aktuelle Taktnummer wird in rise_2 gespeichert. Die Differenz der Taktnummern mal die Zykluszeit gibt das Messergebnis. Damit der aufrufende Prozess dieses Messergebnis auch sieht, wird noch 10 ps gewartet (ohne Wartezeit, wäre das Ergebnis noch nicht verfügbar). Damit ist die Messung abgeschlossen und die Prozedur wird per return verlassen.

In CheckLedPeriod wird anhand der zu erwartenden Periode die doppelte Zeitspanne Zeit gegeben (durch das Einsynchronisieren kann man ja bis zu einer Periode verlieren) und die Periode gemessen.

Der Vergleich mit der erwarteten Zeit ergibt einen Fehler oder nicht. Dementsprechend wird eine Meldung generiert und u. U. das Fehlersignal aktiviert.

begin
	-- Instantiate the Unit Under Test (UUT)
	uut: BlinkingLed
		generic map (
			Simulation	=> true
		)
		port map (
			CLK100MHZ 	=> CLK100MHZ,
			BTN 		=> Button,
			LED 		=> Led
		);

	--! Generate system clock
	p_clk : process
	begin
		CLK100MHZ <= not CLK100MHZ;
		wait for Clk_period/2;
	end process;

	--! Check for test bench time out
	p_control : process
	begin
		wait for 20 ms;
		-- the test bench should already have terminated!
		assert false
		report "Test timed out!"
		severity failure;
	end process;

Nachdem jetzt alle Hilfsmittel zur Verfügung stehen, kann mit der Simulation begonnen werden.

Zuerst wird wieder die uut instantiiert und dann noch zwei Hilfsprozesse gestartet.

Da wir jetzt ja einen Takt haben, muss der generiert werden. Das geschieht in p_clk. Das Taktsignal wird darin nach jeweils einer halben Periode invertiert. So ein Prozess braucht keine Schleife, damit er das anschließend wiederholt. Ein Prozess startet automatisch am Beginn, wenn er am Ende angekommen ist.

p_control, dient als Time-out für die Simulation, falls irgendetwas hängen bleiben sollte. So ist sichergestellt, dass - in diesem Fall - nach 20 ms Schluss ist.

Man erkennt hier auch, dass alle Prozesse parallel laufen. Alle Prozesse werden beim Simulationsstart gestartet und laufen nebeneinander her. Das war ja im Sourcecode des FPGA genau das gleiche. Ein Prozess ist z. B. ein Register und alle Register arbeiten ja parallel.

	-- Stimulus process
	stim_proc: process
		variable expected_time : time;
	begin
		-- prepare reporting
		file_open(report_file,"../../../TestBlinkingLed.log",WRITE_MODE);
		tb_report("Testing BlinkingLed:");
		tb_report("====================");

		TestCase <= 1;
		tb_report("Test Case 1: LED Period after power on...");
		expected_time := LED_res_per;
		CheckLedPeriod(expected_time, LED_per, TestErr);

		TestCase <= 2;
		tb_report("Test Case 2: Reducing LED speed to minimum");
		for i in 1 to 4 loop
			-- press frequency reduce button
			Button(3) <= '1';
			WaitCycles(10);
			Button(3) <= '0';
			expected_time := 2 * expected_time;
			CheckLedPeriod(expected_time, LED_per, TestErr);
		end loop;

		TestCase <= 3;
		tb_report("Test Case 3: Reducing LED speed after minimum");
		-- press frequency reduce button
		Button(3) <= '1';
		WaitCycles(10);
		Button(3) <= '0';
		-- expected time does not change any more
		CheckLedPeriod(expected_time, LED_per, TestErr);

Nachdem alles so schön vorbereitet worden ist, ist die Testbench jetzt recht übersichtlich:

  • TestCase 1: Es wird erwartet, dass nach dem Reset der Multiplexer auf Stellung 8 steht und die gemessene Blinkfrequenz überprüft.
  • TestCase 2: Durch viermaliges Drücken auf Button(3) wird der Multiplexer sukzessive auf 12 gestellt und nach jedem Schritt getestet, ob die Blink-Frequenz passt.
  • TestCase 3: Der Taster wird ein weiteres mal betätigt und abgetestet, dass sich jetzt die Blinkfrequenz nicht mehr ändert.
		TestCase <= 4;
		tb_report("Test Case 4: Reset FPGA and check reset period...");
		Button(0) <= '1';
		WaitCycles(10);
		Button(0) <= '0';
		expected_time := LED_res_per;
		CheckLedPeriod(expected_time, LED_per, TestErr);

		TestCase <= 5;
		tb_report("Test Case 5: Increase LED speed to maximum");
		for i in 1 to 8 loop
			-- press frequency reduce button
			Button(2) <= '1';
			WaitCycles(10);
			Button(2) <= '0';
			expected_time := expected_time / 2;
			CheckLedPeriod(expected_time, LED_per, TestErr);
		end loop;

		TestCase <= 6;
		tb_report("Test Case 6: Increase LED speed after maximum");
		-- press frequency reduce button
		Button(2) <= '1';
		WaitCycles(10);
		Button(2) <= '0';
		-- expected time does not change any more
		CheckLedPeriod(expected_time, LED_per, TestErr);
  • TestCase 4: Der Reset wird ausgelöst, was den Multiplexer wieder auf Stellung 8 bringen muss und die gemessene Blinkfrequenz überprüft.
  • TestCase 5: Durch achtmaliges Drücken auf Button(2) wird der Multiplexer sukzessive auf 0 gestellt und nach jedem Schritt getestet, ob die Blink-Frequenz passt.
  • TestCase 6: Der Taster wird ein weiteres mal betätigt und abgetestet, dass sich jetzt die Blinkfrequenz nicht mehr ändert.
		TestCase <= 0;
		WaitCycles(1);
		TestErr <= not TestErr;
		WaitCycles(1);
		TestErr <= not TestErr;
		WaitCycles(500);
		if (TestErr = '0') then
			tb_report("Test completed successfully");
		else
			tb_report("Test completed with error(s)");
		end if;
		file_close(report_file);
		finish;
		wait;
	end process;

end;

Der Test ist durchgelaufen, jetzt kommt nur noch der Daumen nach oben oder unten, eine abschließende Meldung und das Schließen des Files.

Die Dateiausgabe sollte jetzt so aussehen:


Testing BlinkingLed:
====================
Test Case 1: LED Period after power on...
Expected 102400000 ps: passed.
Test Case 2: Reducing LED speed to minimum
Expected 204800000 ps: passed.
Expected 409600000 ps: passed.
Expected 819200000 ps: passed.
Expected 1638400000 ps: passed.
Test Case 3: Reducing LED speed after minimum
Expected 1638400000 ps: passed.
Test Case 4: Reset FPGA and check reset period...
Expected 102400000 ps: passed.
Test Case 5: Increase LED speed to maximum
Expected 51200000 ps: passed.
Expected 25600000 ps: passed.
Expected 12800000 ps: passed.
Expected 6400000 ps: passed.
Expected 3200000 ps: passed.
Expected 1600000 ps: passed.
Expected 800000 ps: passed.
Expected 400000 ps: passed.
Test Case 6: Increase LED speed after maximum
Expected 400000 ps: passed.
Test completed successfully

Das Signalfenster des Simulators sollte jetzt so aussehen:

Signale von TestBlinkinLed