Jak to się robi w FPGA. Ekspresowy start z "miękkim" mikroprocesorem 32-bitowym

Jak to się robi w FPGA. Ekspresowy start z "miękkim" mikroprocesorem 32-bitowym

Mikroprocesory programowe implementowane w FPGA wprowadzają wiele nowych możliwości w dziedzinie układów programowalnych. Jedną z ich największych zalet jest możliwość łatwego połączenia aplikacji z logiką opisaną za pomocą takich języków jak Verilog czy VHDL w jednym układzie scalonym. W niniejszym artykule zostanie opisany przykład pokazujący, w jaki sposób zintegrować program napisany w języku C i wykonywany na mikroprocesorze Nios II z prostym sterownikiem diody RGB WS2812B zaimplementowanym w języku VHDL. Przykład został przygotowany w środowisku Quartus Prime Lite dla zestawu maXimator z układem 10M08DAF256C8G i dołączonym rozszerzeniem maXimator Expander. 

Nowy projekt z mikroprocesorem Nios II


Pracę zaczynamy od przygotowania nowego projektu w środowisku Quartus Prime Lite (File → New Project Wizard). W kolejnych oknach kreatora wybieramy domyślne opcje, a w zakładce Device układ 10M08DAF256C8GES, tak jak zostało pokazane na rysunku 1. Po utworzeniu nowego projektu wybieramy z menu opcję Assignments Device i klikamy na: Device and Pin Options → Configuration → Configuration Mode i wybieramy wariant Single Uncompressed Image with memory initialization. Pozwoli nam ona na późniejsze dodanie pamięci RAM z możliwością inicjalizacji początkowego stanu. Następnie do projektu dodajemy pusty schemat (File → New → Block Diagram/Schematic File), w którym będziemy umieszczać kolejne elementy systemu.

Rysunek 1. Tworzenie nowego projektu za pomocą kreatora

Pierwszy komponent, który zostanie dodany do projektu, będzie zawierał mikroprocesor Nios II oraz sterownik RGB. Zostanie on utworzony za pomocą narzędzia Platform Designer (Tools → Platform Designer), które znane jest z wcześniejszych wersji Quartusa jako Qsys. W nowym oknie, w zakładce System Contents, znajduje się lista wszystkich dodanych elementów, na której początkowo widnieje jedynie Clock Source, będący źródłem sygnału zegarowego i resetu dla wszystkich pozostałych podsystemów. Dodajmy zatem Niosa II, wybierając go z listy po lewej stronie (Library → Processors and Peripherals → Embedded Processors → Nios II Processor). Po dwukrotnym kliknięciu pojawia się nowe okno, w którym możemy dokonać odpowiedniej konfiguracji mikroprocesora. W pierwszej zakładce - Main, pokazanej na rysunku 2, można wybrać wersję procesora. W przykładzie została wykorzystana wersja Nios II/e o mniejszych możliwościach, lecz wymagająca zdecydowanie mniej zasobów logicznych i dostępna za darmo, bez licencji. Pozostałe ustawienia można pozostawić na razie bez zmian, zatwierdzając konfigurację przyciskiem Finish i tym samym dodając mikroprocesor jako kolejny podsystem do listy.

Rysunek 2. Wybór wersji mikroprocesora Nios II

Następnym elementem, który musimy dodać, jest pamięć RAM (Library → Basic Functions → On Chip Memory → On-Chip Memory (RAM or ROM)) - na potrzeby przykładu jej rozmiar został ustawiony na 16384 B. Pozostałe parametry pozostawiamy bez zmian, jak na rysunku 3. Ostatnim elementem bibliotecznym, który trzeba dodać do listy podsystemów, jest System ID Peripheral (Libary → Basic Functions → Simulation; Debug and Verification → Debug and Performance). Jego zadaniem jest ustalenie identyfikatora systemu, który jest sprawdzany, m.in. podczas programowania mikroprocesora. Jedynym jego parametrem jest 32-bitowy identyfikator, który może przyjąć dowolną wartość (rysunek 4).

Rysunek 3. Konfiguracja pamięci RAM

Rysunek 4. Konfiguracja identyfikatora systemu

Rysunek 5. Połączone sygnały zegara, resetu i magistral pamięci Niosa II

Mając dodane wszystkie potrzebne elementy biblioteczne, można przystąpić do łączenia sygnałów resetu, zegara oraz magistral pamięci pomiędzy poszczególnymi elementami. Sposób połączenia został przedstawiony na rysunku 5. Warto zwrócić szczególną uwagę na dwa źródła resetu: zewnętrzny sygnał clk_in_reset oraz debug_reset_request pochodzący z interfejsu JTAG i sposób prowadzenia magistral pamięci: instruction_master (pamięć programu) - do pamięci RAM oraz data_master (pamięć danych) - do pamięci RAM i pozostałych komponentów.

Rysunek 6. Ustawienie pamięci RAM używanej przez mikroprocesor Nios II

Po połączeniu sygnałów trzeba jeszcze ustawić w konfiguracji Niosa II pamięć, w której znajdą się wektory resetu i wyjątków. Można to zrobić w znanym już oknie konfiguracji (prawy klik myszy na nios2_gen2_0 → Edit) w zakładce Vectors, ustawiając wartości pól Reset vector memory i Exception vector memory na onchip_memory2_0.s1, czyli pamięć RAM, którą wcześniej dodaliśmy, tak jak zostało to pokazane na rysunku 6. Pozostałe błędy dotyczące pokrywających się przestrzeni adresowych dodanych komponentów możemy naprawić, wybierając opcję Assign Base Addresses z menu System.

Rysunek 7. Okno generowania plików syntezy

W tym momencie mamy skonfigurowany i gotowy do użycia mikroprocesor Nios II ze wszystkimi niezbędnymi do działania peryferiami w postaci jednego komponentu, który można dodać do projektu. Wystarczy go zapisać, a z menu Generate wybrać opcję Generate HDL, której okno zostało pokazane na rysunku 7. Tak wygenerowane pliki nie są automatycznie dodawane do projektu, dlatego w głównym oknie Quartusa należy wybrać opcję Project → Add/Remove Files in Project i dodać plik z rozszerzeniem qip, znajdujący się w katalogu z wygenerowanym systemem, np. nios_example/synthesis/nios_example.qip. Utworzony system można dodać do edytora schematów, klikając dwukrotnie w obszarze okna i w wyświetlonym dialogu wybierając plik z rozszerzeniem bsf znajdujący się wśród wygenerowanych poprzednio plików, np. nios_example/nios_example.bsf w katalogu głównym projektu. Teraz zajmiemy się dodaniem własnego komponentu i połączeniem go z Niosem II.

Sterownik RGB WS2812B

Układ WS2812B ma jedno wejście, na które należy podać odpowiedni przebieg, w którym zakodowana jest 24-bitowa wartość koloru (po 8 bitów na każdą składową). Przebieg ten oraz sposób kodowania każdego z bitów wraz z czasami trwania impulsów pokazane są na rysunku 8. Układy WS2812B można łączyć szeregowo, wówczas każdy z nich będzie przyjmował pierwsze 24 bity kodujące jego ustawienie, a pozostałe bity wystawiał na swoim wyjściu, które jest jednocześnie wejściem kolejnego elementu.

Rysunek 8. Sposób komunikacji z układem WS2812B

Sterownik w przykładzie będzie obsługiwał dwie połączone szeregowo diody RGB dostępne na płytce maXimator Expander. Z poziomu programu działającego na mikroprocesorze Nios II, będzie on widoczny pod postacią trzech rejestrów: dwóch służących do ustawiania kolorów na obu diodach i jednego zawierającego bity stanu zajętości i rozpoczęcia transmisji.

Tworzenie sterownika zaczynamy w oknie Platform Designera, dodając go do poprzednio utworzonego systemu z Niosem II. W oknie IP Catalog, po lewej stronie, należy kliknąć dwukrotnie na New Component… W nowo otwartym oknie edytora komponentów, w zakładce Component Type wpisujemy nazwę sterownika, natomiast w zakładce Signals & Interfaces definiujemy wszystkie sygnały potrzebne do połączenia sterownika z resztą systemu:

  • avalon_slave (Avalon Memory Mapped Slave)- magistrala pamięci
    • avalon_slave_address [2] - dwubitowa szyna adresu (dla trzech rejestrów steownika)
    • avalon_slave_read [1] - sygnał odczytu danych
    • avalon_slave_readdata [32] - szyna danych do odczytu
    • avalon_slave_write [1] - sygnał zapisu danych
    • avalon_slave_writedata [32] - szyna danych do zapisu
  • clock_sink (Clock Input)
    • clock_sink_clk [1] - wejście sygnału zegarowego
  • conduit_end (Conduit)
    • rgb_out [1] - wyjście sygnału dla układów WS2812B (Direction: output)
  • reset_sink (Reset Input)
    • reset_sink_reset [1] - wejście sygnału resetu

Rysunek 9. Tworzenie nowego komponentu

W ustawieniach interfejsu avalon_slave trzeba ustawić źródła sygnałów zegara i resetu na clock_sink i reset_sink, co można zobaczyć na rysunku 9.
Na koniec, w zakładce Files, na podstawie dodanych sygnałów tworzymy nowy plik VHDL za pomocą opcji Create Synthesis Filefrom Signals. Następnie klikamy Finish, zapisujemy wszystkie zmiany i dodajemy nowo utworzony komponent do listy podsystemów, dwukrotnie klikając na jego nazwę na liście po lewej stronie. Po jego dodaniu trzeba jeszcze podłączyć magistralę pamięci i sygnały zegara oraz resetu, podobnie jak w przypadku poprzednich komponentów. Sygnał wyjściowy, służący do sterowania układem WS2812B, trzeba wyeksportować, klikając dwukrotnie na niego w kolumnie Export. Ostatnie dwie czynności, jakie musimy wykonać, to generowanie adresów (System → Assign Base Addresses) i plików do syntezy oraz symbolu nowego komponentu (Generate → Generate HDL), tak jak zostało to opisane w poprzednim rozdziale. Ostateczny efekt powinien być taki jak na rysunku 10. Teraz możemy już zamknąć okno Platform Designera i zabrać się do tworzenia kodu sterownika.

Rysunek 10. System z dołączonym sterownikiem RGB

Jeżeli wcześniej dodaliśmy plik qip do projektu, to teraz musimy zaktualizować symbol, klikając na niego prawym przyciskiem myszy i wybierając opcję Update Symbol or Block. Wygenerowany plik VHD sterownika możemy otworzyć, korzystając z okna Project Navigator znajdującego się po lewej stronie, wybierając wyświetlanie listy plików w projekcie (opcja Files). Wygenerowany plik znajdziemy, rozwijając listę plików dodanych razem z nios_example.qip. Jeżeli nie ma go na liście, należy ponownie skorzystać z opcji Add/Remove Files in Project, usuwając i jeszcze raz dodając wspomniany plik qip. Po jego otwarciu zobaczymy wygenerowany kod zawierający listę wszystkich zdefiniowanych wcześniej sygnałów. W pliku tym należy zdefiniować architekturę sterownika, zastępując tę wygenerowaną domyślnie. Architektura sterownika użytego w przykładzie została pokazana na kilku kolejnych listingach.

  • rgbValue1 i rgbValue2 - 24-bitowe wartości koloru dla każdej diody,
  • ready - flaga bitowa ustawiana po ukończeniu wysyłania danych przez sterownik,
  • firstSent i secondSent - flagi bitowe oznaczające wysłanie odpowiednio pierwszej i drugiej 24-bitowej wartości,
  • counter - licznik służący do generowania impulsów dla wartości '0' i '1' oraz resetu,
  • steps - licznik wysłanych bitów,
  • rgbShiftReg - rejestr przesuwny, zawierający aktualnie wysyłane bity,
  • state - aktualny stan (s0 - oczekiwanie na rozkaz wysłania danych, s1 - wysyłanie danych).

Rysunek 11. Konfiguracja PLL dla częstotliwości wyjściowej 20 MHz

Potrzebne będą także piny: wejściowy dla sygnału zegarowego i wyjściowy dla sygnału sterującego diodami oraz symbol VCC dla nieużywanego w przykładzie sygnału reset_reset_n. Po podłączeniu pinów zmieniamy ich nazwy na CLK_IN i RGB_OUT. Rysunek 12 prezentuje gotowy schemat. Po wykonaniu kompilacji możemy przejść do testowania i uruchamiania przykładu.

Rysunek 12. Gotowy schemat systemu

Symulacja

Symulację wykonamy w narzędziu ModelSim dostępnym w menu Tools → Run Simulation Tool → RTL Simulation. W przykładzie ograniczymy się tylko do symulacji naszego sterownika. W oknie Library, po lewej stronie, odszukujemy sterownik znajdujący się wśród plików dodanych do projektu (nios_example → rgb_driver). Po jego dwukrotnym kliknięciu, w oknie Objects, powinniśmy zobaczyć nazwy wszystkich użytych w nim sygnałów, z których możemy wybrać te, których chcemy użyć w symulacji. Aby przeanalizować dokładne zachowanie sterownika, zaznaczmy wszystkie sygnały i przeciągnijmy je do okna Wave, w którym będą wyświetlane przebiegi.

Symulacją możemy sterować za pomocą przycisków znajdujących się na pasku narzędzi, m.in.:

  • Restart - czyści aktualne wyniki symulacji
  • Run Length - ustala długość kroku symulacji
  • Run - wykonuje krok symulacji.

Po wciśnięciu przycisku Run zobaczymy przebiegi sygnałów dodanych do okna Wave. Możemy więc ustawić wartości sygnałów wejściowych i przeanalizować stan sygnałów wewnątrz sterownika i zachowanie wyjścia sterującego diodami. Zaczniemy od ustawienia sygnału zegarowego - w tym celu klikamy prawym przyciskiem myszy na odpowiedni sygnał w oknie Wave (clock_sink_clk), wybieramy opcję Clock i ustawiamy okres na 50 ns (20 MHz). Jeżeli wartość pola Run Length ustawimy także na 50 ns, wówczas jeden krok symulacji będzie odpowiadał dokładnie jednemu okresowi zegara. Następnie ustawiamy wartości pozostałych sygnałów wejściowych, tak aby wykonać zapis do rejestru zawierającego kolor pierwszej diody. Zmianę stanu sygnału można wymusić, klikając prawym przyciskiem myszy na wybrany sygnał i wybierając opcję Force. W polu Value można podać wartość sygnału - domyślnie jest to wartość binarna, a wartość szesnastkowa wymaga przedrostka x i użycia cudzysłowu, np. x"2". Wpiszmy więc następujące wartości sygnałów:

  • avalon_slave_address - 01
  • avalon_slave_write - 1
  • avalon_slave_writedata - x"00AAAAAA"

W wyniku powyższych ustawień po jednym cyklu zegara zaobserwujemy zmianę wartości rgbValue1, a po kolejnym - rgbShiftReg. Podobnie możemy zmienić wartość rgbValue2, wykonując zapis pod adres 10. Na koniec ustawmy poniższe wartości sygnałów, które spowodują rozpoczęcie generowania sygnału wyjściowego rgb_out:

  • avalon_slave_address - 00
  • avalon_slave_write - 1
  • avalon_slave_writedata - x"00000002"

Można także wydłużyć długość kroku za pomocą wartości Run Length, aby jednym kliknięciem wygenerować większą liczbę okresów zegara. W ten sposób można sprawdzić, czy sygnał wyjściowy spełnia wymagania czasowe zawarte w dokumentacji układu WS2812B. Przykładowy wynik symulacji został przedstawiony na rysunku 13.

Rysunek 13. Okno symulatora ModelSim po dodaniu sygnałów


Uruchomienie przykładu

Po upewnieniu się, że symulacja pokazuje poprawne wyniki, możemy uruchomić przykład na zestawie maXimator. Wróćmy więc do głównego okna Quartusa i wybierzmy z menu Assignments opcję Pin Planner. W oknie, które zobaczymy, możemy dołączyć sygnały wejściowe i wyjściowe do fizycznych wyprowadzeń układu jak na rysunku 14:

  • wejście sygnału zegarowego - PIN_L3, 3.3-V LVTTL
  • wyjście sygnału sterującego - PIN_C15, 3.3-V LVTTL.

Rysunek 14. Przyporządkowanie pinów układu do projektu

Po skonfigurowaniu pinów ponownie kompilujemy projekt, podłączamy programator i programujemy układ za pomocą opcji Tools → Programmer lub odpowiedniego przycisku na pasku narzędzi (rysunek 15). Do programowania wybieramy plik output_files/nios_rgb_example.sof. Brakuje już tylko jednego - programu dla Niosa II, który będzie sterował diodami RGB.

Rysunek 15. Okno ustawień programatora

Program w C

Razem z Quartusem dostarczane jest środowisko Eclipse, w którym możemy przygotować BSP (Board Support Package) oraz program dla Niosa II. Możemy je otworzyć z menu Tools → Nios II Software Build Tools for Eclipse lub bezpośrednio z katalogu instalacyjnego Quartusa (nios2eds/bin/eclipse-nios2).

Rysunek 16. Okno kreatora BSP

Po otwarciu Eclipsa wybieramy File → New → Nios II Board Support Package. W oknie kreatora, przedstawionym na rysunku 16, podajemy nazwę projektu i ścieżkę do pliku sopcinfo, który znajduje się w głównym katalogu projektu. Zawiera on wszystkie niezbędne informacje o samym mikroprocesorze i o jego peryferiach, które przygotowaliśmy w poprzednich rozdziałach. Dzięki temu będziemy mieli przygotowane m.in. wszystkie definicje adresów. Klikamy przycisk Finish i czekamy na skompilowanie BSP. Następnie tworzymy nowy projekt aplikacji za pomocą File → New → Nios II Application. Podajemy nazwę projektu i ścieżkę do przed chwilą utworzonego BSP, po czym klikamy Finish.

W projekcie aplikacji nie ma żadnych plików źródłowych, więc klikamy prawym przyciskiem myszy na jego nazwę, wybieramy New → Source File i nadajemy mu nazwę main.c. W pliku tym umieszczamy kod przykładowej aplikacji obsługującej diody RGB, przedstawiony na listingu 4.

Listing 4. Kod przykładowej aplikacji
#include <stdint.h>
#include „system.h”
#include „io.h”

int main(void)
{
	uint32_t rgb1 = 0x00000000;
	uint32_t rgb2 = 0x00FFFFFF;

	while(1) {
		rgb1 += 0x00000001;
		rgb2 -= 0x00000001;

		if(rgb1 > 0x00FFFFFF)
			rgb1 = 0;
		if(rgb2 > 0x00FFFFFF)
			rgb2 = 0x00FFFFFF;

		IOWR(RGB_DRIVER_0_BASE, 1, rgb1);
		IOWR(RGB_DRIVER_0_BASE, 2, rgb2);
		IOWR(RGB_DRIVER_0_BASE, 0, 2);

		while((IORD(RGB_DRIVER_0_BASE, 0) 
		& 0x00000001) == 0);
	}
}

Dołączane pliki system.h oraz io.h pochodzą z wygenerowanego BSP i zawierają definicje adresów komponentów w systemie i makra służące do zapisu i odczytu rejestrów. W pętli głównej programu ustawiane są wartości kolorów, zmieniające się w każdym jej obiegu. Dodatkowo sprawdzany jest bit ready, mówiący o tym, że transmisja danych została zakończona. Zapis wartości 2 do rejestru RGB_DRIVER_0_BASE powoduje rozpoczęcie transmisji sygnału sterującego diodami.

Aby uruchomić program, klikamy prawym przyciskiem myszy na nazwę projektu po lewej stronie i wybieramy opcję Debug As → Nios II Hardware. Po uruchomieniu programu mamy dostęp do pracy krokowej i innych opcji debugowania. Efekty działania programu można sprawdzić za pomocą analizatora stanów logicznych - otrzymane przebiegi przedstawione są na rysunku 17.

Rysunek 17. Przykładowy przebieg na wyjściu RGB_OUT zarejestrowany
za pomocą analizatora stanów logicznych

Zewnętrzna magistrala pamięci

Na tym moglibyśmy zakończyć omawianie przykładu, jednak najpierw wprowadzimy do niego jedną zmianę. Wcześniej, za pomocą Platform Designera utworzyliśmy moduł składający się z Niosa II, sterownika RGB i innych peryferiów. Alternatywnym rozwiązaniem jest wyprowadzenie magistrali pamięci na zewnątrz systemu i utworzenie drugiego, zawierającego sterownik RGB. Dzięki takiemu podejściu można dołączać do magistrali pamięci inne systemy i modyfikować je bez potrzeby edycji pozostałych. Otwórzmy więc ponownie Platform Designera i utworzony wcześniej moduł. W pierwszej kolejności usuwamy z listy sterownik RGB. Zamiast niego dodajemy Library → Basic Functions → Bridges and Adaptors → Memory Mapped → Avalon-MM Pipeline Bridge. W oknie konfiguracji możemy ustawić m.in. szerokość szyny adresu - w naszym przypadku są to 4 bity kodujące zakres adresów 0x0000 - 0x000F. Okno konfiguracji zostało pokazane na rysunku 18.

Rysunek 18. Konfiguracja komponentu Avalon-MM Pipeline Bridge

Po dodaniu nowego elementu łączymy odpowiednio sygnały zegara, resetu i magistrale pamięci - sygnał s0 powinien być dołączony do data_master Niosa II, natomiast m0 wyeksportowany na zewnątrz modułu, tak jak na rysunku 19. Następnie należy ponownie wygenerować adresy: System → Assign Base Addresses i pliki syntezy: Generate → Generate HDL.

Rysunek 19. Nios II z komponentem Avalon-MM Pipeline Bridge

Kolejnym krokiem jest utworzenie nowego systemu: File → New System i dodanie do niego komponentu Avalon-MM Pipeline Bridge z takimi samymi ustawieniami, jak poprzednio, jednak tym razem przy łączeniu sygnałów należy wyeksportować sygnał s0. Na koniec dodajemy sterownik RGB, łączymy wszystkie sygnały jak na rysunku 20, zapisujemy nowy system, generujemy adresy i pliki syntezy. Możemy już zamknąć okno Platform Designera i przystąpić do modyfikacji schematu blokowego.

Rysunek 20. System ze sterownikiem RGB i komponentem Avalon-MM Pipeline Bridge

Na początek trzeba dodać nowo wygenerowany plik qip i zaktualizować istniejące komponenty, klikając prawym przyciskiem myszy i wybierając opcję Update Symbol or Block lub usuwając starą wersję i dodając nową. Dodajemy także nowy komponent zawierający sterownik i łączymy odpowiednie sygnały tak jak na rysunku 21. Podczas generacji plików dla nowego systemu ze sterownikiem został wygenerowany nowy plik VHDL z kodem sterownika. Musimy zatem przenieść zmiany z poprzedniej wersji - nasz kod znajduje się w pliku nios_rgb_example/nios_example/synthesis/submodules/rgb_driver.vhd, do nowego pliku znajdującego się w katalogu nios_rgb_example/rgb_example/synthesis/submodules/rgb_driver.vhd.

Rysunek 21. Schemat blokowy projektu z zewnętrzną magistralą pamięci

Ostatnim krokiem jest kompilacja projektu i symulacja, którą można wykonać podobnie jak w pierwotnej wersji przykładu. Aby uruchomić przykład, musimy także wygenerować nową wersję BSP i odpowiednio zmodyfikować kod programu, ponieważ zmieniły się definicje adresów, pod które musimy zapisywać ustawienia kolorów.

Krzysztof Chojnowski

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