NIOS II na maXimatorze, czyli mikroprocesor w układzie FPGA (2). Wyjście na świat, czyli obsługa GPIO i inne praktyczne porady

NIOS II na maXimatorze, czyli mikroprocesor w układzie FPGA (2). Wyjście na świat, czyli obsługa GPIO i inne praktyczne porady
Pobierz PDF Download icon

W poprzednim artykule zaimplementowaliśmy w układzie FPGA podstawowy system mikroprocesorowy oparty o rdzeń NIOS II. Cóż jednak po naszej ciężkiej pracy, skoro nasz procesor potrafi tylko i wyłącznie komunikować się ze środowiskiem programistycznym, nie potrafiąc przy tym nawet zamigać diodą? Teraz przyszedł czas, aby to zmienić!

Aby móc zrealizować zadanie, o którym wspominałem we wstępie przyda się, aby nasz procesor wyposażony został w kilka wyprowadzeń GPIO (ang. General Purpose Input Output – wejście/wyjście ogólnego przeznaczenia). W tym momencie, albo możemy przygotować nowy system według wskazówek z poprzedniego spotkania, albo wykorzystać przygotowany wtedy projekt. Tym razem podążymy tą drugą ścieżką, aby przy okazji nauczyć się modyfikowania projektu Qsys.

Czas na GPIO

Na początek otwieramy plik projektu Quartusa (*.qpf). Wybieramy File ’ Open… i odszukujemy plik projektu systemu (*.qsys). Teraz wyszukujemy komponent PIO (Parallel I/O), a następnie klikamy Add… Zostaje wyświetlone okno konfiguracji tego komponentu, jak na rysunku 1. Teraz pokrótce omówmy ustawienia tego komponentu:

  • Width – określamy tutaj szerokość, czyli liczbę bitów nowo tworzonego portu. Możemy utworzyć pojedynczy port o szerokości od 1 do 32 bitów.
  • Direction – określa kierunek (konfigurację) potu. Do wyboru mamy opcje:
  • Bidir – port 2-kierunkowy, w którym kierunek każdego z pinów może być wybrany oddzielnie (wejściowy lub wyjściowy) z poziomu programu i może być w dowolnym jego momencie zmieniany. Pozostałe opcje nie pozwalają na kontrolę kierunku pinów z poziomu programu.
  • Input – wszystkie piny portu są wejściami.
  • InOut – tworzone są 2 porty o podanej szerokości, z których jeden jest wejściowy a drugi wyjściowy. Czyli w efekcie podając np. szerokość 4 bitów zostanie utworzony 4-bitowy port wejściowy i drugi 4-bitowy port wyjściowy.
  • Output – wszystkie piny portu są wejściami.
  • Output Port Reset Value – wartość, którą przyjmą piny portu po restarcie układu. Podajemy ją w zapisie heksadecymalnym. Ustawienie takie ma szczególne znaczenie w sytuacjach, gdy chcemy mieć pewność jak zachowa się nasze urządzenie przed uruchomieniem się programu, aby np. silnik, który jest sterowany określonym pinem nie zaczął się przez chwilę kręcić.
  • Enable individual bit setting/clearing – to ustawienie powoduje dodanie dodatkowych rejestrów, które umożliwiają szybkie ustawienie stanu wysokiego lub niskiego na danym wyjściu, bez konieczności odczytu poprzedniego stanu portu, wykonania operacji logicznych w programie i zapisu nowego stanu.
  • Synchronously capture – powoduje dodanie do naszego portu modułu wykrywania zboczy na pinach. Informacje o tym, że wystąpiło określone zbocze dostępne są potem w odpowiednim rejestrze, bez konieczności ciągłego monitorowania pinu w programie.
  • Edge Type – wybieramy typ zbocza, które chcemy wykrywać (FALLING – opadające, RISING – narastające, ANY – narastające i opadające).
  • Enable bit-clearing for edge capture register – umożliwia kasowanie pojedynczych bitów w rejestrze wykrywania zboczy.
  • Generate IRQ – powoduje, że dodany zostaje moduł generowania przerwań wraz z rejestrem maskującym. Rejestr maskujący pozwala na wybranie, które piny mają generować przerwanie, a które mają być ignorowane. O przerwaniach dowiemy się nieco szerzej w kolejnych częściach naszego kursu.
  • IRQ Type – wybieramy kiedy ma być generowane przerwanie: LEVEL – stan wysoki na pinach, które są wybrane poprzez rejestr maskujący powoduje generowanie przerwania tak długo, jak dowolny z tych pinów pozostaje w stanie wysokim;EGDE – korzysta z opisanego wcześniej modułu wykrywania zboczy (który musi być aktywny i w którym musimy wybrać typ zbocza). Przerwanie jest generowane tak długo, aż nie zostaną skasowane wszystkie bity w rejestrze wykrywania zboczy.
  • Test bench wiring – pozwala na zadanie konkretnej stałej wartości pinów wejściowych w czasie symulacji

Pełną dokumentację każdego modułu zawsze możemy uzyskać klikając na Documentation i później korzystając z podanych tam linków do dokumentacji online. Czasem po załadowaniu strony musimy poczekać dłuższy moment, aż przeglądarka przejdzie do interesującej nas sekcji w dokumentacji – kliknięcie myszą czy poruszenie kółkiem myszy może spowodować że nie zostaniemy automatycznie przeniesieni do interesującej nas sekcji.

W naszym przykładowym projekcie chcemy sterować czterema LEDami na płytce maXimatora, zatem wybieramy port o szerokości 4 bitów pracujący jako wyjście, z wartością początkową 0xA (binarnie 1010). Zaznaczamy także opcję Enable individual bit…. Klikamy na Finish.

Następnie musimy wykonać połączenia odpowiednich portów naszego modułu. Łączymy clk z tym samym sygnałem, co zegar taktujący rdzeń. Podobnie postępujemy z sygnałem reset. Port s1 łączymy z data_master rdzenia. Pozostało nam jeszcze dokonać „eksportowania” external_connection. W tym celu klikamy dwukrotnie na Double-click to export w odpowiednim wierszu i podajemy nazwę, pod jaką nasz port ma być widoczny na zewnątrz procesora. Warto też zmienić nazwę modułu na bardziej znaczącą – ja w obu miejscach wpisałem LED. Połączenia powinny wyglądać, jak na rysunku 2. Moduły możemy przenosić w górę lub w dół na naszym „schemacie” za pomocą niebieskich strzałek w lewej części okna System Contents.

Po zakończeniu tej czynności wybieramy System ’ Assign Base Addresses, a następnie klikamy na Finish. W oknie Save System Completed wybieramy Close, a w kolejnym odpowiadamy twierdząco na pytanie o to, czy wygenerować system po modyfikacjach. Następnie klikamy w kolejnych (już znajomych oknach) Generate a potem Finish. Zostanie pokazane również nam znane okno informujące o konieczności dołączenia wygenerowanego pliku do projektu, jednak jest on już dodany do projektu, więc wystarczy zamknąć wyświetlone okno.

Czego jeszcze brakuje nam od strony sprzętowej? Rzecz jasna – przypisania utworzonego przez nas przed chwilą portu do konkretnych wyprowadzeń układu FPGA. Wzorem poprzedniego projektu najpierw uruchamiamy proces Analysis & Synthesis, a po jego zakończeniu uruchamiamy Assignments ’ Pin Planner. Tam powinniśmy zobaczyć nowe piny o nazwach led_export[…]. Przypisujemy im lokalizacje zgodne z projektem płytki, czyli M16, N16, P16, R16. Następnie musimy ponownie zmienić standard napięciowy wyjścia na 3.3-V LVTTL. Aby jednak nie robić tego dla każdego pinu z osobna, możemy nieco przyśpieszyć ten proces. Na początku wybieramy klikamy prawym przyciskiem myszy na jednym z interesujących nas pinów i zaznaczany Node Properties. Niestety, to okno często zostaje wyświetlone „zwinięte” pod Pin Legend, po prawej stronie. Czasem także różny jest tytuł tego okna (Node albo Pin Properties – rysunek 3). Musimy rozciągnąć okienko, po czym (z wciśniętym Ctrl lub Shift) możemy zaznaczyć wszystkie interesujące nas piny i ustawić w niedawno otwartym okienku wspomniany wcześniej standard wyprowadzeń.

Zamykamy Pin Planner’a i uruchamiamy Compile Design, po czym wgrywamy konfigurację do układu. Właśnie wyposażyliśmy nasz procesor w 4 piny GPIO przyłączone do diod LED na naszej płytce, z których 2 powinny świecić, a dwie pozostawać zgaszone.

Kolej na miganie – piszemy pierwszy program

Teraz przyszła kolej na napisanie pierwszego programu obsługującego właśnie dodany przez nas port PIO. W tym celu, jak poprzednio, uruchamiamy środowisko NIOS II Software Build Tools for Eclipse. Nasz projekt z poprzednich zajęć powinien być już czytany do środowiska. Jeśli jednak tak nie jest musimy go zaimportować. Wybieramy File ’ Import…, z folderu General wybieramy Existing Project into Workspace. Następnie, w Search root directory wybieramy folder, który zawiera podfoldery z nazwą projektu oraz nazwą projektu z sufiksem _bsp. W polu Projects powinny pojawić się 2 pozycje, z zaznaczonymi polami wyboru, jak na rysunku 4. Klikamy Finish i po chwili w naszym środowisku mamy oba projekty. Jeśli prostokąty wyboru są „wyszarzone” oznacza to, że nazwy projektu się dublują (patrz porada poniżej).

Jeśli decydujemy się kopiować projekty to należy pamiętać, że w obrębie danego workspace nie mogą znajdować się 2 projekty o takich samych nazwach. Rozwiązaniem tego jest albo utworzenie innego workspace, albo usunięcie projektów, z którymi nie chcemy pracować (poprzez kliknięcie na nich PPM i wybranie Delete). Uważajmy jednak, aby opcja Delete project contents on disk … nie była zaznaczona, gdyż w przeciwnym razie nieodwracalnie skasujemy pliki projektu i nie będzie możliwe ich ponowne zaimportowanie. Jeśli wszystko zrobimy poprawnie i w przyszłości będziemy chcieli wrócić do jakiegoś projektu o dublującej się nazwie – wystarczy wykonać import, jak pokazano powyżej. Równie dobrze moglibyśmy teraz wykonać nowy projekt, jednak kontynuując/importując ten, nad którym pracowaliśmy chcę pokazać, jak należy postępować w wypadku zmiany strony sprzętowej procesora.

Na początku zaczynamy od kliknięcia PPM na projekt z końcówką _bsp (u mnie Tutorial01_bsp) i wybrania Nios II ’ Generate BSP. Spowoduje to aktualizację pakietu BSP, która uwzględni dokonane przez nas zmiany. Możemy podejrzeć plik system.h z projektu BSP i znaleźć tam ważne informacje o nowo dodanym module (rysunek 5). Widzimy tu m. in. informacje o adresie bazowym, o tym, jakie funkcje włączyliśmy podczas konfiguracji lub jaki jest stan początkowy tego portu.

W projekcie BSP w folderze drivers/inc/ znajdziemy plik altera_avalon_pio_regs.h. Zawiera on definicje makr, które ułatwią nam komunikację z modułem (rysunek 6). Makra parami pozwalają na odczyt i zapis odpowiednich rejestrów, o których pisałem wcześniej.

Teraz pora na napisanie kilku linijek programu – pokazano je na rysunku 7. Pierwszy plik nagłówkowy jest nam już dobrze znany, drugi definiuje funkcje odpowiedzialne za standardowy strumień wejścia/wyjścia (czyli u nas funkcję, która pozwala na wysyłanie znaków przez debugger). Kolejny plik nagłówkowy zapewnia nam dostęp m.in. do funkcji generującej opóźnienie. Zawartość ostatniego z plików nagłówkowych także znamy. W programie deklarujemy zmienną i, którą za każdym obiegiem pętli inkrementujemy. Dodatkowo jej zanegowaną wartość wyświetlamy na naszych diodach. Dlaczego jednak zanegowaną? Powód tej negacji tkwi w sposobie zasilania LEDów – są one zaświecane wtedy, gdy wyjście układu FPGA jest wyzerowane.

Zanim wgramy program do naszego procesora muszę jeszcze przekazać jedną ważną uwagę, dotyczącą funkcji ALT_USLEEP. Po pierwsze, jest ona dosyć niedokładna – sekunda wcale nie jest sekundą. Po drugie – generowanie opóźnień w ten sposób w programie jest naganną praktyką. W skrócie – poza celami demonstracyjnymi, jeśli w programie znajdzie się tego typu funkcja, oznacza to, że program jest napisany źle. Zwykle do odmierzania czasu używa się timerów, ale o tym opowiem nieco później.

Teraz, ponieważ zmieniliśmy strukturę procesora (a co za tym idzie zmieniły się parametry identyfikacyjne systemu), musimy wejść do Run ’ Debug Configurations, tam w zakładce Target Connection kliknąć na Refresh Connections (oczywiście na liście po lewej stronie powinna być podświetlona konfiguracja Tutorial). Klikamy na Appy a potem Close. Teraz zapisujemy wszystkie zmiany w plikach (File ’ Save All), kompilujemy nasz projekt (Project ’ Build All) i na koniec, jak poprzednio, rozpoczynamy debuggowanie i uruchamiamy program za pomocą zielonej strzałki.

Diody na płytce powinny zacząć bardzo powoli migać, realizując zliczanie w kodzie binarnym. Sukces! Po zakończonej obserwacji nie zapomnijmy zatrzymać debuggera za pomocą czerwonego kwadratu.

Jeszcze wejście

Teraz dla wprawy wprowadźmy jeszcze do naszego systemu dodatek – 3 bitowy port wejściowy, który posłuży nam do odczytywania danych z przycisków (możemy podpiąć je przewodami do układu lub skorzystać z nakładki maXimator Expander).

Otwórzmy Qsys, a w nim nasz projekt. Dodajemy do niego komponent PIO, z szerokością 3 bitów oraz funkcją wejścia. Resztę parametrów pozostawiamy bez zmian, wykonujemy połączenia analogicznie jak w wypadku modułu LED. Zmieńmy nazwę modułu np. na SW, oraz pod taką samą nazwą dokonajmy „eksportowana” external_connection. Na sam koniec System ’ Assign Base Addresses.

Identycznie, jak poprzednio zamykamy Qsys jednocześnie wykonując generowanie systemu. Uruchamiamy Analysis & Synthesis (jeśli program zapyta nas, czy chcemy powtórzyć ten proces, odpowiadamy twierdząco – przecież dokonaliśmy modyfikacji), po czym w Pin Planner musimy przypisać do wyprowadzeń sw_export[…] odpowiednie piny układu. Niech do najmniej znaczącego bitu będzie podpięty będzie przycisk RES (pin R15), do kolejnego bitu L (B16) a do najwyższego bitu R (B15). Nie zapomnijmy o zmianie standardu na 3.3-V LVTTL. Po zakończeniu operacji nowo dodane linie powinny wyglądać jak na rysunku 8.

Teraz uruchamiamy Compile Design. W czasie oczekiwania na jej zakończenie możemy przejść do środowiska Eclipse i wykonać generowanie BSP (mam nadzieję, że pamiętacie, gdzie trzeba kliknąć). Jeśli wszystko wykonaliśmy prawidłowo w pliku system.h powinny zostać wyświetlone wpisy odpowiedzialne za nowo dodany moduł SW.

W pliku main.c dokonujemy prostej modyfikacji (rysunek 9) – zmniejszamy czas oczekiwania oraz dodajemy funkcję odczytującą stan przycisków i w zależności od niego zwiększająca, zmniejszającą lub resetującą wartość naszego licznika. Mam nadzieję, że analiza tego programu w ramach ćwiczenia nie sprawi wielkich trudności.

Teraz czas na zapisanie zmian i skompilowanie programu. Po niej, ponieważ znów zmodyfikowaliśmy nasz procesor, musimy wejść do stawień debuggera i wykonać Refresh Connections. Ooops… Coś nie działa tak jak powinno (rysunek 10)?

Wręcz przeciwnie – system pokazuje nam informację, wynikającą z faktu, że… nie wgraliśmy nowego systemu do układu FPGA – próbujemy wgrać nowy program do starego procesora, który przecież nie miał żadnych portów wejściowych. Na szczęście, system uratował nas przed tą katastrofą. Szybko naprawiamy błąd wgrywając konfigurację układu FPGA i potem rozpoczynając procedurę uruchomienia debuggowania od początku.

Nasz program napisany jest znów w sposób demonstracyjny i nie reaguje na przyciśnięcia przycisku, ale za każdym obiegiem pętli sprawdza, czy trzymamy przycisk. To znów nie jest dobra praktyka (podobnie jak opóźnienie), ale w tej sytuacji stosuję ją abyśmy szybko mogli sprawdzić efekty naszej pracy, bez zbędnego wysiłku programistycznego.

Refresh Connectins jest wymagane tylko wtedy, gdy zmieniamy projekt systemu w Qsys. Jeśli nie zmieniamy systemu, a jedynie zajmujemy się oprogramowaniem po prostu

Programie, nie uciekaj, czyli jak sprawić, aby nasz program pozostał w układzie na dłużej

Nasze dotychczasowe wysiłki wydają się być ekstremalnie nietrwałe – wystarczy odłączyć zasilanie i tracimy całą naszą pracę. Aby temu zapobiec musimy po pierwsze zdać sobie sprawę z tego, że nasz projekt teraz składa się z 2 części – konfiguracji układu FPGA, zawierającej opis naszego procesora oraz oprogramowania działającego w procesorze. O ile, aby trwale skonfigurować układ FPGA wystarczy za pomocą programatora wgrać plik *.pof zamiast *.sof to, aby program także stał się trwały, musimy dołączyć go do plików konfiguracyjnych procesora.

Na początek, jeszcze w Eclipse, klikamy PPM na nasz projekt (bez _bsp) i wybieramy Make Targets ’ Build…. Wybieramy mem_init_generate i klikamy na Build. Spowoduje to wygenerowanie pliku z naszym programem, który zaraz będziemy wykorzystywać dalej.

Przechodzimy do Quartus’a i wybieramy Assignments ’ Device. Tam klikamy na Device and Pin Options…, w nowo otwartym oknie wybieramy kategorię Configuration i w polu Configuration mode wskazujemy Single Uncompressed Image with Memory Initialization…. Dwukrotnie klikamy w OK. Dzięki tej zmianie umożliwimy inicjalizację pamięci RAM po podłączeniu zasilania wskazaną przez nas zawartością. Z tą możliwością otwieramy Qsys, a w nim nasz projekt. Klikamy na nasza pamięć RAM w widoku systemu, a następnie zaznaczamy opcję Initialize memory content (rysunek 11).

Znów zamykamy Qsys po drodze generując nową wersję systemu. W Eclipse uruchamiamy Generate BSP, a następnie dokładnie jak poprzednio generujemy plik z naszym programem (mem_init_generate). Wracamy do Quartusa i wybieramy Project ’ Add/Remove files in Project…, wskazujemy na plik meminit.qip, który powinien znajdować się w folderze z projektem naszego oprogramowania, w podfolderze mem_init. Spowoduje to dodanie do ustawień projektu ścieżki, w której znajduje się plik z programem, dzięki czemu oprogramowanie będzie mogło zostać dołączone do projektu.

Na koniec rozpoczynamy Compile Design. Po zakończeniu całej procedury możemy wgrać do naszego układu FPGA plik *.sof lub *.pof. Pierwszy z nich spowoduje wgranie konfiguracji układu FPGA wraz z naszym programem sterującym diodami do pamięci RAM układu FPGA – program zacznie od razu działać po zakończeniu programowania, bez konieczności używania debuggera. Drugi plik powoduje wgranie konfiguracji układu FPGA wraz z naszym programem do pamięci Flash, dzięki czemu nasz mikroprocesor wraz z programem będą na stałe zapisane i będą uruchamiać się po podłączeniu zasilania.

Biorąc pod uwagę powyższe uwagi, warto znów otworzyć nasz projekt Qsys (można to zrobić także będąc w widoku HierarchyProject Navigator – wystarczy kliknąć PPM na najbardziej nadrzędnym elemencie i wybrać Qsys) i usunąć zaznaczenie z pola dotyczącego inicjalizacji pamięci RAM.

Zadania domowe

Im dalej w las tym więcej drzew, zatem teraz czeka Was nieco więcej zadań niż ostatnio:

  • Należy zapoznać się z funkcją ustawiania i kasowania poszczególnych bitów. Poeksperymentujcie z makrami z pliku altera_avalon_pio_regs.h: IOWR_ALTERA_AVALON_PIO_CLEAR_BITS oraz IOWR_ALTERA_AVALON_PIO_SET_BITS. Pierwsze z nich powoduje ustawienie na „0” wszystkich tych bitów, dla których w argumencie funkcji znajduje się „1”, zaś druga funkcja ustawia te bity na „1”.
  • Należy do naszego systemu dodać 2 kolejne porty PIO, które będą portami wyjściowymi: 4-bitowy, do sterowania wyborem wyświetlacza na płytce maXimator Expander (DS1..4), 8-bitowy, do sterowania segmentami wyświetlacza na płytce ekspandera (A..G oraz DP).
  • Należy przypomnieć sobie zasadę multipleksowania wyświetlaczy 7-segmentowych (można korzystając z posiadanych już informacji spróbować napisać prosty program, sterujący tym wyświetlaczem).

Jak pewnie niektórzy się już domyślają, w kolejnej części zmierzymy się ze sterowaniem programowym tego wyświetlacza oraz tematyką timerów i przerwań.

Piotr Rzeszut, AGH

---

Uwaga!

Należy zachować ostrożność w momencie modyfikowania systemu – plik z programem nie aktualizuje się samoczynnie (za każdym razem trzeba wywołać mem_init_generate) oraz nie jest wykonywane sprawdzenie, czy został on skompilowany pod prawidłowy system. Najlepiej używać opcji dołączania programu do konfiguracji układu FPGA tylko i wyłącznie w sytuacji, gdy chcemy finalnie zaprogramować układ FPGA, a nie w fazie projektowania.

Programowanie plikiem *.pof należy wykonywać także wyłącznie w momencie, gdy chcemy, żeby nasza aplikacja działała w pełni autonomicznie. Pamięć FLASH ma ograniczoną ilość cykli programowania oraz proces ten przebiega znacznie wolniej, niż w przypadku pamięci RAM i plików *.sof.

Artykuł ukazał się w
Elektronika Praktyczna
styczeń 2018
DO POBRANIA
Pobierz PDF Download icon

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik listopad 2021

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio listopad - grudzień 2021

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka Podzespoły Aplikacje listopad 2021

Automatyka Podzespoły Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna listopad 2021

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich grudzień 2021

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów