Wykład 5: Komunikacja ze światem zewnętrznym — UART¶
Data: 17.11.2020
Treść
Sygnały asynchroniczne¶
Załóżmy, że chcemy w naszym układzie synchronicznym obserwować stan sygnału zewnętrznego, który nie jest zsynchronizowany z naszym zegarem. Okazuje się, że jest to problematyczne.
Działanie przerzutników, metastabilność¶
Mowiliśmy, że przerzutnik zapisuje stan wejścia w momencie rosnącego zbocza zegara. Jest to jednak duże uproszczenie — w prawdziwym świecie wykonanie idealnego przerzutnika jest niemożliwe:
Prawdziwe przerzutniki nie obserwują stanu wejścia w jednym punkcie czasu, tylko w pewnym przedziale, w którym wejście nie powinno się zmieniać. Przedział ten zdefiniowany jest dwoma parametrami:
setup time: czas przed zboczem zegarowym od którego wejście powinno być stabilne
hold time: czas po zboczu zegarowym do którego wejście powinno być stabilne
Aby przerzutnik poprawnie zapisał stan wejścia w danym cyklu, wejście nie może zmieniać stanu w przedziale
(zbocze - setup, zbocze + hold)
.Zdarzają się (dość często) przerzutniki o zerowym bądź ujemnym czasie hold — pozwala to zagwarantować, że dane wysyłane synchronicznie z zegarem zostaną poprawnie zarejestrowane. Możliwe jest też wyprodukowanie przerzutników z zerowym bądź ujemnym czasem setup, choć rzadko się to zdarza. Nie istnieją jednak (i nie mogą istnieć) przerzutniki, w których oba czasy są zerowe bądź ujemne.
Prawdziwe sygnały danych nie zmieniają stanu z 0 na 1 (i na odwrót) natychmiast — w rzeczywistości, każdy sygnał jest analogowy i podczas tranzycji sygnał przyjmie każdą wartość pośrednią.
W przypadku naruszenia czasów hold/setup, bądź w przypadku podania na wejściu wartości pośredniej (pomiędzy 0 a 1), przerzutnik zapisze w sobie stan pośredni, nie będący ani zerem ani jedynką. Taki stan nie jest stabilny — prędzej czy później nastąpi jego „rozpad” albo do 0 albo do 1. Potrafi jednak trwać całkiem długo (tym dłużej, im bliżej stanu 0.5 jest zapisany stan). Z tego powodu taki stan nazywa się stanem metastabilnym, a zjawisko występowania takich stanów nazywa się metastabilnością.
Okazuje się, że nie istnieje żaden limit czasu na istnienie stanu metastabilnego — może on teoretycznie trwać dowolnie długo. Zachowuje się jednak jak rozpad promieniotwórczy — można określić jego czas półtrwania, a prawdopodobieństwo zostania w stanie metastabilnym spada wykładniczo z czasem (i bardzo szybko staje się pomijalnie małe).
Obsługa sygnałów asynchronicznych w praktyce — wejście¶
Aby poradzić sobie ze zjawiskiem metastabilności i nie pozwolić na przedostanie się stanów metastabilnych do naszego układu, należy na każdym asynchronicznym wejściu zastosować tzw. synchronizator — układ, który „zatrzyma” stan wejściowy odpowiednio długo, by ewentualne stany metastabilne zdążyły się rozpaść. Synchronizator składa się po prostu z kilku przerzutników połączonych szeregowo:
# UWAGA: nie używać; w prawdziwym kodzie należy użyć synchronizatora z biblioteki
# Synchronizator z 3 przerzutników.
stage1 = Signal()
stage2 = Signal()
sync_input = Signal()
m.d.sync += [
stage1.eq(async_input), # async_input jest sygnałem zewnętrznym
stage2.eq(stage1),
sync_input.eq(stage2),
]
# W dalszej części układu używamy sync_input.
Idea działania synchronizatora jest prosta: stan jest trzymany w każdym przerzutniku przez cały okres zegara, co prawie na pewno wystarczy, by ewentualny stan metastabilny zdążył się rozpaść zanim dotrze do ostatniego przerzutnika (którego wyjście jest już używane przez logikę synchroniczną).
Minimalny (i najczęściej stosowany) rozmiar synchronizatora to 2 przerzutniki. Gdy potrzebujemy dodatkowej pewności, stosuje się synchronizatory z 3-ma przerzutnikami. W praktyce to wystarcza, by prawdopodobieństwo wystąpienia metastabilności na wyjściu synchronizatora było mniejsze niż inne powody potencjalnego błędnego działania układu (promieniowanie kosmiczne itp).
W nMigen nie należy pisać synchronizatora samemu — zamiast tego, używamy gotowej implementacji z biblioteki:
from nmigen.lib.cdc import FFSynchronizer
# ...
sync_input = Signal()
m.submodules.my_input_synchronizer = FFSynchronizer(async_input, sync_input,
# opcjonalne parametry i ich wartości domyślne
o_domain='sync', # możemy zmienić, gdy chcemy synchronizować do innej domeny niż sync
reset=0, # wartość początkowa przerzutników w synchronizatorze
stages=2, # liczba przerzutników w synchronizatorze
)
Dzięki użyciu tej wersji, nMigen automatycznie dostosuje wygenerowany kod do docelowej platformy, emitując odpowiednie atrybuty np. wyłączające optymalizację dla tych przerzutników (która popsułaby działanie synchronizatora) czy wyłączające wejście pierwszego przerzutnika z analizy czasowej.
Należy zauważyć, że synchronizatory z natury opóźniają sygnał wejściowy o co najmniej dwa cykle — jest to główny powód, dla którego przechodzenie między domenami zegarowymi wprowadza duże opóźnienia. Niestety, nie da się tego uniknąć w przypadku asynchronicznych zegarów.
Obsługa sygnałów asynchronicznych w praktyce — wyjście¶
Gdy wyprowadzamy z naszego układu wyjście, które będzie obserwowane przez
asynchroniczny układ (pracujący w innej domenie zegarowej), również należy
uważać. Tym razem jedynym problemem są potencjalne glitche w kombinacyjnej
części naszego układu. Aby ich uniknąć, wystarczy zapewnić, że każde
asynchroniczne wyjście naszego układu jest bezpośrednim wyjściem przerzutnika
(czyli, w przypadku nMigen, nie jest przypisywane z domeny comb
).
Port szeregowy, czyli UART¶
RS-232, popularnie zwany portem szeregowym, jest jednym z najstarszych standardów komunikacji, które wciąż są w użyciu.
Standard ten oryginalnie powstał w roku 1960 w celu łączenia dalekopisów z komputerami mainframe za pośrednictwem modemów i sieci telefonicznej. Od tego czasu był wielokrotnie przystosowywany do najróżniejszych celów:
łączenie komputerów PC ze sobą (za pośrednictwem modemów bądź bezpośrednio)
podłączenie do komputerów myszy, drukarek, bądź innych urządzeń peryferyjnych
podłączenie terminali do urządzeń bez wyjścia graficznego, jak serwery czy routery
podłączenie interfejsów do debugowania i konfiguracji do rozmaitych urządzeń
Oryginalny standard RS-232 wykorzystywał złącze DB-25 (później zamiast niego zazwyczaj DB-9) i transmitował dane cyfrowo używając napięć +12V i -12V. Miał 8 sygnałów danych:
RxD (receive data): dane szeregowe z modemu do komputera
TxD (transmit data): dane szeregowe z komputera do modemu
RTS (request to send): kontrola przepływu z komputera do modemu
CTS (clear to send): kontrola przepływu z modemu do komputera
DTR (data terminal ready): sygnał gotowości z komputera do modemu
DSR (data set ready): sygnał gotowości z modemu do komputera
DCD (data carrier detect): sygnał gotowości połączenia telefonicznego, z modemu do komputera
RI (ring indicator): przychodzące połączenie telefoniczne, z modemu do komputera
Pierwsze dwa z tych sygnałów transmitują dane protokołem szeregowym, podczas gdy pozostałe są prostymi stanami logicznymi. Układ który potrafi odbierać i wysyłać dane szeregowe w tym formacie nazywa się UART (universal asynchronous receiver/transmitter).
Większość z tych sygnałów jest niepotrzebna gdy używamy portu szeregowego do innych zastosowań niż podłączenie modemu — w praktyce zazwyczaj używa się tylko sygnałów RxD i TxD, czasem dodając jeszcze RTS i CTS do kontroli przepływu.
We współczesnych czasach, fizyczny port szeregowy (ze złączem DB-9) w komputerach to rzadkość — jeśli takiego potrzebujemy, zazwyczaj musimy kupić konwerter na USB, które zazwyczaj są złej lub bardzo złej jakości. Często zdarzają się jednak układy, które mają interfejs logicznie taki sam jak RS-232, ale używający innych poziomów logicznych (zazwyczaj 0V i 5V lub 0V i 3.3V) i innych złączy (bądź bezpośrednio połączone ze sobą na płytce). Na wielu płytkach z FPGA możemy spotkać układ konwertujący z USB na UART (np. FTDI z rodziny FT232*), podłączony do FPGA interfejsem 3.3V, umożliwiający komunikację z komputerem.
Na płytce używanej na zajęciach (Pynq-Z2) mamy układ FT2232HL, wystawiający jednocześnie interfejsy UART i JTAG przez USB. Ten UART jest jednak połączony na płytce nie do FPGA, a do UARTa procesora ARM zawartego w Zynq. Na zajęciach możemy jednak używać interfejsu szeregowego w FPGA przez podłąćzenie go do drugiego UARTa procesora ARM przez wewnętrzny interfejs EMIO, nawiązując w ten sposób komunikację między procesorem ARM a naszym układem w FPGA.
Protokół szeregowy¶
RS-232 jest interfejsem asynchronicznym — nie zakłada wspólnego sygnału zegarowego między nadawcą a odbiorcą. Zakłada jednak uzgodnione wcześniej parametry po obu stronach oraz w miarę (±5%) dopasowaną szybkość transmisji i odbioru.
Parametry protokołu szeregowego są następujące:
szybkość transmisji, w bitach na sekundę; najczęściej używane wartości to 9600 (często spotykany default), 57600 (maksymalna szybkość jaką da się przesłać przez linię telefoniczną), 115200 (maksymalna szybkość na starych UARTach).
liczba bitów danych w bajcie: spotyka się od 5 do 9 bitów, najczęściej jest to 8
typ bitu parzystości:
N: none, brak (najczęściej używany)
E: even, bit parzystości ustawiony tak, by razem z bitami danych było parzyście wiele jedynek
O: odd, bit parzystości ustawiony tak, by razem z bitami danych było nieparzyście wiele jedynek
M: mark, bit parzystości zawsze równy 1
S: space, bit parzystości zawsze równy 0
ilość bitów stopu: 1, 1.5, bądź 2 bity (najczęściej jest to 1)
Protokół działa następująco:
gdy nic nie jest przesyłane, linia przesyłu ma stan logiczny 1
gdy nadawca chce przesłać bajt, wysyła następujące bity (przesył każdego z nich trwa
(1 / bity na sekundę)
sekund):1 (jeden) bit startu: ma zawsze stan logiczny 0
bity danych, od najniższego bitu
bit parzystości, jeśli jest w użyciu
bit lub bity stopu: mają zawsze stan logiczny 1
po przesłaniu bajtu, nadawca może natychmiast rozpocząć transmisję kolejnego bajtu, bądź wrócić do stanu nieaktywnego przez dalsze utrzymywanie linii w stanie logicznym 1
W tym protokole bit startu służy za synchronizację — gdy odbiorca zobaczy, że
wcześniej nieaktywna linia zmieniła stan logiczny z 1 na 0, rozpoczyna odbiór
danych i kalibruje swój zegar odbiorczy tak, by każdy bit próbkować w środku
przedziału w którym powinien być przesłany ((idx + 0.5) * czas trwania bitu
od początku bitu startu). Bit (lub bity) stopu służą natomiast za padding
między bajtami (by odbiorca miał czas wrócić do stanu nieaktywnego i zauważyć
kolejną zmianę z 1 na 0 sygnalizującą kolejny bit startu).
O ile implementacja nadawcy jest dość oczywista i nie zawiera wiele pola na twórczość, implementacja odbiorcy to bardziej ciekawa kwestia — można napisać układy odbiorcze różniące się funkcjami wykrywania i obsługi błędów protokołu czy wykrywania niedopasowanych parametrów transmisji.