Wykład 2: Wprowadzenie do języka Verilog

Data: 15.10.2019

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

Ostrzeżenie

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.

Ostrzeżenie

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 .