Kurs FPGA Lattice (19). Odbiornik UART

Kurs FPGA Lattice (19). Odbiornik UART

W poprzednim odcinku nauczyliśmy się, jak wykonać nadajnik interfejsu UART. Czas dowiedzieć się, jak zbudować odbiornik, aby opanować komunikację dwukierunkową. Modułów opracowanych w tej i poprzedniej części będziemy używać również w kolejnych odcinkach kursu.

Informacje: co to jest UART, jak działa i jakie są jego możliwości – zostały podane w 18 części kursu. Jeżeli jej nie czytałeś, gorąco zachęcam, by zapoznać się z nią przed przystąpieniem do dzisiejszego odcinka.

Trochę teorii

Omówmy najpierw, w jaki sposób funkcjonuje odbiornik UART. Zobacz rysunek 1. Na zielono zaznaczono sygnał na wyjściu odbiornika. W stanie spoczynkowym linia transmisyjna ma stan wysoki.

Transmisja rozpoczyna się bitem startu, który zawsze ma stan niski. Wywołuje on zbocze opadające, a zjawisko takie ma za zadanie poinformować odbiornik, że zaczyna się ramka transmisyjna i za chwilę zostaną przetransmitowane bity danych. Zaznaczono to czerwoną strzałką. W jaki sposób będziemy wykrywać zbocze opadające? Zastosujemy tutaj moduł EdgeDetector, z którego korzystaliśmy już wielokrotnie w poprzednich odcinkach kursu.

Sygnał z wykrywacza zbocza uruchomi moduł StrobeGeneratorTicks, opracowany w poprzednim odcinku na potrzeby nadajnika UART. Jego zadanie polega na okresowym wyznaczaniu impulsów strobe, które powodować będą odczytywanie wejścia linii transmisyjnej. Następnie stan wejścia będzie sukcesywnie zapisywany do stanu rejestru wyjściowego, skąd będzie można odczytać odebrany bajt danych.

Rysunek 1. Próbkowanie poprawnie zsynchronizowane

Spójrz jeszcze raz na rysunek 1. Próbkowanie zaznaczono czarnymi strzałkami. W idealnej sytuacji badanie stanu wejścia następuje dokładnie w połowie trwania każdego bitu. Odstęp czasowy pomiędzy pobraniem poszczególnych próbek jest równy czasowi trwania każdego bitu. Jednak odstęp pomiędzy zboczem opadającym na początku bitu startu a pobraniem pierwszej próbki to czas trwania półtora bitu. Zatem proces próbkowania musimy opóźnić o pół bitu. Jak to zrobić? Możliwe są dwa rozwiązania:

  1. Po wykryciu zbocza opadającego na linii transmisyjnej uruchamia się jednorazowo pierwszy timer, którego celem jest czekanie przez pół bitu. Następnie uruchamia on drugi timer, który odmierza odstępy czasu równe jednemu bitowi i pracuje do końca ramki transmisyjnej.
  2. Po wykryciu zbocza opadającego na linii transmisyjnej uruchamia się timer, który pracuje przez cały czas trwania transmisji. Generuje on sygnały co pół bitu, lecz w czasie wysyłania bitów danych co drugi z tych sygnałów jest ignorowany.

W naszym module odbiornika zastosujemy rozwiązanie drugie.

Kiedy już uzbieramy osiem próbek, odpowiadających ośmiu bitom, możemy zakończyć pracę modułu i zgłosić sygnał informujący o tym, że na wyjściu modułu odbiornika znajdują się dane gotowe do odczytania.

A co z bitem stopu? W gruncie rzeczy nie jest on nam do niczego potrzebny. Nie będziemy sprawdzać jego stanu ani go zapisywać (zawsze ma stan wysoki). Interesować nas będzie już tylko wykrycie zbocza opadającego, ponieważ zasygnalizuje ono rozpoczęcie transmisji kolejnej ramki danych.

Bardzo ważne jest, aby zegary nadajnika i odbiornika były dobrze zsynchronizowane. Rysunek 2 ukazuje sytuację, w której próbkowanie następuje szybciej, niż powinno. W rezultacie bit czwarty próbkowany jest dwa razy, a bitu siódmego nie odczytujemy wcale. Prowadzi to do odebrania błędnych danych.

Rysunek 2. Próbkowanie zbyt szybkie

Rysunek 3 obrazuje odwrotną sytuację. Próbkowanie odbywa się zbyt wolno, przez co nadajnik kończy transmisję dużo wcześniej, niż odbiornik kończy odbieranie. W przykładowej sytuacji bit piąty nie jest odczytywany wcale, a bit stopu traktowany jest jako najstarszy bit danych.

Rysunek 3. Próbkowanie zbyt wolne

Moduł UartRx

Przejdźmy teraz do omówienia kodu odbiornika UART, który pokazano na listingu 1.

// Plik uart_rx.v

`default_nettype none
module UartRx #(
parameter CLOCK_HZ = 10_000_000,
parameter BAUD = 115200 // 1
)(
input wire Clock,
input wire Reset,
input wire Rx_i,
output reg Done_o,
output reg [7:0] Data_o
);

// Zmienne
reg Busy; // 2
reg [8:0] RxBuffer; // 3
reg [4:0] Counter; // 4

// Synchronizacja wejścia Rx z domeną zegarową
wire RxSync; // 5

Synchronizer Synchronizer_Rx( // 6
.Clock(Clock),
.Reset(Reset),
.Async_i(Rx_i), // 7
.Sync_o(RxSync) // 8
);

// Rozpoznawanie początku ramki transmisyjnej
// Wykrywanie bitu startu, który zawsze ma poziom 0
wire RxFallingEdge; // 9

EdgeDetector EdgeDetector_inst( // 10
.Clock(Clock),
.Reset(Reset),
.Signal_i(RxSync), // 11
.RisingEdge_o(),
.FallingEdge_o(RxFallingEdge) // 12
);


// Timing
wire Strobe; // 13
localparam TICKS_PER_HALF_BIT = CLOCK_HZ / (BAUD * 2); // 14

StrobeGeneratorTicks #( // 15
.TICKS(TICKS_PER_HALF_BIT)
) StrobeGeneratorTicks_inst(
.Clock(Clock),
.Reset(Reset),
.Enable_i(Busy || !RxSync), // 16
.Strobe_o(Strobe) // 17
);

wire SampleEnable = Strobe && !Counter[0]; // 18

always @(posedge Clock, negedge Reset) begin
if(!Reset) begin // 19
Busy <= 0;
Counter <= 0;
Data_o <= 0;
Done_o <= 0;
RxBuffer <= 0;
end else begin

// Stan bezczynności
if(!Busy) begin // 20
if(RxFallingEdge) begin // 21
Counter <= 5’d0;
Busy <= 1’b1;
end

Done_o <= 1’b0; // 22
end

// Transmisja w trakcie
else begin
if(SampleEnable) begin // 23
RxBuffer <= {RxSync, RxBuffer[8:1]}; // 24
end

if(Counter == 5’d17) begin // 25
Data_o <= RxBuffer[8:1];
Done_o <= 1’b1;
Busy <= 1’b0;
end

if(Strobe) begin // 26
Counter <= Counter + 1’b1;
end
end
end
end

endmodule
`default_nettype wire

Listing 1. Kod pliku uart_rx.v

Moduł odbiornika konfigurujemy za pomocą dwóch parametrów, podobnie jak moduł nadajnika. Oprócz częstotliwości zegara, zdefiniowanej parametrem CLOCK_HZ, mamy także szybkość transmisji danych w bitach na sekundę, ustawianą parametrem BAUD (linia 1).

Moduł wyposażono w następujące porty wejściowe i wyjściowe:

  • Clock – wejście zegara,
  • Reset – wejście resetujące,
  • Rx_i – wejście odbiornika, należy ten port połączyć z pinem układu FPGA,
  • Done_o – wyjście informujące o odebraniu bajtu danych poprzez ustawienie stanu wysokiego na jeden takt zegarowy,
  • Data_o – 8-bitowe wyjście odebranych danych.

Na początku mamy kilka zmiennych. W linii 2 tworzymy zmienną Busy. Stan wysoki tej zmiennej informuje, że moduł jest w trakcie odbierania danych. 9-bitowy rejestr RxBuffer (linia 3) posłuży do tymczasowego przechowywania odebranych bitów, zanim zostaną skopiowane na port wyjściowy po zakończeniu transmisji. Licznik Counter (linia 4) zliczać będzie impulsy generowane co pół bitu przez moduł StrobeGeneratorTicks. Licznik ten będzie liczyć od 0 do 17, co wymaga 5-bitowej rozdzielczości.

Zacznijmy od zsynchronizowania wejścia odbiornika z domeną zegarową. Należy pamiętać, że sygnały pochodzące z pinów FPGA mogą wywoływać stany metastabilne, jeżeli nie zostaną zsynchronizowane (ten temat szczegółowo omówiliśmy w 11 odcinku kursu).

W linii 6 tworzymy instancję modułu Synchronizer. Do jego wejścia doprowadzamy wejście Rx_i (linia 7), a wyjście wyprowadzamy (linia 8) za pomocą zmiennej wire RxSync, utworzonej w linii 5. Stan zmiennej RxSync jest taki sam, jak na wejściu Rx_i, ale z tą różnicą, że wszystkie zmiany stanu są zsynchronizowane z zegarem Clock. Zmienna RxSync będzie wielokrotnie używana w dalszej części kodu.

Następnie utworzymy moduł wykrywający zbocze opadające na zsynchronizowanym sygnale RxSync. Aby to uczynić, w linii 10 tworzymy instancję modułu EdgeDetector, który również znamy z poprzednich odcinków kursu. Jego wejście łączymy z RxSync (linia 11), a wyjście zostanie wyprowadzone (linia 12) za pomocą zmiennej RxFallingEdge typu wire, utworzonej w linii 9. Po każdym wystąpieniu zbocza opadającego na linii Rx, na zmiennej RxFallingEdge pojawi się krótka szpilka stanu wysokiego o długości jednego cyklu zegarowego.

Kolejnym modułem będzie generator impulsów okresowych – niezbędny, by móc odczytywać stan wejścia linii transmisyjnej w ściśle określonych odstępach czasu. Użyjemy tutaj modułu StrobeGeneratorTicks, którego instancję tworzymy w linii 15. Moduł ten konfigurujemy za pomocą parametru TICKS_PER_HALF_BIT (linia 14), określającego liczbę taktów zegarowych upływających pomiędzy połówkami każdego bitu ramki transmisyjnej. Wyjście modułu (linia 17) stosuje zmienną Strobe typu wire, utworzoną w linii 13.

Przyjrzyjmy się bliżej wejściu Enable_i. Moduł pracuje tylko wtedy, kiedy na tym wejściu mamy stan wysoki. Łączymy je z dwoma sygnałami, połączonymi ze sobą bramką OR (linia 16). Pierwszy ze wspomnianych sygnałów to Busy, ponieważ moduł StrobeGeneratorTicks ma pracować, kiedy trwa odbieranie.

Drugi to zanegowany sygnał RxSync – linia Busy ustawiana jest bowiem w stan wysoki dopiero po wykryciu zbocza opadającego na wejściu Rx. Dzięki uruchomieniu modułu StrobeGeneratorTicks, natychmiast po wystąpieniu stanu niskiego na Rx, pierwszy impuls będzie bliżej środka bitu startu (w przeciwnym razie byłby nieco przesunięty).

Jak już wcześniej pisałem, licznik Counter będziemy inkrementować co połowę czasu trwania transmitowanego bitu, ale próbki odczytywać musimy co cały bit. Aby ułatwić sobie to zadanie, w linii 18 tworzymy zmienną SampleEnable typu wire, przyjmującą stan wysoki na czas trwania jednego taktu zegarowego, co oznaczać będzie, że w kolejnym takcie ma zostać odczytane wejście odbiornika. W tym celu za pomocą bramki AND łączymy ze sobą dwa sygnały. Pierwszy to Strobe, pochodzący z wyjścia StrobeGeneratorTicks, który ustawiany jest w stan wysoki na jeden takt zegarowy co pół bitu. Drugi – to zanegowany zerowy bit licznika Counter. Korzystamy tutaj z faktu, że najmłodszy bit licznika po każdej inkrementacji zmienia swój stan na przeciwny. W taki sposób SampleEnable będzie przyjmować stan wysoki, gdy jednocześnie w stanie wysokim jest Strobe i kiedy licznik Counter ma wartość parzystą.

Przejdźmy dalej, do jedynego bloku always w module odbiornika, będącego blokiem sekwencyjnym reagującym na zbocze rosnące zegara oraz zbocze opadające sygnału resetującego. Jak zawsze, w pierwszej kolejności sprawdzamy stan Reset – jeżeli jest on niski, to zerujemy wszystkie zmienne typu reg (linia 19).

Logika bloku always dzieli się na dwie części, w zależności od tego, czy moduł znajduje się w stanie bezczynności, tzn. oczekuje na zbocze opadające bitu startu, czy transmisja jest w trakcie. Odróżniamy te dwa stany, sprawdzając zmienną Busy (linia 20).

W stanie bezczynności badamy sygnał RxFallingEdge, sterowany przez detektor zbocza opadającego (linia 21). Po jego wykryciu zerujemy licznik Counter i ustawiamy Busy w stan wysoki.

Niezależnie od wszystkiego, w tym samym czasie zerujemy zmienną Done_o (linia 22). Zmienna ta jest ustawiana w stan wysoki po zakończeniu odbierania bajtu, kiedy Busy zmienia się z 1 na 0, więc musimy ją wyzerować.

Następnie, podczas odbierania bajtów, wykonywane są trzy instrukcje if. Pierwsza z nich sprawdza (linia 23), czy w bieżącym takcie zegarowym należy odczytać wejście odbiornika. W takiej sytuacji do 9-bitowego rejestru RxBuffer wpisujemy wartość powstałą ze sklejenia zmiennej RxSync i dotychczasowych ośmiu bitów RxBuffer (linia 24). Inaczej mówiąc, rejestr ten przesuwamy w prawo o jeden bit, a w miejsce najstarszego bitu wstawiamy wartość RxSync, czyli najnowszy odczytany bit z wejścia odbiornika.

Następnie sprawdzamy, czy aktualna wartość licznika Counter wynosi 17 (linia 25). Jeżeli tak – to znaczy, że odebraliśmy już wszystkie bity z ramki danych. Kopiujemy je zatem do wyjścia Data_o, ustawiamy Done_o w stan wysoki oraz zerujemy Busy.

W ostatnim warunku sprawdzamy, czy sygnał Strobe pochodzący z wyjścia modułu StrobeGeneratorTicks jest w stanie wysokim. Jeżeli tak, to inkrementujemy licznik.

Zwróć uwagę, że nie ma tu żadnego else – zatem wszystkie trzy instrukcje warunkowe wykonują się jednocześnie w każdym takcie zegarowym. Ponadto moglibyśmy opisane bloki warunkowe pozamieniać miejscami i nie miałoby to żadnego wpływu na syntezowaną logikę.

Testbench modułu UartRx

Zanim wgramy nasz kod do FPGA, przetestujemy go w symulatorze Icarus Verilog. Kod testbencha pokazano na listingu 2.

// Plik uart_rx_tb.v

`timescale 1ns/1ns
`default_nettype none
module UartRx_tb();

parameter CLOCK_HZ = 1_000_000;
parameter real HALF_PERIOD_NS = 1_000_000_000.0 / (2 * CLOCK_HZ);
// Generator sygnału zegarowego
reg Clock = 1’b1;
always begin
#HALF_PERIOD_NS;
Clock = !Clock;
end

// Zmienne
reg Reset = 1’b0;

reg [7:0] TxData;
wire TxDone;
reg TxRequest = 1’b0;

wire [7:0] RxData;
wire RxDone;

wire TxRxCommon;

// Nadajnik UART
UartTx #(
.CLOCK_HZ(CLOCK_HZ),
.BAUD(100_000)
) UartTx_Inst(
.Clock(Clock),
.Reset(Reset),
.Start_i(TxRequest), // 1
.Data_i(TxData), // 2
.Busy_o(),
.Done_o(TxDone), // 3
.Tx_o(TxRxCommon) // 4
);

// Odbiornik UART
UartRx #(
.CLOCK_HZ(CLOCK_HZ),
.BAUD(100_000)
) UartRx_Inst(
.Clock(Clock),
.Reset(Reset),
.Rx_i(TxRxCommon), // 5
.Done_o(RxDone), // 6
.Data_o(RxData) // 7
);

