Kurs FPGA Lattice (4). Generator, dzielnik i licznik

Kurs FPGA Lattice (4). Generator, dzielnik i licznik

W tej części kursu nauczymy się używać generatora sygnału zegarowego w FPGA Lattice MachXO2 i zastosujemy go do taktowania prostych układów sekwencyjnych. Sprawdzimy, ile zasobów zużywa nasz kod, a następnie skorzystamy z timera wbudowanego w FPGA MachXO2, aby zaoszczędzić uniwersalne zasoby logiczne.

Uproszczony schemat układu, jaki zbudujemy w dzisiejszym odcinku kursu, został pokazany na rysunku 1. Centralną częścią układu będzie 4-bitowy licznik, zliczający w kodzie binarnym od 0 do 15, lub od 15 do 0 w zależności od kierunku, jaki wybierzemy przyciskiem. Stan licznika będzie wyświetlany na czterech diodach LED, podłączonych do pinów układu FPGA poprzez rezystory ograniczające prąd. Pomiędzy licznikiem a diodami umieścimy bufor trójstanowy sterowany przyciskiem. Kiedy przycisk zostanie wciśnięty, wówczas wszystkie wyjścia układu ustawią się w stan wysokiej impedancji, a w rezultacie diody natychmiast przestaną świecić. W ten sposób zapoznamy się z układem TSALL. Licznik będzie miał możliwość liczenia w górę lub w dół, a kierunek liczenia będzie wybierany przyciskiem, ponadto jego stan będzie można wyzerować przy pomocy sygnału resetującego, poprowadzonego przez globalną linię resetującą.

Rysunek 1. Uproszczony schemat układu dla tego odcinka kursu FPGA

W projekcie zastosujemy wbudowany generator RC jako źródło sygnału zegarowego. Częstotliwość tego sygnału jest za duża, by ludzkie oko dostrzegło pracę licznika, więc pomiędzy generatorem a licznikiem umieścimy dzielnik częstotliwości w dwóch wersjach - jeden zbudowany z uniwersalnych zasobów logicznych FPGA, a drugi przy pomocy gotowego timera wbudowanego w strukturę FPGA. Zobaczymy ile zasobów uda się zaoszczędzić dzięki drugiemu rozwiązaniu.

Zaczynamy

Przeanalizujmy kod pokazany na listingu 1. Należy go zapisać w pliku top.v. Oprócz portów widocznych na schemacie poniżej, dostępne jest także wyjście sygnału zegarowego o częstotliwości 1 Hz. Można z niego skorzystać w celach diagnostycznych. Zwróć uwagę, że wyjście LED jest typu reg.

Listing 1. Moduł top pierwszego przykładu

module top(
input Reset, // Pull-up, 1 - normalna praca, 0 - reset
input Direction, // Pull-up, kierunek liczenia, 1 - w górę, 0 - w dół
input TristateAll, // Pull-down, sterowanie buforem trójstanowym
output reg [3:0] LED, // 4 diody
output Clock1Hz // Wyjście sygnału zegarowego o częstotliwości 1Hz
);

// Bufor trójstanowy na wszystkich pinach
TSALL TriStateBuffer(
.TSALL(TristateAll) // 0 - normalna praca, 1 - wszystkie piny HI-Z
);

// Global set reset
GSR GlobalSetReset(
.GSR(Reset) // 0 - reset aktywny, 1 - normalna praca
);

// Generator sygnału zegarowego 2.08 MHz
wire Clock2M08;
defparam Generator1.NOM_FREQ = “2.08”;
OSCH Generator1(
.STDBY(!Reset), // Tryb standby, 1 - generator wyłączony, 0 - praca
.OSC(Clock2M08), // Wyjście sygnału zegarowego
.SEDSTDBY() // Tylko do symulacji
);

// Dzielnik częstotliwości w module Divider
Divider DzielnikCzestotliwosci1(
.Clock2M08(Clock2M08), // Wejście sygnału o wysokiej częstotliwości
.Reset(Reset), // Wejście resetujące
.Clock1Hz(Clock1Hz) // Wyjście sygnału o niskiej częstotliwości
);

// Licznik dwukierunkowy
always @(posedge Clock1Hz, negedge Reset) begin
if(!Reset)
LED <= 4’d0;
else
if(Direction)
LED <= LED + 1’b1;
else
LED <= LED - 1’b1;
end

endmodule

Tristate All oraz Global Set/Reset

W pierwszej kolejności (na rozgrzewkę) utworzymy bufor trójstanowy na pinach GPIO. Układy MachXO2 wyposażone są w peryferium TSALL. Musimy utworzyć instancję tego obiektu, nadając mu dowolną nazwę (ale inną od jakichkolwiek wcześniej użytych nazw). W naszym przykładzie będzie to TriStateBuffer.

Ma on tylko jedno wejście, które również nazywa się TSALL. Należy je połączyć z sygnałem sterującym - w naszym przypadku będzie to sygnał TristateAll, który podłączony jest do przycisku. Stan wysoki na wejściu TSALL powoduje, że wszystkie piny GPIO układu FPGA ustawiają się w stan wysokiej impedancji - czyli nie emitują ani stanu ani 1, ani 0. Będzie się to objawiać natychmiastowym wygaszeniem wszystkich diod LED podłączonych do FPGA.

Kolejnym układem jaki zastosujemy jest GSR czyli Global Set/Reset. W naszym przykładzie instancja tego modułu zostanie nazwana GlobalSetReset. Sygnał podłączony do wejścia GSR zostanie rozprowadzony do reszty układów przy pomocy specjalnych linii resetujących, wbudowanych w strukturę FPGA. Alternatywą jest prowadzenie sygnałów resetujących przy pomocy uniwersalnych zasobów, które są stosowane do wszystkich innych sygnałów. Jednak to pierwsze rozwiązanie jest szybsze i bezpieczniejsze.

Nie ma potrzeby, by w każdym projekcie stosować TSALL i GSR. Syntezator na ogół sam odnajdzie sygnał resetujący. Możemy to zweryfikować w Netlist Analyzer, co zostało opisane dokładniej w poprzednim odcinku kursu. Więcej na temat peryferiów TSALL, GSR i PUR znajdziesz w linku [1].

Generator sygnału zegarowego

Trzecim układem peryferyjnym jaki musimy zainicjować jest generator sygnału zegarowego OSCH (niektóre FPGA Lattice mają generator sygnału o niskiej częstotliwości, który nazywa się OSCL). Instancja tego układu będzie nosić nazwę Generator1 - podobnie jak w przypadku GSR i TSALL, możemy utworzyć tylko jedną instancję tego peryferium.

Moduł OSCH ma trzy porty:

  1. STDBY służy do włączania i wyłączania generatora. Kiedy STDBY jest w stanie niskim, wówczas generator pracuje, a kiedy jest w stanie wysokim to wchodzi w stan uśpienia. Do STDBY możemy podłączyć zanegowany sygnał resetujący, a jeżeli chcemy, by generator działał zawsze, to można ten sygnał na zawsze ustawić w stan niski, wpisując 1’b0,
  2. OSC to wyjście generatora, na którym uzyskujemy sygnał zegarowy o stałej częstotliwości. Wyprowadzimy ten sygnał przy pomocy zmiennej wire Clock2M08,
  3. SEDSTDBY możemy pozostawić niepodłączone do niczego, ponieważ jest potrzebne tylko na potrzeby symulacji.

Jest jeszcze jedna rzecz jaką musimy ustawić - częstotliwość sygnału, jaką chcemy otrzymać. W tym celu musimy posłużyć się instrukcją defparam, której jeszcze w tym kursie nie widzieliśmy. Przy pomocy tej instrukcji można skonfigurować moduły, których instancje tworzymy - np. można ustawić liczbę bitów licznika czy rozmiar pamięci.

Parametr NOM_FREQ definiuje częstotliwość sygnału zegarowego. Należy go ustawić na jedną z dostępnych możliwości, które opisano w rozdziale 20 instrukcji MachXO2 sysCLOCK PLL Design and User Guide dostępnej pod linkiem [2]. Screenshot tabeli z możliwymi ustawieniami znajduje się na rysunku 2. Mamy możliwość uzyskania sygnału zegarowego o częstotliwości od 2,08 MHz do 133 MHz. Wybierzmy najmniejszą - ona jest i tak zbyt duża, by sterować nią diodami LED. Dlatego musimy zastosować dzielnik częstotliwości.

Rysunek 2. Fragment dokumentacji pokazujący możliwe częstotliwości sygnału zegarowego

Dzielnik częstotliwości

Przeskoczmy z pliku top.v do pliku divider.v, gdzie znajduje się moduł Divider, którego kod pokazuje listing 2. Celem tego modułu jest przetworzenie sygnału wejściowego Clock2M08 o częstotliwości 2,08 MHz w taki sposób, aby uzyskać sygnał wyjściowy Clock1Hz o częstotliwości 1 Hz, która będzie odpowiednia do migania diodami LED.

