Kurs Verilog cz.4 – start w jeden dzień
W tej części kursu znacznie przyspieszymy. Chciałbym pokazać, że możliwe jest opanowanie podstaw języka Verilog w jeden dzień (właściwie kilka godzin). Będzie to więc omówienie najważniejszych pojęć, zilustrowane prostymi przykładami. Wiedza, którą posiądziesz czytając tą część, pozwoli Ci podjąć samodzielne próby projektowania urządzeń cyfrowych w języku Verilog.
Zakładam, że miałeś już do czynienia z programowaniem w jakimkolwiek języku i znasz podstawy działania układów cyfrowych.
W kolejnych wpisach, nie będących częścią kursu, opiszę jak zamienić opis w plik, który możemy wpisać do układu programowalnego, oraz przedstawię prosty zestaw startowy, na którym uruchomisz przykłady i własne projekty.
1. Cykl projektowania
- specyfikacja
- schemat blokowy
- projekt niskiego poziomu
- kodowanie RTL
- weryfikacja
- synteza
Zastosujmy się do cyklu projektowego – zacznijmy więc od specyfikacji projektu.
2. Specyfikacja
Co zamierzamy zaprojektować? Na początek spróbujemy ze stosunkowo prostym układem. Dodatkowo chciałbym, aby możliwe było obejrzenie wyników naszej pracy. Dlatego spróbujemy z prostym efektem przesuwającego się punktu na diodach LED.
Zakładamy, że mamy do dyspozycji 8 LEDów i chcemy, aby punkt przemieszczał się co 0,2s. Użyjemy także dwóch przycisków: jeden będzie włączał, a drugi wyłączał generowanie efektu.
Przejdźmy więc do kolejnego etapu projektowania – schematu blokowego.
3. Schemat blokowy

Rysunek 1. Schemat blokowy
W tym momencie jedyne co musimy zrobić, to określić porty (wejścia, wyjścia, porty dwukierunkowe), w które wyposażony będzie nasz układ. Schemat blokowy stanowi reprezentację przepływu sygnałów w naszym projekcie. Na tym etapie nie zastanawiamy się, co będzie wewnątrz naszej czarnej skrzynki.
4. Projekt niskiego poziomu
Teraz spróbujemy narysować automat realizujący założone funkcje.

Rysunek 2. Automat
Od razu powiem – można to zrobić inaczej, ale my pozostaniemy przy takiej konstrukcji.
W stanie IDLE automatu wpisywany jest początkowy stan LEDów, w naszym przypadku wartość 8'hFE, ponieważ chcemy, aby poruszał się jeden punkt (przy aktywnym stanie niskim). Licznik mierzący czas 0,2s nie będzie startowany ani zatrzymywany – będzie on działał cały czas. Stan START osiągany jest po wystąpieniu logicznej jedynki na wejściu i_start. Gdy automat jest w stanie START, może przejść do stanu SHIFT (pod wpływem osiągnięcia odpowiedniej wartości przez licznik). W stanie SHIFT następuje przesunięcie zawartości rejestru sterującego diodami, a następie powrót do stanu START.
Po narysowaniu automatu tradycyjny proces projektowania zakłada: stworzenie tablicy prawdy z przejściami stanów dla każdego przerzutnika, mapy Karnaugh i kolejne nużące kroki.
Na szczęście w Verilogu po narysowaniu automatu (w małych projektach może wydawać się to zbędne, jednak warto przyzwyczajać się do tego, gdyż ich przydatność znacznie wzrasta wraz ze złożonością układu), ten etap się kończy.
Jeśli nie wiesz jak stworzyć i odczytać automat stanów, musisz na własną rękę uzupełnić tę wiedzę.
5. Kodowanie RTL
Tutaj rozpoczynamy już przygodę z Verilogiem.
Kodowanie rozpoczynamy od stworzenia modułu. O modułach w Verilogu pisałem w drugiej części kursu – jeśli jej nie czytałeś najwyższy czas, abyś to zrobił. W tym miejscu posłużymy się schematem blokowym z rysunku 1.
module leds_effect( input clk, input rst, input i_start, input i_stop, output reg [7:0] o_LEDs ); ... endmodule
W naszym projekcie występują tylko dwa rodzaje portów: wejściowe oraz wyjściowe. W drugiej części kursu przeczytałeś, że istnieją także porty dwukierunkowe inout. W przyszłości będziemy korzystać także z nich np. do komunikacji z układem FT245.
Wektor sygnałów
W naszym przykładzie napotykamy:
output reg [7:0] o_LEDs
Zapis [7:0] przed nazwą portu, oznacza 8-bitowy wektor, gdzie najstarszy bit ma numer 7 a najmłodszy 0. Możliwa jest także konstrukcja
output reg [0:7] o_LEDs
W takim przypadku najstarszy bit ma numer 0, a najmłodszy 7.
Jeśli chcemy się odwołać do jednego z sygnałów wektora, piszemy:
assign x = o_LEDs[0] | o_LEDs[3];
co w tym przypadku oznacza, przypisanie do x sumy logicznej najmłodszego oraz trzeciego bitu wektora o_LEDs.
Typy danych
W układach logicznych mamy do czynienia z dwoma rodzajami wymuszeń:
- wymuszenie, które przechowuje wartość,
- wymuszenie, które łączy ze sobą dwa lub więcej punktów.
Pierwszy typ reprezentowany jest w Verilogu przez rejestr (słowo kluczowe reg od ang. register), natomiast drugi to przewód (wire).
Istnieje spora liczba innych typów danych dostępnych w Verilogu, jednak ich znajomość wymagana jest dopiero przy zaawansowanych projektach.
Przykłady deklaracji:
reg y; // rejestr 1-bitowy reg [15:0] counter; // rejestr 16-bitowy wire a, b, c; // trzy sygnały typu wire
Operatory
Operatory w Verilogu podobne są do tych znanych z innych języków programowania i nie będziemy ich szczegółowo omawiać. Dostępne operatory zebrane zostały w poniższej tabeli.
| Typ operatora | Symbol | Znaczenie |
|---|---|---|
| Arytmetyczny | * | Mnożenie |
| / | Dzielenie | |
| + | Dodawanie | |
| - | Odejmowanie | |
| % | Modulo | |
| Logiczny | ! | Negacja logiczna |
| && | Logiczne AND | |
| || | Logiczne OR | |
| Relacji | > | Większe niż |
| < | Mniejsze niż | |
| >= | Większe niż lub równe | |
| <= | Mniejsze niż lub równe | |
| Równości | == | Równe |
| != | Różne | |
| Bitowy | ~ | Negacja bitowa |
| & | Bitowy AND | |
| | | Bitowy OR | |
| ^ | Bitowy XOR | |
| ^~ lub ~^ | Bitowy XNOR | |
| Redukcji | ~ | Redukcja NOT |
| ~& | Redukcja NAND | |
| | | Redukcja OR | |
| ~| | Redukcja NOR | |
| ^ | Redukcja XOR | |
| ^~ lub ~^ | Redukcja XNOR | |
| Przesunięcia | >> | Przesunięcie w prawo |
| << | Przesunięcie w lewo | |
| Złożenia (konkatenacji) | { } | Złożenie |
| Warunkowy | ? | Warunek |
Wyrażenia warunkowe
Popatrzmy na słowa kluczowe związane z wyrażeniami warunkowymi: if, else, repeat, while, for, case. Dla osób, które znają jakiś język programowania (nawet jeśli jest to np. PHP) słowa te na pewno zabrzmią znajomo. Zakładam, że i Ty jesteś taką osobą. Nie będziesz więc miał problemów ze zrozumieniem idei wyrażeń warunkowych.
Jedyne na co należy zwrócić uwagę, to fakt, że opis w języku Verilog zostanie przetłumaczony na układ logiczny i nie każda konstrukcja może się nadawać do zaimplementowania w sprzęcie.
if, else
Wyrażenie if, else pozwala na warunkowe wykonanie kodu. Jeśli warunek jest spełniony wykonywana jest część za if, w przeciwnym wypadku część za else, przy czym część else nie musi się pojawić.
Przykład:
always @(posedge clk or posedge rst) if(rst) counter <= 0; else if(a[7]) counter <= counter + 1;
Konstrukcji if, else nie stosuje się poza blokiem always. Jeśli instrukcji, które mają się wykonać jest więcej, należy objąć je blokiem begin, end (analogia do {} z języka C).
case
Wyrażenie case stosowane jest, gdy jedna zmienna musi zostać sprawdzona pod kątem wielu wartości. Wyrażenie case zastępuje wielokrotne wykorzystanie konstrukcji if, else.
Blok case zaczyna się słowem kluczowym case a kończy słowem endcase.
Dobrym zwyczajem jest stosowanie opcji default. Dlaczego? Rozważmy przypadek automatu, który z jakiegoś powodu (np. silne zakłócenia elektromagnetyczne) wchodzi w nieistniejący stan. Bez opcji default automat taki pozostanie w nieistniejącym stanie zawieszając działanie urządzenia (lub jego części). Opcja default stanowi zbiór wszystkich możliwych, a nieopisanych stanów i pozwala wybrnąć z opisanej sytuacji.
Przykład:
always @(posedge clk or posedge rst) if(rst) rx_state <= 0; else case(rx_state) 0: rx_state <= i_stb ? 1 : 0; 1: rx_state <= i_stb ? 1 : 2; 2: rx_state <= 0; default: rx_state <= 0; endcase
Konstrukcji case, endcase nie stosuje się poza blokiem always.
Układy kombinacyjne oraz synchroniczne
W układach cyfrowych występują dwa typy bloków, są to: bloki kombinacyjne oraz bloki sekwencyjne.
Dla nas, istotne jest, w jaki sposób modelować te elementy w Verilogu:
- elementy kombinacyjne mogą być modelowane za pomocą przypisania (
assign) oraz procesów (blokalways), - elementy sekwencyjne mogą być modelowane tylko jako procesy (bloki
always)
Blok always
Słowo always (z ang. zawsze) sugeruje nam sposób działania tego bloku. Mianowicie, jest on wykonywany zawsze, gdy spełniony jest warunek z listy czułości. Lista czułości informuje blok always, kiedy wykonać kod zawarty w jego wnętrzu. Symbol @ (at) występujący po słowie kluczowym always tworzy z nim wyrażenie always at, czyli „zawsze gdy”.
Blok always nie może sterować daną typu wire. Dopuszczalne są tylko dane typu reg oraz integer. Kolejną ważną zasadą konstrukcji bloków always, jest sterowanie konkretną daną tylko z jednego bloku. Jeśli przypisania znajdą się w dwóch lub więcej blokach, synteza zakończy się niepowodzeniem.
Lista czułości może być:
- listą czułą na poziom (dla układów kombinacyjnych),
- listą czułą na zbocze (dla układów sekwencyjnych).
Przykład:
always @(a or b or sel) if(sel == 0) y <= a; else y <= b;
Przykład:
always @(posedge clk or posedge rst) if(rst) y <= 0; else if(sel == 0) y <= a; else y <= b;
Możliwe jest także reagowanie na zbocze opadające stosując słowo kluczowe negedge. Nie można natomiast umieszczać na jednej liście, wyrażeń reagujących na poziom i wyrażeń reagujących na zbocze.
Przypisania wewnątrz bloków always mogą być dwojakiego typu. Pierwszy typ, to przypisania nieblokujące wykorzystujące zapis <= np.:
always @(posedge clk or posedge rst) if(rst) begin x <= 0; y <= 0; z <= 0; end else if(flag) begin x <= 1; y <= x; z <= y; end
Drugi typ, to przypisania blokujące:
always @(posedge clk or posedge rst) if(rst) begin x = 0; y = 0; z = 0; end else if(flag) begin x = 1; y = x; z = y; end
W pierwszym przypadku, gdy kod zależny od zmiennej flag, wykona się po raz pierwszym, zawartość zmiennych będzie następująca x = 1, y = 0, z = 0, ponieważ w x oraz y, w poprzednim cyklu zegarowym były zera i ta wartośc zostanie wpisana do rejestrów y oraz z. Natomiast w drugim przypadku, będzie to x = 1, y = 1, z = 1, ponieważ instrukcję wykonują się sekwencyjnie (kolejne przepisania są opóźniane).
Stosowanie przypisań blokujących jest niezalecane i ich stosowanie powinno być ograniczone do niezbędnego minimum. Zabronione jest mieszanie typów przypisań w jednym bloku always.
Możliwa jest także konstrukcja always bez listy czułości, stosowana w symulacji.
always #5 sys_clk = ~sys_clk;
Zapis #5 oznacza opóźnienie (w jednostkach określonych w projekcie) i nie jest syntezowalny, tzn. nie doprowadzi do wygenerowania sygnału zegarowego w zaimplementowanym urządzeniu. Sygnał taki wystąpi tylko w symulacji.
Przypisanie (assign)
Przypisanie służy do modelowania układów kombinacyjnych w Verilogu.
Przykład:
assign y = a & b; assign not_q = ~q;
Blok inicjujący (initial)
Blok initial jest niesyntezowalny, tzn. wykorzystywany jest tylko do symulacji. Jest on wykonywany przy starcie symulacji.
Przykład:
initial begin sys_clk = 0; sys_rst = 0; data = 8'h41; end
Uwaga, ważne! Blok initial nie powoduje wpisania wartości do rejestru lub pamięci w fizycznie zaimplementowanym układzie. Jest to konstrukcja, która służy jedynie do symulacji programowej projektu.
6. Weryfikacja projektu
Dobrze, napiszmy opis naszego efektu świetlnego i spróbujmy zasymulować jego działanie. Przesunięcie punktu na LEDach ma następować co 0,2s, załóżmy więc, że nasz układ działa z oscylatorem 10MHz – jest to nam potrzebne do obliczenia wartości wpisywanej do licznika. Jeśli częstotliwość będzie inna, należy odpowiednio zmodyfikować wspomnianą wartość.
module leds_effect( input clk, input rst, input i_start, input i_stop, output reg [7:0] o_LEDs ); integer main_state; reg [18:0] cnt; // licznik zliczający w dół always @(posedge clk or posedge rst) if(rst) cnt <= 19'h30D3E; else if(cnt[18]) cnt <= 19'h30D3E; else cnt <= cnt - 1; // automat always @(posedge clk or posedge rst) if(rst) main_state <= 0; else case(main_state) 0: main_state <= i_start ? 1 : 0; 1: if(i_stop) main_state <= 0; else if(cnt[18]) main_state <= 2; 2: main_state <= 1; default: main_state <= 0; endcase // LEDy always @(posedge clk or posedge rst) if(rst) o_LEDs <= 0; else if(main_state == 0) o_LEDs <= 8'hFE; else if(main_state == 2) begin o_LEDs[7:1] <= o_LEDs[6:0]; o_LEDs[0] <= o_LEDs[7]; end endmodule
Mamy już opis naszego efektu świetlnego, teraz spróbujmy zasymulować jego działanie. Napiszmy więc testbench:
module leds_effect_tb(); reg sys_clk, sys_rst; reg start, stop; wire [7:0] LEDs; initial begin sys_clk = 0; sys_rst = 0; start = 0; stop = 0; sys_rst = #200 1; sys_rst = #200 0; start = #200 1; start = #200 0; stop = #6000 1; end always #50 sys_clk = ~sys_clk; // opóźnienie w nanosekundach, częstotliwość 10MHz leds_effect UUT( .clk(sys_clk), .rst(sys_rst), .i_start(start), .i_stop(stop), .o_LEDs(LEDs) ); endmodule
Wynik symulacji (wartość wpisywana do licznika została zmieniona na 3, aby na przebiegach czasowych widoczne były zmiany):
Jest kod, widzisz przebiegi czasowe, ale jak czy wiesz jak uruchomić symulację? O tym w kolejnym wpisie (poza kursem), a w następnych częściach przyjrzymy się bliżej elementom języka Verilog.


(głosów: 3, średnia ocen: 4.33)
2 komentarzy do “Kurs Verilog cz.4 – start w jeden dzień”
Ciekawy artykuł! Czy istnieje podobny język dla mikrokontrolerów 8051?
@Atmel programowanie: Język Verilog nie jest językiem programowanie, a językiem opisu sprzętu (HDL). Nie służy więc do pisania programów na mikrokontrolery, tylko do projektowania układów cyfrowych.
Za pomocą Veriloga można np. stworzyć rdzeń mikokontrolera 8051 i wyposażyć go w dowolne peryferia (SoC).