logo elektroda
logo elektroda
X
logo elektroda
REKLAMA
REKLAMA
Adblock/uBlockOrigin/AdGuard mogą powodować znikanie niektórych postów z powodu nowej reguły.

ESP32 i wyświetlacz dotykowy - część 2 - jak rysować piksele, linie, kształty, kwestia wydajności

p.kaczmarek2 24 Cze 2024 12:47 2214 6
REKLAMA
  • Zdjęcie wyświetlacza z fraktalem Julii
    Dzisiaj kontynuujemy przygodę z płytką ESP32-2432S028R. W poprzedniej części uruchomiliśmy wyświetlacz oraz ekran dotykowy, więc dzisiaj z tego skorzystamy. Zobaczymy jakie mamy dostępne możliwości i kształty do rysowania a potem rozważymy jakie są sposoby na wydajne rysowanie tak, aby częstotliwość odświeżania ekranu była wysoka. Rozważymy tu kilka sposobów rysowania, w tym odświeżanie tylko tego, co się zmieniło oraz użycie DMA.

    Poprzedni temat z serii:
    https://www.elektroda.pl/rtvforum/topic4058635.html#21111347

    Podstawowe kolory i kształty
    Przede wszystkim mamy tu dwa rodzaje funkcji - draw (funkcje rysujące bez wypełnienia) oraz fill (funkcje rysujące i wypełniające kolorem kształt). Szczegóły znajdziemy w dokumentacji:
    https://www.arduino.cc/reference/en/libraries/tft_espi/
    Na bazie dokumentacji zebrałem w jedno miejsce różne funkcje rysujące. Możemy rysować różne kształty, wypełniać je kolorem bądź nie, wypełniać gradientami, a dla niektórych nawet zaokrąglać rogi. Kod powinien być zrozumiały, pierwsze argumenty to z reguły pozycja, kolejne - to zależy, odsyłam do dokumentacji bądź podpowiedzi Visual Code.
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Rezultat:



    Z tych funkcji możemy tworzyć własne animacje i interfejsy, ale nie jest to konieczne - w dalszej części poznamy LVGL, który zrobi za nas całą robotę.

    Fraktal Julii i szybkość rysowania
    Zaczerpnęliśmy już nieco wiedzy o samym rysowaniu, to może teraz coś bardziej zaawansowanego. Sprawdźmy jak na tym wyświetlaczu będzie prezentować się fraktal. Czym fraktal jest - tego tu nie będę omawiać, ale w dużym skrócie, małe obliczenia mogą dać zaskakujące efekty:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Rysowanie realizuję w dwóch pętlach, czyli dla każdego piksela.
    Rezultat, ale to nie najciekawsze:
    Zdjęcie wyświetlacza z fraktalem Julii
    Film ładnie przedstawia, że całość jednak dość powoli się oblicza i rysuje:



    Pewnie dałoby się to zoptymalizować, np. tworząc grafikę do bitmapy a potem raz wywołując wyświetlanie tej bitmapy...

    Czy można rysować szybciej? Demo Bouncy Circles
    Tu z pomocą przychodzi przykład od samego autora używanej przez nas biblioteki.
    Źródło:
    https://github.com/Bodmer/TFT_eSPI/blob/maste.../DMA%20test/Bouncy_Circles/Bouncy_Circles.ino
    Przeanalizujmy ten kod:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Widzimy tutaj, że autor... osobno rysuje górną połowę ekranu, a osobno dolną:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Funkcja rysująca najpierw osobno, bez wyświetlacza, czyści daną bitmapę (danej połowy ekranu) i rysuje na niej nasze koła (bez wyświetlacza):
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Potem odbywa się już wyświetlanie na ekranie - narysowana połowa ekranu z bitmapy jest wysyłana do niego bezpośrednio przez DMA:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Potem autor aktualizacje pozycje kół, ale dla nas to już mniej ważne:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Tutaj przydałby się filmik jak to działa, ale na razie mi się zagubił, będę mieć chwilę to uzupełnię.
    Najważniejsze są tu dwie funkcje - initDMA oraz pushImageDMA.
    Nazwa funkcji już sugeruje nam, że korzysta ona z DMA - Direct Memory Access, czyli szybkiego, bezpośredniego dostępu do pamięci. Ale jak ona działa?
    Można podejrzeć do kodu źródłowego by znaleźć na to pytanie odpowiedź:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Powyższy fukcja tylko dodaje transfer DMA do kolejki. Potem normalnie wykonuje się reszta kodu, a w ten czas jednocześnie wysyłane są piksele do wyświetlacza.

    Demo zegar
    Oczywiście nie każdy program jednak zrealizowany jest w oparciu o DMA. Bez DMA też da się wyświetlić ciekawe animacje.
    Rozważmy tutaj demko-zegar, demo pochodzi z:
    https://git.wiyixiao4.com/Learning/TFT_eSPI/s...xamples/320%20x%20240/TFT_Clock/TFT_Clock.ino
    Przejrzyjmy kod źródłowy:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Powyżej zamieszczony fragment kodu to głównie funkcja setup, ona wykonywana jest raz. Mimo to w niej rysowany jest prawie cały zegar - bez wskazówek. Nie jest to przypadek, gdyż w praktyce nie ma potrzeby rysować tego więcej niż raz. W celu optymalizacji tutaj pętla loop tylko kolejno kasuje stare wskazówki (zamalowuje je kolorem tła) a potem rysuje nowe. Zobaczmy:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Dodatkowo odświeżanie jest dodatkowo podzielone, bo np. gdy porusza się wskazówka sekundowa, a godzinowa stoi w miejscu, to tej godzinowej nie trzeba ponownie rysować.
    Zostały funkcje pomocnicze:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Rezultat:





    Podsumowanie
    Najpierw pokazałem tutaj podstawy samego rysowania (najmniejsze klocki budulcowe, rysowanie kształtów, itd.) a potem zaprezentowałem też różne metodyki rysowania. Można:
    - albo rysować niewydajnie całość, najgorzej jest robić to piksel po pikselu (patrz przykład z fraktalem)
    - albo zaprząc do całości DMA, np. dzieląc ekran na 2 części tak aby w trakcie wysyłania jednej części już renderowała się druga część (przykład 'bouncy circles')
    - albo narysować raz statyczne tło a dynamiczne obiekty kolejno wymazywać kolorem tła a potem na nowo rysować (przykład zegar)
    Wszystkie te metody mają swoje plusy i minusy, ale raczej wiadomo, że pierwsze podejście jest najprostsze i też najmniej wydajne. Wszystko zależy od tego co i w jakiej formie chcemy narysować.
    Czy spotkaliście się z problemem wydajności rysowania? Jak go rozwiązaliście u siebie? Zapraszam do komentowania.

    Fajne? Ranking DIY
    Pomogłem? Kup mi kawę.
    O autorze
    p.kaczmarek2
    Moderator Smart Home
    Offline 
  • REKLAMA
  • #2 21130973
    LA72
    Poziom 41  
    Ale nie korcisz tymi swoimi materiałami nt. ESP32.
    Widzę wiele ciekawych zastosowań dla siebie.
    Szkoda, że człowiek ma tak mało czasu na wszystkie hobby.
  • REKLAMA
  • #3 21131089
    p.kaczmarek2
    Moderator Smart Home
    Naprawdę warto zainteresować się ESP32, zarówno płytek, jak i przykładów jest naprawdę mnóstwo. Praktycznie każde bardziej typowe zastosowanie ma już swoje gotowe rozwiązanie. To nie jest to samo co było, gdy ja się lata temu bawiłem PICami i musiałem od 0 kombinować lub szukać inspiracji w nieutrzymywanych bądź niekompatybilnych projektach.
    Pomogłem? Kup mi kawę.
  • REKLAMA
  • #4 21131461
    p.kaczmarek2
    Moderator Smart Home
    Ok, znalazłem ten zagubiony filmik z demka z kółkami (bouncy circles):



    Proszę zwrócić uwagę na to, jaka to płynna animacja! Ilość klatek na sekundę względem przykładu z fraktalem jest powalająca. Może warto to z tym fraktalem przepisać tak aby używało dwóch bitmap oraz DMA i porównać o ile będzie szybciej?
    Pomogłem? Kup mi kawę.
  • REKLAMA
  • #5 21134297
    katakrowa
    Poziom 23  
    p.kaczmarek2 napisał:
    Proszę zwrócić uwagę na to, jaka to płynna animacja!


    Nigdy nie robiłem projektu na ESP32 ani z tym ekranem ale wydaje mi się, że tu nie ma się czym zachwycać.
    Przecież gry na 80286 chodziły płynnie... Tu mamy procesor nie dość, że 32-bitowy zamiast 16. To zegar jest ponad 10 razy szybszy do tego dual-core. RAM ilość porównywalna.
    Zatem wg mnie żadne to osiągnięcie, że się kuleczki szybko i płynnie się odbijają. Aby zachwycić to raczej powinny tu śmigać kulki 3D obliczane w real-time.
    Przecież grafikę rysujemy w buforze w RAM a jedynie 20 razy/sekundę wysyłamy całość do ekranu (zwykła grafika buforowana).
    Przesłanie całego ekranu 320x240x16bpp to 156600 bajtów zatem w sekundę mamy do przesłania jedynie ~3MB.

    Ze specyfikacji ekranu https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf wynika, że przesłanie jednej klatki po (SPI/DMA) powinno zająć nie więcej niż 40ms - czyli samo to nie zajmuje nam pełnego czasu jednego rdzenia.

    A pewnie da się też przesyłać dużo szybciej jakimś innym interfejsem / może równoległym 8/16 bitowym.

    p.kaczmarek2 napisał:
    Proszę zwrócić uwagę na to, jaka to płynna animacja! Ilość klatek na sekundę względem przykładu z fraktalem jest powalająca. Może warto to z tym fraktalem przepisać tak aby używało dwóch bitmap oraz DMA i porównać o ile będzie szybciej?

    Na tym urządzeniu ten fraktal powinien być co najmniej animowany jak np., tu: https://getbutterfly.com/canvas-julia-fractal-animation/ lub https://slicker.me/fractals/animate.htm


    ... a podejrzewam, że moja metoda "na chama" wcale nie jest na tym urządzeniu najbardziej optymalną propozycją. Szczególnie widząc takie rzeczy:

    https://www.youtube.com/watch?v=uWpWOoKFdeE
    https://www.youtube.com/watch?v=23uQax7Acyw
    https://www.youtube.com/watch?v=ZtCMIAmLSh8


    Wracając do Fraktala Julia

    Zastosuj bufor dla grafiki potem w całości go przerzuć do wyświetlacza. Czy to po DMA czy też inną szybką metodą byle nie pixel po pixelu.

    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    Być może warto też przyjrzeć się funkcji: pushPixelsDMA(uint16_t* image, uint32_t len);
    Niestety nie mam tego urządzenia i tylko teoretyzuję...
  • #6 21134505
    p.kaczmarek2
    Moderator Smart Home
    Widzisz @katakrowa , ja zaczynałem od PICów samodzielnie lutując płytki, ew. od Arduino, więc dla mnie wyświetlacz taki i co dopiero dotykowy to jednak postęp, ale jednocześnie nie twierdzę, że to jest jakiś cud techniki. Na pewno gdybym zaczynał od takich wyświetlaczy i to jeszcze w takich cenach to moja nauka by wyglądała nieco inaczej.

    Ten fraktal to miał być tylko przykład jak duże znaczenie ma to jak się napisze rysowanie, wiadomo, że można by ulepszyć.

    Natomiast co do Twojego pseudokodu, to ja bym nie trzymał bufora na stosie, tym bardziej że to implikuje że flipBufferToScreen robi memcpy, ja bym trzymał globalnie dwa bufory i w momencie gdy do jednego rysuje CPU to drugi by wysyłało DMA... a przynajmniej tak mi się wydaje na ten moment, może się mylę.
    EDIT: Widzę że troszkę ulepszyłeś kod gdy pisałem odpowiedź, ale moja uwaga i tak pozostaje - ja bym rozważył tzw:
    https://www.geeksforgeeks.org/double-buffering/
    I DMA by z jednego bufora wysyłało po SPI, a drugi byśmy my wypełniali.

    Added after 3 [minutes]:

    Można też popatrzyć jak to LVGL robi:
    https://docs.lvgl.io/8.0/porting/display.html
    W dalszej części chcę LVGL zademostrować też. A na powyżej stronie jest fragment:
    Cytat:

    If only one buffer is used LVGL draws the content of the screen into that draw buffer and sends it to the display. This way LVGL needs to wait until the content of the buffer is sent to the display before drawing something new in it.

    If two buffers are used LVGL can draw into one buffer while the content of the other buffer is sent to display in the background. DMA or other hardware should be used to transfer the data to the display to let the MCU draw meanwhile. This way, the rendering and refreshing of the display become parallel.

    Pogrubienie ode mnie.
    Pomogłem? Kup mi kawę.
  • #7 21134579
    katakrowa
    Poziom 23  
    >>21134505
    p.kaczmarek2 napisał:
    Widzisz @katakrowa , ja zaczynałem od PICów


    Nie miałem zamiaru umniejszać Twoim odkryciom z ESP a jedynie chciałem zwrócić uwagę, że istnieją pewne stare jak świat sposoby na obsługę grafiki z animacją.
    Chodzi oczywiście o buforowanie. Ja zaczynałem od ZX Spectrum i już wtedy buforowanie było wykorzystywane.

    p.kaczmarek2 napisał:
    Ten fraktal to miał być tylko przykład jak duże znaczenie ma to jak się napisze rysowanie, wiadomo, że można by ulepszyć.


    I to jest bardzo dobry przykład. Bo teraz komuś kto nie ma pojęcia o takich zabawach może się wydawać, że kluczowym problemem jest czas obliczania fraktala.
    Tymczasem należy wiedzieć, że w większości przypadków gdy rysujemy wprost do ekranu pixel po pixelu to lwią część czasu zabiera wywoływanie funkcji rysowania.
    Jak wcześniej napisałem nie mam tego zestawu ale podejrzewam, że przerobienie tego na grafikę z buforem może mocno zaskoczyć.

    p.kaczmarek2 napisał:
    EDIT: Widzę że troszkę ulepszyłeś kod gdy pisałem odpowiedź, ale moja uwaga i tak pozostaje - ja bym rozważył tzw:
    https://www.geeksforgeeks.org/double-buffering/
    I DMA by z jednego bufora wysyłało po SPI, a drugi byśmy my wypełniali.


    Być może tak. Nie znam tej platformy i jedynie chciałem wskazać ogólne metody postępowania w takich przypadkach. Ciekaw jestem jakie byłyby różnice w wydajności między różnymi metodami.

    Oczywiście masz rację co do lokalizacji zmiennej screenBuffer - lepiej aby była globalna.

    p.kaczmarek2 napisał:
    Można też popatrzyć jak to LVGL robi:


    Ja myślę, że dużo więcej można pokombinować w szczególności, że mamy tu procesor z dwoma rdzeniami więc można napisać to wielowątkowo (dwu).
    Wówczas nikt na nic nie musi czekać a jedynie wywołujemy to kiedy chcemy. O sposobach buforowania grafiki pisali książki już 30 lat temu. Sposobów są dziesiątki.
    Pewne jest jedno - każdy będzie lepszy niż dochodzić do celu pixel po pixelu :-)
REKLAMA