Przetwarzanie grafiki jest z natury dość zasobożerne. Popularne moduły oferujące wyświetlacz dotykowy sterowany przez ESP32 starają się zaradzić temu poprzez integrację zewnętrznej kości pamięci PSRAM, ale przerzucenie na niej LVGLa może wymagać dodatkowej konfiguracji oraz może wiązać się z utratą wydajności. Postaram się to tutaj zaprezentować i sprawdzić.
Prezentację wykonałem na płytce Waveshare, bazowałem na projekcie ESP32_Display_Panel.
Waveshare ESP32-S3-Touch-LCD-5 - Wi-Fi, BLE, CAN, RS485 i ekran dotykowy 800x480
ESP32-S3-Touch-LCD-4.3 czyli ESP32-S3 i ekran dotykowy - uruchomienie w PlatformIO
Cała historia wzięła się stąd, że brakowało mi troszkę pamięci RAM do użycia zewnętrznej biblioteki, której kodu nie chciałem edytować, a nie znalazłem opcji by przełączyć ją na dostępny na mojej płytce PSRAM.
PSRAM, choć nie zawsze dostępny i nieco wolniejszy niż SRAM, może stanowić dobrą alternatywę gdy brakuje "zwykłej" pamięci - ale czy na pewno? Sprawdźmy.
Na początku próbowałem przerzucić własne alokacje do PSRAM, co jest bardzo proste, wystarczy używać odpowiedniego zamiennika malloc:
Kod: C / C++
Można też użyć heap_caps_realloc, jeśli potrzebna jest realokacja. Do zwolnienia mamy z kolei heap_caps_free. Wszystko to działa podobnie jak standardowe funkcje malloc, ale pozwala kontrolować typ używanej pamięci – w tym przypadku wymuszając użycie zewnętrznego PSRAM przez flagę MALLOC_CAP_SPIRAM.
Niestety w żaden sposób to nie starczało.
Z tego powodu zdecydowałem się przerzucić obiekty LVGL alokowane przez lv_mem_alloc właśnie do zewnętrznej PSRAM, co tutaj pokażę.
Zacząłem od zbadania sytuacji - ile miejsca oferują każda z pamięci:
Kod: C / C++
Przed zmianą z wbudowanym RAM było dość krucho, raptem 30kB:
Free heap before connect: 33220
Free PSRAM before connect: 5744836
PSRAM z kolei oferuje całe 5 744 836 bajtów, aż szkoda z tego nie skorzystać.
LVGL konfiguruje się w pliku lv_conf.h. Jedna z sekcji tam odpowiada za wybór sposobu alokacji pamięci dla obiektów graficznych.
Kod: C / C++
W użytym ESP32_Display_Panel ta alokacja była skierowana bezpośrednio na wbudowany mechanizm malloc/free/realloc z stdlib.h. My jednak mapujemy go na alokowania pamięci w PSRAM, które opiera się na funkcjach:
- heap_caps_malloc
- heap_caps_free
- heap_caps_realloc
z nagłówka esp_heap_caps.h.
Zmodyfikowana sekcja lv_conf.h:
Kod: C / C++
Zapisałem zmiany i wgrałem na nowo skompilowany projekt. FPS się nie zmienił. Użycie pamięci po zmianie:
Free heap before connect: 85556
Free PSRAM before connect: 5714896
Sukces! Teraz zewnętrzna biblioteka korzystająca z malloc/free/realloc już działa normalnie, a LVGL na niczym też nie ucierpiał. Sprawdziłem FPS w wersji na RAM i w wersji na PSRAM, bo obawiałem się, że PSRAM może spowolnić wyświetlanie, ale nie ma odczuwalnych problemów.
To jednak przerzuca tylko pamięć alokowaną przez lv_mem_alloc, ale to nie jest jedyny sposób alokacji. Weźmy na przykład takie bufory ekranu z LVGL:
Kod: C / C++
Powyższy fragment pochodzi z projektu ESP32_Display_Panel, lvgl_v8_port.cpp. Tutaj mamy kolejną opcję do zmiany - LVGL_PORT_BUFFER_MALLOC_CAPS, ale musimy się zastanowić, czy ta zmiana nam się opłaca?
W lvgl_v8_port.h mamy dwie możliwości:
Kod: C / C++
Albo alokujemy bufory ekranu LVGL w pamięci wewnętrznej ESP, czyli w SRAM. SRAM jest szybszy oraz wspiera LCD przez SPI/SQPI (SPI DMA nie wspiera drugiej opcji - PSRAM). PSRAM z kolei jest większy...
Spróbowałem przerzucić bufory na PSRAM na Waveshare 5 i WaveShare 4
Użycie pamięci z buforami w PSRAM:
getFreeHeap: 175972
getFreePsram: 5697684
Użycie pamięci z buforami w SRAM:
loop getFreeHeap: 112192
loop getFreePsram: 5763148
63780 przerzucone ze SRAM do PSRAM jednak robi różnice. Zostaje kwestia wydajności. Sprawdziłem to na najbardziej skomplikowanym widoku jaki mam w projekcie. Tu sytuacja jest ciekawa, gdyż normalnie FPS jest bez zmian, 33FPS tak jak wcześniej. W trakcie odświeżania panelu było 14-15FPS, teraz jest 11FPS. W trakcie przewijania było 9FPS, jest 4FPS. Wygląda na to, że przerzucenie tych buforów do PSRAM działa na moich płytkach, choć raczej się nie opłaca. Ostatecznie tę zmianę cofnąłem - nie warto.
Podsumowując, przerzucenie alokacji pamięci LVGL (przez lv_mem_alloc) do PSRAM rzeczywiście pomogło odciążyć SRAM w moim projekcie i nie miało negatywnego wpływu na wydajność. Moje własne bufory też przerzuciłem do PSRAM (heap_caps_malloc) i też nie odczułem z tym problemu. Użytych bibliotek nie przerzucałem, bo nie chciałem modyfikować ich kodu, choć pewnie to też by było bardzo proste.
Natomiast próba przerzucenia buforów ekranu (draw bufferów) nie opłaciła się w moim przypadku. Może gdyby wyświetlacz miał pokazywać statyczne obrazy, to sytuacja by była inna, ale u mnie interfejs jest dynamiczny i często ulega zmianie, więc wolniejszy rendering grafik jest odczuwalny.
Pokazany tu sposób nie jest jedynym rozwiązaniem, można też np. po prostu na nowo tworzyć i niszczyć obiekty LVGLa przy zmienianiu widoków - co kto woli.
PS: To była oczywiście tylko krótka prezentacja pokazująca jak rozwiązałem problem niewystarczającej pamięci SRAM który mi uniemożliwił korzystanie z zewnętrznej biblioteki. Inną kwestią jest wciąż sama wydajność LVGL, co planuję sprawdzić w osobnym temacie w oparciu o sugestie od Waveshare, ale to innym razem. Postaram się wtedy sprawdzić większość dostępnych opcji oraz użyję nieco wygodniejszego i miarodajniejszego sposobu przedstawienia wyników - czas klatki zamiast FPS.
Fajne? Ranking DIY Pomogłem? Kup mi kawę.