Wykład 7: Wejście i wyjście, popularne interfejsy

Data: 21.11.2018

Bufory wejścia/wyjścia

Zewnętrzne sygnały nie są podłączone bezpośrednio do wewnętrznej sieci połączeń FPGA – przechodzą najpierw przez bufory wejścia/wyjścia (IOB). Ich zadania to:

  1. Zmiana interfejsu elektrycznego. FPGA pracuje wewnętrznie na ustalonym napięciu zasilającym (1.2V w przypadku Spartana 3E) i w technologii CMOS. Na zewnątrz natomiast możemy wybrać jedno z wielu wspieranych napięć interfejsowych i standardów elektrycznych.

  2. Konwersja pojedynczych sygnałów w sygnały różnicowe i z powrotem.

  3. Rozdzielenie zewnętrznych sygnałów dwukierunkowych na 3 sygnały wewnętrzne (wejście, wyjście, włączenie wyjścia).

  4. Synchronizacja danych przychodzących i wychodzących do zegara.

Bufory wejścia/wyjścia tworzone są przez narzędzia syntezy automatycznie z portów zewnętrznych na głównym module, ale możemy też zinstancjonować je ręcznie:

module my_ram(
    input wire write_enable,
    input wire read_enable,
    inout wire data);

reg storage = 0;

wire data_in;

// Rzadki przypadek celowej syntezy zatrzasku.
always @(write_enable, data) begin
    if (write_enable)
        storage <= data_in;
end

`ifdef MANUAL

// Wersja ręczna:

// Bufor wejścia/wyjścia: stan pinu data zawsze jest widoczny na data_in.
// Jeśli T == 0, bufor nadaje na pinie data wartość storage.  Jeśli T == 1,
// bufor wyjściowy jest wyłączony i nic nie nadaje.
IOBUF my_buffer(.IO(data), .I(storage), .O(data_in), .T(!read_enable);

`else

// Wersja automatyczna (równoważna):

assign data = read_enable ? storage : 1'bz;
assign data_in = data;

`endif

endmodule

Druty wiszące, pullup, pulldown

Rozważmy sygnał zewnętrzny, do którego akurat nie jest podłączony żaden aktywny bufor wyjściowy (nazywany wiszącym drutem (floating)). Jaki będzie jego stan?

Zależy to od technologii. W przypadku logiki TTL, stan takiego sygnału ustabilizuje się na 1. Jednak w przypadku logiki CMOS, stan takiego sygnału jest nieustalony – drut efektywnie stanie się anteną zbierającą zakłócenia z okolicy i będzie oscylował, często zmieniając stan i powodując zwiększone zużycie prądu na wszystkich podłączonych układach. Z tego powodu, wiszących drutów należy unikać.

Z drugiej strony, w przypadku sygnałów dwukierunkowych zawsze musi istnieć krótki moment, w którym żaden z układów nie ma aktywnego wyjścia (w przeciwnym wypadku mielibyśmy jeszcze większe problemy).

Rozwiązaniem problemu jest dołączenie elementów ciągnących w dół / ciągnących w górę (pulldown / pullup) – są to po prostu oporniki podłączone z drugiej strony do masy (pulldown) bądź napięcia zasilającego (pullup), ustalające stan sygnału jeśli nie ma żadnego aktywnego bufora wyjściowego (który jest silniejszy od opornika). W układach FPGA możemy wybrać jedną z 4 opcji dla każdego sygnału zewnętrznego:

  • użycie wbudowanego opornika pulldown

  • użycie wbudowanego opornika pullup

  • użycie układu keeper (pullup jeśli aktualny stan to 1, pulldown jeśli 0) – efektywnie utrzymuje obecny stan na sygnale

  • brak wbudowanego opornika (używamy, jeśli mamy zewnętrzny pullup/pulldown na płytce)

Wyboru dokonujemy przez zinstancjonowanie odpowiedniego prymitywu w Verilogu (PULLDOWN, PULLUP, KEEPER) bądź w pliku UCF.

Układy FPGA mają dużo nóżek i prawdopodobnie nie użyjemy wszystkich. Narzędzia syntezy automatycznie aktywują na nich pulldown.

Sygnały open-drain

Podstawowym problemem przy projektowaniu układu z sygnałami dwukierunkowymi jest ustalenie, który układ akurat ma prawo nadawać (aby uniknąć sytuacji, gdy jeden układ nadaje 0, a inny 1). Dość ciekawym i powszechnym rozwiązaniem tego problemu jest użycie sygnałów open-drain (nazywanych też open collector) – wspólnej linii komunikacyjnej z pullupem, na której układom wolno nadawać tylko stan 0, bądź nic nie nadawać (i pozwolić innemu układowi nadać 0, bądź pozwolić pullupowi podciągnąć stan do poziomu 1).

Najprostszym przykładem użycia takiej linii są współdzielone linie przerwań na szynie PCI – urządzenie zgłaszające przerwanie wystawia 0, a natura linii open-drain efektywnie tworzy bramkę logiczną AND z wszystkich urządzeń. Bardziej skomplikowanymi przykładami użycia open-drain są szyna I2C bądź interfejs klawiatury i myszy PS/2.

Rodzaje interfejsów

Interfejsy sprzętowe możemy z grubsza podzielić na następujące kategorie:

  • wolne (<~10 MHz) – używamy byle jakiego zegara i przepuszczamy wszystkie wejścia przez synchronizator, cykl w tą czy w tamtą stronę nic nie zmienia. Przykłady: wolne SPI/JTAG, UART, I2C, PS/2, EPP, przyciski/diody.

  • średnie (<~200MHz) – używamy zegara zsynchronizowanego z interfejsem. Przykłady: szybkie SPI/JTAG, VGA, PCI, ISA/ATA.

  • szybkie (<~1GHz) – jak wyżej, ale używamy sygnałowania różnicowego, ostrożnie dobieramy impedancję połączeń i terminatory, zapewniamy równej długości ścieżki na płytce, wyrównujemy fazę zegara, itp. Nie ma już nadziei na zrobienie szyny – tylko łącza P2P. Przykłady: interfejsy do pamięci DDR*.

  • bardzo szybkie (>~1GHz) – nie ma już nadziei na interfejs równoległy, musimy użyć interfejsu szeregowego i odtwarzać zegar z sygnału. Wymaga dedykowanych skomplikowanych bloków nadawczych i odbiorczych (nie mamy takich w Spartanie 3E). Przykłady: PCI Express, SATA, TMDS (DVI/HDMI), DisplayPort.

VGA, DVI, HDMI

Monitor (bądź telewizor) CRT składa się z:

  • ekranu, w którym punkty świecą gdy zostaną trafione elektronem

  • działka elektronowego, które strzela elektronami (a właściwie trzech, dla każdego koloru)

  • magnesu odchylenia pionowego, sterującego działkiem w osi pionowej

  • potężnego magnesu odchylenia poziomego i potężnego transformatora do niego

  • układu sterującego powyższym

Wyświetlanie obrazu rastrowego na monitorze CRT odbywa się następująco:

  • zaczynamy (arbitralnie – proces jest cykliczny, a proces synchronizacji skomplikowany) od lewego górnego piksela obrazu

  • wyświetlamy pierwszą linię obrazu, przesuwając działko od lewej do prawej krawędzi ekranu, modulując intensywność działka przez kolor piksela

  • wyłączamy działko i szybko przesuwamy działko z powrotem na lewą krawędź ekranu

  • włączamy działko z powrotem i wyświetlamy kolejną linię

  • przez cały proces przesuwamy powoli działko w dół

  • gdy dojdziemy do dołu ekranu, wyłączamy działko i przesuwamy je w górę, zaczynając cały proces od początku (trwa to o wiele dłużej niż powrót do lewej krawędzi ekranu)

Ponieważ monitory CRT są starą i bardzo analogową technologią, układ poziomy jest tak naprawdę niezależny od układu pionowego i oba działają bez przerwy – w trakcie powrotu działka na górę ekranu, układ poziomy cały czas jeździ między lewą a prawą krawędzią ekranu.

Monitory CRT sterowane przez złącze VGA mają następujące piny:

  • R, G, B – analogowa wartość aktualnie wyświetlanego piksela

  • HSYNC – cyfrowy sygnał powrotu do lewej krawędzi

  • VSYNC – cyfrowy sygnał powrotu do górnej krawędzi

Na płytce Basys2 mamy bieda-konwerter cyfrowo-analogowy, z 3 bitami precyzji na kolory czerwony i zielony oraz 2 bitami pracyzji na kolor niebieski.

Aby uzyskać obraz na monitorze CRT przez VGA należy wykonać następujące kroki:

  • wybrać jakąś rozdzielczość i częstotliwość odświeżania (http://martin.hinner.info/vga/timing.html, http://tinyvga.com/vga-timing)

  • uzyskać skądś (DCM) stosowny pixel clock

  • dla każdej wyświetlanej linii:

    • przez (active video) cykli wysyłać kolejne piksele obrazu, HSYNC=inactive

    • przez (front porch) cykli wysyłać czarny piksel, HSYNC=inactive (ten obszar jest „strefą buforową”, która może być zniekształcona przez monitor ze względu na bliskość sygnału powrotu poziomego)

    • przez (sync pulse) cykli wysyłać czarny piksel, HSYNC=active (działko wraca na lewą stronę)

    • przez (back porch) cykli wysyłać czarny piksel, HSYNC=inactive (kolejna strefa buforowa)

  • dla każdej wyświetlanej ramki:

    • przez (active video) linii wysyłać kolejne linie obrazu, VSYNC=inactive

    • przez (front porch) linii wysyłać same czarne piksele, VSYNC=inactive

    • przez (sync pulse) linii wysyłać same czarne piksele, VSYNC=active

    • przez (back porch) linii wysyłać same czarne piksele, VSYNC=inactive

  • wyświetlać ramki bez przerwy w pętli (trzeba poczekać kilkadziesiąt ramek aż monitor się zsynchronizuje)

  • pamiętać, że mapowanie wartości active i inactive na 0 i 1 zależy od wybranego trybu (i może być różne dla HSYNC i VSYNC)

Do tego, złącze VGA ma interfejs I2C do EEPROMu wbudowanego w monitor, w którym opisane są możliwości monitora (tzw. EDID). Niestety, Basys 2 nie pozwala nam dostać się do tych sygnałów.

Aby uzyskać obraz na monitorze LCD przez interfejs VGA, należy wspólnie z nim udawać, że jest on monitorem CRT.

DVI

DVI jest złączem zawierającym, oprócz sygnałów analogowych zgodnych z VGA, sygnał cyfrowy. Sygnał cyfrowy używa 4 par różnicowych (R, G, B, zegar), ma dokładnie 10× szybszy zegar niż pixel clock i przesyła dokładnie te same dane co sygnał analogowy (z 8 bitami na kolor, zakodowanymi przez kodowanie 8b10, ze specjalnymi symbolami zastępującymi sygnały HSYNC i VSYNC).

HDMI

HDMI to cyfrowa część DVI na (wstecznie zgodnych) sterydach – m.in. wysyła audio w nieużywanym obszarze danych w pionowym front/back porch. Ogólna struktura sygnału wciąż jest taka sama jak VGA.

Praktycznie każde złącze DVI we współczesnych kartach graficznych jest tak naprawdę źródłem sygnału HDMI.

PS/2

Interfejs PS/2 opiera się na dwóch sygnałach 5V open-drain:

  • DATA

  • CLK

Zegar interfejsu nie jest dokładnie wyspecyfikowany, ale powinien być w zakresie 10-16.7 kHz.

Komunikacja na interfejsie PS/2 jest sterowana przez urządzenie wejściowe (mysz/klawiaturę). Wysyłanie poleceń do urządzenia jest możliwe (np. polecenie zapal diody na klawiaturze), ale jest dość skomplikowane. Jeśli chcemy tylko odbierać dane:

  • nie wysyłamy nic na pinach CLK/DATA

  • dopóki CLK = 1, nic się nie dzieje

  • gdy CLK = 0, urządzenie transmituje bit – należy go złapać w momencie zmiany z 1 na 0

  • każdy bajt ma 11 bitów: - bit startowy, równy 0 (jeśli widzimy inny, zgubiliśmy synchronizację) - 8 bitów danych, od najniższego - bit parzystośći (1 jeśli parzysta liczba bitów danych jest równa 1) - bit stopu, równy 1

Klawiatura wysyła powyższym protokołem kody klawiszy (nie są w ASCII – są to tzw. scan codes i należy użyć tabelki https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_2). Niektóre z nich są 2-bajtowe (w tym wypadku pierwszy bajt to 0xe0). Przy naciśnięciu klawisza wysyłany jest jego kod, a przy puszczeniu klawisza wysyłany jest jego kod poprzedzony bajtem 0xf0.

Mysz wysyła powyższym protokołem 3-bajtowe pakiety opisujące ruch myszy:

  • bajt 0: - bit 0: stan lewego przycisku - bit 1: stan prawego przycisku - bit 2: stan środkowego przycisku - bit 3: zawsze równy 1 (jeśli widzimy inny stan, zgubiliśmy synchronizację pakietów) - bit 4: bit 8 przesunięcia w osi X - bit 5: bit 8 przesunięcia w osi Y - bit 6: jeśli 1, przesunięcie w osi X się nie zmieściło w 9 bitach - bit 7: jeśli 1, przesunięcie w osi Y się nie zmieściło w 9 bitach

  • bajt 1: bity 0-7 przesunięcia w osi X

  • bajt 2: bity 0-7 przesunięcia w osi Y

Klawiatura i mysz wysyłają również kod statusu po uruchomieniu (po którym można m.in. rozpoznać, z którym z nich mamy do czynienia). Niestety, nie zobaczymy go na naszej płytce (FPGA nie zdąży się skonfigurować zanim klawiatura/mysz wystartuje).

Po pełny opis interfejsu odsyłam do:

EPP

EPP (enhanced parallel port) był oryginalnie jednym z kilku trybów, w których mógł pracować stary port równoległy komputerów PC (zwany również IEEE 1284, LPT, bądź portem drukarki). Z jakiegoś powodu Digilent uznał za stosowne uczynić z niego interfejs używany na niektórych płytkach do komunikacji między komputerem a układem FPGA.

Piny interfejsu są następujące:

  • EppDB[7:0] – dwukierunkowa szyna danych

  • EppAstb – z komputera do FPGA, komputer żąda przesyłu adresu, gdy ten pin jest równy 0

  • EppDstb – z komputera do FPGA, komputer żąda przesyłu danych, gdy ten pin jest równy 0

  • EppWR – z komputera do FPGA, komputer pisza dane gdy równe 0, czyta gdy równe 1

  • EppWait – z FPGA do komputera, 1 oznacza zakończenie przesyłu

Protokół jest całkowicie sterowany przez komputer i działa następująco:

  1. Dopóki Epp*stb są równe 1, nic się nie dzieje. FPGA powinna nie wysyłać nic na EppDB i wysyłać 0 na EppWait.

  2. Gdy zauważymy 0 na EppAstb lub EppDstb, komputer żąda transakcji. Jeśli transakcja jest zapisem, powinniśmy złapać dane (bądź adres) z pinów EppDB, a jeśli jest odczytem, powinniśmy wysłać dane (bądź aktualny adres) na piny EppDB.

  3. Gdy wykonamy zapis bądź odczyt (i, w przypadku odczytu, zaczniemy wysyłać dane na EppDB), zaczynamy wysyłać 1 na linii EppWait.

  4. Czekamy aż EppAstb/EppDstb wróci do stanu 1.

  5. Jeśli to był odczyt, przestajemy wysyłać dane na EppDB.

  6. Przywracamy EppWait do stanu 0.

W standardowym użyciu portu EPP, komputer zawsze zapisuje adres, a potem zapisuje bądź odczytuje dane, pozwalając nam na realizację 256 8-bitowych rejestrów danych.

RS-232

RS-232, zwany również portem szeregowym lub UARTem, jest prostym interfejsem oryginalnie używanym do podłączania dalekopisów do komputerów, być może za pośrednictwem modemów i linii telefonicznych.

Ma następujące linie sygnałowe:

  • TxD (komputer do dalekopisu), RxD (dalekopis do komputera) – dane szeregowe

  • RTS (komputer do dalekopisu), CTS (dalekopis do komputera) – kontrola przepływu

  • DTR (komputer do dalekopisu), DSR (dalekopis do komputera) – sygnały gotowości do pracy

  • DCD (modem do komputera) – stan obecności sygnału

  • RI (modem do komputera) – przychodzące połączenie telefoniczne

Zazwyczaj używa się tylko TxD i RxD.

Standard RS-232 specyfikuje standard elektryczny następująco:

  • 0: przesyłane jako od +3V do +15V względem masy

  • 1: przesyłane jako od -3V do -15V względem masy

Jest to zbyt duże napięcie, by podłączyć je do FPGA czy większości mikrokontrolerów. Dlatego też zawsze używa się konwertera napięć (bądź, w przypadku komunikacji w ramach jednej płytki, w ogóle używa się tylko niskich napięć).

Na liniach danych (TxD i RxD) komunikacja przebiega następująco:

  1. Nadawca i odbiorca muszą używać tych samych parametrów przesyłu (standard nie przewiduje żadnej możliwości wykrycia ich).

  2. Komunikacja jest symetryczna (TxD wygląda tak samo jak RxD).

  3. Dopóki nic się nie dzieje, linia jest w stanie 1.

  4. Gdy któraś strona chce wysłać bajt:

    1. Wysyła przez jeden bit time stan 0 (bit startu).

    2. Wysyła bity danych (może ich być od 5 do 9, zazwyczaj jest ich 8).

    3. Być może wysyła bit parzystości. W zależności od konfiguracji, jest to zawsze bit 0, zawsze bit 1, XOR wszystkich bitów danych (even parity), bądź zanegowany XOR (odd parity).

    4. Wysyła 1, 1.5, bądź 2 bity stopu (zawsze równe 1).

    5. Utrzymuje stan 1 aż do momentu, gdy chce wysłać kolejny bajt.

Komunikacja jest asynchroniczna – odbiorca musi odtworzyć zegar przez zmierzenie, kiedy zaczął się bit startu i odliczanie odpowiedniego czasu do kolejnych bitów.