Dzisiejszy odcinek rozpoczniemy od utworzenia nowej implementacji projektu, utworzonego w poprzednim odcinku. Implementacje to wersje projektu, które mogą się różnić ustawieniami optymalizacji lub plikami źródłowymi. Najprościej można utworzyć nową implementację, kopiując w całości obecną. W tym celu klikamy prawym przyciskiem myszy na nazwie obecnej implementacji (domyślnie impl1) w drzewku projektu, a następnie wybieramy Clone Implementation, jak pokazano na rysunku 1. Program zapyta nas, jak ma się nazywać nowa implementacja i gdzie ma być zapisana. W polu Name i Directory wpiszmy DzielnikEFB. Nie chcemy mieć wspólnych plików źródłowych między obiema implementacjami i dlatego zaznaczamy Copy files into new implementation source directory.
Nowa implementacja powinna pojawić się w drzewku projektu. Musimy ją jeszcze uaktywnić, klikając na nią prawym przyciskiem myszy i wybierając Set As Active Implementation. Plik divider.v nie będzie nam już potrzebny. Można go usunąć, klikając prawym przyciskiem myszy i następnie Remove. Aby wrócić do poprzedniej implementacji, wystarczy ją kliknąć prawym przyciskiem myszy w drzewku projektu i wybrać opcję Set As Active Implementation.
IP Express
Moduł IP Express to generator kodu, który ułatwia korzystanie z różnych peryferiów takich jak pamięci ROM, RAM, układy arytmetyczne, komunikacyjne, a nas dzisiaj będzie interesował blok EFB. Jest to blok, który zawiera interfejs SPI, dwa interfejsy I²C, pamięć flash oraz 16-bitowy timer (szczerze mówiąc, umieszczenie tego wszystkiego w jednym module wydaje się trochę dziwne i mało wygodne). Dokładny opis wszystkich funkcjonalności bloku EFB znajdziesz w instrukcjach, do których linki zamieszczono na końcu artykułu.
Z menu Tools wybierz IP Express. Pojawi się lista różnych peryferiów, jakie można dodać za pomocą tego narzędzia. Wybierz EFB, a następnie w polu File Output wpisz EFB, a w Module Output wybierz Verilog. Wygenerowany moduł powinien być umieszczony w katalogu, którym znajduje się plik top.v. Z tego powodu w Project Path należy podać ścieżkę do katalogu, w którym znajduje się ten plik. W moim przypadku jest to Kurs04/DzielnikEFB/source, gdzie Kurs04 to katalog projektu, a DzielnikEFB to katalog implementacji. Następnie kliknij Customize.
Pokazuje się okno, w którym musimy wybrać, które peryferia z bloku EFB chcemy zastosować. Zaznaczamy tylko Timer, a poniżej wybierz Static setup only. Dynamic setup z interfejsem WISHBONE polega na tym, że timer konfiguruje się tak, jak w dowolnym mikrokontrolerze - jest do niego podłączona magistrala danych, ma rejestry umieszczone pod jakimiś adresami w pamięci i procesor umieszczony w FPGA musi skonfigurować rejestry timera na podstawie programu, jaki procesor wykonuje, a w trakcie pracy programu konfiguracja timera może być dowolnie zmieniana. Nam takie możliwości nie są potrzebne - skonfigurujemy timer jednorazowo poprzez IP Express.
Będzie działał od razu po załadowaniu bitstreamu do FPGA. Nie będzie można zmienić jego konfiguracji w trakcie pracy, ale nie potrzebujemy takiej możliwości. Dlatego zaznaczamy Import IPX to Diamond project w dolnej części okna, a następnie wybieramy zakładkę Timer/Counter. Pokaże się okno dialogowe jak na rysunku 2.
W ramce Mode Selection musimy wybrać tryb pracy timera. Dostępne są cztery tryby:
- CTCM - najprostszy tryb, w którym timer liczy od zera aż do osiągnięcia wartości top, po czym jest zerowany i powtarza ten cykl w nieskończoność. W momencie przejścia z wartości maksymalnej do zera stan wyjścia tc_oc jest odwracany. Wyjście tc_int przyjmuje stan wysoki tylko wtedy, gdy stan licznika jest równy zeru.
- WATCHDOG - timer startuje od zera i liczy aż do wartości top. Jeżeli osiągnie wartość top, wówczas się zatrzymuje i ustawia wyjście w stan wysoki tak długo, aż zostanie zresetowany. Ideą tego timera jest kontrola czasu wykonywania jakiegoś procesu. Proces powinien cyklicznie resetować timer, a jeżeli nie zresetuje - to znaczy, że proces się zawiesił, więc timer generuje sygnał pozwalający zresetować proces.
- FASTPWM - timer liczy od zera do wartości top. Kiedy wartość licznika jest mniejsza od wartości ocr, wówczas stan wyjścia jest niski, a kiedy jest powyżej, to stan wyjścia jest wysoki.
- PFCPWM - działa podobnie jak FASTPWM z tą różnicą, że timer liczy naprzemiennie w górę i w dół.
W naszym przypadku wybieramy tryb CTCM, a Output function ustawiamy jako TOGGLE.
W ramce Clock Selection musimy określić, jakie źródło będzie taktować nasz timer. Maksymalna częstotliwość sygnału zegarowego może wynosić 133 MHz. Zaznaczamy opcję Use On-Chip Oscillator, a Clock Edge Selection ustawiamy jako Positive. W ramce Enable Interrupt Registers musimy zaznaczyć Standalone Overflow (No Wishbobe Bus).
Pozostaje jeszcze wybrać preskaler, który dodatkowo podzieli częstotliwość sygnału zegarowego, dostarczonego do wejścia tc_clki. Możliwe wartości preskalera to 1, 8, 64, 256 i 1024. Wartość top może być dowolną liczbą 16-bitową, tzn. od zera do 65535.
Wzór pozwalający ustalić wartość rejestru top, aby uzyskać sygnał o żądanej częstotliwości, wygląda następująco:
W naszym przypadku fCLK to 2080000 Hz, ponieważ taki sygnał dostarcza generator OSCH, a fINT, którą chcemy uzyskać, to 1 Hz. Musimy spróbować obliczyć wartość top dla poszczególnych wartości preskalera. Wynik powinien być mniejszy niż wartość maksymalna, jaka jest możliwa dla timera 16-bitowego (czyli 65535), a w miarę możliwości powinien być liczbą całkowitą. Nie ma możliwości, by rejestr top był liczbą ułamkową, a zaokrąglenie wyniku spowoduje, że uzyskamy sygnał o nieznacznie innej częstotliwości. Wyniki obliczeń zestawiono w tabeli 1.
Aby obliczyć częstotliwość wyjściową, znając fCLK, preskaler oraz top, należy posłużyć się wzorem:
Dochodzimy więc do wniosku, że powinniśmy ustawić preskaler na 64, a top na 16249. Wpisujemy te liczby, po czym klikamy Generate. Powinniśmy zobaczyć raport, podobny do tego, jaki pokazano na rysunku 3.
W drzewku projektu powinien pojawić się plik efb.ipx w tym samym katalogu, co plik top.v. Jeżeli zajdzie potrzeba zmiany ustawień timera, należy otworzyć plik ipx do edycji, klikając go dwukrotnie (rysunek 4).
Dotychczas utworzyliśmy moduł timera, ale by został on użyty w projekcie, musimy utworzyć jego instancję i podłączyć jakieś sygnały do jego portów. Przeanalizujmy listing 1. Jest on bardzo podobny do listingu, który był w poprzedniej części kursu. Są tylko dwie różnice. Pierwsza to dodanie wyjścia Int, które pozwoli zaobserwować na oscyloskopie sygnał przerwań timera (pamiętaj, by przypisać to wyjście do jakiegoś pinu GPIO w Spreadsheet). Druga to zamiana modułu typu Divider o nazwie DzielnikCzestotliwosci1 na nowy timer. Powstaje pytanie - skąd mam wiedzieć, jak się nazywa moduł wygenerowany przez IP Express i jak go połączyć z resztą aplikacji?
module top(
input Reset, // Pull-up, 1 - normalna praca, 0 - reset
input Direction, // Kierunek liczenia, 1 - w górę, 0 - w dół
input TristateAll, // Sterowanie buforem trójstanowym
output reg [3:0] LED, // 4 diody
output Clock1Hz, // Testowe wyjście sygnału zegarowego
output Int // Testowe wyjście przerwań z timera
);
// 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 bloku EFB
efb EFB1(
.tc_clki(Clock2M08), // Wejście sygnału zegarowego 2,08 MHz
.tc_rstn(Reset), // Wejście resetujące, 1 - praca, 0 - reset
.tc_int(Int), // Wyjście przerwań, stan 1, gdy timer = 0
.tc_oc(Clock1Hz) // Wyjście sygnału zegarowego 1 Hz
);
// 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
Z pomocą przychodzi raport, który jest widoczny na rysunku 3. Interesują nas w szczególności pozycje Module Type oraz Inputs i Outputs. Zwróć uwagę, że nazwa modułu efb pisana jest małymi literami, co jest trochę brakiem konsekwencji, gdy TSALL, GSR i OSCH pisane są dużymi literami.
Przeprowadźmy syntezę i wgrajmy bitstream do FPGA. Diody LED będą migać dokładnie tak samo, jak w poprzednim odcinku kursu. Podłączmy oscyloskop do wyjść Clock1Hz i Int, aby sprawdzić, czy generowane sygnały są zgodne z oczekiwaniami. Screenshot z oscyloskopu pokazano na rysunku 5.
W górnej części screenshotu pokazano przebiegi w takiej skali czasu, aby było widać, że sygnał zegarowy jest przebiegiem prostokątnym o wypełnieniu 50% i częstotliwości 1 Hz. Widać, że sygnał Int to krótkie szpileczki przyjmujące stan wysoki w momencie, kiedy zmienia się stan zegara. W dolnej części pokazano przybliżenie na moment zmiany stanu sygnału zegarowego oraz szpilkę sygnału Int. Szerokość szpilki to 30,9 μs. Teoretycznie obliczona szerokość to 30,77 μs - jest to odwrotność z częstotliwości 2080000 Hz podzielonej przez preskaler 64.
Kontrola zasobów
Po syntezie i wygenerowaniu bitstreamu możemy sprawdzić, ile zasobów używa nasz kod i porównać do wyników kodu z poprzedniego odcinka kursu, który działał tak samo, ale dzielnik częstotliwości był wykonany z użyciem uniwersalnych zasobów logicznych FPGA.
Zobaczmy raport Map. Jest on domyślnie otwarty po uruchomieniu programu Diamond, a jeżeli go zamknąłeś, to możesz go ponownie otworzyć, wybierając menu View i Reports. Raport został pokazany na rysunku 6.
Kod z czwartej części kuru używał 25 przerzutników, 19 slice’ów i 37 LUT-ów. Kod z dzisiejszego odcinka kursu potrzebuje tylko 4 przerzutniki, 3 slice’y i 6 LUT-ów. Widzimy zatem, że zastosowanie modułu EFB istotnie zmniejszyło zapotrzebowanie na zasoby uniwersalne, które możemy zastosować do innych celów.
Dzielnik CLKDIVC
FPGA MachXO2 ma jeszcze inne sposoby na zmniejszanie częstotliwości sygnałów zegarowych. Jednym z nich jest zastosowanie peryferiów CLKDIVC, których dostępne są cztery sztuki. Moduł CLKDIVC jest bardzo prosty w działaniu. Do portu CLKI musimy doprowadzić sygnał zegarowy, a z portu CDIVX wyprowadzamy sygnał o podzielonej częstotliwości. Stopień podziału należy określić za pomocą instrukcji defparam i ustawić parametr NOM_FREQ na 2.0, 3.5 lub 4.0. Trochę to rozczarowujące, że możliwe są tylko te trzy ustawienia. Więcej na temat CLKDIVC możesz przeczytać w rozdziale 11 instrukcji MachXO2 sysCLOCK PLL Design and User Guide, dostępnej pod adresem [3].
module top(
// Pull-up, 1 - normalna praca, 0 - reset
input Reset,
// Kierunek liczenia, 1 - w górę, 0 - w dół
input Direction,
// Sterowanie buforem trójstanowym
input TristateAll,
// 4 diody
output reg [3:0] LED,
// Testowe wyjście sygnału zegarowego 520 kHz
output Clock520k,
// Testowe wyjście sygnału zegarowego 130 kHz
output Clock130k,
// Testowe wyjście sygnału zegarowego 32,5 kHz
output Clock32k5,
// Testowe wyjście sygnału zegarowego 8,125 kHz
output Clock8k125
);
// Bufor trójstanowy na wszystkich pinach
TSALL TriStateBuffer(
// 0 - normalna praca, 1 - wszystkie piny HI-Z
.TSALL(TristateAll)
);
// Global set reset
GSR GlobalSetReset(
// 0 - reset aktywny, 1 - normalna praca
.GSR(Reset)
);
// Generator sygnału zegarowego 2.08 MHz
wire Clock2M08;
defparam Generator1.NOM_FREQ = "2.08";
OSCH Generator1(
// Tryb standby, 1 - generator wyłączony, 0 - praca
.STDBY(!Reset),
// Wyjście sygnału zegarowego
.OSC(Clock2M08),
// Tylko do symulacji
.SEDSTDBY()
);
// Dzielniki częstotliwości
defparam Divider1.DIV = "4.0";
CLKDIVC Divider1(
.CLKI(Clock2M08),
.CDIVX(Clock520k),
.RST(!Reset),
.ALIGNWD(1’b0),
.CDIV1()
);
defparam Divider2.DIV = "4.0";
CLKDIVC Divider2(
.CLKI(Clock520k),
.CDIVX(Clock130k),
.RST(!Reset),
.ALIGNWD(1’b0),
.CDIV1()
);
defparam Divider3.DIV = "4.0";
CLKDIVC Divider3(
.CLKI(Clock130k),
.CDIVX(Clock32k5),
.RST(!Reset),
.ALIGNWD(1’b0),
.CDIV1()
);
defparam Divider4.DIV = "4.0";
CLKDIVC Divider4(
.CLKI(Clock32k5),
.CDIVX(Clock8k125),
.RST(!Reset),
.ALIGNWD(1’b0),
.CDIV1()
);
// Licznik dwukierunkowy
always @(posedge Clock8k125, negedge Reset) begin
if(!Reset)
LED <= 4’d0;
else
if(Direction)
LED <= LED + 1’b1;
else
LED <= LED - 1’b1;
end
endmodule
Dzielniki CLKDIVC możemy połączyć w łańcuch, przez co wypadkowy stopień podziału będzie iloczynem wszystkich dzielników w szeregu, czyli maksymalnie 256. Przykład takiego rozwiązania pokazuje listing 2. Wyjście z każdej instancji dzielnika CLKDIVC jest wyprowadzone także na piny FPGA, aby dało się zobaczyć przebiegi tych sygnałów na oscyloskopie. Screenshot z oscyloskopu wraz z pomiarem częstotliwości na poszczególnych kanałach przedstawiono na rysunku 7.
Podsumowanie
To wszystko w tym wydaniu EP. Dotychczasowa wiedza o sygnałach zegarowych i dzielnikach częstotliwości przyda się w kolejnych częściach kursu. Aby przetestować, czy układ działa prawidłowo, podłączałem sondy oscyloskopu do pinów FPGA. W większych projektach taka metoda testowania układu jest uciążliwa lub nawet niemożliwa do realizacji. Dlatego w następnym odcinku zapoznamy się z analizatorem Reveal - jest to funkcjonalność programu Diamond, polegająca na umieszczeniu w FPGA analizatora, który bada interesujące nas sygnały, a następnie przesyła wyniki pomiarów przez JTAG do komputera, gdzie możemy je wygodnie analizować.
Dominik Bieczyński
leonow32@gmail.com
Czytaj więcej:
- Using User Flash Memory and Hardened Control Functions in MachXO2 Devices - https://bit.ly/3KxBcxe
- Using User Flash Memory and Hardened Control Functions in MachXO2 Devices Reference Guide - https://bit.ly/3KxBcxe
- MachXO2 sysCLOCK PLL Design and User Guide - https://bit.ly/3j1Bml5