Kurs FPGA Lattice (2). Pierwszy projekt

Kurs FPGA Lattice (2). Pierwszy projekt

W tym odcinku kursu utworzymy bardzo prosty projekt, aby zaprezentować cały proces tworzenia aplikacji na FPGA – od otwarcia programu Lattice Diamod do wgrania gotowego bitstreamu do pamięci FPGA.

Utworzenie projektu

Zacznijmy od uruchomienia Lattice Diamond i utworzenia nowego, pustego projektu. Po otwarciu programu Diamond pokazuje nam się strona startowa Start Page, gdzie umieszczone są linki do różnych instrukcji. Na środku ekranu widoczne są przyciski Open, New i lista ostatnio otwieranych projektów. Klikamy przycisk New. Uruchamia się kreator tworzenia nowego projektu. Klikamy Next. Następnie musimy podać nazwę projektu oraz wskazać folder na dysku, w którym będzie on umieszczony. Proponuję projekt nazwać HelloWorld i umieścić go w katalogu o takiej samej nazwie.

Następnie musimy podać nazwę implementacji. Implementacje to są wersje projektu, różniące się na przykład kodem źródłowym, ustawieniami syntezy, optymalizacji i innymi parametrami. Różne implementacje mogą wykorzystywać te same pliki lub każda implementacja może mieć swoje własne pliki źródłowe. Do tego tematu wrócimy za jakiś czas, a na potrzeby naszego pierwszego projektu zostawmy domyślną nazwę impl1. Klikamy Next.

W kolejnym kroku mamy możliwość dodania do projektu istniejących już plików. Możemy w tym momencie dodać utworzone wcześniej moduły. W zależności, czy zaznaczona jest opcja Copy source to implementation directory możemy te pliki źródłowe używać globalnie i dzielić je pomiędzy różnymi projektami lub możemy utworzyć ich kopię i zapisać w katalogu projektu. Nie dodajemy żadnych plików i klikamy Next.

Kreator pyta nas jaki układ FPGA chcemy zastosować. W moim przykładzie będę pracował z układem LCMXO2-1200HC-4TG100C. Czytelnik może wybrać inny układ, w zależności od używanej płytki deweloperskiej. Po prawej stronie okna wyświetla nam się skrócona specyfikacja wybranego układu oraz informacje o peryferiach, jakimi on dysponuje. W dolnej części możemy dodać układ ASC – jest to programowalny manager zasilania. Umożliwia sterowanie źródłami napięć zasilających, monitorowanie prądów, temperatury, itp.

W naszym projekcie nie będziemy stosować tych układów, więc w polu External ASC Number wpisujemy liczbę 0. Klikamy Next.

Kolejnym krokiem jest wybór syntezatora. W podstawowej wersji Lattice Diamond umożliwia zastosowanie dwóch:

  • Lattice LSE – syntezator opracowany przez firmę Lattice. Działa szybko i jest dobrze zintegrowany ze środowiskiem Diamond. Jego wadą jest brak obsługi języka SystemVerilog.
  • Synplify Pro – czas syntezy nawet prostego projektu jest wielokrotnie dłuższy niż w przypadku Lattice LSE, jednak umożliwia uzyskanie bitstreamu zajmującego mniej zasobów logicznych. Obsługuje SystemVerilog. Wadą jest generowanie bardzo większej liczby ostrzeżeń dotyczących braku zdefiniowania wielu różnych parametrów, co dla początkujących może być irytujące.

Wybieramy Lattice LSE i klikamy Next. Pojawia się podsumowanie wszystkich wybranych opcji. Klikamy Finish.

Pliki źródłowe

W lewym panelu widoczna jest struktura projektu w postaci drzewka, które pokazano na rysunku 1. Pierwsza pozycja to układ scalony LCMXO2-1200HC-4TG100C. Jeżeli zajdzie potrzeba zmiany układu, wystarczy kliknąć tę pozycję prawym przyciskiem myszy i wybrać Edit.

Rysunek 1. Widok lewego panelu

Poniżej mamy strategie. Strategia to zestaw ustawień wykorzystywanych przez toolchain. Dostępnych jest kilka domyślnych strategii, kładących nacisk na różne aspekty optymalizacji, takie jak liczba zajętych zasobów logicznych, szybkość pracy, itp. Aby zmienić wybraną strategię, klikamy ją prawym przyciskiem myszy i następnie klimaty Set as Active Strategy. Możemy także utworzyć kopię domyślnych strategii wybierając Clone strategy, by ją później edytować po wybraniu opcji Edit. Możemy pozostawić aktywną domyślną strategię – Strategy1.

Następna pozycja to nasza implementacja impl1. Każda implementacja zawiera foldery z różnymi plikami. Są to:

  • Input files – pliki z kodem źródłowym w języku Verilog, SystemVerilog lub VHDL;
  • Synthesis constraint files – są to pliki z dodatkowymi informacjami dla syntezatora, jakich nie uwzględnia się w plikach z kodem źródłowym. Na przykład są to informacje o częstotliwości sygnałów zegarowych czy maksymalny czas propagacji sygnału pomiędzy wejściem i wyjściem;
  • LPF Constraint Files – zwykle jest tu tylko jeden plik, w którym zapisane są informacje pozwalające połączyć abstrakcyjny opis w języku Verilog z rzeczywistymi peryferiami układu FPGA. W szczególności są to powiązania sygnałów wejściowych i wyjściowych z numerami pinów układu. Dodatkowo mogą się pojawić informacje czy wybrane przez nas piny wejściowe mają mieć włączone rezystory pull-up, pull-down, z jakim napięciem mają pracować, czy mają mieć włączoną histerezę i cały szereg innych sprzętowych ustawień;
  • Debug files – są to pliki analizatora logicznego Reveal Analyzer. Ta funkcjonalność polega na umieszczeniu w układzie FPGA dodatkowego modułu, który obserwuje wybrane przez nas sygnały i przekazuje o nich informacje do komputera poprzez interfejs programujący JTAG. Analizatorowi Reveal poświęcimy osobny odcinek kursu;
  • Script files – są to skrypty automatyzujące różne zadania, wykorzystywane m.in. przez symulator ModelSIM.
  • Analysis files – w tym miejscu będą zapisane pliki analiz wykonanych po przeprowadzeniu syntezy. Znajdziemy tu pliki Power Calculatora, który wyliczy, ile energii będzie zużywał układ FPGA oraz do jakiej temperatury się nagrzeje;
  • Programming files – tutaj są skrypty używane przez programator. Istnieje dużo sposobów programowania układów FPGA. W przypadku MachXO2 bitstream możemy wgrać do pamięci flash, do pamięci RAM lub do zewnętrznego układu pamięci. Programator JTAG oferuje możliwość zaprogramowania wielu różnych układów połączonych w łańcuch i wykorzystujących to samo złącze programujące. Dobrze jest sobie zrobić dwa skrypty programatora. Jeden do programowania pamięci flash i drugi do programowania pamięci RAM, ponieważ to oszczędza czas podczas testowania wielu różnych modyfikacji.

Kod w języku Verilog

W naszym pierwszym projekcie zrobimy prostą demonstrację bramek logicznych. Sygnały wejściowe będziemy dostarczać dwoma przyciskami, a działanie układu będą sygnalizować diody LED tak, jak na schemacie pokazanym na rysunku 2. Dodajmy pierwszy plik z kodem źródłowym. Klikamy prawym przyciskiem myszy na Input files i wybieramy Add New File. Z listy wszystkich dostępnych rodzajów plików wybieramy Verilog File, po czym wpisujemy nazwę pliku top. Przyjęło się, że pierwszy i najważniejszy plik projektu nazywa się top.

Rysunek 2. Schemat ilustrujący działanie pierwszego projektu

Automatycznie otworzy się edytor pliku top.v. Kod naszego pierwszego projektu pokazano na listingu 1. Język Verilog jest językiem opisu sprzętu. Kodu w Verilogu nie należy rozumieć tak, jakby to był język programowania. W wielu sytuacjach kolejność linii kodu nie ma znaczenia, ponieważ prowadzą one do wygenerowania układów logicznych, które zostaną fizycznie umieszczone w FPGA i które będą działać jednocześnie.

Listing 1. Plik top.v zgodny ze standardem Verilog 2001

module top(
input Button1, // Wejścia z pull-down
input Button2,
output LED1, // Diody LED
output LED2,
output LED3,
output LED4
);

// Przypisania ciągłe
assign LED1 = Button1;
assign LED2 = ~Button1; // Not
assign LED3 = Button1 & Button2; // And
assign LED4 = Button1 | Button2; // Or

endmodule

Kod w języku Verilog podzielony jest na moduły. Każdy moduł ma wejścia, wyjścia oraz opis, w jaki sposób wejścia są powiązane z wyjściami. Słowo-klucz module informuje syntezator, że rozpoczynamy opis nowego modułu. Następnie podajemy nazwę modułu i w nawiasach okrągłych definiujemy wszystkie porty wejścia i wyjścia.

  • input – wejście logiczne,
  • output – wyjście logiczne,
  • inout – linia dwukierunkowa.

W tym miejscu musimy zwrócić uwagę na to, że język Verilog ewoluował i na przestrzeni lat zmieniał się sposób definiowania portów modułu. Sposób pokazany na listingu 1 jest właściwy dla Veriloga w standardzie 2001 i późniejszych. W Verilogu 95 i wcześniejszych należałoby najpierw podać nazwy wszystkich porów, a dopiero później zdefiniować, który port jest wejściem, a który wyjściem. Ten sposób widać na listingu 2. W kursie będziemy używać tylko nowego sposobu, a stary podajemy tylko dla informacji, ponieważ w Internecie wciąż można znaleźć dużo kodów zapisanych starym sposobem.

Listing 2. Plik top.v zgodny ze standardem Verilog 95

module top(Button1, Button2, LED1, LED2, LED3, LED4);
input Button1; // Wejścia z pull-down
input Button2;
output LED1; // Diody LED
output LED2;
output LED3;
output LED4;

// Przypisania ciągłe
assign LED1 = Button1;
assign LED2 = ~Button1; // Not
assign LED3 = Button1 & Button2; // And
assign LED4 = Button1 | Button2; // Or

endmodule

Instrukcja assign powoduje przypisanie sygnału do innego sygnału lub większej liczby sygnałów połączonych różnymi operatorami logicznymi. Sygnał LED1 przepisaliśmy bezpośrednio do przycisku, więc dioda LED1 będzie świecić się, kiedy przycisk jest wciśnięty. Sygnał LED2 przypisany jest do tego samego przycisku, jednak zastosowaliśmy operator negacji, więc ta dioda będzie świecić się wtedy, kiedy przycisk będzie puszczony.

Synteza i implementacja

W okienku pokazanym na rysunku 1 klikamy etykietę Process, znajdującą się na dole okna. Powinna pokazać się lista procesów, tak jak na rysunku 3. Zaznaczamy opcję Bitstream file oraz JEDEC file, a resztę opcji wyłączamy. Nie będą teraz potrzebne.

Rysunek 3. Lista procesów

Proces przetwarzania kodu Veriloga na plik gotowy do wgrania do pamięci FPGA podzielony jest na cztery etapy:

  1. Synteza – polega na wygenerowaniu abstrakcyjnej sieci różnych bramek i przerzutników oraz listy połączeń między nimi;
  2. Mapowanie – powiązanie abstrakcyjnych bramek otrzymanych w poprzednim etapie z rzeczywistymi peryferiami wybranego przez nas układ FPGA;
  3. Place and route – wybór odpowiednich bramek, przerzutników i innych peryferiów na strukturze FPGA oraz wygenerowanie sieci połączeń pomiędzy nimi. Rozmieszczenie oraz połączenia generowane są zgodnie ze zdefiniowanym wcześniej wymogami co do czasu propagacji sygnałów, zadanych częstotliwości, itp.;
  4. Eksportowanie – generowanie finalnego pliku, który wgrywa się do pamięci FPGA. Będziemy korzystać głównie z dwóch typów. Bitstream file to plik, który wgrywa się do pamięci RAM. Wgrywa się go błyskawicznie, jednak po wyłączeniu zasilania trzeba wgrać go na nowo. JEDEC file to plik dla pamięci flash. Wgrywa się go trochę dłużej, ale zostaje do czasu, aż skasujemy pamięć i zastąpimy go innym plikiem.

Mając gotowy kod w języku Verilog, możemy uruchomić syntezę. Kliknamy dwukrotnie na Synthesize Design. Po chwili przy tej pozycji powinien pokazać się zielony ptaszek. Jeżeli pojawi się czerwony krzyżyk – przeglądamy logi w konsoli i szukamy błędu.

Zanim przejdziemy do kolejnych kroków, musimy zdefiniować z jakimi pinami mają być połączone abstrakcyjne sygnały Button1, Button2, LED1, LED2, LED3, LED4. W tym celu klikamy Tools i następnie Spreadsheet View, jak widać na rysunku 4. Otworzył się edytor sygnałów wejściowych i wyjściowych. W kolumnie Pin wpisujemy numery pinów, do których chcemy połączyć sygnały z kolumny Name. Zamiast numerów pinów można wpisać ich nazwy, jak np. PT9A, PR2A, jednak wpisywanie numerów jest wygodniejsze i szybsze.

Rysunek 4. Wygląd okna Spreadsheet View

Czytelnik powinien wpisać tu numery pinów zgodnie ze schematem używanej płytki deweloperskiej.

Następnie musimy określić typ elektryczny pinu. Do wyboru są standardowe wyjścia push-pull o różnych poziomach napięć, wyjścia różnicowe LVDS i parę innych możliwości. Lewym przyciskiem myszy klikamy pierwszą komórkę w kolumnie IO_TYPE i przeciągamy na dół, zaznaczając wszystkie komórki. Klikamy prawym przyciskiem myszy i wybieramy LVCMOS33.

W kolejnej kolumnie ustawiamy rezystory pull-up oraz pull-down. Przyciski przedstawione na rysunku 2 zwierają wejścia FPGA z zasilaniem, więc musimy włączyć rezystory pull-down. Zaznaczamy je podobnie jak w poprzednim akapicie i wybieramy DOWN. Diody LED nie potrzebują żadnych rezystorów podciągających, więc dla nich wybieramy NONE. Zapisujemy ustawienie, klikając ikonę dyskietki.

Wracamy do drzewka plików projektu. W katalogu LPF Constraint Files pojawił się plik HelloWorld.lpf. Jeżeli otworzysz ten plik, zobaczysz dokładnie te same ustawienia jakie podałeś w Spreadsheet View, ale w formie tekstowej. Może je także edytować w edytorze tekstu, podobnie jak kod programu. Czasami edycja tekstowa jest szybsza niż wyklikiwanie wszystkich ustawień w Spreadsheet. W Spreadsheet View istnieje możliwość skonfigurowania bardzo wielu rzeczy, takich jak sygnały zegarowe, czas propagacji sygnałów, itp.

Wgrywanie bitstreamu

Klikamy dwukrotnie Bitstream file na liście procesów. Po chwili pojawi się zielony ptaszek. Klikamy Tools i Programmer. Zostaniemy poproszeni o wybór programatora. Zakładając, że do komputera podłączony jest jeden programator, możemy śmiało kliknąć OK. Pokazuje się okno programatora, widoczne na rysunku 5. Programator powinien automatycznie rozpoznać układ i wyświetlić jego nazwę. W niektórych przypadkach rozpoznanie może być błędne, ponieważ istnieją podobne układy FPGA, które mają ten sam identyfikator, ale nie są identyczne! W takiej sytuacji nazwa układu zostanie podświetlona na żółto – trzeba ją kliknąć i ręcznie podać właściwą nazwę.

Rysunek 5. Wygląd okna programatora

W kolumnie Operation domyślnie ustawiona jest akcja FLASH Erase, Program, Verify. Wygenerowaliśmy bitstream dla pamięci RAM, więc musimy zmienić to ustawienie. Klikamy dwukrotnie to pole, a następnie w pozycji Access mode wybieramy Static RAM Cell Mode, a w pozycji Operation wybieramy SRAM Fast Program. Trochę niżej należy podać adres do pliku z bitsreamem. Powinien być automatycznie podany adres do pliku *.bit. Wisienką na torcie jest kliknięcie przycisku zielonej strzałki w dół (rysunek 5). Bitstream zostanie przesłany do FPGA i nasz układ wreszcie ożyje!

Możemy zapisać ustawienia programatora klikając przycisk dyskietki. W ramach ćwiczeń utworzymy drugi projekt programatora, przystosowany do wgrywania pamięci flash. W tym celu klikamy prawym przyciskiem myszy na Programming files, następnie wybieramy Add i New File. W kreatorze dodawania pliku klikamy Other files i Programmer Project File. Nazywamy plik wedle własnego uznania i podobnie jak w poprzednich akapitach, modyfikujemy akcje programatora. Wybieramy Flash Programming Mode i FLASH Erase, Program, Verify. Pamiętaj, że właściwym plikiem dla pamięci flash jest plik z rozszerzeniem *.jed!

Rysunek 6. Dwie konfiguracje programatora

Proponuję utworzyć dwa projekty programatora i zapisać je tak, jak pokazano na rysunku 6 – dzięki temu można szybko zaprogramować wybraną pamięć bez potrzeby wyklikiwania wszystkich ustawień od początku.

Netlist analyzer

Ostatnią rzeczą, jaką poznamy w tym odcinku kursu jest Netlist Analyzer. Jest to narzędzie, które pozwala przedstawić kod w postaci schematu, dzięki czemu łatwiej jest zrozumieć strukturę projektu i można szybciej znaleźć interesujący nas fragment kodu. Klikamy Tools i Netlist Analyzer. Powinniśmy zobaczyć schemat taki, jak na rysunku 7. Domyślnie otwiera się schemat RTL (Register Transfer Level), który jest ilustracją kodu z bramek, przerzutników i innych abstrakcyjnych elementów logiki cyfrowej. Kliknij dowolną bramkę prawym przyciskiem myszy, a następnie wybierz Jump to HDL file. Zostaniemy przeniesieni do edytora kodu, w którym będzie podświetlony fragment odpowiadający klikniętej bramce.

Rysunek 7. Schemat RTL w Technology View

Klikamy przycisk Technology View w pionowym pasku narzędzi po lewej stronie. Zostanie wyświetlony ten sam schemat, ale z użyciem zasobów układu FPGA. Widok pokazano na rysunku 8. W tym widoku bramki logiczne zostały pokazane jako elementy look up table (LUT), które w układach FPGA służą do wygenerowania dowolnych logicznych układów kombinacyjnych (LUT jest de facto multiplekserem, który ma 16 wejść połączonych z pojedynczymi bajtami pamięci RAM, 4 wejścia wybierające oraz jedno wyjście).

Rysunek 8. Schemat technologiczny w Netlist Analyzer

Pojawiło się kilka peryferiów, które mogą być ręcznie dodane do kodu Veriloga, ale jeśli ich nie zdefiniowano, to zostaną dodane automatycznie. Są to:

  • GSR – Global Set Reset,
  • PUR – Power Up Reset,
  • TSALL – Tristate All Pins,
  • VHI – stan logiczny 1, połączenie z zasilaniem,
  • VLO – stan logiczny 0, połączenie z masą.

Te peryferia zostaną omówione w kolejnych odcinkach.

Klikamy teraz przycisk Post-Mapping View w pasku narzędzi po lewej stronie. Zostanie pokazany schemat z użyciem fizycznych zasobów układu FPGA. Widzimy, że zostały wykorzystane dwa elementy slice – każdy z nich składa się z dwóch tablic LUT, kilku multiplekserów oraz dwóch przerzutników.

To wszystko w tym wydaniu EP! Choć ten odcinek był dosyć długi, to dopiero rozgrzewka!

Dominik Bieczyński
leonow32@gmail.com

Artykuł ukazał się w
Elektronika Praktyczna
grudzień 2022

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 marzec - kwiecień 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje marzec 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 kwiecień 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów