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: