TestSysService: Testbenches mit XML/HTML
Gute VHDL-Module haben eine Spezifikation und eine Testbench die genau darauf basiert. Alles was spezifiziert ist muss bewiesen werden, dass es funktioniert. Was nicht spezifiziert wurde, gehört auch nicht getestet. Wenn es wichtig wäre müsste es ja spezifiziert sein.
Nachdem ich in den vorhergehenden Testbenches schon die einfachen Methoden für die Ausgabe der Testprotokolle gezeigt habe, die mit VHDL-Mitteln einfach bereit gestellt werden, gehen wir jetzt einen Schritt weiter und erstellen aussagekräftigere und strukturierte XML-Datein aus der Testbench heraus. Dazu benutzen wir das package xml_pck aus meiner Feder.
Mit einem von mir geschriebenen Tool weden diese XML dann in einer HTML-Tabelle ausgegeben.
library ieee;
use ieee.std_logic_1164.all;
use std.textio.all;
use ieee.numeric_std.all;
use ieee.std_logic_unsigned.all ;
library std;
use std.env.all;
library work;
use work.xml_pck.all;
entity TestSysService is
generic (
SIM_DIR : string := "../../../"
);
end TestSysService;
architecture behavior of TestSysService is
-- Component Declaration for the Unit Under Test (UUT)
component SysService
port (
ExtClk : in std_logic; --! External clock
Clk : out std_logic; --! Processing clock
Reset : in std_logic; --! High acitve asynchronous reset input
Init : out std_logic; --! High acitve synchronous reset output
TimeBase : out std_logic_vector(8 downto 0); --! CE for 1 Hz .. 100 MHz
TimeStamp : out std_logic_vector(54 downto 0) --! Timestamp (> 10 years @ 100 MHz)
);
end component;
Unterschiede zu früheren Testbenches sind:
- Es wird die library work verwendet. Darin sind Bibliotheken enthalten, die wir selber erzeugt haben.
- Daraus wird xml_pck verwendet. Darin enthalten sind die Funktionen um die Ausgabe-XML zu beschreiben.
- Es wird jetzt optional ein Verzeichnis angegeben, worin die XML-Datei landen soll
-- Time definitions
constant Clk_period : time := 10 ns;
--Inputs
signal Ext_Clk : std_logic := '0';
signal Reset : std_logic := '0';
--Outputs
signal Clk : std_logic;
signal Init : std_logic;
signal TimeBase : std_logic_vector(8 downto 0);
signal TimeStamp : std_logic_vector(54 downto 0);
--Test
signal TestCase : integer;
signal TestStep : integer;
signal TestErr : std_logic := '0';
signal TestStepErr : std_logic := '0'; -- Flag for error reporting
signal iTimeStampHi : integer; -- MSBs of time stamp
signal iTimeStampLo : integer; -- LSBs of time stamp
signal iTimeBase : integer; -- Time base as integer
signal iTimeStMsr : integer; -- LSBs of time stamp from measurement
--! 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(Ext_Clk);
end loop;
wait for 10 ps; -- Let variable value settle
end WaitCycles;
--! get timestamp at next time base event
procedure MeasureTimeStamp(constant maxCycles : in integer;
constant baseIdx : in integer; signal timeStamp : out integer) is
begin
timeStamp <= 0; -- initial state
for i in 1 to maxCycles loop -- timeout condition
wait until rising_edge(Ext_Clk); -- measure in clock ticks
if (TimeBase(baseIdx) = '1') then
timeStamp <= iTimeStampLo;
wait for 10 ps; -- Let variable value settle
return; -- got result
end if;
end loop;
end MeasureTimeStamp;
--! check time stamp
procedure CheckTimeStamp(constant maxCycles : in integer;
constant baseIdx : in integer; constant expected : in integer;
signal timeStamp : inout integer; signal TestErr : out std_logic) is
--variable line : string(1 to 60);
begin
MeasureTimeStamp(maxCycles, baseIdx, timeStamp);
--line := "Waiting for TimeBase(" & integer'Image(baseIdx) &"), expected TimeStamp: " & integer'Image(expected);
if (timeStamp /= expected) then
xml_reason("Detection TimeStamp: " & integer'Image(timeStamp));
end if;
check_point((timeStamp /= expected), "Waiting for TimeBase(" & integer'Image(baseIdx) &
"), expected TimeStamp: " & integer'Image(expected), TestErr);
end CheckTimeStamp;
Nach den üblichen Deklarationen und der schon bekannten Prozedur WaitCycles kommen zwei Prozeduren zum Messen und Testen.
MeasureTimeStamp wartet auf das gewünschte Triggersignal innerhalb der TimeBase und gibt den TimeStamp zurück in dem dieser aufgetreten ist.
CheckTimeStamp verwendet diese Funktion um zu testen, ob das Ergebnis richtig ist und schreibt das Ergebnis in die XML-Datei. Dazu gleich genaueres.
Was hier aber neu ist sind die Funktionen zur Texterzeugung:
- & wird verwendet um etwas aneinander zu hängen. In diesem Fall einzelne Strings/Zeichen. Es wird aber auch für std_logic_vector verwendet.
- integer'Image(expected) bedeutet: Interpretiere expected als integer und gib es mir als text.
begin
-- Instantiate the Unit Under Test (UUT)
uut: SysService
port map (
ExtClk => Ext_Clk,
Clk => Clk,
Reset => Reset,
Init => Init,
TimeBase => TimeBase,
TimeStamp => TimeStamp
);
iTimeStampLo <= to_integer(unsigned(TimeStamp(30 downto 0)));
iTimeStampHi <= to_integer(unsigned(TimeStamp(54 downto 31)));
iTimeBase <= to_integer(unsigned(TimeBase));
--! Generate system clock
p_clk : process
begin
wait for Clk_period/2;
Ext_Clk <= not Ext_Clk;
end process;
--! Check for test bench time out
p_control : process
begin
wait for 30 ms;
-- the test bench should already have terminated!
assert false
report "Test timed out!"
severity failure;
end process;
--! Check for test bench time out
p_checktime : process
begin
wait for 100 ns;
wait until iTimeStampHi /= 0;
-- this must not happen!
assert false severity failure;
end process;
Nach der Instantiierung der uut und dem bekannten Taktgenerator, kommen jetzt noch zwei Überwachungsprozesse, die die Testbench abbrechen, wenn etwas unerwartetes passiert, was ich aber nicht extra checken will.
Den Timeout-Prozess p_control hatten wir ja schon. p_checktime dient dem Vergleich der oberen bits von TimeStamp. Da VHDL keine 64 Bit Integer kennt, wurde der TimeStamp von mir in die unteren und oberen Bits aufgeteilt. p_checktime stellt nur sicher, dass keines der oberen Bits während des tests gesetzt wird (so lange läuft ja der Test nicht). Damit hat man eine Testabdeckung auch für diese Bits (wenn auch nicht vollständig. Dafür müsste man ja 10 Jahre simulieren).
-- Stimulus process
stim_proc: process
variable expected_time : time;
begin
-- prepare reporting
xml_open_tb(SIM_DIR & "TestSysService.xml");
xml_header("TestSysService", "SysService");
xml_open_tag("Testcases", 1);
TestCase <= 1;
xml_open_tc("Reset behaviour", 1, TestCase);
xml_test_step("Power on reset", 1, TestStep);
Reset <= '0';
WaitCycles(1);
check_point((Init /= '1'), "Check if Power On causes Init", TestStepErr);
WaitCycles(2);
check_point((Init /= '0'), "Check if Init is released after 2 cycles", TestStepErr);
xml_test_step("Reset input", 2, TestStep);
Reset <= '1';
WaitCycles(2);
check_point((Init /= '1'), "Check if Reset input causes Init", TestStepErr);
WaitCycles(2);
xml_test_step("Signal initialization", 3, TestStep);
check_point((iTimeBase /= 1), "Check if only TimeBase(0) is active", TestStepErr);
check_point((iTimeStampLo /= 0), "Check if TimeStamp is 0", TestStepErr);
TestErr <= TestErr or TestStepErr;
xml_close_tc("Reset behaviour test", TestStepErr /= '0');
TestStepErr <= '0';
Reset <= '0';
WaitCycles(1);
Damit kann unser Test stim_proc starten.
- xml_open_tb erzeugt die Ausgabedatei
- xml_header schreibt dann Informationen über den durchgeführten Test
- xml_open_tag öffnet eine Ausgabe-Gruppe, diese muss mit xml_close_tag wieder geschlossen werden
- xml_open_tc beginnt einen Testcase , dieser muss mit xml_close_tc wieder beendet werden
- xml_test_step untergliedert die einzelnen Testcases in eine Abfolge von Schritten
- xml_reason gibt Kontext zu einem Fehler aus (Siehe Testcase 3)
- check_point vergleicht Simulationsergebnisse mit Erwartungswerten, protokolliert sie und signalisiert Fehler
Im ersten Testcase wird die Generierung der Ausgangssignale auf Grund von Poweron und der Reset Eingang überprüft.
TestCase <= 2;
xml_open_tc("Clock handling", 2, TestCase);
xml_test_step("Clock output following clock input", 1, TestStep);
wait until falling_edge(Ext_Clk);
check_point((Clk /= '1'), "Clk stays high until ExtClk goes low", TestStepErr);
wait for 10 ps;
check_point((Clk /= '0'), "Clk goes low after ExtClk goes low", TestStepErr);
wait until rising_edge(Ext_Clk);
check_point((Clk /= '0'), "Clk stays low until ExtClk goes high", TestStepErr);
wait for 10 ps;
check_point((Clk /= '1'), "Clk goes high after ExtClk goes high", TestStepErr);
WaitCycles(1);
TestErr <= TestErr or TestStepErr;
xml_close_tc("Clock handling test", TestStepErr /= '0');
TestStepErr <= '0';
Im zweiten Testcase wird überprüft, ob ExtClk auch wirklich Clk folgt. Dieser Test ist eigentlich nicht vollständig, da er ExtClk nur um die Flanken von Clk herum misst. Eigentlich bräuchte man da noch Messprozesse. Aber bei so etwas trivialem war mir das dann doch zu böd.
Was ich aber mit diesem Code zeigen wollte ist, dass Clk, welches ja von ExtClk versorgt wird um einen Tick nach ExtClk kommt. wait for 1 ps hätte hier übrigens gereicht.
TestCase <= 3;
Reset <= '1';
WaitCycles(1);
Reset <= '0';
WaitCycles(2);
xml_open_tc("TimeStamp counting", 3, TestCase);
xml_test_step("Reset condition", 1, TestStep);
if (iTimeStampLo /= 0) then
xml_reason("TimeStamp found: " & integer'Image(iTimeStampLo));
end if;
check_point((iTimeStampLo /= 0), "TimeStamp starts with 0", TestStepErr);
WaitCycles(1);
if (iTimeStampLo /= 1) then
xml_reason("TimeStamp found: " & integer'Image(iTimeStampLo));
end if;
check_point((iTimeStampLo /= 1), "TimeStamp starts counting after reset", TestStepErr);
xml_test_step("TimeStamp counting", 1, TestStep);
WaitCycles(99);
if (iTimeStampLo /= 100) then
xml_reason("TimeStamp found: " & integer'Image(iTimeStampLo));
end if;
check_point((iTimeStampLo /= 100), "TimeStamp reaches 100 after 100 cycles", TestStepErr);
WaitCycles(900);
if (iTimeStampLo /= 1000) then
xml_reason("TimeStamp found: " & integer'Image(iTimeStampLo));
end if;
check_point((iTimeStampLo /= 1000), "TimeStamp reaches 1000 after 1000 cycles", TestStepErr);
WaitCycles(1);
TestErr <= TestErr or TestStepErr;
xml_close_tc("TimeStamp counting test", TestStepErr /= '0');
TestStepErr <= '0';
Testcase 3 schaut sich jetzt nach Reset das TimeStamp Signal an und testet es dann noch mal nach 1, 100 und 1000 Takten. Das ist natürlich nur exemplarisch. Aber bei einem simplen Zähler reicht das schon.
Hier wird jetzt xml_reason verwendet. Es wird ja auf einen bestimmten Erwartungswert z.B. 100 überprüft. Jetzt nur einfach zu sagen "100 war es aber nicht" ist für den Entwickler nicht hilfreich. Deswegen wird im Fehlerfall das Ergebnis der Simulation protokolliert.
Der Grund muss davor angegeben werden, da die Bibliothek sehr einfach gestickt ist und check_point seine Ausgabezeile abschließt (Danach kann man da also nichts mehr reinschreiben).
TestCase <= 4;
Reset <= '1';
WaitCycles(1);
Reset <= '0';
WaitCycles(1);
xml_open_tc("TimeBase generation", 3, TestCase);
xml_test_step("TimeBase events upwards", 1, TestStep);
CheckTimeStamp( 20, 1, 9, iTimeStMsr, TestStepErr);
CheckTimeStamp( 20, 1, 19, iTimeStMsr, TestStepErr);
CheckTimeStamp( 200, 2, 99, iTimeStMsr, TestStepErr);
CheckTimeStamp( 200, 2, 199, iTimeStMsr, TestStepErr);
CheckTimeStamp( 2000, 3, 999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 2000, 3, 1999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 20000, 4, 9999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 20000, 4, 19999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 200000, 5, 99999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 200000, 5, 199999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 2000000, 6, 999999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 2000000, 6, 1999999, iTimeStMsr, TestStepErr);
-- CheckTimeStamp( 20000000, 7, 9999999, iTimeStMsr, TestStepErr);
-- CheckTimeStamp( 20000000, 7, 19999999, iTimeStMsr, TestStepErr);
-- CheckTimeStamp(200000000, 8, 99999999, iTimeStMsr, TestStepErr);
-- CheckTimeStamp(200000000, 8, 199999999, iTimeStMsr, TestStepErr);
xml_test_step("TimeBase events downwards", 2, TestStep);
CheckTimeStamp( 200000, 5, 2099999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 20000, 4, 2109999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 2000, 3, 2110999, iTimeStMsr, TestStepErr);
CheckTimeStamp( 200, 2, 2111099, iTimeStMsr, TestStepErr);
CheckTimeStamp( 20, 1, 2111109, iTimeStMsr, TestStepErr);
TestErr <= TestErr or TestStepErr;
xml_close_tc("TimeBase generation test", TestStepErr /= '0');
TestStepErr <= '0';
Jetzt kommt der eigentliche Test der TimeBase, denn die ist ja nicht ganz trivial.
Zuerst wird ein Reset erzeugt, damit der Test mit TimeStamp 0 startet.
Anschließend wird auf einen der Trigger der TimeBase gewartet. Es wird angegeben wie lange maximal gewartet wird (das Doppelte des erwarten), welcher Trigger und vor allem wann dieser erwartet wird.
TimeBase(1) wird z. B. im Takt 9 und dann wieder im Takt 19 erwartet, usw.
Warum 9 und nicht 10? Weil ich diese Trigger verarbeite und wenn ich jetzt ein Signal alle 100 Takte toggeln lassen möchte, passiert das halt einen Takt später, also im Takt 100, 200, etc.
Um nicht zu lange Testen zu müssen, habe ich mir die Tests für die Trigger 100 ms und 1 s gespart. Nicht ganz richtig, aber ich bin halt Pragmatiker. Da der Code in einer Schleife generiert worden ist, ist eigentlich mit dem Trigger Index 2 schon der gesamte Code getestet worden.
Das Ergebnis der Simulation befindet sich in der konvertierten HTML-Datei.
Weiter zu Schritt 2.