Akcelerator grafiki 2D na FPGA i STM32F429
Artykuł publikujemy dzięki uprzejmości Andy’ego Browna, autora bloga AnydsWorkshop.
Jest to mój najbardziej ambitny projekt do tej pory. Spróbuję zaprojektować i wykonać akcelerator grafiki 2D, który będzie pracował jako koprocesor dla mikrokontrolera. Wykorzystując tanie komponenty kupione w sklepie mam nadzieję osiągnąć wydajność porównywalną do tej, którą zapewniają popularne, komercyjne przenośne konsole do gier.
Mam nadzieję, że przy okazji nauczę się kilku nowych umiejętności. Po głowie chodzi mi obecnie kilka pomysłów, które zaczynają nabierać kształtów. Na końcu napiszę demo lub dwa, a może nawet małą grę, która udowodni poprawne działanie akceleratora. Oczywiście będzie to w pełni otwarty projekt – każdy, kto chce go skopiować, rozszerzyć lub po prostu dodać coś od siebie, będzie mile widziany.
Mam nadzieję, że ten projekt będzie interesujący. Jednak jego realizacja może zabrać dużo czasu.
Projekt systemu
Zdecydowałem na wstępie, że będzie to akcelerator grafiki 2D w postaci bitmap (sprite). Bitmapy są obiektami graficznymi, które programista może umieścić w dowolnym miejscu na ekranie. Mogą ona nachodzić na siebie, zachowując przewidywalną kolejność na osi Z (głębię obrazu). Ponadto mogą zawierać obszary przezroczyste, dzięki czemu grafiki nie będą prostokątne. Ramka obrazu w grze jest tworzona jako zbiór bitmap – niektóre będą reprezentowały środowisko gry, inne gracza oraz pozostałe postacie, jeszcze inne – efekty graficzne, takie jak eksplozje.
Bitmapy są jedynymi grafikami na ekranie, a każda ramka jest tworzona niezależnie poprzez umieszczenie każdej bitmapy na ustalonej pozycji w kolejności wynikającej z osi z. Oznacza to, że nie są potrzebne dodatkowe układy, takie jak blitter. Przemieszczenie dużej bitmapy kosztuje tyle samo mocy obliczeniowej, co przesunięcie małej, a gwałtowne zmiany między ramkami kosztują tyle, co brak jakichkolwiek zmian.
Wszystkie ekrany LCD, jakie widziałem w telefonach, mają domyślną szybkość odświeżania 60 fps. Obrałem sobie zatem cel w postaci 30 fps zapewnianych przez główny silnik. Mogę zatem poświęcić 1/60 sekundy na przygotowanie bufora następnej ramki i 1/60 sekundy na przesłanie go do wyświetlacza.
Ta technika jest znana jako „podwójne buforowanie”. Dzięki odpowiedniej synchronizacji wyświetlacza pozwala zapobiec rozrywaniu obrazu.
Wyświetlacz LCD odczytuje dane z wewnętrznej pamięci w kolejności od góry do dołu i od lewej do prawej. Jeśli zmienilibyśmy zawartość pamięci w tym samym czasie, w którym ekran odświeża dane, zobaczymy okropny efekt w postaci rozrywania obrazu – obraz będzie składał się częściowo ze starej, a częściowo z nowej ramki.
Efekt ten można zaobserwować w niektórych grach PC, które mają opcję wyłączenia synchronizacji pionowej (disable vsync), pozwalając graczom uzyskać szybsze odświeżanie obrazu kosztem jego jakości.
Szczęśliwie wyświetlacze LCD dostarczają sygnał, który zazwyczaj nosi nazwę „Tearing Effect” (TE). TE przechodzi w stan wysoki pomiędzy zapisem kolejnych ramek (vertical blanking period) – efekt ten nie jest widoczny.
Aby zapewnić pracę bez migotania, ramkę należy zacząć odświeżać, gdy sygnał TE przechodzi w stan aktywny i zakończyć przynajmniej przed odświeżeniem ekranu, aby nie rozpoczął on wyświetlania kolejnej ramki, zanim wszystkie dane zostaną załadowane.
Dotrzymanie rygorów czasowych jest kluczowym wymaganiem projektu. Kontroler wyświetlacza LCD musi mieć wystarczającą szybkość zapisu, aby wgrać kompletną ramkę w czasie krótszym, niż czas odświeżania ekranu. Jednocześnie akcelerator grafiki musi być w stanie zapisywać dane z taką szybkością.
Zrzut ekranu pokazuje sygnał TE wyświetlacza LCD wykorzystanego w projekcie zarejestrowany za pomocą analizatora logicznego Ant18e.