Menu

Blok always w Verilogu: logika kombinacyjna i sekwencyjna

Jak działają bloki always, czym różni się kombinacyjne always @(*) od taktowanego always @(posedge clk) i jakie reguły decydują o tym, jaki sprzęt powstaje z każdego z nich.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Główne narzędzie behawioralnego Veriloga

assign opisuje logikę kombinacyjną w postaci jednego równania. Gdy tylko potrzebujesz if/else, case albo pamięci, sięgasz po always. Blok always to fragment kodu proceduralnego, który uruchamia się ponownie za każdym razem, gdy zmienią się określone sygnały. Sygnały wyzwalające ponowne uruchomienie tworzą listę czułości.

Najczęściej spotkasz dwie postacie always:

  1. always @(*): uruchamia się ponownie, gdy zmieni się dowolny sygnał odczytywany w bloku. Buduje logikę kombinacyjną.
  2. always @(posedge clk): uruchamia się ponownie tylko na zboczu narastającym clk. Buduje taktowaną logikę sekwencyjną (przerzutniki, czyli flip-flopy).

Istnieją też inne formy (@(a or b), @(negedge clk), @(posedge clk or negedge reset_n)), ale te dwie powyżej obejmują prawie każdy syntezowalny blok, jaki napiszesz.

Kombinacyjne always @(*)

Trzy rzeczy, na które warto zwrócić uwagę:

  • out jest typu reg. Wszystko, co jest przypisywane wewnątrz always, musi być reg. To słowo kluczowe nie oznacza "to jest przerzutnik". Tutaj znaczy tylko "zapisuję to z bloku proceduralnego".
  • always @(*). Gwiazdka * mówi "obudź się, gdy zmieni się cokolwiek, co odczytuję". Symulator sam ustala listę czułości. Możesz wypisać listę ręcznie, na przykład always @(sel), ale @(*) jest bezpieczniejsze, bo pominięty sygnał to klasyczne źródło błędów.
  • Brak zegara. Ten blok opisuje logikę kombinacyjną. Syntezator tworzy układ, który oblicza out bezpośrednio z sel, bez przerzutników i bez wejścia zegarowego.

Gałąź default jest opcjonalna w składni, ale w praktyce nie powinna być pomijana. Bez niej każda nieobsłużona wartość wejścia sprawia, że out zachowuje poprzednią wartość, a to syntezuje się do niezamierzonego zatrzasku (latch). Zawsze dodawaj default.

Sekwencyjne always @(posedge clk)

Najważniejsze różnice względem wersji kombinacyjnej:

  • always @(posedge clk). Blok uruchamia się ponownie tylko na zboczu narastającym clk. Między zboczami nic się nie dzieje.
  • Przypisanie nieblokujące <=. W bloku taktowanym to właściwy operator. Oznacza "zaplanuj, że count przyjmie nową wartość na końcu kroku czasowego", a dokładnie tak zachowuje się przerzutnik. Powody i alternatywę znajdziesz w artykule Przypisania blokujące i nieblokujące.
  • default nie jest potrzebny. if obsługuje obie gałęzie (reset i brak resetu), więc nie ma ryzyka zatrzasku.

Syntezator rozpoznaje ten kształt (taktowana lista czułości i przypisanie nieblokujące) i tworzy 4-bitowy rejestr (cztery przerzutniki) oraz logikę kombinacyjną obliczającą count + 1 i multiplekser wybierający między resetem a inkrementacją.

Co widzi synteza

Ten sam kod modułu może opisywać dwa zupełnie różne układy, zależnie od kształtu bloku always:

BlokSprzęt
always @(*) y = expr;Czysta logika kombinacyjna. Bez pamięci.
always @(posedge clk) y <= expr;Przerzutnik. Zapamiętuje expr raz na cykl zegara.
always @(*) if (en) y = expr;Zatrzask, zwykle błąd. W przypadku "else" zostaje stara wartość.
always @(posedge clk) if (en) y <= expr;Przerzutnik z wejściem enable. Zapamiętuje tylko, gdy en jest w stanie wysokim.

Trzeci przypadek to pułapka zatrzasku. Zatrzask to przezroczysta komórka pamięci, która trzyma swoje wyjście, gdy wejście sterujące nie jest aktywne. Bywa przydatny w konkretnych projektach, ale gdy powstaje przypadkiem, prawie zawsze jest błędem. Większość narzędzi do syntezy głośno ostrzega, gdy wykryje zatrzask, o który nie prosisz. Traktuj to ostrzeżenie jak błąd.

