Wykład 3: Układy synchroniczne

Data: 17.10.2018

Układy kombinacyjne i sekwencyjne

Układ kombinacyjny to acykliczna sieć bramek logicznych. W układzie kombinacyjnym wartość sygnałów wyjściowych zależy tylko od wartości sygnałów wejściowych (pomijając opóźnienia).

Układ sekwencyjny to sieć bramek logicznych zawierająca cykle. Stany logiczne na tych cyklach stanowią pamięć wewnętrzną układu, a żeby przewidzieć wartość sygnałów wyjściowych musimy znać ich wartość (jak również wartośći sygnałów wejściowych).

Wyróżniamy dwa typy układów sekwencyjnych:

  • układy synchroniczne: zmiana stanu układu następuje tylko na skutek zmian jednego wyróżnionego sygnału wejściowego, zwanego wejściem zegarowym

  • układy asynchroniczne: wszystkie inne

Zatrzaski

Najprostszym układem sekwencyjnym (ale nie synchronicznym) są zatrzaski (latch), czyli układy z 1-bitową pamięcią i sygnałem zapisu.

../_images/dl.svg

Wyjście Q zawsze pokazuje stan zatrzasku. Gdy wejście E ma stan 1, stan zatrzasku jest ustawiany na wejście D, a gdy ma stan 0, stan zatrzasku się nie zmienia.

Aby opisać w Verilogu logikę z zatrzaskami, wystarczy napisać proces podobny do logiki kombinacyjnej, ale nie zawsze ustawiający swój rejestr wyjściowy:

reg q;
wire d, e;

// Równoważne pojedynczemu zatrzaskowi typu D.
always @(d, e) begin
    if (e)
        q <= d;
end

Pisząc logikę kombinacyjną za pomocą procesu, należy uważać, by zawsze ustawiać stan wyjść – w przeciwnym wypadku dostaniemy niechciany zatrzask. W języku SystemVerilog ten problem został rozwiązany przez wprowadzenie słów kluczowych always_comb (jak always, ale powoduje błąd syntezy, gdy powstały układ nie będzie kombinacyjny) oraz always_latch (powoduje błąd syntezy, gdy powstały układ nie jest zatrzaskiem).

W zależności od technologii, zatrzask typu D może mieć więcej dostępnych wejść. Po szczegóły odsyłam do dokumentacji konkretnego układu (bądź biblioteki komórek w przypadku ASICów). Ten dostępny w Spartanie 3E w pełnej konfiguracji (LDCPE) zachowuje się następująco:

wire clr;      // Asynchronous clear/reset.
wire pre;      // Asynchronous set/preset.
wire d;        // Data input.
wire g;        // Gate input (równoważne wejściu e powyżej).
wire ge;       // Gate enable input.
reg q = <...>; // Data output (możemy wybrać wartość początkową).

always @(clr, pre, d, g, ge) begin
    if (clr)
        q <= 0;
    else if (pre)
        q <= 1;
    else if (g && ge)
        q <= d;
end

Możliwość ustawienia wartości początkowej zatrzasków i przerzutników jest cechą charakterystyczną układów FPGA i niektórych układów CPLD – jeśli projektujemy ASIC, stan początkowy jest nieustalony i musimy samemu zapewnić odpowiedni układ resetujący stan przy włączeniu zasilania.

W logice, zatrzasków używa się rzadko – szczególnie w przypadku FPGA, gdzie zatrzaski i przerzutniki mają ten sam koszt. Zatrzasków używa się natomiast jako pamięć – jeżeli używamy zewnętrznej asynchronicznej pamięci SRAM, jest to po prostu wielka tablica zatrzasków i steruje się nią dość podobnie.

Przerzutniki

Najprostszym układem synchronicznym (i podstawowym elementem, z którego składają się wszystkie układy synchroniczne) jest przerzutnik:

../_images/dff.svg

Przerzutnik działa podobnie do zatrzasku, ale kopiuje stan wejścia D do wyjścia Q tylko w momencie zmiany wejścia C z 0 na 1 – gdy stan wejścia będzie już ustabilizowany na 1, przerzutnik (w przeciwieństwie do zatrzasku) nie będzie reagował na dalsze zmiany wejścia D.

W Verilogu możemy go opisać następująco:

reg q;
wire d, c;

always @(posedge c) begin
    q <= d;
end

Nigdy nie należy próbować samemu składać przerzutnika z bramek logicznych w układzie FPGA (a w przypadku ASICa należy być bardzo, bardzo ostrożnym) – jego poprawne działanie jest zależne od odpowiedniego ułożenia czasów opóźnień propagacji na bramkach.

Przerzutniki, podobnie do zatrzasków, miewają więcej wejść. W układzie Spartan 3E mamy dostępne dwa rodzaje przerzutników: z synchronicznymi i asynchronicznymi wejściami resetu. Ten z synchronicznym resetem (FDRSE) działa następująco:

wire r;     // Synchronous reset input.
wire s;     // Synchronous set input.
wire ce;    // Clock enable input.
wire c;     // Clock input.
wire d;     // Data input.
reg q = <...>;

always @(posedge clk) begin
    if (r)
            q <= 0;
    else if (s)
            q <= 1;
    else if (ce)
            q <= d;
end

Nie ma on większych możliwości od postawowego przerzutnika (poza wartością początkową) – dodatkowe wejścia dałoby się emulować logiką kombinacyjną.

Przerzutnik z asynchronicznym resetem (FDCPE) jest ciekawszy:

wire clr;   // Asynchronous clear input.
wire pre;   // Asynchronous set input.
wire ce;    // Clock enable input.
wire c;     // Clock input.
wire d;     // Data input.
reg q = <...>;

always @(posedge clk, posedge clr, posedge pre) begin
    if (clr)
            q <= 0;
    else if (pre)
            q <= 1;
    else if (ce)
            q <= d;
end

Działa on następująco:

  • zawsze, gdy ustawiony jest asynchroniczny reset, stan jest ustawiany na 0 (pozostałe wejścia, w tym wejście zegarowe, są ignorowane)

  • zawsze, gdy ustawiony jest asynchroniczny preset, ale nie reset, stan jest ustawiany na 1

  • jeśli żaden z asynchronicznych sygnałów nie jest ustawiony, ustawiony jest sygnał clock enable, i następuje zbocze rosnące na wejściu zegarowym, kopiuje stan wejścia d do stanu wewnętrznego

  • w pozostałych momentach, utrzymuje poprzednią wartość stanu

Ostrzeżenie

Asynchronicznego resetu nie da się emulować inną funkcjonalnością. Przed użyciem bardziej skomplikowanych kombinacji, należy skonsultować się z dokumentacją używanego układu FPGA. Czasem można spotkać następujące ograniczenia:

  1. Przerzutnik może mieć asynchroniczny reset albo asynchroniczny preset, ale nie oba naraz.

  2. Asynchroniczne wejście musi ustawiać przerzutnik na stan początkowy – np. jeśli używamy asynchronicznego presetu, stanem początkowym musi być 1.

Układy synchroniczne, czasy setup i hold

Układ synchroniczny składa się z pewnej liczby przerzutników używających wspólnego sygnału zegarowego i układów kombinacyjnych wyznaczająych nowy stan układu. Działa on następująco:

  1. Przed rosnącym zboczem zegarowym stan układu i jego wejść jest stabilny.

  2. Przychodzi rosnące zbocze zegarowe.

  3. Każdy przerzutnik w układzie jednocześnie sampluje stan wejścia D i kopiuje je na wyjście Q.

  4. Część kombinacyjna układu przez pewien czas liczy nowy stan.

  5. Układ się stabilizuje – wyjścia Q przerzutników wciąż trzymają stary stan układu, a na wejściach D jest już gotowy nowy stan układu.

  6. Nadchodzi kolejne rosnące zbocze zegarowe.

Moment nadejścia opadającego zbocza zegarowego nie jest zbyt istotny – liczy się tylko rosnące zbocze.

Aby układ działał poprawnie, należy zapewnić, że wszystko stanie się w odpowiednim czasie. Każdy przerzutnik (lub inny gotowy układ synchroniczny) ma następujące ważne parametry, których wartości można znaleźć w dokumentacji:

  • Setup time (T_xS): wymagany przez przerzutnik czas, przez który stan wejścia D musi być stabilny przed rosnącym zboczem zegarowym, bo jego stan został poprawnie przeczytany.

  • Hold time (T_xH): wymagany przez przerzutnik czas, przez który stan wejścia D musi być stabilny po rosnącym zboczu zegarowym, bo jego stan został poprawnie przeczytany.

  • Clock-to-Output time (T_CKO): czas od rosnącego zbocza zegarowego do pojawienia się nowego stanu na wyjściu.

W zależności od konstrukcji, przerzutnik może mieć zerowy (bądź nawet ujemny) czas setup albo hold, ale nie oba naraz. Ponieważ czas hold jest problematyczny (nie chcemy, by inny przerzutnik w układzie mógł popsuć układ licząc nowy stan zbyt szybko), zazwyczaj dąży się do minimalizacji bądź wyeliminowania go. Co więcej, każda sensowna technologia ma T_CKO > T_xH, więc nie musimy się przejmować czasem hold dopóki nie mamy bardzo nierównomiernych opóźnień w transmisji sygnału zegarowego i nie mieszamy technologii (np. przez podłączenie innego układu do FPGA).

Pozostałe czasy determinują maksymalną możliwą częstotliwość sygnału zegarowego, przy której układ synchroniczny działa poprawnie – jest to 1 / (T_CKO + T_xS + największe opóźnienie na logice kombinacyjnej i przesyłaniu danych między wyjściem Q a wejściem D).

Analizę czasową wykonują za nas narzędzia – jeśli nasz zegar mieści się w podanej przez narzędzia maksymalnej częstotliwości naszego układu (i spełnione są ograniczenia czasowe na synchroniczne sygnały wejścia/wyjścia), wszystko jest w porządku. W przeciwnym wypadku, musimy zwolnić zegar, bądź wziąć się za przeprojektowanie układu.

Metastabilność

Niestety, w prawdziwym świecie nie istnieją układy w pełni cyfrowe – każdy układ jest tak naprawdę układem analogowym i można wprowadzić go w stany pośrednie (gdzieś pomiędzy 0 i 1). Zapisując taki stan (szczególnie stan bliski 0.5) do zatrzasku lub przerzutnika, możemy go wprowadzić w stan równowagi niestabilnej – dowolne zaburzenie spowoduje, że spadnie w stan 0 lub stan 1, lecz może to zająć dużo więcej czasu niż normalny parametr T_CKO. Takie zjawisko nazywa się metastabilnością.

Metastabilność jest zjawiskiem niebezpiecznym, gdyż dany stan może być różnie zinterpretowany przez podłączone wejścia różnych bramek logicznych, powodując w naszym układzie synchronicznym przejścia niespójne zarówno ze stanem logicznym 0 jak i stanem logicznym 1. Byłoby bardzo niedobrze, gdyby np. różne części składowe procesora nie zgadzały się na temat tego, czy w danym cyklu procesor rozpoczyna obsługę przerwania.

Metastabilność może powstać na dwa sposoby:

  1. Dajemy układowi synchronicznemu stan pośredni jako wejście (należy tego nie robić).

  2. Nie przestrzegamy czasu setup bądź czasu hold.

Metastabilność jest zjawiskiem nieuniknionym, gdy dany układ synchroniczny ma wejścia asynchroniczne (a praktycznie każdy ma) – prędzej czy później, wejście zmieni się odpowiednio blisko zbocza sygnału zegarowego, powodując naruszenie czasu setup bądź hold. Musimy więc sobie jakoś z tym poradzić.

Z drugiej strony, stany metastabilne trwają bardzo krótko – praktycznie zawsze rozwiązują się w ciągu jednego, ewentualnie dwóch cykli zegara. Z tego powodu, na sygnałach asynchronicznych używa się tzw. synchronizatorów – łańcuchów dwóch lub trzech przerzutników:

wire async_in;
reg tmp1, tmp2;
reg out;

always @(posedge clk)
    tmp1 <= async_in;
    tmp2 <= tmp1;
    out <= tmp2;
end

Jeśli przerzutnik tmp1 <= async_in złapie wejście w momencie zmiany stanu i wejdzie w stan metastabilny, z bardzo dużym prawdopodobieństwem stan ten rozpadnie się do stanu 0 bądź 1 zanim jego wyjście zostanie użyte przez kolejny przerzutnik (ma na to cały cykl zegara). Jeśli nawet to się nie uda, na przerzutniku tmp2 <= tmp1 mamy kolejną szansę. Na wyjściu out z bardzo dużym prawdopodobieństwem dostajemy już czyste wyjście bez metastabilności.

Zapobieganie metastabilności ma charakter probabilistyczny, a odporność układu na metastabilność podaje się jako MTBF (mean time between failures) – szacowany średni czas bezawaryjnej pracy. Jeśli osiągniemy MTBF rzędu setek tysięcy lat, możemy go uważać za odporny na metastabilność. MTBF zależy od:

  • Częstotliwości zegara. Większa częstotliwość to:

    • więcej szans, że coś pójdzie nie tak

    • krótszy okres czasu w synchronizatorze na rozpad stanu metastabilnego

  • Technologii wykonania (nowsze układy FPGA mają dedykowane bloki synchronizacyjne z przerzutnikami wykonanymi w specjalnej technologii zoptymalizowanej na zapobieganie metastabilności)

  • Liczby przerzutników w synchronizatorze (MTBF rośnie wykładniczo z liczbą przerzutników). Dwa przerzutniki to absolutne minimum, dla bezpieczeństwa zaleca się trzy.

Synchronizatorów należy używać zawsze na asynchronicznych wejściach. W przypadku wejść z układów synchronicznych pracujących z inną częstotliwością zegara, należy zapewnić jeszcze jeden przerzutnik na początku, używający sygnału zegarowego układu źródłowego – zapobiega to wydostawaniu się stanów pośrednich z logiki kombinacyjnej. Jedyny przypadek, w którym wolno pominąć synchronizator na wejściu to wejście z innego układu synchronicznego pracującego na ściśle związanym sygnale zegarowym (np. naszym sygnale zegarowym podzielonym bądź pomnożonym przez stałą przez układ generowania zegara) – w tym wypadku należy dobrze opisać wzajemną relację tych zegarów, by narzędzia analizy czasowej mogły sprawdzić zachowanie czasów setup i hold.

Zjawisko metastabilności występuje zawsze, gdy dwie konfliktujące zmiany stanów mogą się zdarzyć blisko siebie – w szczególności może też dotyczyć asynchronicznych sygnałów reset, bądź wejścia do zatrzasku (gdy zmienia się jednocześnie ze zmianą 1 na 0 na wejściu E).

O stanie początkowym i uruchamianiu FPGA

W układach FPGA mamy możliwość ustawienia stanów początkowych przerzutników, więc sygnały reset do każdego przerzutnika nie są ściśle potrzebne (w przeciwieństwie do ASICów). Jeśli używamy sygnałów reset (by móc przywrócić część układu do stanu początkowego), zazwyczaj łatwiej jest użyć resetów synchronicznych. Polecam tutaj lekturę:

https://www.xilinx.com/support/documentation/white_papers/wp272.pdf

Natomiast należy zauważyć, że jeśli nasz układ ma zacząć pracę od razu po starcie (jak np. mikroprocesor, który natychmiast po włączeniu zaczyna wykonywać kod z pamięci), samo zjawisko startu FPGA może prowadzić do metastabilności jeśli stanie się to blisko stanu zbocza zegarowego. Rozwiązania tego problemu są dwa:

  1. Zaprojektować stan początkowy układu tak, żeby był stabilny (następny stan == stan początkowy), z wyjątkiem jednego przerzutnika, stanowiącego początek synchronizatora, który spowoduje rozpoczęcie pracy układu („iskry życia”):

    reg start0 = 0;
    reg start1 = 0;
    reg running = 0;
    
    always @(posedge clk) begin
        start0 <= 1;
        start1 <= start0;
        running <= start1;
    end
    
  2. Zsynchronizować start układu FPGA do własnego zegara.

Procedura startowa FPGA

W przypadku układu Spartan 3E, procedura uruchamiania wygląda następująco:

  1. Układ power-on reset monitoruje stan linii zasilających i utrzymuje cały układ w stanie początkowym dopóki nie osiągną one poziomu napięcia odpowiedniego do rozpoczęcia pracy.

  2. Układ zaczyna generować zegar za pomocą wewnętrznego (bardzo niedokładnego) oscylatora.

  3. Układ czyści pamięć konfiguracyjną.

  4. Układ sprawdza stan linii sterujących wybierających źródło konfiguracji (na płytce Basys2 możemy wybrać dwie opcje za pomocą zworki).

  5. W zależności od źródła konfiguracji:

    • jeśli układ ma sam wczytać konfigurację z kości flash, używa wewnętrznego oscylatora jako zegara konfiguracyjnego (CCLK) i rozpoczyna wczytywanie

    • jeśli układ ma być konfigurowany przez zewnętrzne źródło, wyłącza wewnętrzny oscylator i używa zewnętrznego zegara jako CCLK

    • jeśli układ ma być konfigurowany przez JTAG, układ wyłącza wewnętrzny oscylator i zatrzymuje się, czekając na instrukcje (i zegar) na porcie JTAG

  6. Układ konfiguruje się, używając zewnętrznego bądź wewnętrznego sygnału CCLK bądź sygnału zegarowego z portu JTAG

  7. Układ sprawdza poprawność konfiguracji.

  8. Układ przechodzi z zegara konfiguracyjnego na zegar startowy, wybrany w konfiguracji (opcja StartupClk polecenia bitgen). Może to być:

    • JTAGClk: zegar z portu JTAG. Używając tego ustawienia z inną metodą konfiguracji niż JTAG, zawiesimy układ przed uruchomieniem.

    • CCLK: ten sam zegar, co używany przy konfiguracji metodammi innymi niż JTAG. Używając tego zegara z metodą JTAG, możemy zawiesić układ, jeśli nic nie podaje stosownego sygnału na odpowiednią nóżkę FPGA.

    • UserClk: dowolny zegar wybrany przez użytkownika przez podłączenie go do odpowiedniego bloku STARTUP.

  9. Układ przechodzi przez kolejne fazy startu, w kolejności wybranej przez użytkownika (za pomocą opcji programu bitgen). Te fazy to:

    • włączenie sygnału DONE, oznajmiającego światu zewnętrznemu, że FPGA zakończyło konfigurację i rozpoczęło pracę. W przypadku użycia wielu układów FPGA, mamy możliwość monitorowania sygnałów DONE z innych FPGA i poczekania, aż wszystkie będą gotowe (synchroniczny start układu złożonego z wielu FPGA).

    • wyłączenie specjalnego sygnału GTS (global three-state), blokującego wszystkie sygnały wyjściowe (zabezpiecza on przed nadawaniem nieustalonych sygnałów na wyprowadzeniach układu w trakcie konfiguracji).

    • oczekiwanie na uruchomienie i ustabilizowanie wybranych układów DCM (generatorów zegara).

    • włączenie specjalnego sygnału GWE (global write enable), umożliwiającego zapis do pamięci zawartych w układzie (przerzutników oraz pamięci RAM) – do tego momentu, układ jest w stanie początkowym.

  10. Układ wyłącza oscylator wewnętrzny, jesli nie zrobił tego wcześniej.

Prymityw STARTUP_SPARTAN3E

Możemy kontrolować pewne aspekty procedury startowej instancjonując prymityw STARTUP_SPARTAN3E (można to zrobić co najwyżej raz):

STARTUP_SPARTAN3E my_startup (
        // 4 wejścia, każde z nich opcjonalne.
        .CLK(clk),      // Zegar startowy (używany przez opcję UserClk)
        .GSR(gsr),      // Global Set/Reset
        .GTS(gts),      // Global Three-State
        .MBT(mbt)       // Multiboot trigger
);

GSR jest globalnym sygnałem, który asynchronicznie resetuje wszystkie przerzutniki w FPGA na ich stan początkowy (jest używany wewnętrznie w czasie konfiguracji) – możemy go użyć do restartu naszego układu. Nie jest to kompletny powrót do stanu początkowego – nie dotyczy on wewnętrznych pamięci RAM.

GTS został opisany powyżej – można go użyć po uruchomieniu FPGA, by na chwilę wyłączyć wszystkie wyjścia.

MBT powoduje pełny restart i rekonfigurację FPGA z alternatywnej konfiguracji w równoległej pamięci flash. Nie jest to dla nas zbyt interesujące, gdyż nasze płytki nie mają takiej pamięci flash.

Używając wejścia CLK możemy zapewnić, że uruchomienie FPGA będzie zsynchronizowane z naszym zegarem i uniknąć metastabilności przy starcie.