Czym naprawdę jest testbench
Testbench to po prostu moduł w Verilogu. Od modułu syntezowalnego różni się zawartością:
- Nie ma portów, bo jest na szczycie hierarchii symulacji.
- Tworzy instancję testowanego projektu.
- Steruje wejściami DUT z bloków
initialialways. - Obserwuje wyjścia DUT przez
$display,$monitoralbo zapis do pliku VCD. - Kończy symulację przez
$finish.
To cała jego rola. Nie ma specjalnego słowa kluczowego "testbench": to, że coś jest testbenchem, poznajesz po tym, co robi.
Standardowy szkielet
To cały wzorzec. Pięć sekcji, łatwych do skopiowania i dostosowania.
Dla kombinacyjnych DUT (takich jak ten sumator) zegar nie jest potrzebny. Dla sekwencyjnych DUT jest, a to kolejna warstwa.
Generowanie zegara dla sekwencyjnych DUT
Gdy DUT ma wejście clk, testbench musi generować zegar. Standardowa jednolinijkowa wersja:
reg clk = 0;
always #5 clk = ~clk;
To przełącza clk co 5 jednostek czasu. Każde przełączenie to połowa okresu zegara, więc pełny okres to 10 jednostek (jeden cykl = 5 w stanie wysokim + 5 w niskim). Jeśli timescale to 1ns / 1ps (domyślna wartość w większości symulatorów), daje to zegar 100 MHz.
Dobierz połowę okresu do tego, co chcesz modelować. Dla zegara 10 MHz przy tym samym timescale użyj #50. Dla zegara 200 MHz użyj #2.5 (albo zmień timescale).
Sekwencja resetu
Projekty synchroniczne potrzebują resetu trzymanego w stanie wysokim przez kilka cykli po chwili 0, a potem zwalnianego. Konwencjonalny kształt:
reg reset = 1; // start asserted
initial begin
// Hold reset for a few clocks.
#20 reset = 0;
end
To trzyma reset w stanie wysokim do chwili 20, a potem go opuszcza. Zanim reset spadnie, zegar wykona kilka cykli, każdy przerzutnik zapamięta wartość resetu, a projekt znajdzie się w znanym stanie.
Dla resetu aktywnego stanem niskim (reset_n zamiast reset) odwróć wartość początkową:
reg reset_n = 0; // active-low: 0 means asserted
initial begin
#20 reset_n = 1;
end
Kompletny testbench sekwencyjny
Połączenie zegara, resetu i pobudzenia:
To kompletny testbench dla 4-bitowego licznika. Kliknij Run, a zobaczysz, jak licznik wychodzi z resetu, liczy w górę, gdy jest włączony, zatrzymuje się, gdy enable spada, wznawia liczenie, gdy rośnie, i się kończy.
Trzy rzeczy, na które warto zwrócić uwagę w strukturze:
- Trzy odrębne ośrodki aktywności: generator zegara (jeden
always), pobudzenie (jedeninitial) i monitor (kolejnyalways). Każdy ma jedno zadanie. - Opóźnienia
#w bloku pobudzenia są mierzone w jednostkach czasu, a nie w cyklach zegara. Jeśli okres zegara to 10 jednostek,#10to dokładnie jeden cykl. Niektóre testbenche używają zamiast tego@(posedge clk), które przesuwa o jeden cykl niezależnie od okresu. - Monitor używa
$displayz blokualways @(posedge clk). To wypisuje coś w każdym cyklu. Do bardziej rozbudowanego wyjścia przejdź na$monitor(omówiony dalej).
Pobudzenie liczone w cyklach
Czasem "poczekaj N cykli" czyta się lepiej niż "poczekaj N jednostek czasu":
initial begin
@(posedge clk); // wait for next clock edge
reset = 0;
repeat (5) @(posedge clk); // wait 5 more cycles
enable = 1;
repeat (8) @(posedge clk);
enable = 0;
repeat (10) @(posedge clk);
$finish;
end
@(posedge clk) blokuje do następnego zbocza narastającego zegara. repeat (N) @(posedge clk); czeka N cykli. Ten styl nie zależy od okresu zegara: jeśli zmienisz częstotliwość zegara, pobudzenie nadal robi to samo w przeliczeniu na cykle.
W pierwszych testbenchach prostsze są opóźnienia #. W produkcyjnych testbenchach, które mogą działać przy różnych częstotliwościach zegara, bezpieczniejszy jest styl oparty na cyklach.
Testy samosprawdzające
Dotychczasowy testbench wypisuje, co się stało, a czytanie wyjścia zostawia tobie. Testbench samosprawdzający zamiast tego sprawdza wyjście i zgłasza wynik pozytywny albo negatywny:
initial begin
#1;
a = 10; b = 20;
#1;
if (sum !== 30) begin
$display("FAIL: 10 + 20 = %0d (expected 30)", sum);
$finish;
end
a = 250; b = 5;
#1;
if (sum !== 255) begin
$display("FAIL: 250 + 5 = %0d (expected 255)", sum);
$finish;
end
$display("PASS");
$finish;
end
Dla bezpieczeństwa używaj !== (operatora nierówności dokładnej): nie zwraca x, gdy jeden z argumentów ma nieznane bity. Wzorzec: podaj wejścia, poczekaj na ustabilizowanie, porównaj z oczekiwaną wartością, zgłoś wynik.
Samosprawdzanie jest bezcenne w zestawach testów regresyjnych: tysiące testów mogą działać bez nadzoru, a uwagi wymagają tylko niepowodzenia.
Co dalej
Masz już kształt testbencha, który wystarczy dla każdego modułu. Kolejne artykuły omawiają narzędzia używane wewnątrz testbencha: Display i Monitor do bogatszego wyjścia tekstowego, Dumpfile i VCD do graficznego debugowania przebiegów oraz Timescale i opóźnienia do dokładnego kontrolowania, jak czas symulacji odnosi się do czasu rzeczywistego.
Najczęściej zadawane pytania
Czym jest testbench w Verilogu?
Testbench to moduł w Verilogu, zwykle bez portów, którego jedynym celem jest sprawdzanie testowanego projektu (DUT). Tworzy instancję DUT, generuje zegar, podaje pobudzenie z bloków initial i always, obserwuje wyjścia przez $display/$monitor, opcjonalnie zapisuje przebieg VCD i kończy symulację przez $finish.
Jak wygenerować zegar w testbenchu w Verilogu?
Standardowy wzorzec to jedna linia: always #5 clk = ~clk; (z wcześniejszą deklaracją reg clk = 0;). Przełącza ona clk co 5 jednostek czasu symulacji, co daje okres 10 jednostek (zegar 100 MHz, jeśli timescale jest w nanosekundach). #5 to połowa okresu: przez połowę czasu clk jest w stanie wysokim, a przez drugą połowę w niskim.
Czym jest DUT w Verilogu?
DUT to skrót od Design Under Test, czyli modułu, który sprawdza testbench. Zgodnie z konwencją instancja DUT w testbenchu nazywa się dut albo u_dut: my_module dut(.clk(clk), .reset(reset), .in(in), .out(out));. Nazwa to tylko etykieta. Liczy się to, że moduł ma instancję, jego porty są podłączone do sygnałów testbencha, a testbench tymi sygnałami steruje.
Jak długo powinna trwać symulacja w Verilogu?
Wystarczająco długo, żeby sprawdzić wszystko, co chcesz zweryfikować, a potem $finish. Większość testbenchy ustawia jawny limit czasu (#1000 $finish), żeby symulacja nie zawisła w oczekiwaniu na zdarzenie, które nigdy nie nastąpi. W tym oknie podaj pobudzenie, pozwól DUT się ustabilizować i najlepiej dodaj samosprawdzające instrukcje if, które wypiszą FAIL, gdy wyjście nie zgadza się z oczekiwaniami.