Menu

Testbench w Verilogu: podstawy weryfikacji modułu

Jak napisać testbench w Verilogu: generowanie zegara, sekwencja resetu, pobudzenie, obserwacja i standardowy szkielet, który napędza każdą uruchamianą symulację.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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 initial i always.
  • Obserwuje wyjścia DUT przez $display, $monitor albo 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:

  1. Trzy odrębne ośrodki aktywności: generator zegara (jeden always), pobudzenie (jeden initial) i monitor (kolejny always). Każdy ma jedno zadanie.
  2. Opóźnienia # w bloku pobudzenia są mierzone w jednostkach czasu, a nie w cyklach zegara. Jeśli okres zegara to 10 jednostek, #10 to dokładnie jeden cykl. Niektóre testbenche używają zamiast tego @(posedge clk), które przesuwa o jeden cykl niezależnie od okresu.
  3. Monitor używa $display z bloku always @(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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