Wykład 6: Sygnały zegarowe

Data: 24.11.2020, 01.12.2020

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:

  1. Okres (period) — czas między kolejnymi zboczami rosnącymi, mierzony w ns, bądź częstotliwość zegara (odwrotność okresu, mierzona w MHz).

  2. 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.

  3. 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.

  4. 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:

  1. PLL ma wejście zegarowe (nazwijmy je CLKIN), do którego podłączamy otrzymany skądś bazowy sygnał zegarowy.

  2. 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.

  3. 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 je CLKOUT<idx>.

  4. PLL ma drugie wejście zegarowe (nazwijmy je CLKFB), do którego należy podłączyć wyjście CLKOUT0 poprzez sieć dystrybucji zegara — tą samą (bądź wystarczająco podobną), co ewentualni użytkownicy sygnałów wyjściowych.

  5. PLL zawiera układ porównywania fazy (phase comparator), który cały czas porównuje wejście CLKFB z wejściem CLKIN 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 przez DIV0) stał się identyczny z CLKIN. 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).

  6. PLL ma wyjście LOCKED, które mówi czy PLL osiągnał już stan locked.

  7. PLL ma wejście RESET, które rozpoczyna od nowa procedurę dostosowywania VCO do wejścia CLKIN. 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ść 50MHz

  • DIV0 wynosi 16

  • DIV1 wynosi 12

  • VCO ustabilizuje się na częstotliwości CLKIN * DIV0, czyli 800MHz

  • CLKOUT0 będzie miał częstotliwość VCO / DIV0, czyli 50MHz (jak CLKIN 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 wersja

  • prymityw 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