Listing 2. Moduł dzielnika częstotliwości

module Divider(
input Clock2M08, // Wejście sygnału o wysokiej częstotliwości
input Reset, // Wejście resetujące, 0 resetuje
output reg Clock1Hz // Wyjście sygnału o niskiej częstotliwości
);

reg [19:0] Counter = 20’d0;
always @(posedge Clock2M08, negedge Reset) begin
if(!Reset) begin
Counter <= 20’d0;
Clock1Hz <= 1’b0;
end
else begin
if(Counter == 2080000 / 2 - 1) begin
Clock1Hz <= !Clock1Hz;
Counter <= 20’d0;
end else begin
Counter <= Counter + 1’b1;
end
end
end

endmodule

Idea działania modułu jest bardzo prosta. Przy każdym zboczu rosnącym sygnału zegarowego Clock2M08, inkrementowany jest licznik Counter. Licznik zaczyna liczyć od zera i liczy aż do momentu, kiedy osiągnie wartość 1039999. Wtedy licznik jest zerowany, a stan wyjścia reg Clock1Hz jest odwracany. Taki algorytm pozwala uzyskać sygnał o częstotliwości 1 Hz i wypełnieniu 50%.

Musimy jeszcze wyjaśnić, skąd wzięła się liczba 1039999. Sygnał wejściowy ma częstotliwość 2080000 Hz. Zatem gdyby licznik liczył od 0 do 2079999, osiągnąłby wartość maksymalną dokładnie co jedną sekundę. Jednak nas interesuje, aby stan sygnału Clock1Hz zmieniać co pół sekundy, aby uzyskać częstotliwość 1 Hz. Zatem musimy wejściową częstotliwość podzielić przez dwa, czyli musimy zliczyć 1040000 impulsów, a więc liczymy od 0 do 1039999.

Zastanówmy się, ile bitów powinien mieć licznik Counter. Powinno być ich tyle, aby „pomieścić” największą wartość, którą ma przechowywać. Aby to określić, liczbę 1039999 musimy przeliczyć (np. kalkulatorem Windows) na system binarny - jest to 1111_1101_1110_0111_1111. Liczymy bity i okazuje się, że potrzebujemy zmiennej 20-bitowej.

Licznik dwukierunkowy

Wróćmy do bloku always, który występuje po inicjalizacji wszystkich peryferiów w listingu 1. Jest to nasz właściwy licznik, który pokazano na rysunku 1, sterujący czterema diodami LED. Licznik jest układem sekwencyjnym, reagującym na zbocze rosnące (posedge) sygnału Clock1Hz. Licznik może być asynchronicznie wyzerowany przy pomocy sygnału Reset - zerowanie następuje natychmiast po wystąpieniu zbocza opadającego (negedge) i trwa tak długo, dopóki sygnał Reset pozostaje w stanie niskim.

Sygnał Direction decyduje o kierunku liczenia. Jeżeli jest w stanie wysokim, to wartość licznika jest zwiększana o 1 przy każdym takcie zegara, a kiedy jest w stanie niskim, to wartość licznika jest pomniejszana o 1. Zwróćmy uwagę, że sygnał Direction nie występuje na liście wrażliwości bloku always @(). Oznacza to, że stan tego sygnału jest istotny tylko w momencie, kiedy zmienia się stan jednego z sygnałów występujących na liście wrażliwości - w tym przypadku w momencie zbocza rosnącego posedge Clock1Hz. W innych przypadkach stan tego sygnału jest zupełnie nieistotny. Może się zmieniać w dowolny sposób i nie będzie to miało wpływu na nic.

Jak to działa?

Spróbujmy przeanalizować jak działa kod z listingów 1 i 2, ale pod względem sprzętowym, bowiem abstrakcyjne polecenia języka Verilog prowadzą do syntezy fizycznych bramek, przerzutników i innych elementów, które pracują w strukturze układu FPGA. Uruchom syntezę, a następnie kliknij Tools i Netlist Analyzer. Powinien pokazać się widok jak na rysunku 3. Po lewej stronie schematu widzimy wejścia Direction, Reset i TristateAll, a po prawej znajduje się wyjście LED[3:0] oraz Clock1Hz. Analizę zacznijmy od sygnału resetującego. Jest to sygnał, który łączy GlobalSetReset, Generator1, DzielnikCzestotliwosci1 oraz finalnie licznik LED, który zaznaczyłem na czerwono.

Rysunek 3. Widok schematu powstałego po syntezie kodu z listingu 1 i 2

Licznik LED sterowany jest sygnałem zegarowym, zaznaczonym na zielono, doprowadzonym do wejścia clock. Na schemacie widać wyraźnie, że sygnał zegarowy powstaje w module Generator1 i następnie przechodzi poprzez DzielnikCzestotliwosci1. Wyjście z licznika LED poprowadzone jest bezpośrednio do pinów wyjściowych, a także do elementów sub_7 oraz add_6. Są to sumatory, które zostały wywnioskowane z kodu. Można kliknąć je prawym przyciskiem myszy, a następnie wybierając Jump To HDL File ukaże się plik z podświetlonym fragmentem kodu, który odpowiedzialny jest za utworzenie tego elementu. Okazuje się, że są to fragmenty LED <= LED + 1’b1 oraz LED <= LED - 1’b1, czyli inkrementacja i dekrementacja licznika LED. Zwróć uwagę, że sub_7 oraz add_6 mają dwa wejścia - pierwsze wejście to stan licznika LED, a drugie wejście to po prostu stała 4-bitowa wartość 1. Wyjście z tych sumatorów jest kierowanie do multipleksera, który dostał trochę dziwną nazwę LED_3__I_O. Ten multiplekser decyduje, który z dwóch elementów arytmetycznych zostanie podłączony do wejścia rejestru LED. Decyduje o tym sygnał Direction.

Zwróć uwagę, że licznik w FPGA zrealizowany jest jako szereg przerzutników D. Przerzutnik D to prosty układ pamięciowy, który kopiuje stan wejścia na wyjście w momencie wykrycia zbocza rosnącego sygnału zegarowego. Stan wejścia przerzutnika D może się dowolnie zmieniać i nie powoduje to żadnej zmiany stanu wyjścia, aż do momenty wystąpienia kolejnego zbocza rosnącego sygnału zegarowego. Elementy odpowiedzialne za zwiększanie lub zmniejszanie wartości licznika to układy kombinacyjne. Działają natychmiast po tym, jak zmieni się stan wyjścia przerzutników D i po jakimś krótkim czasie, zwanym stanem propagacji, obliczane są nowe wartości licznika, powiększone i pomniejszone o 1, a multiplekser przepuszcza dalej tylko jedną z tych wartości. Kiedy znowu wystąpi zbocze rosnące sygnału zegarowego, w przerzutnikach D ponownie kopiowany jest stan wejść na wyjścia.

W czasie kiedy elementy arytmetyczne i multiplekser przetwarzają nowe sygnały zaraz po zboczu rosnącym sygnału zegarowego, możliwe jest, że przerzutniki D będą przez chwilę otrzymywać nieprawidłowe, szybko zmieniające się sygnały, tzw. glitch. Nie ma to jednak żadnego znaczenia, jeżeli kolejne zbocze sygnału zegarowego wystąpi po ustaleniu się sygnałów w układach kombinacyjnych. Wynika z tego ważny wniosek - częstotliwość sygnału zegarowego musi być taka, aby układy kombinacyjne miały szansę przetworzyć dane pomiędzy zboczami sygnału zegarowego.

W naszym projekcie jest jeden mały błąd. Czy potrafisz go znaleźć? Błąd jest dobrze ukryty i w większości przypadków układ będzie działał całkowicie prawidłowo. Jednak co się stanie, kiedy w chwili zbocza rosnącego sygnału zegarowego zmieni się także stan wejścia Direction? Bramki w multiplekserze zaczną się przełączać, aby przestawić stan wyjścia multipleksera. Spowoduje to, że przez chwilę zaczną się przełączać stany sygnałów na wejściu przerzutników D. Jednak w tym samym czasie przerzutniki D przepisują stan swoich wejść na wyjście. Co trafi na wyjścia przerzutnika D w momencie, kiedy zmienia się stan jego wejścia? Ten problem to metastabilność. Sygnał Direction nie jest zsynchronizowany z zegarem taktującym wszystkie peryferia układu FPGA. Powinniśmy uzupełnić go o synchronizator. Zajmiemy się nim dokładniej w jednym z kolejnych odcinków kursu.

Kontrola zasobów

Pisząc program na procesor interesuje nas, ile program zajmuje pamięci, a pisząc kod na FPGA musimy kontrolować ile zużywamy zasobów. Jednak to stwarza pewien problem, ponieważ w FPGA są różne elementy: Slice, LUT i przerzutniki. Ponadto problematyczne może być porównywanie zasobów między różnymi FPGA - na przykład Lattice MachXO2 zawiera elementy LUT z czterema wejściami, a Xilinx Spartan-6 ma LUT-y z sześcioma wejściami. W rezultacie jeden LUT z Xilinxa może wykonywać zadanie, jakie w Lattice wymaga kilku bloków LUT. Dokładny opis co to jest LUT, Slice i inne elementy znajdziemy w rozdziale 2 instrukcji MachXO2 Family Datasheet, podanym w linku [3].

Istnieją dwa sposoby, aby sprawdzić, ile nasz kod zużywa zasobów. Pierwszy to raport Map, który widoczny jest w prawej stronie okna programu Diamond. Domyślnie jest otwarty po uruchomieniu programu, ale jeżeli go zamknąłeś, można go ponownie otworzyć, wybierając menu View i następnie Reports. Z drzewka raportów wybieramy raport Map. Jeżeli nie jest dostępny to znaczy, że nie została przeprowadzona synteza i kolejne kroki implementacji. W tym raporcie zaprezentowane są wszystkie możliwe do zastosowania zasoby sprzętowe.

Drugi sposób kontroli używanych zasobów dostępny jest poprzez zakładkę Hierarchy - Post Map Resources, która dostępna jest w dolnej części lewego okna, obok drzewka z listą plików źródłowych File List oraz czynności syntezy, mapowania, programowania Process. Po kliknięciu na zakładkę Hierarchy - Post Map Resources, zobaczymy wszystkie moduły ułożone w sposób hierarchiczny, zaczynając od modułu top. W naszym prostym projekcie mamy tylko dwa moduły - top oraz DzielnikCzestotliwosci1, który został powołany do życia w module top.

Rysunek 4. Kontrola zasobów

Budując bardziej skomplikowane projekty, będziemy tworzyć moduły w modułach, a w nich będą kolejne moduły i tak dalej. Widok hierarchii pozwala nam analizować projekt metodą od ogółu do szczegółu. Zwróć uwagę na liczby znajdujące się w kolumnach po prawej stronie od nazwy modułów i nazwy plików. Są w nich pokazane zasoby zużywane w każdym module - są to po kolei: LUT-y, przerzutniki (Registers), przeniesienia (Carry) oraz Slice-y.

Możemy porównać, jakie zapotrzebowanie na zasoby mają poszczególne moduły. Weźmy liczbę przerzutników modułu top - jest to 25(4). Jak należy rozumieć te liczby? Liczba w nawiasie mówi ile przerzutników zawiera sam moduł top, nie licząc żadnych podrzędnych modułów, jakie zostały w nim utworzone. Liczba bez nawiasu mówi ile przerzutników zawiera moduł top łącznie ze wszystkimi podrzędnymi modułami. Jeżeli jakiś moduł nie zawiera podmodułów, to liczby w nawiasie i bez nawiasu są sobie równe, co widzimy w module DzielnikCzestotliwosci1 - 21(21). Taki sposób prezentowania danych ułatwia optymalizację kodu, bo od razu widać, które moduły są najbardziej zasobożerne, dzięki czemu możemy spróbować je zoptymalizować w pierwszej kolejności.

Weźmy pod uwagę właśnie moduł dzielnika częstotliwości - zawiera on aż 21 przerzutników, podczas gdy moduł top wykorzystuje tylko 4. Liczba 21 bierze się stąd, że w tym module mamy 20-bitowy licznik Counter oraz 1-bitowy rejestr Clock1Hz.

Moduł dzielnika częstotliwości celowo zawarłem w osobnym pliku. Opisanie go w języku Verilog i synteza w przy pomocy uniwersalnych zasobów FPGA jest oczywiście możliwa i prawidłowa, ale niekoniecznie efektywna. Układy MachXO2 dostarczają nam różne peryferia gotowe do użycia - są prostsze i szybsze w implementacji, a przede wszystkim nie zużywają uniwersalnych zasobów, które można wykorzystać na bardziej ambitne zastosowania niż zwyczajne liczniki. Jak to zrobić? Zobaczymy w następnym odcinku.

Dominik Bieczyński
leonow32@gmail.com

Czytaj więcej:

  1. How to use GSP, PUR and TSALL - https://bit.ly/3kIpTYl
  2. MachXO2 sysCLOCK PLL Design and User Guide - https://bit.ly/3j1Bml5
  3. MachXO2 Family Datasheet - https://bit.ly/3XChvrU
Artykuł ukazał się w
Elektronika Praktyczna
luty 2023

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik kwiecień 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio maj - czerwiec 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje kwiecień 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna kwiecień 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich maj 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów