Trochę teorii
Zacznijmy od omówienia, jak w ogóle działa interfejs UART. Daje on możliwość połączenia ze sobą dwóch urządzeń, tak by były równorzędne. Oba urządzenia wyposażone są w nadajnik i odbiornik. Pin nadajnika zwyczajowo oznacza się symbolem Tx, natomiast pin odbiornika nazywa się Rx. Wyjście Tx jednego układu połączone jest z wejściem Rx drugiego, a więc w standardzie UART linie transmisyjne krzyżują się w charakterystyczny sposób. Jeżeli komunikacja ma być jednokierunkowa, wystarczy tylko jedna linia transmisyjna. Oba urządzenia muszą mieć wspólną masę.
Istnieje możliwość dodania jeszcze kolejnych sygnałów odpowiedzialnych za tzw. hardware handshake, czyli informacje o tym, że nadajnik ma jakieś dane do wysłania, że odbiornik może je odebrać itp. Jednak nie będziemy wchodzić w szczegóły tej funkcjonalności, bo współcześnie są one już tylko zaszłością historyczną.
W omawianym interfejsie nie ma podziału na master i slave, z którym spotkamy się w przypadku I²C czy SPI. Każde urządzenie może rozpocząć nadawanie w dowolnej chwili, a odbiornik w drugim urządzeniu musi te dane odebrać i ewentualnie zapisać do bufora. Komunikacja w obie strony może zachodzić niezależnie od siebie. Możliwa jest także transmisja w obu kierunkach w tym samym czasie, czyli komunikacja full-duplex.
Interfejs UART, jak sama nazwa wskazuje, jest asynchroniczny. Oznacza to, że pomiędzy urządzeniami nie ma żadnego sygnału zegarowego. Zarówno nadajnik, jak i odbiornik muszą mieć swoje własne zegary, które synchronizują ze sobą na początku każdej ramki transmisyjnej. Ponadto muszą być one odpowiednio dokładne, aby transmisja przebiegała prawidłowo.
W stanie spoczynkowym linie transmisyjne pozostają w stanie wysokim. Ramka danych rozpoczyna się bitem startu, który zawsze jest reprezentowany przez stan niski – to sygnał dla odbiornika, że ma uruchomić swój zegar i rozpocząć próbkowanie linii transmisyjnej.
Następnie nadawane są bity danych, w kolejności od najmłodszego do najstarszego. Najczęściej mamy do czynienia z transmisją 8-bitową. Czasami wykorzystuje się ramki 9-bitowe. Standard przewiduje także możliwość 7-, 6- czy nawet 5-bitowych, ale możliwość ta raczej nie ma praktycznego zastosowania. Dane mogą mieć dodatkowo bit parzystości lub nieparzystości, którego celem jest weryfikacja poprawności transmisji. Często jednak pomija się go, ponieważ taka metoda kontroli jest mało skuteczna, a obecność dodatkowego bitu sprawia, że transmisja zajmuje więcej czasu.
Każda ramka kończy się bitem stopu, który zawsze ma stan wysoki. Opcjonalnie można ustawić dwa bity stopu (jeszcze nigdy nie spotkałem się jednak z sytuacją, w której ktoś faktycznie stosowałby takie rozwiązanie). Jeżeli jest to ostatnia ramka, wówczas linia pozostaje w stanie wysokim, a jeżeli nie, to następuje zbocze opadające i mamy kolejny bit startu (stan niski). Zwróć uwagę, że przesyłając dane 8-bitowe, musimy w rzeczywistości przesłać 10 bitów.
Istnieje dość sporo standardowych prędkości transmisji (baud rate), wyrażonych w bitach na sekundę. Niestety, wartości te są „dziwne”. W rezultacie czas trwania pojedynczego bitu jest niewymierny – nie sposób osiągnąć dokładnie takich przedziałów czasu, dzieląc jakąś typową częstotliwość kwarcu, jak np. 10 MHz, 20 MHz, 25 MHz, przez liczbę całkowitą. Trzeba zastosować dzielnik frakcjonalny, kwarc o nietypowej częstotliwości lub... pogodzić się z faktem, że uzyskany timing będzie trochę niedokładny. W naszym przykładzie zadowolimy się trzecią opcją.
Podczas ćwiczeń z tego odcinka kursu opracujemy moduł, który – po naciśnięciu przycisku – wyśle przez UART prosty komunikat tekstowy „Hello”. Moduł nadawczy będzie miał możliwość konfiguracji jedynie prędkości transmisji. Długość ramki danych ustawimy na 8 bitów, bez możliwości zmiany. Nie będziemy stosować bitów parzystości, a na końcu ramki transmisyjnej znajdzie się jeden bit stopu. Jest to najczęściej stosowana konfiguracja.
Moduł StrobeGeneratorTicks
Do realizacji nadajnika i odbiornika potrzebujemy modułu, który będzie precyzyjnie wyznaczał odstępy czasu. Wielokrotnie w tym kursie stosowaliśmy moduł StrobeGenerator, który ustawiał stan wysoki na swoim wyjściu co pewien czas, określony w mikrosekundach. Taka rozdzielczość okazuje się jednak niewystarczająca na potrzeby komunikacji przez UART.
Zapewne większość Czytelników pomyśli, że aby rozwiązać ten problem, należy zmienić rozdzielczość z mikrosekund na nanosekundy. Owszem, jest to prawidłowe rozwiązanie, jednak wywołuje dość nieoczekiwany problem w Lattice Synthesis Engine. W jednej sekundzie jest miliard nanosekund, częstotliwość zegara wyrażona jest w milionach herców, a żądane odstępy czasu mogą być bardzo małe lub bardzo duże. Lattice Synthesis Engine obsługuje obliczenia na liczbach 32-bitowych ze znakiem, co oznacza, że największa liczba, jaką jest w stanie przetworzyć, to 2.147.483.647. Głównym problemem okazuje się fakt, że w przypadku przekroczenia maksymalnej wartości syntezator nie zgłosi żadnego błędu!
Nadmiarowa część liczby zostaje obcięta i wynik traktowany jest nadal jako zwykła zmienna 32-bitowa, co prowadzi do nieprawidłowego działania układu. Trzeba przyznać, że jest to dość poważny błąd w Lattice Diamond… podczas gdy w Icarus Verilog obliczenia na dużych liczbach działają prawidłowo.
Obejdziemy problem dookoła, tworząc moduł podobny do StrobeGenerator, który już znamy. Różnić będzie się tylko tym, że zamiast czasu w mikrosekundach, podawać będziemy liczbę taktów zegarowych, jaka ma mijać między impulsami stanu wysokiego na wyjściu Strobe_o. Przeanalizujmy kod pokazany na listingu 1.
Przejdźmy do jedynego bloku always w tym module. W momencie resetu układu ładujemy licznik Counter jego wartością maksymalną (linia 5), a wyjście Strobe_o, które jest typu reg, ustawiamy w stan niski.
Dalsza praca układu warunkowana jest wejściem Enable_i, sprawdzanym w linii 6. Jeżeli pozostaje ono w stanie niskim, to moduł nie pracuje i licznik Counter ładowany jest wartością początkową (linia 12). Jeżeli natomiast to wejście jest w stanie wysokim, to moduł pracuje.
W linii 7 sprawdzamy, czy licznik Counter osiągnął już wartość zerową. Używamy w tym celu operatora negacji logicznej. Jeśli wartość zerowa została osiągnięta, ponownie ładujemy licznik wartością maksymalną, aby rozpocząć kolejny cykl pracy, a wyjście Strobe_o ustawiamy w stan wysoki. Jeżeli natomiast Counter nie ma wartości zerowej, wykonywane są linie 10 i 11, tzn. pomniejszamy stan licznika o jeden i ustawiamy wyjście Strobe_o w stan niski.
Przeprowadźmy na szybko symulację modułu StrobeGeneratorTicks. Nie będziemy tutaj prezentować kodu testbenchu, ponieważ wygląda banalnie (https://github.com/leonow32/verilog-fpga). Widzimy, że po zmianie stanu wejścia Enable_i z 0 na 1 licznik pomniejsza swoją wartość wraz z każdym taktem sygnału zegarowego. Na wyjściu Strobe_o widzimy krótkie szpilki stanu wysokiego, które pojawiają się co 10 taktów zegara.