Moduły i instancje modułów
Kod języka Verilog dzielimy na moduły, podobnie jak kod w językach programowania dzieli się na funkcje czy klasy. Każdy moduł komunikuje się z pozostałymi modułami przy pomocy portów – mogą one być wejściem input, wyjściem output lub sygnałem dwukierunkowym inout. Moduł nadrzędny, od którego zaczyna się cała aplikacja zwyczajowo nazywany jest top. Jest odpowiednikiem funkcji main() w C++. Każdy inny moduł należy opisać, a następnie powołać do życia tworząc co najmniej jedną instancję tego modułu. Moduły wygodnie jest zapisywać w oddzielnych plikach, których nazwa jest taka sama jak nazwa modułu.
Prześledźmy przykład zaprezentowany na listingu 1. W liniach oznaczonych jako #4 i #5 definiujemy moduły BramkaAND i BramkaOR. Każda z nich ma dwa wejścia x i y oraz wyjście result. Samo zdefiniowanie tych modułów jeszcze nie spowoduje wygenerowania ich w układzie FPGA. W module top w liniach oznaczonych #1 i #2 zostały utworzone dwie instancje modułu BramkaAND. W nawiasach podane są nazwy portów, jakie ma moduł top. W ten sposób wejścia i wyjścia modułu top, które są fizycznymi pinami układu FPGA, zostają połączone z wirtualnymi pinami modułów.
Listing 1. Przykład podziału kodu na moduły
// Plik top.v
module top(
input ButtonA,
input ButtonB,
input ButtonC,
output LED,
output Buzzer,
output Relay
);
// Instancje modułów typu BramkaAND o nazwie AND1 i AND2
BramkaAND AND1(ButtonA, ButtonB, LED); // #1
BramkaAND AND2(ButtonA, ButtonC, Buzzer); // #2
// Instancja modułu typu BramkaOR o nazwie OR1
BramkaOR OR1( // #3
.x(ButtonA),
.y(ButtonB),
.result(Relay)
);
endmodule
// Plik bramka_and.v
module BramkaAND( // #4
input x, y,
output result
);
assign result = x & y;
endmodule
// Plik bramka_or.v
module BramkaOR( // #5
input x, y,
output result
);
assign result = x | y;
endmodule
Należy zwrócić uwagę, że utworzyliśmy dwie instancje AND1 i AND2, które działają dokładnie tak samo, jednak połączone są z innymi sygnałami. Istnieją dwa sposoby tworzenia instancji i łączenia ich z sygnałami z innych modułów. Sposób pokazany w liniach #1 i #2 jest podobny do kodu C++, w którym tworzony jest obiekt jakiejś klasy, a jego konstruktor przyjmuje kilka argumentów. W tym przypadku kolejność argumentów ma znaczenie. Taki sposób jest przydatny dla bardzo prostych modułów, które mają niewiele portów. Drugi sposób został pokazany w linii #3. Każdy z portów modułu wymieniony jest po znaku kropki. Następnie w nawiasach podana jest nazwa sygnału, z którym dany port ma być połączony. Taki opis jest bardziej rozwlekły i czasochłonny, jednak ma kilka istotnych zalet. Po pierwsze kod staje się bardziej czytelny, ponieważ widzimy nazwę portu wewnątrz modułu i jednocześnie sygnał, z którym jest połączony na zewnątrz. Dodatkowo każdy z nich możemy opatrzyć komentarzem. Dzięki temu, że każdy port jest zapisany w osobnej linii, w razie błędu syntezator powie nam, w której linii jest błąd – w przypadku pierwszego sposobu, gdzie wszystko opisujemy w jednej linijce, komunikatory syntezatora mogą być trudniejsze do zrozumienia. Ponadto, kolejność przypisań jest dowolna.