Odmiany listy czułości

Spotkasz też kilka rzadszych list czułości:

  • always @(a or b or c): jawna lista. Verilog-2001 dodał separator ,: always @(a, b, c). Działa każdy z zapisów.
  • always @(posedge clk or negedge reset_n): reset asynchroniczny. Blok uruchamia się na zboczu narastającym zegara albo na zboczu opadającym resetu. Stosuje się go, gdy reset musi zadziałać natychmiast, bez czekania na następny takt zegara.
  • always @(negedge clk): taktowanie zboczem opadającym. Rzadkie. Niektóre projekty używają go dla przerzutników wyzwalanych zboczem opadającym zamiast narastającym.

W nowych projektach wybieraj always @(*) dla logiki kombinacyjnej i always @(posedge clk) dla sekwencyjnej. Po reset asynchroniczny sięgaj tylko wtedy, gdy projekt naprawdę go potrzebuje.

Dwa bloki to dwa układy

Kilka bloków always w tym samym module działa niezależnie. Każdy staje się osobnym fragmentem sprzętu:

Blok taktowany tworzy rejestr z przerzutników. Blok kombinacyjny tworzy bramkę XOR. Działają obok siebie i nic o sobie nie wiedzą. Oba wyjścia zmieniają się według zupełnie różnych harmonogramów.

Czego bloki always nie potrafią

Kilka rzeczy wygląda kusząco, ale jest niedozwolonych:

  • Przypisanie do wire: cel musi być typu reg. Kompilator to wymusza.
  • Przypisanie do tego samego reg z dwóch różnych bloków always: w symulacji daje niezdefiniowane zachowanie i nie da się tego zsyntezować. Jeden sygnał, jedno źródło sterowania.
  • Odczyt i zapis tego samego sygnału w tym samym bloku kombinacyjnym w sposób tworzący pętlę sprzężenia zwrotnego: always @(*) x = x + 1; to pętla o zerowym opóźnieniu, której symulator nie potrafi rozwiązać.

Dwa pierwsze problemy wyłapie kompilator. Trzeci czasem ujawnia się dopiero w symulacji jako zawieszenie.

Co dalej

Następny artykuł, Blok initial, omawia rodzeństwo always: blok, który wykonuje się dokładnie raz na początku symulacji. To podstawowe narzędzie testbenchy. Potem przyjdą reguły przypisań blokujących i nieblokujących, które decydują o tym, czy blok taktowany robi to, co zamierzasz.

Najczęściej zadawane pytania

Czym jest blok always w Verilogu?

always wprowadza blok proceduralny, który uruchamia się ponownie za każdym razem, gdy zmienią się sygnały z jego listy czułości. Są dwa warianty: always @(*) buduje logikę kombinacyjną (uruchamia się ponownie przy zmianie dowolnego wejścia), a always @(posedge clk) buduje logikę sekwencyjną (uruchamia się przy każdym zboczu narastającym clk). Ciało bloku always może zawierać if, case, for i przypisania proceduralne.

Jaka jest różnica między always @(*) a always @(posedge clk)?

always @(*) reaguje na dowolny sygnał odczytywany w bloku i tworzy logikę kombinacyjną bez pamięci. always @(posedge clk) reaguje tylko na zbocze narastające clk i tworzy przerzutniki, które zapamiętują stan raz na cykl zegara. Pierwszy nie ma ani zegara, ani rejestru, drugi ma jedno i drugie.

Czym jest lista czułości w Verilogu?

To lista sygnałów po @, która określa, kiedy blok always uruchamia się ponownie. @(*) to skrót od 'każdy sygnał odczytywany w bloku'. @(posedge clk) działa tylko na zboczu narastającym clk. @(posedge clk or negedge reset_n) działa przy każdym z tych dwóch zdarzeń i służy do resetów asynchronicznych. Błędna lista czułości to jedno z najczęstszych źródeł rozbieżności między symulacją a syntezą.

Czy można przypisać wartość do wire wewnątrz bloku always?

Nie. Bloki always mogą przypisywać tylko do reg (albo logic w SystemVerilogu). Kompilator to wymusza. Jeśli wire ma być wyjściem logiki proceduralnej, zadeklaruj pośredni reg, steruj nim wewnątrz always i poza blokiem podłącz wire do tego reg przez assign. Możesz też po prostu zmienić wire na reg.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