Kurs FPGA Lattice (12). Symulacja w Icarus Verilog i GTKWave

Kurs FPGA Lattice (12). Symulacja w Icarus Verilog i GTKWave

Symulacja jest bardzo ważnym narzędziem. Pozwala uruchomić nasz kod języka Verilog w wirtualnym układzie FPGA, co pozwala zweryfikować wszystkie sygnały. Dzięki temu możemy wykryć błędy w kodzie nawet przed wgraniem bitstreamu do prawdziwego FPGA.

W 8 części kursu zaprezentowałem symulator EDA Playground. Jest to świetne narzędzie dla początkujących. Wystarczy wejść na stronę www.edaplayground.com, założyć konto i od razu możemy testować nasze kody w przeglądarce internetowej – bez potrzeby instalowania czegokolwiek na naszym komputerze. Symulator ten jest świetny, ale ma pewne wady:

  • Wymaga połączenia z internetem.
  • Serwer musi działać, a niestety rosnąca popularność symulatora sprawiła, że stał się on ofiarą własnego sukcesu i często serwery są przeciążone.
  • Mamy osobne pliki do symulacji w chmurze i osobne pliki do syntezy na komputerze. Zmiany musimy wprowadzać zarówno w chmurze jak i na komputerze. Łatwo stracić nad tym kontrolę, przez co może okazać się, że pliki do symulacji i syntezy nie będą identyczne.

Trzeci problem staje się tym większy im bardziej rozbudowany projekt tworzymy. EDA Playground nie obsługuje żadnych narzędzi do zarządzania kodem, takich jak np. Git, więc jedyny sposób na „synchronizację” pomiędzy komputerem a chmurą, to ręczne kopiowanie plików.

W tej części poznamy symulator Icarus Verilog. Kod syntezowalny oraz testbench będziemy przechowywać w projekcie Lattice Diamond. Napiszemy także proste skrypty, dzięki którym będziemy uruchamiać symulator jednym kliknięciem. Skonfigurujemy program GTK Wave, który pozwoli nam oglądać i analizować symulowane sygnały.

Instalacja Icarus Verilog

Icarus jest projektem open source i możemy go bezpłatnie stosować bez ograniczeń. Powstał jako hobbystyczny projekt Stephena Williamsa w 2007 roku. Teraz autor wraz z kilkoma innymi osobami ciągle rozwijają projekt – już 16 lat.

Strona główna projektu znajduje się pod adresem iverilog.icarus.com a różne instrukcje symulatora znajdziemy na stronie steveicarus.github.io. Ale jak to w ogóle zainstalować? Autor udostępnia tylko kody źródłowe i instrukcję długą na kilka ekranów, jak sklonować repozytorium kodu i skompilować samemu, wklepując ręcznie litanię magicznych zaklęć w konsoli systemowej. Co gorsza – instrukcja dotyczy tylko systemu Linux.

Na ratunek przychodzi Pablo Bleyer, który na swojej stronie udostępnia normalny windowsowy instalator. Ze strony bleyer.org/icarus możemy ściągnąć instalator najnowszej wersji. W chwili pisania tego artykułu najnowsza wersja to v12 wydana 11 czerwca 2022 roku. Instalacja jest banalna i nie wymaga komentarza. Oprócz symulatora zostanie zainstalowana także przeglądarka GTKWave, która w postaci graficznej umożliwia analizowanie wyników symulacji. Instalator skonfiguruje system tak, że pliki z rozszerzeniem *.vcd i *.gtkw mają być otwierane przez GTKWave.

Przetestujmy, czy wszystko zainstalowało się poprawnie. Kliknij przycisk Start, a następnie Uruchom i wpisz cmd, aby uruchomić wiersz poleceń. Wpisz polecenie iverilog -v i wciśnij enter. Powinien pokazać się komunikat z numerem wersji oraz informacjami o autorze i licencji (rysunek 1). Jeżeli natomiast pojawi się komunikat, że nie rozpoznano polecenia, sprawdź czy instalator poprawnie dodał ścieżki do zmiennej środowiskowej Path.

Rysunek 1. Sprawdzanie wersji programu Icarus Verilog

W systemie Windows 11 robi się to następująco. Kliknij przycisk Start i wpisz „zmienne”. Pojawi się opcja Edytuj zmienne środowiskowe systemu, którą wybieramy. Otworzy się okienko, w którym klikamy przycisk Zmienne środowiskowe. Pojawi się okno takie, jakie pokazano w lewej części rysunku 1.

Wybieramy z listy zmienną Path, po czym klikamy Edytuj. Wyświetlone zostanie okno, które widać w prawej części rysunku 2. Upewnij się, że zostały dodane ścieżki zaznaczone czerwoną ramką i że prowadzą one do katalogu, w którym został zainstalowany Icarus Verilog oraz GTKWave (w szczególności, jeżeli zainstalowałeś je w innym katalogu niż domyślny).

Rysunek 2. Konfiguracja zmiennych środowiskowych

Prosty testbench

W dzisiejszym odcinku będziemy pracować na kodzie, z którego korzystaliśmy w 9 i 10 części kursu, tworząc sterownik wyświetlacza multipleksowanego i klawiatury matrycowej z maszyną stanów. Aby zrozumieć kod, który będziemy omawiać w tym odcinku, trzeba już znać podstawy symulacji w języku Verilog, które zostały omówione w 8 odcinku.

Zacznijmy od czegoś prostego, aby zaznajomić się z symulatorem Icarus Verilog oraz przeglądarką GTKWave – nasz pierwszy testbench będzie służył do symulacji modułu StrobeGenerator, który omawialiśmy w 9 części kursu. Moduł ten służy do generowania krótkich impulsów stanu wysokiego, pojawiających się co pewien stały okres czasu, definiowany parametrem. Jego kod pokazano na listingu 1 w odcinku 9.

Listing 1. Testbench modułu StrobeGenerator

// Plik strobe_generator_tb.v
`timescale 1ns/1ps // 1

module StrobeGenerator_tb();

parameter CLOCK_FREQ_HZ = 10_000_000; // 2
parameter HALF_PERIOD_NS = 1_000_000_000 / (2*CLOCK_FREQ_HZ); // 3

// Generator sygnału zegarowego
reg Clock = 1’b1; // 4
always begin // 5
#HALF_PERIOD_NS; // 6
Clock = !Clock; // 7
end

// Zmienne
reg Reset = 1’b0; // 8
wire Strobe;

// Sekwencja testowa
initial begin
$timeformat(-6, 3, “us”, 10); // 9
$display(“===== START =====”); // 10
$display(“FREQUENCY_HZ = %9d”, DUT.FREQUENCY_HZ); // 11
$display(“PERIOD_US = %9d”, DUT.PERIOD_US); // 12
$display(“DELAY = %9d”, DUT.DELAY); // 13
$display(“WIDTH = %9d”, DUT.WIDTH); // 14

#1 Reset = 1’b1; // 15

repeat(3) begin // 16
@(posedge Strobe); // 17
$display(“Strobe detected at %t”, $realtime); // 18
end

@(negedge Strobe); // 19
#1000; // 20

$display(“===== END =====”); // 21
#1 $finish; // 22
end

// Utworzenie instancji testowanego modułu
StrobeGenerator #( // 23
.FREQUENCY_HZ(CLOCK_FREQ_HZ), // 24
.PERIOD_US(10) // 25
) DUT( // 26
.Clock(Clock),
.Reset(Reset),
.Strobe(Strobe)
);

// Zapis danych wynikowych
initial begin // 27
$dumpfile(“strobe_generator.vcd”); // 28
$dumpvars(0, DUT); // 29
end

endmodule

W Lattice Diamond otwórz projekt z 10 części kursu. W drzewku projektu kliknij prawym przyciskiem myszy katalog Input Files, a następnie wybierz Add i New File. Wybieramy plik języka Verilog, nazywamy go strobe_generator_tb.v i zapisujemy w katalogu StateMachine/source. Nazwy testbenchy będziemy nazywać tak samo, jak nazywają się testowane moduły, ale z dopiskiem „_tb” na końcu nazwy pliku.

W drzewku projektu pojawił się nowy plik. Musimy jeszcze zmienić przeznaczenie tego pliku. Klikamy go prawym przyciskiem myszy, a następnie wybieramy menu Include for. Domyślnie zaznaczoną opcją jest Synthesis and Simulation. Jednak pliki testbench musimy wykluczyć z syntezy. Aby to uczynić, wybieramy Include for Simulation. Dzięki temu syntezator nie będzie brał plików symulacji pod uwagę.

W tym pliku umieścimy kod, który pokazano na listingu 1. Moduł StrobeGenerator został omówiony w 9 części kursu – zachęcam, aby przeczytać ponownie opis jego działania, bo inaczej opis testbencha tego modułu może być mało zrozumiały.

Zadaniem testbenacha będzie utworzenie modułu StrobeGenerator_tb, w którym utworzymy instancję modułu StrobeGenerator. Do instancji doprowadzimy sygnały sterujące takie, jakie mogłaby ona otrzymać w prawdziwym układzie FPGA. W wyniku symulacji otrzymamy plik, zawierający zarejestrowane przebiegi wszystkich sygnałów od początku do końca symulacji. Będziemy mogli je przeglądać w programie GTKWave, który działa jak wirtualny analizator logiczny. Istnieje możliwość, by testbench weryfikował czy testowany moduł podaje prawidłowe odpowiedzi na otrzymywane sygnały, jednak w tym przypadku nie będziemy korzystać z możliwości automatycznej weryfikacji i ograniczymy się tylko do ręcznego analizowania wyświetlanych przebiegów.

Przeanalizujmy kod przedstawiony na listingu 1. Testbench zaczynamy standardowo od zdefiniowania jednostek czasu (linia 1) przy pomocy instrukcji `timescale. Zwróć uwagę, że należy zastosować ukośny apostrof, znajdujący się w nad klawiszem tabulatora, a nie zwykły apostrof prosty. Podstawową jednostką opóźnień, występujących po znaku #, to jedna nanosekunda. Rozdzielczość czasowa symulacji, czyli najmniejszy mierzalny okres czasu to jedna pikosekunda. W tym przypadku nie ma potrzeby, by ustawiać tak małą rozdzielczość, ale robimy to w celu edukacyjnym.

Następnie mamy dwa parametry, które służą do wygenerowania sygnału zegarowego, taktującego testowany moduł. Parametr CLOCK_FREQ_HZ to częstotliwość zegara (linia 2). Parametr ten jest przekazywany do wszystkich podrzędnych modułów, dzięki czemu zmiana tego jednego parametru na początku testbencha ma wpływ na wszystkie komponenty. Parametr HALF_PERIOD_NS obliczany jest na podstawie CLOCK_FREQ_HZ i wyznacza czas trwania połowy okresu zegara, tzn. jak długo trwa stan wysoki lub niski (wypełnienie jest równe 50%, więc stan niski i wysoki trwają tak samo długo).

Zwróć uwagę, że kod z linii 3 jest poprawny tylko wtedy, kiedy podstawową jednostką opóźnienia jest jedna nanosekunda. Z tego powodu we wzorze pojawiła się liczba 1_000_000_000 – dokładnie tyle nanosekund jest w jednej sekundzie. Znaki podkreślenia nie mają żadnego znaczenia i służą do zwiększenia czytelności długich liczb.

Aby zrobić generator sygnału zegarowego, musimy najpierw zdefiniować rejestr Clock i przypisać mu jakąś wartość początkową (linia 4). Sposobów na uzyskanie sygnału zegarowego jest wiele. Jednym z nich jest wykorzystanie osobnego bloku always (linia 5), który powtarza się w nieskończoność, równolegle z innymi procesami. Wewnątrz niego znajdują się tylko dwie instrukcje – opóźnienie o połowę okresu zegarowego (linia 6) i negowanie dotychczasowego stanu rejestru Clock (linia 7). W ten sposób dostajemy sygnał o wypełnieniu 50% i częstotliwości określonej parametrem CLOCK_FREQ_HZ.
W linii 8 definiujemy zmienne, które będą wykorzystane doprowadzone do portów testowanego modułu. Zmienna Reset jest typu reg, ponieważ będziemy ją modyfikować z sekwencji testowej. Zmienna Strobe jest zdefiniowana jako wire, ponieważ będzie ona podłączona do wyjścia testowanego modułu o tej samej nazwie i będziemy jedynie obserwować jak zmienia się jej stan.

Przeskoczmy teraz do linii 23, gdzie utworzona zostaje instancja testowanego modułu. Moduł ma dwa parametry. Poprzez parametr FREQUENCY_HZ informujemy moduł, jaka jest częstotliwość sygnału zegarowego (linia 24), a w parametrze PERIOD_US ustawiamy żądany okres czasu pomiędzy sygnałami strobe, jakie moduł ma generować (linia 25). Instancję testowanego modułu zwyczajowo nazywamy DUT, czyli device under test.

Wróćmy na początek sekwencji testowej. W bloku initial (linia 9) konfigurujemy, w jaki sposób ma być wyświetlany czas symulacji – w ten sposób czas będzie wyświetlany w mikrosekundach i będą widoczne trzy miejsca po przecinku. Następnie printujemy kilka parametrów. Są to: częstotliwość sygnału zegarowego, wymagany odstęp czasowy między sygnałami strobe, liczba cykli zegarowych mijająca pomiędzy sygnałami strobe i liczba bitów licznika, który zlicza te impulsy zegarowe (linie 10...14).

W linii 15 zmieniamy stan sygnału Reset z 0 na 1, co powoduje start pracy testowanego modułu (stan 0 oznacza zerowanie wszystkich rejestrów, stan 1 oznacza normalną pracę układu). Moduł zaczyna zliczać zbocza sygnału zegarowego i będzie generował sygnały na wyjściu Strobe co określony czas.
Podczas testu będziemy chcieli zaobserwować trzy sygnały strobe. Jest to wystarczająca liczba, aby wykresy czasowe były czytelne. W tym celu wykorzystamy pętlę repeat, którą umieszczono w linii 16. Można by zastosować także pętlę for, jaką znamy z C++, ale zastosowanie pętli repeat jest wygodniejsze, jeżeli nie potrzebujemy mieć dostępu do iteratora pętli.

Wewnątrz pętli, w linii 17, widzimy instrukcję @(posedge Strobe), która może nieco zaskakiwać. Zwykle widzieliśmy takie konstrukcje na początku bloków always, które zwykle reagowały na zbocza sygnału Clock i Reset. Instrukcja @ powoduje zawieszenie wykonywania programu testowego aż do wystąpienia zdarzenia określonego w nawiasach. Tak więc czekamy, aż sygnał Strobe, pochodzący z testowanego modułu, zmieni swój stan z 0 na 1. Kiedy to się wydarzy to wyświetlamy komunikat, że został wykryty sygnał strobe (linia 18). W ten sposób sprawimy, że na wykresach przebiegów w analizatorze GTKWave widoczne będą trzy szpilki sygnału Strobe.

Przed zakończeniem testu poczekamy jeszcze na zbocze opadające sygnału Strobe (linia 19) i dodatkowo jeszcze 1000 ns (linia 20) – tylko po to, aby na wykresie było widać pewien odstęp między zboczem opadającym a krawędzią ekranu.

Kończymy symulację poleceniem $finish w linii 22. W odcinku 8 używaliśmy polecenia $stop. Istnieje pewna subtelna różnica między tymi poleceniami i ich działanie trochę się różni w zależności od wykorzystywanego symulatora. Instrukcja $finish powoduje zakończenie symulacji i wyjście z symulatora, a instrukcja $stop powoduje wstrzymanie symulacji, a symulator przechodzi w tryb interaktywny, gdzie możemy ręcznie zmieniać sygnały wejściowe i symulować dalej.

W przypadku EDA Playground nie ma żadnej różnicy między tymi instrukcjami. W obu przypadkach symulator kończy pracę i wyświetla przebiegi sygnałów na wykresie. W przypadku ModelSim polecenie $finish powoduje zamknięcie programu (na szczęście najpierw się pyta, czy faktycznie chcemy go zamknąć). Natomiast w przypadku Icarus Verilog polecenie $finish powoduje wyłączenie symulatora, wygenerowanie pliku wynikowego i powrót do konsoli. Polecenie $stop w Icarus Verilog powoduje wejście w tryb poleceń symulatora, gdzie możemy nim sterować poprzez wydawanie poleceń w konsoli.

Nie jest to zbyt wygodne – dlatego lepiej jest zakończyć pracę symulatora poleceniem $finish i resztę zrobić w GTKWave.

W linii 27 mamy jeszcze jeden blok initial, który wykonuje się natychmiast w chwili startu symulacji. Służy on tylko do dwóch celów. Pierwszy to utworzenie pliku, w którym będą zapisywane wszystkie informacje z symulacji przy pomocy polecenia $dumpfile. Proponuję, by pliki nazywać tak samo, jak nazywa się plik testowanego modułu, ale z rozszerzeniem vcd (linia 28).

W linii 29 mamy instrukcję $dumpvars, określającą, jakie zmienne chcemy zapisać w pliku wynikowym, dzięki czemu będziemy mogli je analizować w GTKWave. Pierwszy argument polecenia może przybierać jedną z trzech możliwych wartości:

  • 0 – zostaną zapisane wszystkie zmienne z modułu, którego nazwę podano w drugim argumencie. Zostaną zapisane także wszystkie zmienne we wszystkich podrzędnych modułach. Jeżeli zostanie wskazana nazwa moduły testbench, to symulator zapisze absolutnie wszystko. Należy pamiętać, że w tym przypadku plik wynikowy może okazać się bardzo duży;
  • 1 – w kolejnych argumentach należy podać nazwy modułów, których zmienne nas interesują. Ta opcja pomija wszystkie zmienne z modułów podrzędnych;
  • 2 – w kolejnych argumentach należy podać wszystkie zmienne, jakie nas interesują. Wszystkie zmienne, które nie zostaną jawnie podane, nie zostaną zapisane w pliku wynikowym.

Jeżeli w naszych kodach występują pamięci to każdą z nich musimy dumpować osobnym poleceniem. Będzie o tym więcej w odcinku poświęconym pamięciom.

Prosta symulacja

Skoro mamy już gotowy testbench, możemy przystąpić do symulacji. Icarus Verilog jest symulatorem działającym w środowisku konsolowym. Zatem mamy trzy możliwości w jaki sposób możemy z niego skorzystać:

  1. Możemy otworzyć wiersz poleceń (cmd) i ręcznie wpisywać wszystkie polecenia.
  2. Możemy napisać skrypt, który wykona wszystkie polecenia automatycznie.
  3. Możemy zintegrować symulator z jakimś innym IDE niż Lattice Diamond – na przykład VS Code.

Spróbujemy zastosować metodę drugą. Utworzymy skrypt BAT, który dodamy do projektu w Diamond. Dzięki temu będziemy mogli uruchomić symulację przy pomocy dwóch kliknięć myszką bez potrzeby otwierania żadnych konsol i wpisywania jakichkolwiek poleceń.

Otwórz katalog StateMachine\source i utwórz tam plik strobe_generator.bat, a następnie wpisz do niego kod z listingu 2, korzystając z Notatnika lub innego edytora tekstu. Wróć do programu Diamond. W drzewku plików projektowych kliknij prawym przyciskiem Input Files, a następnie wybierz Add New File.

Listing 2. Skrypt strobe_generator.bat

@echo off 1
cd StateMachine 2
cd source 3
iverilog -o strobe_generator.o strobe_generator.v strobe_generator_tb.v 4
vvp strobe_generator.o  5
del strobe_generator.o  6
pause 7

Wchodzimy do katalogu StateMachine/source i dodajemy plik bat, który przed chwilą utworzyliśmy. Jeżeli go nie widzisz to w polu wyboru Files of Type wybierz All files (*.*). Skrypt pojawi się w drzewku projektowym obok innych plików źródłowych. Aby go uruchomić, wystarczy go dwukrotnie kliknąć.

Na rysunku 3 pokazano drzewko projektowe, zawierające skrypty BAT do symulacji wszystkich modułów projektu oraz pliki GTWK, które uruchamiają analizator GTKWave.

Rysunek 3. Drzewko plików projektowych po dodaniu skryptów symulujących oraz plików konfiguracji przeglądarki GTKWave

Omówimy teraz wszystkie linie skryptu z listingu 2. Linia 1 ma znaczenie czysto kosmetyczne – powoduje, że na konsoli zostaną ukryte nazwy katalogów oraz polecenia, jakie są wykonywane przez system. Zobaczymy tylko efekty tych poleceń. Linie 2 i 3 to polecenia zmiany katalogu, w którym wywoływane są polecenia. Jeżeli wywołujemy skrypt z programu Diamond to startuje on w katalogu głównym projektu, więc trzeba przejść do katalogu implementacji i następnie katalogu plików źródłowych. W przypadku kiedy skrypt zamierzamy wywoływać np. bezpośrednio z eksploratora Windows to należałoby te linie usunąć.

W linii 4 wywołujemy kompilator iverilog. Przetwarza on pliki kodu źródłowego na plik *.o, który jest uruchamiany na maszynie wirtualnej symulatora. W pierwszym argumencie podajemy nazwę pliku wyjściowego (jego nazwa i rozszerzenie mogą być dowolne), a dalej podajemy nazwy plików testbencha, testowanego modułu i ewentualnie pliki wszystkich podrzędnych modułów.

Linia 5 wywołuje symulator vvp, który ma wykonać plik *.o, otrzymany w poprzednim kroku. W tym kroku zostaną wyprintowane wszystkie komunikaty, jeżeli w kodzie testbencha znajdują się takie polecenia jak $display lub $monitor.

W linii 6 kasujemy plik wynikowy kompilatora. Po pierwsze, nie będzie już potrzebny, a po drugie – uniemożliwi to wykonanie symulacji przy kolejnym wywołaniu skryptu, jeżeli podczas kompilacji zostanie wykryty jakiś błąd. Mogłoby się tak zdarzyć, że zmieniamy coś w plikach źródłowych, ale popełniamy jakiś błąd. Wywołanie kompilatora w linii 4 kończy się niepowodzeniem, ale na dysku wciąż mamy plik *.o wygenerowany przy poprzednim wywołaniu skryptu. Ten plik ponownie jest symulowany, a my widzimy różne komunikaty wyświetlane przez symulator. Można w takiej sytuacji nie zauważyć, że kompilator zgłosi błąd. Z tego powodu lepiej skasować plik *.o, bo jeżeli go nie będzie to wtedy symulator także zgłosi błąd i nie będzie możliwości, by tego nie zauważyć.

Linia 7 to pauza. System będzie czekał na wciśnięcie dowolnego przycisku. Zastosowanie tego polecenia ma sens, jeżeli skrypt wywołujemy ze środowiska Diamond albo z eksploratora Windows, ponieważ okno konsoli automatycznie zamyka się po zakończeniu skryptu. W przypadku kiedy skrypt wywołujemy z konsoli to wtedy możemy to polecenie usunąć.

Uruchom skrypt, klikając dwukrotnie jego nazwę w drzewku projektu. Powinno otworzyć się okno wiersza poleceń, na którym zobaczysz takie komunikaty, jakie pokazano na rysunku 4.

Rysunek 4. Komunikaty symulacji modułu StrobeGenerator

Prosta analiza

Dane wynikowe z symulacji zostały zapisane do pliku strobe_generator.vcd – zgodnie z poleceniem, jakie zostało umieszczone w linii 28 listingu 1. Otwórz ten plik. Powinna uruchomić się przeglądarka GTKWave. Widzisz teraz puste okno bez żadnych widocznych sygnałów. Skonfigurujemy program w taki sposób, aby uzyskać efekt taki, jaki pokazano na rysunku 5. W lewym górnym okienku, oznaczonym etykietą SST, widać nazwę modułu nadrzędnego StrobeGenerator_tb.

Rysunek 5. Przebiegi sygnałów z symulacji modułu StrobeGenerator wyświetlone w programie GTKWave

Kliknij ikonkę + po lewej stronie jego nazwy. Rozwinie się drzewko zawierające tylko jeden element podrzędny, mianowicie moduł DUT. Gdyby w naszym projekcie było więcej modułów to w tym miejscu byśmy zobaczyli całą hierarchię projektu. Kliknij moduł DUT. W lewym dolnym okienku pokażą się wszystkie zmienne, które występują w tym module. To, że widzimy tylko zmienne występujące w podrzędnym module DUT, a nie w nadrzędnym module StrobeGenerator_tb wynika z linijki 29 listingu 1.

Kliknij zmienną Reset i przeciągnij ją do okienka Signals. Następnie przeciągnij Clock, Counter, jeszcze raz Counter i Strobe. W oknie wykresy pojawią się poziome linie, a na liniach Counter będzie wyświetlana liczba 63 przez całą szerokość ekranu. Domyślnie powiększenie jest tak duże, że po prostu widzimy fragment obejmujący pojedyncze pikosekundy. Aby zmienić powiększenie, naciśnij przycisk CTRL i pokręć kółkiem myszki. Możesz użyć skrótu CTRL-0 (zero obok przycisku backspace), aby automatycznie dopasować powiększenie w taki sposób, aby cała symulacja od początku do końca zmieściła się na ekranie. Inny sposób na dopasowanie automatyczne powiększenia to kliknięcie przycisku Zoom fit (czwarty od lewej w górnym pasku narzędzi). Możemy powiększyć interesujący nas fragment wykresu, zaznaczając go myszką i trzymając wciśnięty prawy przycisk myszki.

W okienku Signals kliknij sygnał Strobe. Jego nazwa podświetli się na niebiesko. Strzałkami w prawo i w lewo na klawiaturze możesz przesuwać marker do kolejnego i poprzedniego zbocza sygnału. Na górze, po prawej stronie przycisków w pasku narzędzi widoczny jest znacznik czasowy, pokazujący ile czasu upłynęło od początku symulacji do obecnego położenia markera.

Możemy łatwo zmierzyć czas pomiędzy dwoma zdarzeniami. Ustawiamy marker na interesujące nas zbocze sygnału, klikając je myszką lub nawigując strzałkami na klawiaturze. Następnie naciskamy przycisk B lub z menu Markers wybieramy opcję Copy Primary –> B Marker. Następnie możemy przesunąć marker do innego interesującego nas zdarzenia. W pasku na górze zostanie wyświetlony odstęp czasowy między tymi dwoma zdarzeniami. W ten sposób możemy zmierzyć okres sygnału Strobe. Jest to 10 µs. Szpilki stanu wysokiego na tym sygnale trwają 100 ns.

Kliknij dowolną szpilkę widoczną w sygnale Strobe, a następnie przybliż wykres wciskając CTRL i kręcąc kółkiem myszki. Przy odpowiednio dużym powiększeniu staną się widoczne cyferki, przedstawiające wartość dwóch liczników Counter, które dodaliśmy wcześniej.

Stan licznika jest wyświetlany w formacie szesnastkowym. Byłoby wygodniej, abyśmy widzieli liczby w formacie dziesiętnym. Aby to zmienić, prawym przyciskiem myszy klikamy jedną z dwóch etykiet Counter w okienku Signals. Z menu, które się pokaże, wybieramy następnie Data Format i Decimal.

Po zmianie na system dziesiętny okazuje się, że licznik liczy od 99 do 0.

Drugi licznik Counter przedstawimy w sposób analogowy. Klikamy jego etykietę prawym przyciskiem myszy, a następnie wybieramy Analog i Step. Stan licznika zostanie przedstawiony w sposób analogowy, przypominający przebieg trójkątny. Może się okazać, że przebieg analogowy jest mało czytelny, kiedy mieści się na jednej „linijce” wykresu. Aby go rozszerzyć, klikamy ponownie prawym przyciskiem myszy i wybieramy Insert Analog Height Extension. Można tę czynność powtórzyć kilka razy.

Możemy także pozmieniać kolory sygnałów wedle naszego uznania. Klikamy wybrany sygnał prawym przyciskiem myszy i następnie z menu Color Format możemy wybrać kolor jaki nam się podoba. Warto włączyć cienie pod sygnałami, które znajdują się w stanie wysokim. Zdecydowanie to ułatwia analizę wykresów. Aby włączyć cieniowanie, wybieramy menu View, a następnie Show Filled High Values.

Zapiszmy wszystkie ustawienia. W menu File wybieramy Write Save File. Zapisujemy go jako strobe_generator.gtwk. Dziwić może trochę to, że plik analizy został nazwany przez autora programu jako „Save File”. Zatem zapisujemy go przy pomocy opcji Write Save File, a odczytujemy poprzez Read Save File. Pliki *.gtkw możemy otworzyć także poprzez dwukrotnie kliknięcie w Eksploratorze Windows. Pliki *.gtkw możesz także dodać do środowiska Diamond, aby można było je otworzyć bezpośrednio z tego programu.

Wróć do listingu 1 i w linii 25 zmień czas z 10 μs na 12 μs. Uruchom ponownie symulację. Następnie wróć do GTKWave i kliknij przycisk Reload, w górnym pasku narzędzi (pierwszy od prawej) lub naciśnij CTRL+SHIFT+R. Program ponownie wczyta plik VCD, który powstał w wyniku symulacji i wyświetli nowe przebiegi sygnałów. Może będzie potrzebne dostosowanie powiększenia przy pomocy kombinacji przycisków CTRL+0.

Wróć jeszcze raz do linii 25 listingu 1 i zmień czas na 100 μs. Przeprowadź symulację i załaduj ponownie dane do GTKWave. Niespodzianka! Liczniki Counter zniknęły z widoku wykresów. Trzeba dodać je ponownie. Jest to prawdopodobnie jakiś błąd w GTKWave i objawia się w sytuacji, kiedy zmienia się liczba bitów obserwowanego sygnału. Moduł StrobeGenerator sam dobiera sobie liczbę bitów licznika Counter, aby optymalnie wykorzystać zasoby FPGA. W przypadku czasu 10 μs oraz 12 μs, wystarczył licznik 7-bitowy, co zostało wyprintowane w logu z symulacji (WIDTH=7). Kiedy licznik ma zliczać impulsy zegarowe przez 100 μs to potrzebny jest licznik 10-bitowy (WIDTH=10). Zmiana szerokości licznika powoduje usunięcie go z GTKWave.

Bardziej skomplikowany testbench

Napiszemy teraz testbench do modułu MatrixKeyboard w wersji z maszyną stanów, który opracowaliśmy w 10 odcinku kursu. Gorąco zachęcam, aby przed analizowaniem testbencha przypomnieć sobie, działanie tego modułu.

Moduł klawiatury matrycowej działa we współpracy z modułem wyświetlacza multipleksowanego. W naszym wyświetlaczu znajduje się osiem cyfr, a w klawiaturze jest osiem kolumn przycisków, zorganizowanych w cztery rzędy, co daje sumarycznie 32 przyciski.

Moduł wyświetlacza wybiera, która cyfra jest aktywna w danej chwili, ustawiając stan wysoki na jednej z ośmiu katod wyświetlacza. Wraz ze zmianą aktywnej katody zmienia się także aktywna kolumna przycisków. Po uaktywnieniu kolumny, sterownik odczytuje stan rzędów przycisków na wejściu Rows i rozpoczyna analizę.

Nasz testbench będzie zawierał instancję modułu MatrixKeyboard, który zamierzamy przetestować, ale także będziemy musieli umieścić w nim instancję modułu DisplayMultiplex. Dodatkowo, będziemy musieli napisać dodatkowy blok, który będzie zajmował się podawaniem symulowanych sygnałów Rows w taki sposób, aby odpowiadało to rzeczywistemu wciskaniu i puszczaniu przycisku. Zatem testbench będzie musiał obserwować pracę swoich wewnętrznych modułów i na podstawie tego będzie dostosowywał sygnały doprowadzone do testowanego modułu.

Przeanalizujmy testbench symulujący moduł klawiatury matrycowej, pokazany na listingu 3.

Listing 3. Kod pliku matrix_keyboard_tb.v

// Plik matrix_keyboard_tb.v

