.. _w05-uart:


===================================================
Wykład 5: Komunikacja ze światem zewnętrznym — UART
===================================================

Data: 17.11.2020

.. toctree::

.. contents::


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:

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

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