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.