`timescale 1ns/1ps
module MatrixKeyboard_tb();

parameter CLOCK_FREQ_HZ = 1_000_000;
parameter HALF_PERIOD_NS = 1_000_000_000 / (2 * CLOCK_FREQ_HZ);

// Generator sygnału zegarowego
reg Clock = 1’b1;
always begin
#HALF_PERIOD_NS;
Clock = !Clock;
end

// Zmienne testowe
reg Reset = 1’b1; // 1
reg [3:0] Rows = 4’d0;
wire [7:0] Cathodes;
wire [7:0] Segments;
wire SwitchCathode;
wire KeyStrobe;
wire KeyPressed;
wire [4:0] KeyCode;

// Symulacja wejść
always @(Cathodes) begin // 2
if(DUT.State == DUT.ANALYZE) // 3
$error(“Cathodes changed during analysis”); // 4

if(!KeyPressed) // 5
case(Cathodes) // 6
8’b00000001: Rows = 4’b0000;
8’b00000010: Rows = 4’b0000;
8’b00000100: Rows = 4’b0100; // 7
8’b00001000: Rows = 4’b0000;
8’b00010000: Rows = 4’b0000;
8’b00100000: Rows = 4’b0000;
8’b01000000: Rows = 4’b0000;
8’b10000000: Rows = 4’b0000;
default: Rows = 4’bXXXX; // 8
endcase
else
Rows = 4’b0000; // 9
end

// Sekwencja testowa
initial begin // 10
$timeformat(-6, 0, “us”, 10);
$display(“===== START =====”);
$display(“FREQUENCY_HZ = %9d”, CLOCK_FREQ_HZ);
$display(“DELAY = %9d”,
DisplayMultiplex0.StrobeGenerator0.DELAY);
$display(“WIDTH = %9d”,
DisplayMultiplex0.StrobeGenerator0.WIDTH);

#5 Reset = 1’b0;
#5 Reset = 1’b1;

$display(“ Time Cathode Rows Pressed Code”); // 11

repeat(30) begin // 12
@(posedge SwitchCathode); // 13
$display(“%t %d %b %b %d”, // 14
$realtime,
DisplayMultiplex0.Selector,
Rows,
KeyPressed,
KeyCode
);
end

#100;

$display(“===== END =====”);
#1 $finish;
end

// Instancja sterownika wyświetlacza multipleksowanego
DisplayMultiplex #( // 15
.FREQUENCY_HZ(CLOCK_FREQ_HZ),
.SWITCH_PERIOD_US(50), // 16
.DIGITS(8)
) DisplayMultiplex0(
.Clock(Clock),
.Reset(Reset),
.Data(32’h01234567),
.DecimalPoints(8’b00000001),
.Cathodes(Cathodes),
.Segments(Segments),
.SwitchCathode(SwitchCathode)
);

// Instancja sterownika klawiatury matrycowej
MatrixKeyboard DUT( // 17
.Clock(Clock),
.Reset(Reset),
.SwitchCathode(SwitchCathode),
.Rows(Rows),
.Cathodes(Cathodes),
.KeyStrobe(KeyStrobe),
.KeyPressed(KeyPressed),
.KeyCode(KeyCode)
);

// Zapis danych wynikowych
initial begin
$dumpfile(“matrix_keyboard.vcd”);
$dumpvars(0, MatrixKeyboard_tb);
end

endmodule

Testbench rozpoczynamy tworząc generator sygnału zegarowego w taki sam sposób, jak na listingu 1, więc nie będziemy tego fragmentu omawiać drugi raz. Przejdźmy do opisu zmiennych, które rozpoczynają się w linii 1:

  • Reset – sygnał resetujący, stan wysoki to normalna praca, stan niski to inicjalizacja układu;
  • Rows – jest to 4-bitowa zmienna typu reg mająca na celu udawanie wciskania przycisków. Jest sterowana przez algorytm testbencha i doprowadzona jest do wejścia Rows modułu MatrixKeyboard;
  • Cathodes – 8-bitowa zmienna sterowana przez moduł wyświetlacza i doprowadzona do wejścia moduły MatrixKeyboard;
  • SwitchCathode – zmienna sterowana przez moduł DisplayMultiplex i odczytywana przez MatrixKeyboard. Stan wysoki oznacza przełączanie aktywnej katody wyświetlacza i przełączanie aktywnej kolumny przycisków. Sygnał ten jest poprowadzony z wyjścia sterownika wyświetlacza do wejścia sterownika klawiatury;
  • KeyStrobe – sygnał sterowany przez moduł klawiatury, informujący o wciśnięciu przycisku, przyjmujący stan wysoki na jeden cykl zegarowy;
  • KeyPressed – sygnał sterowany przez moduł klawiatury, przyjmujący stan wysoki tak długo, jak wciśnięty jest jeden z przycisków;
  • KeyCode – 5-bitowy kod ostatnio wciśniętego przycisku, może przybierać stany od 0 do 31, sterowany jest przez moduł klawiatury.

Przejdźmy do linii 2. Rozpoczynamy tam blok always, który reaguje wykonuje się po każdej zmianie stanu 8-bitowej zmiennej Cathodes i na jej podstawie ustala nowy stan Rows. Jeżeli żaden z przycisków nie jest aktualnie wciśnięty to spełniony jest warunek z linii 5. Wtedy przy pomocy instrukcji case ustawiamy w stan wysoki tylko jeden bit w zmiennej Rows tylko dla jednej katody (linia 7). Instrukcja default w linii 8 nigdy nie powinna się wykonać, a w razie wystąpienia takiej nieprawidłowej sytuacji, zmienna Rows zostanie ustawiona w stan nieokreślony (linia 8). Można by się pokusić, by w takiej sytuacji także zgłosić błąd przy pomocy $error lub $fatal. Jeżeli już zostało wykryte wciśnięcie któregoś przycisku, czyli warunek z linii 5 nie jest spełniony, wówczas wykona się operacja z linii 9 czyli zmienna Rows będzie miała stan 0 dla każdej katody. Taka sytuacja powinna sprawić, że moduł klawiatury rozpozna puszczenie przycisku.

W ten właśnie sposób będziemy symulować wciskanie i puszczanie przycisku, znajdującego się w drugiej kolumnie i drugim rzędzie (zaczynając liczenie od zera) czyli jest to przycisk numer 10.

Warunek z linii 3 pełni bardzo ważną funkcję – sprawdza czy odstęp czasu pomiędzy przełączaniem katod jest zbyt krótki. Analizowanie danych z wejść klawiatury odbywa się przy pomocy maszyny stanów, która sprawdza wszystkie przyciski po kolei. Zajmuje to ponad 30 taktów zegarowych. W tym czasie nie może dojść do zmiany katody, bo układ będzie działał niepoprawnie. W linii 3 sprawdzamy czy zmiana katody nastąpiła wtedy, kiedy zmienna przechowująca stan była równa ANALYZE (jest to stała nazwana poprzez localparam). Jeżeli ten warunek zostanie spełniony, to symulacja ma się skończyć natychmiast na skutek instrukcji $fatal() i wyświetlić komunikat o błędzie.

Spróbuj wywołać błąd, zmieniając czas przełączania katod na krótszy. W linii 16 jest on ustawiany przy pomocy parametru SWITCH_PERIOD_US. W listingu jest ustawiony na 50 μs, ale spróbuj przeprowadzić symulację, po zmianie go na 10 μs.

Przejdźmy teraz do linii 10, w której rozpoczyna się blok initial z sekwencją testową. Sekwencja w gruncie rzeczy polega na zmianie sygnału Reset z 0 na 1, co uruchamia moduł klawiatury i wyświetlacza, a dalej jedyna aktywność wykonuje się w bloku always symulującym wejścia Rows, omówionym wcześniej.

Podczas symulacji będziemy chcieli wyświetlić na konsoli różne informacje w formie tabelarycznej. W linii 11 wyświetlamy nagłówek tabeli przy pomocy polecenia $display. Spacje w ciągu znaków zostały dodane celowo, aby wyrównać tekstowy nagłówek względem kolumn liczbowych, które zostaną wyprintowane poniżej.

Załóżmy, że chcemy obserwować co się dzieje przez czas 30 przełączeń katod. Nie chcemy wchodzić w szczegóły ile to będzie cykli zegara czy jaki to czas w nanosekundach. Wygodniej będzie odliczać poszczególne przełączenia katod. Takie podejście sprawia, że możemy dowolnie zmieniać częstotliwość symulowanego zegara czy okres przełączania katod bez potrzeby modyfikowania reszty testbenchu.

Posłużymy się pętlą repeat, która wykona się 30 razy (linia 12). Pierwszą instrukcją w tej pętli jest instrukcja oczekiwania na zbocze rosnące sygnału SwitchCathode (linia 13), który nadawany jest przez moduł wyświetlacza i odbierany przez moduł klawiatury. Podkreślić należy, że instrukcja oczekiwania @ zatrzymuje tylko wykonywanie bloku, w którym ta instrukcja występuje – natomiast wszystkie pozostałe bloki mogą wykonywać się dalej w sposób równoległy.

Kiedy już wystąpi zbocze rosnące na sygnale SwitchCathode, to wyświetlamy na konsoli informację o stanie kilku interesujących nas zmiennych, przy pomocy funkcji $display. Można przy jej pomocy wyświetlać zmienne podobnie, jak robi to funkcja printf znana z C++. Znak % w ciągu znaków oznacza, że w jego miejsce ma być wstawiona wartość zmiennej, która jest podana w jednym z kolejnych argumentów tej funkcji. W linii 14 wyświetlamy kilka zmiennych w formacie:

  • %t – czasowym, który został dokładnie określony przy pomocy funkcji $timeformat,
  • %d – dziesiętnym,
  • %b – binarnym.

Skoro testbench mamy już gotowy, to pozostaje tylko przygotowanie skryptu, który przeprowadzi symulację. Został pokazany na listingu 4.

Listing 4. Skrypt matrix_keyboard.bat

@echo off
cd StateMachine
cd source
iverilog -o matrix_keyboard.o matrix_keyboard.v matrix_keyboard_tb.v display_multiplex.v strobe_generator.v decoder_7seg.v
vvp matrix_keyboard.o
del matrix_keyboard.o
pause

Zwróć uwagę, że w argumentach programu iverilog podajemy nazwy plików wszystkich modułów, które są używane w naszym testbenchu. Po uruchomieniu symulacji powinieneś zobaczyć wynik taki, jaki został pokazany na listingu 5.

Listing 5. Zapisy w konsoli po symulacji

===== START =====
FREQUENCY_HZ = 1000000
DELAY = 49
WIDTH = 6
VCD info: dumpfile matrix_keyboard.vcd opened for output.
Time Cathode Rows Pressed Code
50us 0 0000 0 0
100us 1 0000 0 0
150us 2 0100 1 10
200us 3 0000 1 10
250us 4 0000 1 10
300us 5 0000 1 10
350us 6 0000 1 10
400us 7 0000 1 10
450us 0 0000 1 10
500us 1 0000 1 10
550us 2 0000 0 10
600us 3 0000 0 10
650us 4 0000 0 10
700us 5 0000 0 10
750us 6 0000 0 10
800us 7 0000 0 10
850us 0 0000 0 10
900us 1 0000 0 10
950us 2 0100 1 10
1000us 3 0000 1 10
1050us 4 0000 1 10
1100us 5 0000 1 10
1150us 6 0000 1 10
1200us 7 0000 1 10
1250us 0 0000 1 10
1300us 1 0000 1 10
1350us 2 0000 0 10
1400us 3 0000 0 10
1450us 4 0000 0 10
1500us 5 0000 0 10
===== END =====
matrix_keyboard_tb.v:76: $finish called at 1500101000 (1ps)
Press any key to continue . . .

Bardziej skomplikowana analiza

W wyniku symulacji powstał plik matrix_keyboard.vcd, który zawiera informacje o zmianach wszystkich zmiennych. Otwórz go w programie GTKWave, po czym naciśnij kombinację CTRL 0 (zero niedaleko przycisku backspace) aby automatycznie dopasować powiększenie.

Skonfigurujemy przeglądarkę w taki sposób, aby uzyskać rezultat pokazany na rysunku 6. W pierwszej kolejności dodaj sygnały, które są widoczne na tym rysunku i ustaw im odpowiedni format systemu liczbowego – dziesiętny, szesnastkowy lub binarny. Zmienna State ma format enum, który omówimy kilka akapitów dalej.

Rysunek 6. Przebiegi sygnałów uzyskane po symulacji kodu z listingu 3

Czerwone etykiety, jakie widzimy na rysunku 6 to komentarze. Możemy je umieścić klikając na listę sygnałów prawym przyciskiem myszy, a następnie należy wybrać opcję Insert comment. Dzięki nim możemy podzielić gmatwaninę wykresów na sekcje, co sprawia, że wykresy stają się bardziej czytelne.

Zwiększ powiększenie, aby uzyskać obraz taki, jak pokazano na rysunku 7 – wybierz okres pomiędzy zmianą katody, a rozpoznaniem wciśnięcia przycisku numer 10. Przyjrzyjmy się bliżej zmiennej State, która określa maszynę stanów wewnątrz modułu MatrixKeyboard. Formalnie rzecz biorąc, jest to 3-bitowa zmienna typu reg, jednak nam, ludziom, wygodniej jest widzieć etykiety tekstowe niż jedyni i zera. Dlatego skorzystamy z możliwości, by zamiast liczb pokazywać napisy.

Rysunek 7. Przybliżenie na moment rozpoznania wciśnięcia przycisku nr 10

Niestety sposób przypisania etykiet w GTKWave jest absolutnie nieintuicyjny. W pierwszej kolejności musimy utworzyć dodatkowy plik, w którym przypisane są wartości liczbowe do etykiet tekstowych. Utwórzmy plik state.gtkw (nazwa pliku może być dowolna) i umieśćmy w nim dane, jakie przedstawiono na listingu 6.

Listing 6. Kod pliku state.gtkw

001 WAIT
010 READ
100 ANALYZE

Następnie klikamy zmienną State prawym przyciskiem myszy, a później z menu kontekstowego wybieramy Data Format, Translate Filter File i Enable and Select. Pojawi się okienko, w którym należy kliknąć przycisk Add Filter to List. Wskazujemy plik state.gtkw i klikamy OK. Adres do pliku pojawił się w okienku. Musimy go kliknąć, aby został podświetlony na niebiesko tak, jak to zostało przedstawione na rysunku 8. Jeżeli nie będzie podświetlony na niebiesko to etykiety się nie pokażą! Następnie klikamy OK.

Rysunek 8. Okno z wyborem pliku etykiet tekstowych

Klikamy prawym przyciskiem myszy State na liście wyświetlanych przebiegów kolejny raz. Tym razem musimy wybrać Data Format, a następnie Enum. Dopiero wtedy jedynki i zera zostaną zastąpione przez etykiety tekstowe. Zapiszmy plik analizy, klikając menu File i Write Save File. Dzięki temu będzie można szybko powrócić do analizy, kiedy w pliku testbencha lub testowanych modułach zostanie coś zmienione.

Symulator Icarus Verilog oraz przeglądarka GTKWave mają jeszcze mnóstwo innych możliwości, ale nie będziemy ich na razie omawiać. Wiedza zdobyta podczas tego odcinka pozwoli nam tworzyć i symulować nieco bardziej skomplikowane moduły, niż dotychczas omawialiśmy podczas kursu.

W tej części omówiliśmy pliki testbenchy, skryptów i analiz, które dodawaliśmy do środowiska Lattice Diamond. Należy to traktować jako propozycję organizowania plików projektowych. Nie ma bowiem przymusu, by robić to w dokładnie taki sposób. Różni deweloperzy mają różne zwyczaje w tym zakresie. Nie ma potrzeby, by te pliki dodawać do Diamond. Niektórzy ludzie przechowują pliki testbench w innym katalogu niż kod syntezowalny. Niektórzy cały kod źródłowy umieszczają poza katalogiem projektowanym Diamond. Jest to kwestia indywidualnych upodobań i istnieje wiele możliwości.

Tworząc kod w języku Verilog bardzo dużo czasu poświęcam na symulację. Wielokrotnie modyfikuję kod modułu oraz testbencha, przeprowadzam mnóstwo symulacji, szczegółowo analizuję przebiegi sygnałów. Gdy osiągnę oczekiwany rezultat w symulatorze to dopiero wtedy wykonuje syntezę i wgrywam bitstream do FPGA.

W następnej części zastosujemy zdobytą wiedzę na temat symulatora Icarus Verilog, aby przygotować moduł oraz testbench obsługujący wyświetlacz LCD, z czterostopniową multipleksacją i sterowany jest sygnałami przyjmującymi cztery różne napięcia.

Dominik Bieczyński
leonow32@gmail.com

Artykuł ukazał się w
Elektronika Praktyczna
październik 2023
DO POBRANIA
Materiały dodatkowe

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik kwiecień 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio maj - czerwiec 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje kwiecień 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna kwiecień 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich maj 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów