Menu

Przypisania blokujące i nieblokujące w Verilogu: kiedy = a kiedy <=

Temat, który początkującym w Verilogu myli się najczęściej. Co naprawdę znaczą = i <= wewnątrz bloku always i jaka reguła zapobiega większości wyścigów.

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

Dwa operatory

W blokach always i initial Verilog ma dwa operatory przypisania:

  • = to przypisanie blokujące. Aktualizuje lewą stronę od razu, przed przejściem do następnej instrukcji.
  • <= to przypisanie nieblokujące. Oblicza prawą stronę teraz, a aktualizację lewej strony planuje na koniec bieżącego kroku czasowego.

Poza blokami proceduralnymi (w assign) istnieje tylko =, a assign a <= b to błąd składni. W blokach proceduralnych oba są dozwolone, a wybór właściwego to najważniejsza decyzja dla początkujących w Verilogu.

Jak działa przypisanie blokujące

Przypisanie blokujące działa tak, jak w językach programowania: linia po linii, od góry do dołu. Instrukcja N kończy się, zanim zacznie się instrukcja N+1:

To wykonanie czysto sekwencyjne. Każda instrukcja widzi efekty tych nad nią. Zgadza się to z intuicją z programowania.

Jak działa przypisanie nieblokujące

Przypisanie nieblokujące ma kształt sprzętu. Wszystkie prawe strony są obliczane na podstawie wartości z początku kroku czasowego. Wszystkie lewe strony są aktualizowane na końcu kroku czasowego. Kolejność instrukcji w kodzie nie zmienia zależności między sygnałami:

Ten fragment realizuje obrót trzech wartości a → b → c → a w jednym kroku. Z przypisaniem blokującym potrzebna byłaby zmienna tymczasowa, żeby nie nadpisać jednej z nich. Z nieblokującym zamiana jest atomowa, bo każda prawa strona odczytuje wartości sprzed kroku.

Dokładnie tak zachowują się trzy przerzutniki na zboczu zegara: wszystkie zapamiętują swoje wejścia w tej samej chwili, niezależnie od zależności między nimi.

Reguła, która cię ratuje

W blokach taktowanych używaj <=. W blokach kombinacyjnych używaj =.

To wszystko. Zapamiętaj to i stosuj bez zastanowienia. Większość wyścigów, rozbieżności między symulacją a syntezą i błędów typu "u mnie działa, a po syntezie nie" bierze się z łamania tej reguły.

Reguła ma uzasadnienie sprzętowe: bloki taktowane modelują przerzutniki, które próbkują wejścia jednocześnie. Bloki kombinacyjne modelują logikę, przez którą sygnał propaguje się tak szybko, jak może. Semantyka operatorów przypisania odpowiada tym zachowaniom.

Każde <= odczytuje bieżącą wartość rejestru źródłowego i planuje, że rejestr docelowy przyjmie tę wartość na końcu kroku czasowego. Efekt jest dokładnie taki, jak w sprzętowym rejestrze przesuwnym: każdy przerzutnik zapamiętuje wartość sąsiada, wszystkie na tym samym zboczu zegara, bez wyścigu.

A teraz zobacz, co stałoby się z przypisaniem blokującym:

// WRONG - this is not a shift register!
always @(posedge clk) begin
    out[3] = out[2];   // out[3] becomes out[2]
    out[2] = out[1];   // out[2] becomes out[1], which we just set above
    out[1] = out[0];
    out[0] = in;
end

Każda instrukcja nadpisuje źródło, zanim następna je odczyta. W jednym cyklu zegara in przeszłoby aż do out[3], bo każda linia widzi świeżo zapisaną wartość linii nad nią. Zachowanie prawdziwego sprzętu (który działa według semantyki nieblokującej) byłoby zupełnie inne niż to, co pokazał symulator.

Bloki kombinacyjne: tu właściwe jest blokujące

W always @(*) poprawne jest przypisanie blokujące. Nie ma przerzutników ani reguły jednoczesnego zapamiętywania, a zmienne pośrednie są przydatne:

Najpierw obliczane jest sum, a potem result korzysta z właśnie obliczonej wartości. Logika kombinacyjna spłaszcza się do jednego układu: result = ~(a + b). Nie pojawiają się żadne przerzutniki, bo nie ma zegara.

Gdyby użyć tu <=, symulator i tak zaktualizowałby sum, zanim result zostałby na jego podstawie obliczony (bo obie aktualizacje dzieją się na końcu kroku), ale kolejność byłaby subtelnie inna i wiele narzędzi do syntezy by protestowało. Nie mieszaj. Wybierz operator pasujący do typu bloku.

Błąd, który boli najbardziej

Oto on: blok taktowany z przypisaniem blokującym.

// BUG: race condition waiting to happen
always @(posedge clk) begin
    a = b;
    b = c;
    c = a;
end

Symulator może najpierw obsłużyć a, potem b, potem c i dać jeden zestaw wartości. Sprzęt da inny zestaw, bo prawdziwe przerzutniki zapamiętują jednocześnie. Oba wyniki rozjeżdżają się po cichu, a szukanie tego błędu zajmie ci cały dzień. W blokach taktowanych używaj <=.

Po co w ogóle dwa operatory

Twórcy Veriloga mogli wybrać jedną semantykę przypisania i się jej trzymać. Nie zrobili tego, bo język musi modelować dwa różne zachowania sprzętu:

  • Logika kombinacyjna: sygnały propagują się ciągle, zależności mają znaczenie, pytanie "co oblicza ta bramka" ma sens.
  • Logika sekwencyjna: przychodzi zbocze zegara, każdy przerzutnik zapamiętuje wartość jednocześnie, a zależności między wejściami i wyjściami przerzutników są rozdzielone.

Przypisanie blokujące służy do pierwszej, nieblokujące do drugiej. Operator wybiera semantykę, a symulator robi resztę.

Co dalej

Znasz już reguły, które pozwalają poprawnie napisać każdy blok proceduralny. Następny rozdział przechodzi od pojedynczych bloków do konstrukcji sterujących, które się w nich umieszcza: if/else, case i pętli for. Reguły przypisań blokujących i nieblokujących obowiązują w każdej z nich.

Najczęściej zadawane pytania

Jaka jest różnica między przypisaniem blokującym a nieblokującym w Verilogu?

Przypisanie blokujące (=) aktualizuje cel natychmiast, zanim wykona się następna instrukcja. Modeluje wykonanie sekwencyjne. Przypisanie nieblokujące (<=) planuje aktualizację na koniec bieżącego kroku czasowego: każda prawa strona jest obliczana na podstawie starych wartości wszystkich sygnałów, a potem wszystkie lewe strony są aktualizowane w jednym, wspólnym kroku. Tak właśnie zachowują się przerzutniki.

Kiedy używać = a kiedy <= w Verilogu?

Reguła: używaj <= w taktowanych blokach always @(posedge clk), a = w kombinacyjnych blokach always @(*). Ta jedna reguła eliminuje całą klasę wyścigów, które powstają przy mieszaniu przypisań. W blokach initial testbenchy naturalnym wyborem jest =, a <= pojawia się tam rzadko.

Po co w Verilogu przypisanie nieblokujące?

Bo sprzętowe przerzutniki zapamiętują swoje wejścia wszystkie jednocześnie na zboczu zegara. Gdyby w kodzie taktowanym użyć = (przypisania blokującego), kolejność instrukcji w pliku decydowałaby o tym, który sygnał widzi nową wartość którego innego sygnału. To wyścig między symulacją a prawdziwym sprzętem. <= odpowiada zachowaniu sprzętu: najpierw oblicza wszystkie prawe strony, potem wykonuje wszystkie aktualizacje.

Co się stanie, gdy w jednym bloku always w Verilogu zmieszasz = i <=?

Powstaje wyścig. Takie mieszanie daje sprzęt, którego zachowanie zależy od wewnętrznych mechanizmów symulatora (kolejności planowania zdarzeń), a co gorsza symulacja może nie zgadzać się z wynikiem syntezy. Większość narzędzi lint zgłasza to jako błąd. Rozwiązanie to wybór jednego operatora zgodnie z rolą bloku: nieblokujący dla taktowanych, blokujący dla kombinacyjnych.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