Cegiełka: przerzutnik D
Całe projektowanie synchroniczne sprowadza się do jednego małego elementu sprzętu: przerzutnika D. Ma wejście zegarowe, wejście danych i wyjście danych. Przy każdym zboczu narastającym zegara zapamiętuje wartość D i trzyma ją do następnego zbocza. To wszystko.
W Verilogu:
always @(posedge clk) begin
q <= d;
end
Trzy linie, jeden przerzutnik. <= to przypisanie nieblokujące, które zachowuje się dokładnie jak prawdziwe przerzutniki (więcej w artykule Przypisania blokujące i nieblokujące). To czułość na posedge clk sprawia, że logika jest sekwencyjna.
Połącz wiele takich elementów z logiką kombinacyjną między nimi, a zbudujesz dowolny układ synchroniczny.
Rejestr z resetem
Prawdziwe projekty zawsze potrzebują resetu, czyli sposobu na wprowadzenie systemu w znany stan przy włączeniu zasilania albo na żądanie:
To reset synchroniczny: warunek resetu jest sprawdzany na zboczu zegara jak każde inne wejście. Syntezator tworzy przerzutnik z multiplekserem 2 na 1 na wejściu danych. Gdy reset jest w stanie wysokim, multiplekser podaje zero, w przeciwnym razie podaje d.
W większości projektów reset synchroniczny to właściwy wybór. Upraszcza analizę czasową, dobrze współpracuje z FPGA, a sygnał resetu może pochodzić skądkolwiek i nie potrzebuje własnego źródła wyrównanego z zegarem.
Rejestr z enable
Często potrzebujesz rejestru, który aktualizuje się tylko na polecenie. Użyj if wewnątrz bloku taktowanego i skorzystaj z domyślnego zachowania "zachowaj poprzednią wartość" przy brakującym else:
To kanoniczny rejestr z wejściem ładowania (load enable). Pojawia się wszędzie: w rejestrach konfiguracyjnych, w stopniach potoku, które przesuwają dane tylko wtedy, gdy kolejny stopień jest gotowy, w licznikach, które można wstrzymać i wznowić. Pominięcie końcowego else jest celowe i bezpieczne w bloku taktowanym: przerzutnik i tak pamięta poprzednią wartość, więc "nic nie rób" oznacza "trzymaj".
W bloku kombinacyjnym ten sam kod utworzyłby zatrzask. Inne reguły, ta sama składnia.
Licznik
Licznik to rejestr, którego następna wartość to bieżąca wartość plus jeden:
Licznik zwiększa się w każdym cyklu zegara, gdy enable jest w stanie wysokim. Po 16 cyklach wraca do 0 (bo licznik ma 4 bity, a 15 + 1 przepełnia się do 0). To zawijanie wynika z natury arytmetyki N-bitowej i dokładnie tak zachowuje się prawdziwy licznik sprzętowy.
Rejestr przesuwny
Połączenie przerzutników w łańcuch daje rejestr przesuwny. Cała sztuka polega na wykonaniu wszystkich przesunięć w jednej instrukcji nieblokującej:
Ciało to out <= {out[WIDTH-2:0], in}: sklej młodsze bity bieżącego out z nowym in i przypisz całość. Ponieważ przypisanie jest nieblokujące, prawa strona odczytuje stare out, zanim lewa strona się zaktualizuje. Efektem jest czyste przesunięcie N bitów w jednym cyklu zegara.
To wzorzec rejestru przesuwnego w najmniejszej postaci. Uogólnia się na rejestry LFSR, nadajniki szeregowe, deserializery, czyli każdy projekt, w którym dane przechodzą przez łańcuch przerzutników z każdym taktem zegara.
Potoki
Potok (pipeline) to łańcuch rejestrów rozdzielonych logiką kombinacyjną. Każdy stopień przetwarza dane z poprzedniego stopnia i zasila następny:
Trzy stopnie, trzy cykle opóźnienia, ale gdy potok się zapełni, co cykl pojawia się nowy wynik. Opóźnienie wynosi 3 takty, bo dane przechodzą przez trzy przerzutniki. Przepustowość to 1 operacja na takt, bo wszystkie trzy stopnie pracują jednocześnie na różnych danych.
Tak wydajne projekty osiągają docelową przepustowość: krótkie stopnie, głębszy potok i równoległość, która wykonuje pracę.
Reset asynchroniczny (gdy go potrzebujesz)
Czasem nie można czekać na zbocze zegara, żeby aktywować reset: układ jest wyłączany, zegar jest bramkowany albo zewnętrzny watchdog szarpie linię. W takich przypadkach:
always @(posedge clk or negedge reset_n) begin
if (~reset_n) q <= 0;
else q <= d;
end
Lista czułości zawiera teraz zarówno zbocze zegara, jak i zbocze resetu. Przerzutnik reaguje natychmiast na każde z nich. reset_n jest zgodnie z konwencją aktywny stanem niskim, dlatego warunek to ~reset_n.
Reset asynchroniczny ma swoje wady: trudniej go analizować czasowo, może wywołać metastabilność, jeśli jego zwolnienie nie jest starannie obsłużone, i nie jest przenośny między wszystkimi architekturami FPGA. Domyślnie używaj resetu synchronicznego, a asynchronicznego tylko wtedy, gdy projekt tego wymaga.
Co dalej
Potrafisz już zbudować dowolną synchroniczną ścieżkę danych. Następny artykuł, Maszyny stanów, łączy taktowany rejestr z instrukcją case w standardowy idiom automatu skończonego (FSM): podstawę każdego kontrolera, silnika protokołu i bloku podejmującego decyzje w projektowaniu cyfrowym.
Najczęściej zadawane pytania
Czym jest logika taktowana w Verilogu?
To logika, której aktualizacje są wyznaczane przez sygnał zegarowy. Standardowy idiom to always @(posedge clk) target <= next_value;: przy każdym zboczu narastającym clk sygnał target zapamiętuje next_value. Ta jedna linia opisuje w sprzęcie przerzutnik. Połączenie wielu takich przerzutników z logiką kombinacyjną między nimi daje liczniki, rejestry przesuwne, potoki, czyli wszystko, co synchroniczne.
Jaka jest różnica między resetem synchronicznym a asynchronicznym w Verilogu?
Reset synchroniczny to always @(posedge clk) if (reset) ...: reset jest próbkowany na zboczu zegara jak każde inne wejście. Reset asynchroniczny to always @(posedge clk or negedge reset_n) if (~reset_n) ...: blok uruchamia się na zboczu zegara albo przy aktywacji resetu, więc reset działa natychmiast. Synchroniczny to domyślny wybór. Asynchronicznego używa się, gdy reset musi zadziałać nawet przy zatrzymanym zegarze.
Jak zbudować potok (pipeline) w Verilogu?
Ułóż kilka stopni w postaci always @(posedge clk) stageN_reg <= stageN_combinational;: logika kombinacyjna każdego stopnia zasila rejestr następnego stopnia, a wszystkie rejestry zapamiętują wartości na tym samym zboczu zegara. Powstaje potok, do którego co cykl wchodzą nowe dane i wychodzą N cykli później, z przepustowością jednego wyniku na takt.
Czym jest rejestr przesuwny w Verilogu?
To łańcuch przerzutników, w którym wyjście każdego zasila wejście następnego. Każde zbocze zegara przesuwa wszystkie bity o jedną pozycję. Kanoniczna wersja w Verilogu używa przypisań nieblokujących: out <= {out[N-2:0], in};. Ta jedna linia tworzy N-bitowy rejestr przesuwny, który co cykl pobiera jeden bit z in.