Akcelerator grafiki 2D na FPGA i STM32F429
Oscylator
Każdy poważy projekt wykorzystujący FPGA potrzebuje oscylatora zapewniającego sygnał zegarowy. Tani oscylator kwarcowy nie wystarczy, musi być to układ wysokiej jakości. Te kosztują nieco więcej, ale wciąż są przystępne cenowo.
Wybrałem oscylator Fox FXO-HC73 pracujący z częstotliwością 40 MHz, który będzie podłączony do wejścia globalnego zegara FPGA. Układ FPGA ma dedykowane ścieżki o niskim opóźnieniu do rozprowadzenia sygnałów zegarowych, aby zapewnić ścisłą synchronizację wszystkich elementów taktowanych tym zegarem.
Cały projekt FPGA pracuje na częstotliwości 100 MHz, zatem wykorzystałem dostępny układ DCM, aby zamienić zegar 40 MHz na 100 MHz. Nie ma powodu, aby stosować koniecznie oscylator o częstotliwości 40 MHz – jest to po prostu tani układ, którego częstotliwość łatwo pomnożyć, aby uzyskać wartość 100 MHz. Oscylator nie jest jednocześnie tak szybki, aby powodować problemy na ścieżkach PCB.
Mikrokontroler
Wybrany mikrokontroler to model STM32F429VIT6, Jest to 32-bitowy układ firmy STMicroelectronics oparty na rdzeniu ARM Cortex-M4 taktowany zegarem 140 MHz. Zawiera pamięć Flash o pojemności 2 Mb oraz dedykowaną jednostkę zmiennoprzecinkową (FPU), która realizuje operacje dodawania i mnożenia liczb pojedynczej precyzji w jednym cyklu.
Jest to wspaniały mikrokontroler, który wybrałem ze względu na wysoką częstotliwość taktowania rdzenia i dużą liczbę dostępnych zasobów. Całą logika gry musi zostać przetworzona w ograniczonym odcinku czasu, zatem duża szybkość zegara jest tu kluczowa. Najprawdopodobniej wybór tego modelu jest przesadą. W modelu F429 znajduje się już akcelerator Chrome-ART, którego funkcja pokrywa się w dużej mierze z zadaniami akceleratora bitmap. Ostatecznie wolałem zapewnić sobie margines bezpieczeństwa i wybrać najszybszy mikrokontroler STM32 dostępny w tej chwili.
Programowanie tego układu jest proste – wystarczy wyprowadzić piny SWD tak, aby można było do nich podłączyć programator ST-Link/v2 i wykorzystać aplikację OpenOCD.
Ten mikrokontroler jest dostępny w wielu różnych obudowach. Jedną z wersji z najmniejszą liczba wyprowadzeń jest LQFP-100. Nie jest to wcale mało, ale na tej płytce znajdują się już obudowa z setką wyprowadzeń, więc dołożenie kolejnej nie zaszkodzi.
Pamięć Flash
Pamięć Flash S25FL127S firmy Spansion ma pojemność 128 megabitów (16 megabajtów). Wybrałem ten układ ze względu na niski koszt i możliwość pracy z dużą szybkością. Nieskompresowane grafiki wymagają znacznej pojemności, a obrazy złożone z wielu ramek dodatkowo zwielokrotniają zajmowane miejsce. Ten układ pomieści 8 megapikseli, czyli 36 pełnych ramek.
Wydawałoby się, że pamięć Flash z interfejsem SPI nie będzie wystarczająco szybka, ale tu następuje pozytywne zaskoczenie. Układ Spansion może pracować w niestandardowym trybie z 4-bitowym wyjściem z zegarem do 108 MHz, co oznacza maksymalna przepustowość 54 MB/s. Praca z taką magistralą nie stanowi dla FPGA żadnego problemu. Będę taktował pamięć Flash wewnętrznym zegarem FPGA o częstotliwości 100 MHz i wykorzystam 4-bitowy tryb wyjściowy, aby odczytywać 16-bitowy piksel w ciągu 4 x 10 = 40 ns. Okazuje się, że z taką właśnie szybkością powinienem zapisywać piksele do bufora ramki SRAM. Wszystko dokładnie się zgadza.
Pamięć SRAM
Wybrałem układ SRAM ISSI IS61LV5128AL o pojemności 4 Mb. Jest on podzielony na 8 bloków po 512 kb, a jego czas dostępu to 10 ns, co odpowiada zegarowi 100 MHz. Informacja o pikselu LCD zajmuje 16 bitów, zatem odczyt lub zapis jednego piksela trwa 2 cykle zegara. Z drugiej strony, to rozwiązanie pozwoli to zaoszczędzić 8 wyprowadzeń z FPGA. Pojemność 4 megabitów jest wystarczająca do przechowania 262144 pikseli. Rozdzielczość ekranu LCD to 640 x 360 = 230400 pikseli, zatem 31744 pozostaje niewykorzystanych. W tym projekcie nie mam jak użyć tej wolnej przestrzeni, zatem dodatkowe piksele będą po prostu stanowiły wolne miejsce w pamięci.
Czas dostępu równy 10 ns oznacza, że łatwo przeprowadzić zapis pełnego piksela w tym samym czasie, w którym 16-bitów danych o pikselu jest odczytywanych z pamięci Flash. Jednocześnie będę w stanie odczytać pełen piksel w takim samym czasie, jaki jest potrzebny do zapisania go w wyświetlaczu LCD. Układy FPGA są stworzone do wykonywania wielu zadań współbieżnie z nanosekundową precyzją, zatem wszystko powinno ze sobą współgrać.
Pamięć EEPROM
Pamięć EEPROM w tym projekcie jest układem peryferyjnym, który nie jest kluczowy do działania systemu. Jest potrzebna, aby przechować dodatkowe dane, które muszą przetrwać wyłączenie zasilania. W odróżnieniu od popularnych kości Atmel AVR wykorzystywanych w płytkach Arduino, mirkokontrolery STM32 nie zawierają wbudowanych pamięci EEPROM. Istnieje możliwość zapisywania stron pamięci Flash wewnątrz STM32 na życzenie, zatem funkcjonalność EEPROM można emulować – jednak ze względu na niską cenę pamięci EEPROM z interfejsem I2C mogę równie dobrze dodać osobną kość.
Wybrany model to BR24G32FJ firmy ROHM o pojemności 32 Kb, który mieści się w obudowie SOIC-8. Pamięci EEPROM są rzadkim rodzajem układów, w których występuje kompatybilność wyprowadzeń i funkcji między modelami różnych producentów. W praktyce można wybrać dowolny model w obudowie o odpowiednim rozmiarze i będzie on działał po podłączeniu do interfejsu I2C tak samo, jak układ innego producenta. Jeśli tworzysz ten projekt samodzielnie, możesz swobodnie zamienić tę kość na inną, jeśli nie jest dostępna w Twoim miejscu zamieszkania.
Układy zasilania
W tym urządzeniu występuje co najmniej 5 różnych poziomów zasilania – 6 po uwzględnieniu wyjścia przetwornicy podwyższającej napięcie, która obsługuje podświetlenie LCD. Zewnętrzny zasilacz 5 V jest podłączony do stabilizatorów o niskim spadku napięcia (LDO), które zasilają resztę systemu. Niemal wszystkie elementy są zasilane ze stabilizatora AMS1117 generującemu napięcie 3,3V, poza oczywiście FPGA. Ten układ wymaga poziomów 2,5 V oraz 1,2 V dla różnych dodatkowych i wewnętrznych operacji, natomiast poziom 3,3 V jest potrzebny do obsługi wszystkich wejść i wyjść FPGA. Ostatnim poziomem jest 2,8 V potrzebne do zasilenia panelu LCD.
Podczas przetwarzania bitmap, z podświetleniem ekranu LCD ustawionym na 90%, system będzie pobierał prąd prawie 400 mA z linii 5 V. Z tego powodu wybrałem stabilizatory 3,3 V, 2,5 V oraz 1,2 V o dużej wydajności prądowej. Chciałbym uniknąć sytuacji,w której zasilacze okażą się niewystarczające. Stabilizatory 2,5 oraz 1,2 należą do rodziny TS1117 firmy Taiwan Semiconductor, a regulator 2,8 V to model ZXCL280H5T firmy Diodes Inc.
Synchronizacja
Wszystkie wymienione komponenty muszą pracować razem w ściśle określonych rygorach czasowych – zależnych od tego, jak szybko możemy pobrać dane z pamięci Flash i przekazać je do bufora ramki SRAM, a następnie zapisać w pamięci wyświetlacza LCD. Poniższy diagram przedstawia wysokopoziomowy schemat zależności czasowych z punktu widzenia projektanta gier.
Na początku pierwszej ramki FPGA ustawia sygnał busy w stan wysoki, aby zakomunikować, że rozpoczyna parsowanie konfiguracji bitmap przechowywanej w wewnętrznej pamięci BRAM. Ta konfiguracja służy do pobrania odpowiednich grafik z zewnętrznej pamięci Flash i zapisanie ich do bufora ramki SRAM. W tym czasie nie jest bezpieczne przesyłanie do FPGA kolejnych poleceń, które mogłyby zmienić stan bitmap. Gdy układ FPGA zakończy swe zadanie, ponownie ustawi sygnał busy w stan niski. Zmiana musi nastąpić przed rozpoczęciem wyświetlania kolejnej ramki – w przeciwnym wypadku widoczny obraz będzie nieprawidłowy. W tym czasie mikrokontroler powinien obsłużyć logikę gry i przygotować do zapisu nowy stan obrazu.
Podczas drugiej ramki FPGA pobiera dane z bufora ramki i zapisuje kompletną ramkę do wyświetlacza LCD. W tym czasie mikrokontroler może bezpiecznie wgrać do FPGA nowy stan gry do wyświetlenia. O tym, że jest to bezpieczna operacja, świadczy niski poziom sygnału busy.
Gdy druga ramka jest kompletna, cały cykl rozpoczyna się ponownie od pierwszej ramki. Ponieważ wyświetlacz pracuje z szybkością 60 fps, otrzymujemy silnik renderujący bitmapy z szybkością 30 fps.