// Eksport wyników symulacji
initial begin
$dumpfile("uart_rx.vcd");
$dumpvars(0, UartRx_tb);
end

// Sekwencja testowa
initial begin
$timeformat(-6, 3, "us", 12);
$display("===== START =====");
$display("Ticks per half bit = %0d",
UartRx_Inst.TICKS_PER_HALF_BIT);

@(posedge Clock);
Reset <= 1’b1;
repeat(99) @(posedge Clock); // 8

// Wysyłanie pierwszego bajtu
TxData <= 8’hAB; // 9
TxRequest <= 1’b1; // 10
@(posedge Clock); // 11
TxData <= 8’bxxxxxxxx; // 12
TxRequest <= 1’b0; // 13

// Wysyłanie drugiego bajtu
@(posedge TxDone); // 14
TxData <= 8’hCD;
TxRequest <= 1’b1;
@(posedge Clock);
TxData <= 8’bxxxxxxxx;
TxRequest <= 1’b0;

// Czekaj na zakończenie transmisji
@(posedge TxDone); // 15
repeat(100) @(posedge Clock); // 16

$display("====== END ======");
$finish;
end

// Wyświetlanie wysyłanego bajtu na początku transmisji
always begin
@(posedge TxRequest) // 17
$display("%t Transmitting byte: %H %s",
$realtime,
UartTx_Inst.Data_i,
UartTx_Inst.Data_i
);
end

// Wyświetlanie odebranego bajtu po zakończeniu transmisji
always begin
@(posedge RxDone) // 18
$display("%t Received byte: %H %s",
$realtime,
UartRx_Inst.Data_o,
UartRx_Inst.Data_o
);
end

endmodule
`default_nettype wire

Listing 2. Kod pliku uart_rx_tb.v

Podczas symulacji zastosujemy moduł nadajnika UartTx, który opracowaliśmy w poprzednim odcinku kursu. Moduł ten wyemituje dwa bajty 0xAB i 0xCD, a moduł odbiornika będzie miał odebrać je i wyświetlić na konsoli symulatora. Prędkość transmisji w nadajniku i odbiorniku zostanie ustawiona na 100000 bitów na sekundę. Dzięki temu każda ramka transmisyjna będzie trwała 100 μs. Zegar symulacji ustawimy na 1 MHz, aby można było łatwo zaobserwować sygnały ustawiane w stan wysoki na jeden takt sygnału zegarowego. Transmisja całej ramki potrwa 100 taktów zegarowych.

Zacznijmy od omówienia zmiennych używanych podczas symulacji:

  • TxData[7:0] – bajt danych, który ma zostać wysłany, doprowadzony jest do wejścia Data_i nadajnika (linia 2).
  • TxRequest – ustawienie tej zmiennej w stan wysoki na jeden takt zegarowy spowoduje rozpoczęcie wysyłania danych; połączona jest ona z wejściem Start_i nadajnika (linia 1).
  • TxDone – sygnał sterowany przez nadajnik z wyjścia Done_o (linia 3), informujący o tym, że nadawanie zostało zakończone.
  • RxData[7:0] – bajt danych odebrany przez odbiornik i dostępny na wyjściu Data_o odbiornika (linia 7).
  • RxDone – sygnał sterowany przez odbiornik z wyjścia Done_o (linia 6), informujący o tym, że odbieranie zostało zakończone.
  • TxRxCommon – sygnał łączący wyjście Tx_o nadajnika (linia 4) z wejściem Rx_i odbiornika (linia 5).

Następnie widzimy instancje modułów nadajnika UartTx i odbiornika UartRx. Nie znalazło się tu nic zaskakującego, zatem nie wymagają one komentarza.

Przejdźmy do sekwencji testowej. Rozpoczynamy ją od ustawienia zmiennej Reset w stan wysoki po wystąpieniu zbocza rosnącego sygnału zegarowego, po czym czekamy kolejnych 99 taktów zegarowych (czyli razem 100 taktów (linia 8)), co trwać będzie dokładnie 100 μs. Działanie takie ma na celu utworzenie odstępu między krawędzią ekranu symulatora a rozpoczęciem transmisji pierwszego bajtu, a każdy bajt również potrwa 100 μs.

Aby rozpocząć transmitowanie bajtu, wartość żądaną do przesłania wpisujemy do zmiennej TxData (linia 9), a TxRequest ustawiamy w stan wysoki (linia 10). Czekamy na kolejne zbocze zegara (linia 11). W tym momencie nadajnik odczytuje dane z wejścia i rozpoczyna pracę. Do czasu zakończenia transmisji stan jego wejścia danych jest nieistotny – dlatego ustawiamy je w stan nieokreślony w linii 12, a zmienną TxRequest zerujemy (linia 13).

Transmisja jest w toku. Zawieszamy więc wykonywanie sekwencji testowej tak długo, aż zostanie ustawiony sygnał TxDone w stan wysoki (linia 14). Drugi testowy bajt wysyłany jest w dokładnie taki sam sposób, zatem nie wymaga komentarza.

Czekamy na zakończenie transmisji (linia 15) i odliczamy 100 taktów zegarowych (16).

Dodatkowo widzimy dwa bloki always przeznaczone do wyświetlania komunikatów na konsoli symulatora. Oba wykonują się równolegle i jednocześnie z sekwencją testową. W linii 17 oczekujemy na zbocze rosnące sygnału TxRequest (co oznacza żądanie wysyłania bajtu), po czym wyświetlamy na konsoli symulatora informacje o aktualnym czasie oraz zawartości wysyłanego bajtu w postaci szesnastkowej i ASCII.

W linii 18 mamy do czynienia z bardzo podobną logiką, która różni się tylko tym, że wyświetla odebrane bajty, o czym świadczy stan wysoki na RxDone.

Aby przeprowadzić symulację, uruchamiamy skrypt uart_rx.bat, którego kod zaprezentowano na listingu 3.

@echo off
iverilog -o uart_rx.o uart_rx.v uart_rx_tb.v uart_tx.v strobe_generator_ticks.v edge_detector.v synchronizer.v
vvp uart_rx.o
del uart_rx.o

Listing 3. Kod skryptu uart_rx.bat

Po wykonaniu symulacji powinniśmy zobaczyć komunikaty widoczne na listingu 4.

VCD info: dumpfile uart_rx.vcd opened for output.
===== START =====
Ticks per half bit = 5
100.000us Transmitting byte: ab «
190.000us Received byte: ab «
200.000us Transmitting byte: cd Í
290.000us Received byte: cd Í
====== END ======
uart_rx_tb.v:93: $finish called at 400000 (1ns)

Listing 4. Wynik symulacji na konsoli

Otwórzmy plik uart_rx.vcd za pomocą przeglądarki GTKWave i skonfigurujmy ją tak, by uzyskać rezultat widoczny na rysunku 4. Sygnały dotyczące nadajnika i odbiornika są celowo oznaczone czerwonymi etykietami Transmitter i Receiver.

Rysunek 4. Przebiegi uzyskane podczas symulacji

Przybliżmy widok, aby lepiej widzieć transmisję pierwszego bajtu (zobacz rysunek 5).

Rysunek 5. Zbliżenie na pierwszy transmitowany bajt

Wszystko zaczyna się od ustawienia wartości 0xAB na wejściu Data_i nadajnika i ustawienia jego wejścia Start_i w stan wysoki (na rysunku 5 zostało to zaznaczone pomarańczowym kursorem pionowym). W następnym cyklu zegarowym bajt do nadania skopiowany zostaje do ByteCopy. Wciąż trwa wysyłanie poszczególnych bitów (co widzimy na Tx_o), a każdy z nich trwa 10 μs.

Te same bity trafiają na wejście Rx_i odbiornika. Sygnał ten przechodzi przez synchronizator, który wprowadza opóźnienie o dwa takty zegarowe – przypatrz się, w jaki sposób przesunięty zostaje sygnał RxSync względem Rx_i. Następnie wykrywane jest zbocze opadające bitu startu, co widać jako krótką szpilkę stanu wysokiego na RxFallingEdge. Po tym w stan wysoki ustawiana jest zmienna Busy i na Strobe pojawiają się szpileczki co pół bitu. Nam potrzebna jest co druga z tych szpilek – efekt taki uzyskujemy na sygnale SampleEnable. Kiedy znajduje się on w stanie wysokim, inkrementujemy licznik Counter i dokładamy odebrany bit do RxBuffer. Gdy licznik Counter doliczy do 17, wówczas kopiujemy interesujące nas bity z RxBuffer na wyjście Data_o, a wyjście Done_o ustawiamy w stan wysoki na jeden takt zegarowy, by poinformować, że odebrane dane są gotowe do odczytania.

Moduł top

Czas przetestować nasz moduł odbiornika w rzeczywistości, na prawdziwym FPGA. Stosując przejściówkę USB/UART oraz dowolny terminal, jak np. Putty czy Realterm, będziemy przesyłać z komputera do FPGA różne bajty danych. Po odebraniu każdego bajtu zostanie on wyświetlony na wyświetlaczu 7-segmentowym w formacie szesnastkowym. Na płytce User Interface Board znajduje się wyświetlacz ośmiocyfrowy, zatem możliwe będzie wyświetlenie czterech ostatnio odebranych bajtów. Każdy bajt może przyjmować wartości od 0x00 do 0xFF. Podobne rozwiązanie zastosowaliśmy w 10 odcinku kursu na temat klawiatury matrycowej, w którym na wyświetlaczu pojawiały się kody czterech ostatnio wciśniętych przycisków.

Utwórz nowy projekt w Lattice Diamond, a następnie dodaj do niego pliki z kodem źródłowym pokazane na rysunku 6. Wszystkie moduły, oczywiście z wyjątkiem modułu top, były omawiane już wcześniej i możesz pobrać je z adresów widocznych w ramce na początku artykułu.

Rysunek 6. Lista plików projektu

Przejdźmy teraz do omówienia modułu top, którego kod pokazano na listingu 5. Podobnie jak w poprzednim odcinku kursu na temat nadajnika UART, będziemy korzystać z zewnętrznego, kwarcowego generatora sygnału zegarowego o częstotliwości 25 MHz. Generator RC wbudowany w FPGA ma niewystarczającą dokładność, przez co transmisja UART z jego użyciem może przebiegać nieprawidłowo. Na liście portów modułu top dodajemy zatem wejście Clock. Pamiętajmy też, by odpowiednio ustawić częstotliwość zegara parametrem CLOCK_HZ (linia 1).

// Plik top.v

`default_nettype none

module top(
input wire Clock, // Pin 20 (Zegar 25MHz)
input wire Reset, // Pin 17 (Przycisk K0)
input wire Rx_i, // Pin 75 (Oznaczenie Rx na złączu)

output wire [7:0] Cathodes_o,
output wire [7:0] Segments_o
);

parameter CLOCK_HZ = 25_000_000; // 1

// Odbiornik UART
wire RxDone; // 2
wire [7:0] RxData; // 3

UartRx #( // 4
.CLOCK_HZ(CLOCK_HZ),
.BAUD(115200) // 5
) UartRx_Inst(
.Clock(Clock),
.Reset(Reset),
.Rx_i(Rx_i),
.Done_o(RxDone), // 6
.Data_o(RxData) // 7
);

// Wyświetlanie czterech ostatnio odebranych bajtów
reg [31:0] DataToDisplay; // 8

always @(posedge Clock, negedge Reset) begin
if(!Reset)
DataToDisplay <= 0;
else if(RxDone)
DataToDisplay[31:0] <= {DataToDisplay[23:0], RxData}; // 9
end

// Instancja modułu 8-cyfrowego wyświetlacza LED
DisplayMultiplex #( // 10
.CLOCK_HZ(CLOCK_HZ),
.SWITCH_PERIOD_US(1000),
.DIGITS(8)
) DisplayMultiplex0(
.Clock(Clock),
.Reset(Reset),
.Data_i(DataToDisplay), // 11
.DecimalPoints_i(8’b01010101), // 12
.Cathodes_o(Cathodes_o),
.Segments_o(Segments_o),
.SwitchCathode_o()
);

endmodule

`default_nettype wire

Listing 5. Kod pliku top.v

Zaczniemy od utworzenia dwóch zmiennych typu wire. Zmienna RxDone, zadeklarowana w linii 2, informować nas będzie o tym, że został odebrany bajt danych. Moduł odbiornika ustawi ją wtedy w stan wysoki na jeden takt sygnału zegarowego. Ostatnio odebrany bajt będzie można odczytać, korzystając z 8-bitowej zmiennej RxData (linia 3).

W linii 4 tworzymy instancję odbiornika UART, który ma odbierać dane z szybkością 115200 bitów na sekundę, co określamy parametrem BAUD w linii 5. W liniach 6 i 7 łączymy wyjścia modułu z sygnałami wire, utworzonymi wcześniej.

Następnie tworzymy 32-bitową zmienną DataToDisplay typu reg (linia 8). Celem tej zmiennej jest przechowywanie czterech ostatnio odebranych bajtów, aby pokazać je na wyświetlaczu. Przypomnijmy, że moduł wyświetlacza 8-cyfrowego wyświetla znaki od 0 do 9 oraz od A do F. Na każdy znak przypadają cztery bity, zatem na dwa znaki przypada osiem bitów, czyli tyle, ile ma w sobie jeden bajt.

W dalszej części listingu mamy prosty blok always, którego jedynym celem pozostaje aktualizacja zmiennej DataToDisplay po odebraniu nowego bajtu. W linii 9 zapisujemy do tej 32-bitowej zmiennej nową wartość, powstającą w wyniku sklejenia dotychczasowych bitów od 23 do 0 tej zmiennej (czyli 24 najmłodszych bitów) i doklejenia do nich zawartości 8-bitowej zmiennej RxData. Inaczej mówiąc, dotychczasowa zawartość DataToDisplay jest przesuwana w lewo o osiem bitów, a w miejsce pustych bitów wstawiana jest zawartość RxData.

Pozostaje już tylko utworzyć instancję modułu sterującego wyświetlaczem, co czynimy w linii 10. Moduł ten był omawiany dokładnie w 9 odcinku kursu. Do wejścia danych Data_i doprowadzamy zmienną DataToDisplay (linia 11). Aby ułatwić odróżnienie poszczególnych bajtów, zajmujących dwa znaki na wyświetlaczu, włączymy punkty dziesiętne pomiędzy nimi (linia 12).

Przeprowadź syntezę, a następnie otwórz narzędzie Spreadsheet i skonfiguruj piny układu FPGA w taki sposób, jak zaprezentowano na rysunku 7.

Rysunek 7. Konfiguracja pinów

Ponieważ używamy zewnętrznego źródła sygnału zegarowego, doprowadzonego do jednego z pinów układu FPGA, musimy skonfigurować jego częstotliwość. W tym celu w Spreadsheet klikamy zakładkę Timing Preferences, a następnie wybieramy drugi od góry przycisk na pionowym pasku narzędzi.

Dodajemy właściwość FREQUENCY dla pinu Clock i ustawiamy na 25 MHz (zobacz rysunek 8).

Rysunek 8. Konfiguracja sygnału zegarowego

Generujemy bitstream i wgrywamy do FPGA. Pin Tx przejściówki USB/UART należy podłączyć do wejścia Rx na złączu goldpin w płytce User Interface Board. Kiedy z terminalu wyślemy jakikolwiek bajt, zostanie on wyświetlony na dwóch wyświetlaczach 7-segmentowych po prawej stronie. Jeżeli wcześniej zostały odebrane jakiekolwiek bajty, przesuną się one w lewo.

W następnym odcinku użyjemy ponownie modułu odbiornika UART, lecz odebrane bajty będziemy wyświetlać jako normalne, czytelne litery na 14-segmentowym wyświetlaczu LCD. Będzie to rozwinięcie tematu sterowników wyświetlaczy LCD, który zaczęliśmy w 13 odcinku kursu.

Dominik Bieczyński
leonow32@gmail.com

Repozytorium modułów stosowanych w kursie https://github.com/leonow32/verilog-fpga
Artykuł ukazał się w
Elektronika Praktyczna
maj 2024
DO POBRANIA
Materiały dodatkowe

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik październik 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio wrzesień - październik 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje wrzesień 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna październik 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich październik 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów