Wykład 6: Sygnały zegarowe¶
Data: 24.11.2020, 01.12.2020
Treść
O sygnałach zegarowych¶
Sygnałem zegarowym nazywamy sygnał nadający tempo pracy układu. W logice synchronicznej jest to sygnał, którego zbocze powoduje rozpoczęcie jednego cyklu pracy części układu (zwanego domeną zegarową) — przerzutniki przechowujące stan układu otrzymują nowe wartości, synchroniczne porty odczytu pamięci wykonują odczyt, portu zapisu wykonują zapis, po czym kombinacyjna część układu rozpoczyna obliczenia których wyniki zostaną wykorzystane przy następnym aktywnym zboczu zegara.
Zazwyczaj aktywnym zboczem zegara jest zbocze rosnące (tranzycja z 0 na 1), choć równie dobrze może to być zbocze malejące (tranzycja z 1 na 0) — układy FPGA zazwyczaj obsługują te opcje równoprawnie. W niektórych układach można spotkać przerzutniki, w których oba zbocza jednocześnie mogą być aktywne, lecz jest to dość ezoteryczna rzadkość.
Zazwyczaj jako sygnału zegarowego używa się prostego sygnału okresowego pochodzącego z oscylatora — zbocza zegara następują wtedy zawsze w stałych odstępach. Nie jest to jednak wymaganie — w przypadku „zwykłej” logiki synchronicznej nic nie stoi na przeszkodzie, by sygnał zegarowy był dość dowolny. Możliwe jest zatrzymanie zegara na dowolnie długi czas, zmienny odstęp pomiędzy zboczami, itp. Jedyne wymagania, jakie musimy zachować to:
minimalny okres zegara (czas pomiędzy kolejnymi aktywnymi zboczami) — wymagana minimalna wartość jest wyznaczana przez narzędzia P&R na podstawie opóźnienia najdłuższej ścieżki kombinacyjnej w naszym układzie (plus czasy setup i hold), a jej odwrotność jest maksymalną częstotliwością zegara jakiej możemy użyć
minimalny czas wysoki i niski — gdy zegar zmienia stan z 0 na 1, musi pozostać w stanie 1 przez minimalny czas wysoki (podany w dokumentacji przerzutnika, RAMu, DSP, czy innych prymitywów w naszej technologii); analogicznie działa minimalny czas niski; w praktyce jest to mało ważne ograniczenie o ile nasz sygnał zegarowy jest generowany w sensowny sposób (praktycznie na pewno najpierw będziemy mieć problem z minimalnym okresem zegara)
Możliwość zatrzymania zegara bez szkody dla układu jest bardzo przydatna i często stosowana:
w celu ograniczenia poboru prądu przez układ — zatrzymanie zegara kasuje całe dynamiczne zużycie prądu przez zatrzymaną część układu
w celu debugowania układu (choć to jest bardzo skomplikowana sprawa)
Możliwe jest też użycie wyjścia układu synchronicznego (wyjścia przerzutnika) jako sygnału zegarowego dla innego układu synchronicznego. Rzadko jest to jednak dobry pomysł (lepiej użyć specjalistycznego prymitywu do kontroli zegara) — praktycznie jedynym przypadkiem, gdzie ma to sens są różnego rodzaju wolne interfejsy wejścia / wyjścia: SPI, JTAG, I2C, itp.
Niektóre specjalistyczne układy (nazywa się je układami dynamicznymi) mają ściślejsze wymagania co do sygnału zegarowego — wymagają stałego okresu, bądź też po prostu nie pozwalają na zatrzymanie zegara dłużej niż przez określony czas. Przykładami są opisane dalej układy przekształcające sygnały zegarowe bądź szybkie interfejsy wejścia/wyjścia.
Parametry sygnału okresowego¶
Jeśli nasz sygnał zegarowy jest zwykłym sygnałem okresowym (a nie czymś generowanym ręcznie przez układ logiczny), opisujemy go za pomocą następujących parametrów:
Okres (period) — czas między kolejnymi zboczami rosnącymi, mierzony w ns, bądź częstotliwość zegara (odwrotność okresu, mierzona w MHz).
Wypełnienie (duty cycle) — część okresu zegara, przez który jego wartością jest 1, mierzone w %. Zazwyczaj używa się zegarów o wypełnieniu 50%, lecz zdarzają się inne wartości, szczególnie przy prostych dzielnikach częstotliwości.
Wahania (jitter) — miara niedoskonałości zegara, czyli tego, jak bardzo długość kolejnych okresów różni się od siebie; mierzone w ps lub w % okresu. Zdarza się rozróżniać wahania krótkoterminowe (różnica długości okresów w oknie czasowym kilku okresów) i długoterminowe (jak bardzo długość okresów różni się od siebie na przestrzeni sekund). Musimy go odliczać od długości okresu przy analizie czasowej, by zapewnić poprawne działanie układu.
Faza (phase) — przesunięcie początku okresu w czasie. To pojęcie ma sens tylko w przypadku grupy zsynchronizowanych zegarów, gdzie mówimy o ich wzajemnych relacjach. Jest zazwyczaj mierzona w stopniach, rzadziej w ns. Jeśli mamy dwa sygnały zegarowe o tym samym okresie i fazach 0° i 90°, znaczy to, że drugi sygnał jest opóźniony o 1/4 okresu w stosunku do pierwszego. Jeśli mamy sygnały o fazie 0° i 180° oraz wypełnieniu 50%, oznacza to, że drugi jest efektywnie zanegowaną wersją pierwszego. O fazie można też mówić w przypadku zegarów, których współczynnik częstotliwości jest prostym ułamkiem (np. 20MHz i 30MHz), lecz należy wtedy bardzo uważać na stosowane definicje.
Generatory sygnałów zegarowych¶
Okazuje się, że technologia w której wytwarzane są układy cyfrowe nie pozwala na generowanie zegarów dobrej jakości — w każdym układzie mającym nietrywialne wymagania co do zegara stosuje się zewnętrzny generator sygnału zegarowego. Okazuje się jednak, że gdy już mamy sygnał zegarowy dobrej jakości, możemy dość łatwo przekształcić go w dobry sygnał zegarowy o innych parametrach.
Istnieje wiele sposobów na wygenerowanie sygnału zegarowego. Wspomnę tutaj o trzech:
oscylator pierścieniowy (ring oscillator)
oscylator LC
oscylator kwarcowy
Naprostszym typem oscylatora jest oscylator pierścieniowy — jest to po prostu nieparzysta liczba bramek logicznych NOT połączona w pierścień. Jego częstotliwość to 1 / (suma opóźnień bramek i połączeń między nimi). Jest to bardzo niedokładny oscylator (rzędu ±30% różnicy długości okresu, zależnie od temperatury, napięcia zasilania i różnic powstałych w procesie fotolitografii) i nie powinien być stosowany w jakiejkolwiek sytuacji wymagającej konkretnej częstotliwości pracy, ale bywa używany gdy potrzebny jest po prostu jakiś zegar. Przykładem oscylatora pierścieniowego jest wewnętrzny zegar konfiguracyjny układu FPGA (jeśli został wybrany tryb konfiguracji w którym to FPGA generuje sygnał zegarowy).
Ostrzeżenie
Nie należy próbować tworzyć własnego oscylatora pierścieniowego używając programowalnej logiki — nie mamy wystarczającej kontroli nad ułożeniem i połączeniem układu, by sensownie kontrolować częstotliwość.
Trochę bardziej skomplikowanym generatorem jest generator LC, w którym używamy
rezonansu układu złóżonego z kondensatora i cewki do wygenerowania zegara.
Częstotliwością takiego zegara jest 1/(tau * sqrt(L*C))
. Wciąż, jest to
zegar nieco niedokładny (ma wahania rzędu ±1%) i nie może być używany w wielu
interfejsach wejścia/wyjścia (jeśli np. użyjemy go do wygenerowania sygnału VGA,
obraz będzie się dosłownie trząsł na monitorze).
Do generowania sensownie dokładnych (±0.001% wahań) zegarów w elektronice używa się oscylatorów kwarcowych, które używają wibracji kryształu kwarcowego do generowania sygnału zegarowego. Dzięki elektronicznym układom przekształcającym sygnały zegarowe (PLL), często wystarcza jeden kryształ (zazwyczaj o częstotliwości rzędu 10MHz-100MHz) do wygenerowania dowolnej liczby sygnałów zegarowych o dowolnych wartościach.
Dystrybucja sygnałów zegarowych¶
W układach cyfrowych bardzo pożądane jest, by zbocza sygnału zegarowego dochodziły jednocześnie do wszystkich przerzutników, którymi sterują. Różnica w czasie przyjścia zbocza do różnych miejsc nazywa się clock skew i powoduje szereg problemów:
jeśli suma clock skew i czasu hold jest większa niż czas propagacji między wyjściem jednego przerzutnika a wejściem drugiego, mamy naruszenie czasu hold i nasz układ nie będzie działać
clock skew efektywnie dodaje się do wielu czasów propagacji, zmniejszając maksymalną możliwą częstotliwość zegara
Aby zminimalizować clock skew, w układach FPGA (i ASIC) istnieją specjalne sieci
dystrybucji sygnałów zegarowych, zaprojektowane tak, by długość ścieżki
od źródła sygnału do przerzutników była mniej-więcej stała. W układach
Xilinx 7 Series mamy 32 bufory globalne, będące źródłami takich sieci.
Narzędzia do syntezy same wywnioskują, które z naszych sygnałów powinny
używać buforów globalnych, ale jeśli chcemy, możemy poprosić o to jawnie
przez zinstancjonowanie prymitywu BUFG
. Dostępne jest również wiele
buforów regionalnych (obejmujących tylko część układu, za to z mniejszym
opóźnieniem), ale nie będziemy się nimi zajmować.
Co więcej, bufory globalne potrafią również pełnić rolę przełącznika między
dwoma róznymi źródłami zegara (prymityw BUFGMUX
lub BUFGCTRL
) —
przydaje się to, gdy będziemy projektować układ, który powinien działać
na różnych częstotliwościach (tryb turbo, interfejsy sprzętowe
mające wolne/szybkie wersje, różne rozdzielczości VGA, itp). Przełącznik
ten jest dość skomplikowany — bezpieczne przełączenie sygnału zegarowego między
dwoma źródłami wymaga dużej ostrożności, by nie naruszyć wymagań minimalnego czasu
niskiego/wysokiego i nie należy próbować tego robić ręcznie.
Specjalnym (i bardzo częstym) przypadkiem funkcjonalności przełączania zegara
jest możliwość wyłączenia go (czyli przełączania się między naszym źródłem zegara
a sygnałem stale równym zero) — taką funkcjonalność realizuje prymityw
BUFGCE
:
# Układ w domenie sterowanej przez clk_with_enable będzie synchroniczny
# z domeną sterowaną przez clk_bypass, ale będzie wykonywał pracę tylko,
# gdy sig_enable będzie prawdą — odbywa się to przez maskowanie sygnału
# zegarowego.
m.submodules.buf_a = Instance("BUFG",
i_I=clk_orig,
o_O=clk_bypass,
)
m.submodules.buf_b = Instance("BUFGCE",
i_I=clk_orig,
i_CE=sig_enable,
o_O=clk_with_enable,
)
# Uwaga: aby te domeny były poprawnie zsynchronizowane, konieczne jest
# użycie BUFG na clk_bypass, by zapewnić takie same opóźnienia dystrybucji
# zegara.
Przetwarzanie sygnałów zegarowych¶
Technologia produkcji układów cyfrowych nie pozwala na generowanie sygnałów zegarowych dobrej jakości wewnątrz naszego układu. Okazuje się jednak, że mając już taki sygnał z zewnątrz (np. z oscylatora kwarcowego) można wyprodukować układ przetwarzający go w sygnał zegarowy dobrej jakości o innych parametrach. Takim układem jest PLL (phase locked loop).
Istnieje bardzo wiele rodzajów układów PLL (występujących pod różnymi nazwami), a ich użycie zawsze wymaga bezpośredniego użycia dość skomplikowanych prymitywów zależnych od producenta i konkretnej technologii FPGA. Takie układy mają jednak dość podobny ogólny schemat działania:
PLL ma wejście zegarowe (nazwijmy je
CLKIN
), do którego podłączamy otrzymany skądś bazowy sygnał zegarowy.PLL zawiera oscylator o regulowalnej częstotliwości (zazwyczaj nazywany VCO — voltage controlled oscillator). VCO generuje jakiś sygnał zegarowy (zazwyczaj dużej częstotliwości — w przypadku Xilinxa zakres to 800 – 1600MHz), który jest wejściem do kilku dzielników.
PLL zawiera kilka (2-8) programowalnych dzielników zegara, które produkują z wyjscia VCO nowe sygnały zegarowe, których częstotliwość to częstotliwość VCO podzielona przez jakąś niezbyt dużą stałą całkowitą (nazwijmy je
DIV<idx>
). Te dzielniki mają też zazwyczaj możliwość kontrolowania wypełnienia i relatywnej fazy wyjść. Sygnały zegarowe generowane przez dzielniki są wyjściami PLLa (nazwijmy jeCLKOUT<idx>
.PLL ma drugie wejście zegarowe (nazwijmy je
CLKFB
), do którego należy podłączyć wyjścieCLKOUT0
poprzez sieć dystrybucji zegara — tą samą (bądź wystarczająco podobną), co ewentualni użytkownicy sygnałów wyjściowych.PLL zawiera układ porównywania fazy (phase comparator), który cały czas porównuje wejście
CLKFB
z wejściemCLKIN
i tak steruje szybkością VCO, by wyrównać te wejścia w częstotliwości i w fazie.VCO początkowo generuje sygnał wyjściowy o kompletnie nieprzewidywalnych parametrach. Układ porównywania fazy jednak stopniowo poprawia częstotliwość oraz fazę VCO tak, by
CLKFB
(czyli sygnał wygenerowany przez VCO podzielony przezDIV0
) stał się identyczny zCLKIN
. Gdy to nastąpi, PLL nazywa się zablokowanym (locked) — od tego momentu, układ porównywania fazy ciągle monitoruje te dwa sygnały i likwiduje najdrobniejsze odchylenia, a wygenerowany sygnał zgadza się co do cyklu z wejściem tak długo, jak długo wejście jest stabilne (nie zostanie wyłaczone i nie zmieni znacząco swojej częstotliwości).PLL ma wyjście
LOCKED
, które mówi czy PLL osiągnał już stan locked.PLL ma wejście
RESET
, które rozpoczyna od nowa procedurę dostosowywania VCO do wejściaCLKIN
. Powinno się go użyć, gdy źródło sygnału wejściowego ulega zmianie.
Sygnał CLKOUT0
jest de facto równy sygnałowi CLKIN
przesuniętemu w fazie
do tyłu o tyle, ile wynosi opóźnienie dystrybucji zegara między CLKOUT0
a CLKFB
— pozwala to efektywnie zniwelować opóźnienie dystrybucji w naszym
układzie FPGA i sprawić, że zegar na wejściu naszych przerzutników będzie
wyrównany z zegarem na wejściu naszego całego układu, co przydaje się gdy
chcemy przesyłać dane synchronicznie z innymi układami na płytce drukowanej.
Znacznie ciekawszymi sygnałami są jednak pozostałe wyjścia CLKOUT<idx>
—
zauważmy, że są wygenerowane z tego samego VCO co CLKOUT0
przez proste
dzielniki zegara, a zatem są wyrównane do CLKIN
z prostym współczynnikiem
częstotliwości. Na przykład:
CLKIN
ma częstotliwość 50MHzDIV0
wynosi 16DIV1
wynosi 12VCO ustabilizuje się na częstotliwości
CLKIN * DIV0
, czyli 800MHzCLKOUT0
będzie miał częstotliwośćVCO / DIV0
, czyli 50MHz (jakCLKIN
przez pętlę sprzężenia zwrotnego)CLKOUT1
będzie miał częstotliwośćVCO / DIV1 (= CLKIN * DIV0 / DIV1)
, czyli 66MHz
Oznacza to, że nasz układ PLL efektywnie mnoży częstotliwość wejścia przez DIV0 / DIV1
produkując wyjście CLKOUT1
— za pomocą PLLi możemy więc uzyskać w miarę dowolne
częstotliwości mnożąc wejście przez odpowiednie ułamki (choć trzeba ostrożnie dobierać parametry tak,
by zmieścić się w wymaganiach PLLa).
W układach Xilinx 7 Series mamy dostępne dwa rodzaje układów PLL:
prymityw
MMCME2_BASE
bądźMMCE2_ADV
— trochę prostsza wersjaprymityw
PLLE2_BASE
bądźPLLE2_ADV
— ma więcej funkcjonalności
Wersje _ADV
pozwalają na rekonfigurację parametrów w trakcie działania układu.
Po opis użycia tych układów odsyłam do dokumentacji:
https://www.xilinx.com/support/documentation/user_guides/ug472_7Series_Clocking.pdf
Domeny zegarowe w nMigen¶
W nMigen sygnały zegarowe są w większości niejawne — są propagowane przez układ
w ramach obiektu typu ClockDomain
, reprezentującego domenę zegarową.
Domyślnie istnieje jedna domena zegarowa o nazwie sync
, ale możemy stworzyć
ich więcej.
Domena zegarowa (ClockDomain
) to obiekt opakowujący następujące elementy:
sygnał zegarowy: jednobitowy sygnał sterujący pracą tej domeny
wybór aktywnego zbocza zegara (rosnące lub malejące; domyślnie rosnące)
sygnał resetu: opcjonalny jednobitowy sygnał, którego ustawienie na 1 spowoduje ustawienie wszystkich rejestrów w domenie na wartość początkową
typ sygnału resetu: synchroniczny (reset następuje na aktywnym zboczu zegara jeśli sygnał resetu jest ustawiony) bądź asynchroniczny (reset następuje gdy tylko sygnał resetu będzie ustawiony, niezależnie od zegara); domyślny (i zalecany) wybór to reset synchroniczny
Możemy utworzyć nową domenę zegarową następująco:
moja_domena = ClockDomain(
# parametry i ich domyślne wartości:
reset_less=False, # jeśli True, domena nie będzie miała resetu
clk_edge='pos', # wybór aktywnego zbocza zegara — 'pos' oznacza rosnące, 'neg' oznacza malejące
async_reset=False, # jeśli True, reset jest asynchroniczny
local=False, # jeśli True, stworzona domena rozpropaguje się tylko do podmodułów; jeśli False, rozpropaguje się po całym układzie
)
m.domains += moja_domena
# Podłączamy sygnał zegarowy, taki jak w oryginalnej domenie sync
# Zamiast tego można by np. użyć PLL czy BUFGCE do użycia innego zegara.
# Można też nie podłączać tutaj nic, by zegar domeny był wejściem układu
m.d.comb += moja_domena.clk.eq(ClockSignal('sync'))
# Podłączemy reset.
m.d.comb += moja_domena.rst.eq(moj_reset)
ctr = Signal(4)
# To spowoduje wygenerowanie logiki w naszej nowej domenie.
m.d.moja_domena += ctr.eq(ctr + 1)
Domeny zegarowe automatycznie propagują się w całym układzie — wystarczy ją stworzyć w jednym module, by była widoczna wszędzie (chyba, że ustawimy jej parametr local).
Czasem chcemy użyć modułu „przenosząc” go do innej domeny zegarowej — powiedzmy,
że chcemy użyć jakiegoś gotowego modułu z biblioteki przystosowanego do pracy
w domyślnej domenie sync
, lecz chcemy by działał w naszej domenie. Do takich
zastosowań możemy użyć konstrukcji DomainRenamer
:
# Domena 'sync' w moj_podmodul i jego podmodułach jest tym samym co nasza domena
# 'moja_domena' i kompletnie niezależna od naszej domeny 'sync'
m.submodules.moj_podmodul = moj_podmodul = DomainRenamer({'sync': 'moja_domena'})(ModulZBiblioteki(...))
Komunikacja między domenami zegarowymi¶
Mając w układzie cyfrowym wiele domen zegarowych musimy w jakiś sposób przesyłać dane między tymi domenami. Poziom skomplikowania tego zależy od tego, jak dużo danych mamy do przesłania, oraz od tego jaka jest wzajemna relacja zegarów w tych domenach.
Zdarza się, że dwie domeny są synchroniczne wględem siebie i możemy deterministycznie po prostu używać w jednej domenie sygnałów wygenerowanych w drugiej domenie. Dzieje się tak, gdy:
domeny mają ten sam zegar (różnią się tylko resetem)
domeny mają ten sam sygnał zegarowy, ale przeciwne aktywne zbocza
domeny mają różne sygnały zegarowe, ale pochodzące z jednego źródła z dobrze zdefiniowaną relacją fazy, np.
dwa wyjścia PLL o tej samej częstotliwości, ale fazie 0° i 90°
dwa wyjścia PLL, jedno o częstotliwości 100MHz, drugie o częstotliwości 200MHz, wyrównane w fazie (każde rosnące zbocze wolnego zegara jest jednocześnie rosnącym zboczem szybkiego zegara)
sygnały zegarowe w obu domenach są zmodyfikowanymi (np. przez
BUFGCE
) wersjami tego samego sygnału bazowego
W przeciwnym wypadku domeny nazywamy asynchronicznymi względem siebie i musimy bardzo uważać w komunikacji między nimi, by uniknąć problemu metastabilności.
W przypadku prostych sygnałów (np. linii przerwania) wystarcza synchronizator. Problemy zaczynają się jednak, gdy mamy do przesłania bardziej skomplikowane dane.
Kod Graya¶
Załóżmy, że chcemy przekazać między dwiema domenami jakąś liczbę, która może zmienić się co najwyżej o 1 (w górę bądź w dół) w kolejnych cyklach zegara. Przekazanie jej bezpośrednio przez tablicę synchronizatorów nie zadziała — przy zmianie liczby, zmiany różnych bitów mogą dojśc w różnych cyklach do nowej domeny zegarowej. Istnieje jednak kodowanie liczb, które rozwiązuje ten problem — zapewnia, że każde kolejne dwie liczby są kodowane do wektorów bitowych różniących się w dokładnie jednej pozycji. Jest to kod Graya. Dla przykładu, kod 4-bitowy:
0: 0000
1: 0001
2: 0011
3: 0010
4: 0110
5: 0111
6: 0101
7: 0100
8: 1100
9: 1101
10: 1111
11: 1110
12: 1010
13: 1011
14: 1001
15: 1000
Aby zakodować liczbę x
do kodu graya, wystarczy policzyć x ^ (x >> 1)
.
Dekodowanie jest trochę bardziej skomplikowane, ale dośc efektywnie realizowalne
w sprzęcie.
FIFO¶
Do przekazania dużej ilości danych między domenami zegarowymi najczęściej używa się kolejek FIFO zrealizowanych za pomocą bloków RAMu używanych jako buforów cyklicznych:
jeden port działa tylko w trybie zapisu w domenie źródłowej
drugi port działa tylko w trybie odczytu w domenie docelowej
wskaźniki odczytu i zapisu są przekazywane między domenami zegarowymi w kodzie Graya poprzez tablicę synchronizatorów