.. _w02-verilog: ======================================== Wykład 2: Wprowadzenie do języka Verilog ======================================== Data: 15.10.2019 .. toctree:: .. contents:: O języku ======== Verilog jest językiem opisu sprzętu. Ma następujące zastosowania: 1. Opis zachowania układów logicznych do celów symulacji. 2. Opis zachowania układów logicznych do celów syntezy. 3. Tworzenie testów do układów logicznych. W tych trzech zastosowaniach używa się różnych podzbiorów języka -- o ile symulator zaakceptuje dowolny kod Veriloga, w przypadku syntezy możemy używać tylko ściśle określonych konstrukcji. Podzbiór Veriloga akceptowany przez narzędzia do syntezy nazywa się syntezowalnym Verilogiem. Projektując układy logiczne, używamy Veriloga następująco: - piszemy naszą logikę w syntezowalnym Verilogu - używamy (niekoniecznie syntezowalnego) opisu gotowych bloków logicznych dostępnych na układzie FPGA do celów symulacji (ten opis jest dostarczany przez producenta) - piszemy testy do naszej logiki używając pełnego języka Verilog Verilog jest językiem programowania i może być używany jak zwykły język programowania, gdy piszemy testy (możemy nawet np. używać funkcji dostępu do plików) lub model istniejącego układu do celów symulacji. Gdy jednak piszemy model, który będziemy syntezować, musimy uważać na używane konstrukcje, aby nie stworzyć bardzo nieefektywnego sprzętu (na przykład użycie w syntezowalnym kodzie operatora dzielenia prawie na pewno źle się skończy). Ważna dyrektywa =============== Pisząc jakikolwiek kod w Verilogu, najlepiej dodać na sam początek następujący kod:: `default_nettype none Bez tej dyrektywy, zapis do dowolnej niezdefiniowanej nazwy automatycznie utworzy sieć połączeń o danej nazwie, powodując ciężkie w wykryciu błędy. Moduły ====== Kod w Verilogu składa się z modułów. Moduł jest opisem bloku logiki z ustalonym zbiorem wejść i wyjść. Moduły mogą być parametryzowane (np. blok mnożący w którym możemy wybrać szerokość liczb w bitach). Gdy uruchamiamy symulację bądź syntezę, musimy wybrać jeden z modułów dostępnych w projekcie jako główny moduł (który może rekurencyjnie instancjonować inne moduły). Możemy mieć w projekcie wiele instancji danego modułu (np. dowolny duży układ DSP będzie wymagał podstawowego układu mnożącego w wielu kopiach). Przykładowe moduły w Verilogu mogą wyglądać tak:: `default_nettype none // Pół-sumator: dodaje dwa bity, generując bit wyjścia i bit przeniesienia. module half_adder( input wire a, // Wejście 1 input wire b, // Wejście 2 output wire o, // Wyjście główne output wire co // Wyjście przeniesienia ); // Instrukcja assign jest syntezowana w układ kombinacyjny obliczający // dane wyrażenie. assign o = a ^ b; assign co = a & b; endmodule // Pełny sumator: dodaje dwa bity i bit przeniesienia, generując bit // wyjścia i bit przeniesienia. module full_adder( input wire a, // Wejście 1 input wire b, // Wejście 2 input wire ci, // Wejście przeniesienia output wire o, // Wyjście główne output wire co // Wyjście przeniesienia ); // Sieci wewnętrzne -- 1-bitowe. wire h1o; wire h1c; wire h2c; // Instancjonujemy pod-moduły (dwie kopie pół-sumatora). // Możemy też przekazywać wejścia/wyjścia pozycyjnie, ale // nie jest to zalecane. half_adder h1(.a(a), .b(b), .o(h1o), .co(h1c)); half_adder h2(.a(h1o), .b(ci), .o(o), .co(h2c)); // Instancjonujemy wbudowany prymityw (bramkę or) -- możemy tego używać // zamiennie z równoważną instrukcją assign. or carry_or(co, h1c, h2c); endmodule Sieci ===== Siecią w układzie logicznym nazywamy połączony zbiór wejść/wyjść prymitywów. Sieci definiuje się słowem ``wire`` (jak widzimy w powyższym przykładzie) bądź kilkoma podobnymi (których nie używa się we współczesnych FPGA). Możemy przekazać sieć jako parametr do pod-modułu. Verilog pozwala na podłączenie kilku wyjść do jednej sieci -- taka konstrukcja nie jest błędem, jeśli możemy zapewnić, że w danym momencie będzie aktywne tylko jedno z nich (jest to używane np. w dwukierunkowych szynach komunikacyjnych), bądź użyjemy specjalnych typów sieci ze zdefiniowanymi regułami rozwiązywania konfliktów (``wand``, ``wor`` zamiast ``wire``). Takie konstrukcje jednak nie są wspierane wewnątrz współczesnych FPGA (żeby osiągnąć dużą wydajność, każda sieć powinna mieć tylko jedno źródło) i będą przetłumaczone w nieefektywną logikę przez syntezę. Nie należy ich więc używać, z wyjątkiem wyprowadzeń dwukierunkowych (``inout``) na zewnątrz FPGA. Typy danych i operatory ======================= W Verilogu istnieje jeden podstawowy typ danych, którego możemy użyć w syntezowalnym kodzie -- wektor bitów, być może traktowany jako liczba bez znaku. Jeśli chcemy, możemy też wybrać traktowanie go jak liczbę ze znakiem:: wire a; // Jeden bit. wire [0:7] d; // 8 bitów, numerowanie big-endian (najbardziej znaczący bit to 0). wire [13:0] d2; // 14 bitów, numerowanie little-endian (najmniej znaczący bit to 0). wire signed [1:10] d3; // 10 bitów, numerowanie big-endian licząc od 1, ze znakiem. Stałe zapisujemy następująco:: wire [0:3] a; assign a = 4; // Stała bez szerokości, automatycznie staje się 4-bitowa przy zapisie. assign b = 4'd5; // 4-bitowa stała o wartości 5 (zapis dziesiętny) assign b = 4'b1101; // 4-bitowa stała o wartości 13 (zapis binarny) assign b = 4'hd; // 4-bitowa stała o wartości 13 (zapis szesnastkowy) assign b = 27'h123_4567; // 27-bitowa stała szesnastkowa, znak _ jest ignorowany assign c = -5'sha; // 5-bitowa stała ze znakiem o wartości -10, zapis szesnastkowy. Operatory są dość podobne do C, ale znajdziemy też wiele przydatnych dodatków: - ``a[2:3]`` -- wycięcie kilku bitów z wektora - ``{a, b, c}`` -- konkatenacja kilku wektorów - ``|a`` -- szeroki OR (wszystkie bity w wektorze są ORowane w jeden bit) Bit w logice cyfrowej może mieć 4 wartości: - 0 (nie wymaga tłumaczenia) - 1 (j/w) - X: stan nieustalony. Powstaje sam, gdy dwa różne wyjścia na jednej sieci mają stany 0 i 1 (prawdziwy sprzęt nie znosi tego dobrze). Możemy sami przypisać X do jakiejś sieci -- mówimy wtedy syntezie, że nie obchodzi nas wartość tego bitu w danej sytuacji, pozwalając na większą dowolność w optymalizacji układu. - Z: brak wyjścia. Przypisując tą wartość do sieci, wyłączamy dany układ wyjściowy, pozwalając innemu wyjściu kontrolować jej stan. We współczesnych układach FPGA powinno być używane tylko do dwukierunkowych sygnałów zewnętrznych. Procesy ======= Proces jest blokiem kodu w Verilogu uruchamianym zawsze, gdy zmieni się stan danych sieci:: // Układ kombinacyjny zrealizowany przez proces -- będzie wykonywane // zawsze, gdy zmieni się stan któregoś z wejść. reg [1:0] d; always @(a, b, c) begin d = 0; if (a) d = 1; if (b) d = 2; if (c) d = 3; end Głównym zadaniem procesów w syntezowalnym kodzie jest zapis do rejestrów -- rejestry są definiowane słowem ``reg`` i są efektywnie zmiennymi. Możemy je czytać podobnie jak sieci (i przekazywać je do wejść pod-modułów, bądź do własnych wyjść). W procesach mamy dwie instrukcje przypisania: - ``=``: stan rejestru zmienia się natychmiast (nowa wartość jest widoczna w kolejnej instrukcji) - ``<=``: stan rejestru zmienia się w następnym cyklu symulacji (czytanie rejestru w kolejnej instrukcji da starą wartość) W syntezowalnym kodzie należy przestrzegać następujących reguł: - dany rejestr może być zapisywany tylko przez jeden proces - dany proces może być albo kombinacyjny albo tworzyć przerzutniki - w przypadku procesu kombinacyjnego, należy w liście ``@`` wypisać wszystkie sieci używane wewnątrz procesu do liczenia nowych wartości rejestrów - w przypadku procesu do przerzutników, należy w liście ``@`` użyć tylko jednego zbocza jednego zegara i ew. asynchronicznych sygnałów resetu oraz używać *tylko* przypisania ``<=`` Przykład syntezy przerzutnika jest następujący:: wire [3:0] i; // Możemy zdefiniować początkowy stan rejestru. reg [3:0] a = 0; wire clk, rst; // Prosty akumulator -- w każdym zegarze zwiększa licznik o wartość i. always @(posedge clk, posedge rst) begin if rst a <= 0; else a <= a + i; end Pamięć ====== Rolę tablic w Verilogu pełni tzw. pamięć. Tworzy się ją używając nawiasów kwadratowych po nazwie rejestru:: // Pamięć 128-bajtowa (z 7-bitowymi bajtami). reg [6:0] m [127:0]; always @(posedge clk) begin if (write) m[addr] <= wrdata; rddata <= m[addr]; end Aby pamięć była sensownie syntezowalna, należy używać ściśle określonych konstrukcji (omówionych na późniejszych wykładach). Case ==== Wewnątrz procesów możemy używać instrukcji ``case`` (równoważnej ``switch`` z języka C):: wire [3:0] bits; reg err; reg [1:0] which; always @(bits) begin case (bits) 4'b0001 : begin which <= 0; err <= 0; end 4'b0010 : begin which <= 1; err <= 0; end 4'b0100 : begin which <= 2; err <= 0; end 4'b1000 : begin which <= 3; err <= 0; end default : begin which <= 4'hx; err <= 1; end endcase end Często przydaje się instrukcja ``casex``, która działa jak instrukcja ``case``, ale gdy użyjemy bitu ``x`` w stałej, będzie do niego pasować zarówno ``0`` jak i ``1`` (normalna instrukcja ``case`` dopasowałaby w tym wypadku tylko bit ``x``, co oczywiście nie jest syntezowalne):: wire [3:0] bits; reg err; reg [1:0] maxbit; always @(bits) begin casex (bits) 4'b0001 : begin maxbit <= 0; err <= 0; end 4'b001x : begin maxbit <= 1; err <= 0; end 4'b01xx : begin maxbit <= 2; err <= 0; end 4'b1xxx : begin maxbit <= 3; err <= 0; end // Uruchomi się tylko dla bits == 0 default : begin maxbit <= 4'hx; err <= 1; end endcase end .. warning:: Używając instrukcji case do syntezy logiki kombinacyjnej, należy pamiętać, by w każdej gałęzi przypisać wartości do tego samego zbioru rejestrów -- w przeciwnym wypadku, zsyntezujemy logikę z zatrzaskami, a to rzadko jest pożądane. Jeśli nie obchodzi nas wartość przypisana do niektórych rejestrów w pewnych gałęziach, możemy tam przypisać X (jak w powyższym przykładzie). Pozwoli to uprościć zsyntezowanny układ. .. warning:: Symulatory traktują x jako osobną wartość od 0 i 1. Jeśli ``bit == 1'bx``, w poniższym kodzie nie wykona się *żadna* gałąź ``case``:: wire bit; always @(bit) begin case (bit) 1'b0: [...] 1'b1: [...] endcase end Z tego powodu, dobrym pomysłem jest używanie zawsze gałęzi default, nawet w przypadku case, który pokrywa "wszystkie" przypadki. Parametry i generate ==================== Moduły mogą być parametryzowane:: module avg ( input wire [BITS-1:0] a, input wire [BITS-1:0] b, input wire rounddir, output wire [BITS-1:0] res ); // Domyślnie piszemy 24-bitowy procesor. parameter BITS = 24; // Konkatenujemy jeden bit zerowy do a, żeby otrzymać (BITS+1)-bitową // liczbę (pozostałe składniki się same rozszerzą). assign res = ({1'b0,a} + b + rounddir) >> 1; endmodule [...] // Użycie 24-bitowego modułu. avg avg1(.a(a1), .b(b1), .rounddir(rounddir1), .res(res1)); // Użycie 8-bitowego modułu. avg #(.BITS(8)) avg2(.a(a2), .b(b2), .rounddir(rounddir2), .res(res2)); Ze względu na brak enumów w Verilogu (są one dostępne dopiero w SystemVerilogu, którego ISE niestety nie wspiera), parametrów używa się również do definiowania stałych. Czasem chcemy wygenerować wiele kopii danego układu. Pomoże nam w tym konstrukcja generate:: module multi_adder(a, b, ci, o, co); parameter BITS = 8; // Nie możemy użyć parametru przed deklaracją -- stąd definicje typów // po deklaracji modułu. input wire [BITS-1:0] a, input wire [BITS-1:0] b, input wire ci, output wire [BITS-1:0] o, output wire co wire [BITS:0] carry; assign co = carry[BITS]; assign carry[0] = ci; genvar i; generate // Pętlę generate-for trzeba nazwać. for (i = 0; i < BITS; i = i + 1) begin : gen_fa full_adder fa( .a(a[i]), .b(b[i]), .ci(carry[i]), .o(o[i]), .co(carry[i+1]) ); end endgenerate endmodule Analogicznie do pętli ``for`` można używać konstrukcji ``if`` oraz ``case``. Więcej informacji ================= Szybki kurs Veriloga: http://www.asic-world.com/verilog/veritut.html (uwaga: w wielu miejscach używa starej składni Verilog 95). Do wydrukowania i powieszenia na ścianie: http://www.ece.uvic.ca/~fayez/courses/ceng465/vlogref.pdf .