Wykład 2: Wprowadzenie do języka Verilog¶
Data: 15.10.2019
Treść
O języku¶
Verilog jest językiem opisu sprzętu. Ma następujące zastosowania:
Opis zachowania układów logicznych do celów symulacji.
Opis zachowania układów logicznych do celów syntezy.
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óww 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 .