Skocz do zawartości

STM32F103RBT6 + OLED SSD1331 - odświeżanie


Vroobee

Pomocna odpowiedź

Witam,
pytanie moje brzmi następująco - co zrobić, żeby częstotliwość odświeżania wyświetlacza OLED ze sterownikiem SSD1331 (taki sam jak w kursie STM32 HAL + CUBE) była wysoka ? Zastosowałem do uruchomienia biblioteki producenta i szczerze powiem funkcje:

void ssd1331_draw_point(uint8_t chXpos, uint8_t chYpos, uint16_t hwColor) 
{
if (chXpos >= OLED_WIDTH || chYpos >= OLED_HEIGHT) {
	return;
}

   //set column point
   ssd1331_write_byte(SET_COLUMN_ADDRESS, SSD1331_CMD);
   ssd1331_write_byte(chXpos, SSD1331_CMD);
   ssd1331_write_byte(OLED_WIDTH - 1, SSD1331_CMD);
   //set row point
   ssd1331_write_byte(SET_ROW_ADDRESS, SSD1331_CMD);
   ssd1331_write_byte(chYpos, SSD1331_CMD);
   ssd1331_write_byte(OLED_HEIGHT - 1, SSD1331_CMD);

   //fill 16bit colour
ssd1331_write_byte(hwColor >> 8, SSD1331_DATA);
ssd1331_write_byte(hwColor, SSD1331_DATA);   
}

void ssd1331_clear_screen(uint16_t hwColor)  
{
uint16_t i, j;

for(i = 0; i < OLED_HEIGHT; i ++){
	for(j = 0; j < OLED_WIDTH; j ++){
		ssd1331_draw_point(j, i, hwColor);
	}
}
}

powodują odświeżanie wyświetlacza w tempie zjeżdżającej kurtyny. Próbowałem grzebać w komendach wysyłanych do wyświetlacza - ustawianie zegara sterownika, multiplexera, ale nic nie pomogło. Wydaje mi się, że winą jest albo funkcja albo niewystarczająca prędkość transmisji - wyświetlacz jest na SPI z prescalerem 2 w trybie Full Duplex (wiem, że niepotrzebny bo wyświetlacz wymaga tylko MOSI ale potrzebuję ten tryb do innych urządzeń naokoło). Ktoś może potwierdzić czy na Discovery F4 jest podobnie ? I jak się z tym uporać (DMA?).

Link do komentarza
Share on other sites

Jednym ze sposobów jest buforowanie obrazu i wysyłanie go 'hurtowo' do sterownika.

Jeżeli w Twoim kodzie renderujesz np. napis i dla każdego piksela znaku wywołujesz kilka(naście) razy funkcję ssd1331_draw_point() to wykonujesz kilka(naście) razy kod

//set column point 
   ssd1331_write_byte(SET_COLUMN_ADDRESS, SSD1331_CMD); 
   ssd1331_write_byte(chXpos, SSD1331_CMD); 
   ssd1331_write_byte(OLED_WIDTH - 1, SSD1331_CMD); 
   //set row point 
   ssd1331_write_byte(SET_ROW_ADDRESS, SSD1331_CMD); 
   ssd1331_write_byte(chYpos, SSD1331_CMD); 
   ssd1331_write_byte(OLED_HEIGHT - 1, SSD1331_CMD); 

Poszukaj w dokumentacji sterownika wyświetlacza o automatycznym zwiększaniu/zmniejszaniu adresu do którego przesyłane są dane.

Stosując bufor rysujesz najpierw do niego, a cyklicznie wywoływana funkcja (np. co 50ms) przesyła cały ten bufor do sterownika korzystając z błogosławieństwa automatycznego zwiększania adresu.

Link do komentarza
Share on other sites

Generalnie, to potrzebujesz właśnie ten kod, który wkleiłeś, tylko trochę mądrzej napisany.

Najpierw ustawiasz prostokąt, który chcesz wypełnić pixelami (lewy górny róg to x0, y0, prawy dolny to x1, y1):

   ssd1331_write_byte(SET_COLUMN_ADDRESS, SSD1331_CMD); 
   ssd1331_write_byte(x0, SSD1331_CMD); 
   ssd1331_write_byte(x1, SSD1331_CMD); 
   //set row point 
   ssd1331_write_byte(SET_ROW_ADDRESS, SSD1331_CMD); 
   ssd1331_write_byte(y0, SSD1331_CMD); 
   ssd1331_write_byte(y1, SSD1331_CMD);

A następnie iterujesz po wszystkich swoich pixelach i wysyłasz ich wartości:

   ssd1331_write_byte(color >> 8, SSD1331_DATA); 
   ssd1331_write_byte(color, SSD1331_DATA);    

W sumie musisz ich wysłać (x1-x0 + 1) * (y1 - y0 + 1). I tyle.

Link do komentarza
Share on other sites

Najważniejsze to ograniczenie liczby transferów SPI, bo są one seki razy wolniejsze niż operacje na pamięci RAM. Najszybsza będzie więc metoda opisana przez Lukaszm: robisz bufor na cały ekran w RAMie, tam wszystko rysujesz, wypełniasz teksturami czy malujesz znaki a potem kilkoma prostymi komendami ustawiasz w sterowniku obszar pokrywający cały ekran i w jednej szybkiej pętli (lub jednym transferem blokowym DMA) wysyłasz wszystko. Wtedy nadmiar komend jest znikomy (kilkanaście bajtów na początku) a każdy następny wysyłany bajt to wyłącznie pixele. Jeżeli jest to mały OLED szeregowy 128x64 to robiłem na nim jakieś małe gierki hand-held i na prostej ATMega328 (bez DMA oczywiście) osiągałem - o ile pamiętam - kilkanaście klatek na sekundę równolegle z liczeniem samej gry.

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Ograniczenia wielkości pamięci są oczywiste, ale wymuszone się nie tyle sterownikiem LCD co wielkością użytej matrycy. Rzeczywiście nie sprawdziłem co to za wyświetlacz jest w kursie HALa, ale i procesor jest troszkę większy niż mój.

Stosowałem też kiedyś metodę pośrednią i o ile wiem jest ona użyta w którejś bibliotece Arduino obsługującej wyświetlacze graficzne. Gdy cierpisz na brak RAMu w procesorze alokujesz bufor graficzny obejmujący jakiś wycinek ekranu, np. 1/2, 1/4 itp. Najczęściej (i najłatwiej) będzie to poziomy pasek o szerokości matrycy, np. dla ekranu 320x200 może to być 40x200 (to niecałe 8K dla trybu 256 kolorów i 1K dla trybu mono) a dla prostego OLEDa 128x64 można zaalokować bufor 64x64 pixele (512 bajtów). Praca z czymś takim wygląda następująco:

1. Zawsze na początku pętli ustawiasz zakres pionowy tj. wiersze pixeli które rysujesz w buforze. Na początku będzie to (w naszym przykładzie) zakres 0-39.

2. Wywołujesz wszystkie funkcje graficzne, które rysują cały obraz ze wszystkich prymitywów aktualnie obecnych na ekranie. Gdzieś na najniższej warstwie jest obcinanie rysowania pixeli tylko do aktualnego zakresu paska.

3. Po narysowaniu, jednym transferem przesyłasz cały bufor w odpowiednie miejsce rzeczywistej pamięci sterownika LCD.

4. Zmieniasz zakres pionowy i powtarzasz pętlę od pkt 2.

Tym sposobem co prawda dla każdej ramki wołasz wielokrotnie rysowanie całego obrazka, ale wolne transfery SPI są robione tylko raz dla każdego pixela. Dla paska 1/2 ekranu rysujesz dwa razy a dla 1/8 osiem razy, ale to i tak jest szybciej. Optymalizacje mogą polegać na takim modyfikowaniu procedur rysowania prymitywów by jak najwcześniej było wiadomo czego nie trzeba robić, bo wychodzi to poza aktualny obszar rysowania. Tą metodą można spokojnie obsłużyć nawet całkiem spory ekran procesorem z deficytem RAMu nie tracąc czasu na mozolne rysowanie znak po znaku wprost na ekranie.

Link do komentarza
Share on other sites

A zdradź mi jeszcze może w jaki sposób uzyskujesz w tych częściowych buforach dotychczasową zawartość ekranu, tak, żeby nie nadpisać tych pikseli tylko dlatego, że akurat narysowałeś przechodzącą w okolicy linię? Odczytujesz je z pamięci wyświetlacza przez SPI?

Link do komentarza
Share on other sites

Nigdy, to byłaby ogromna strata czasu. Każdą ramkę rysujesz od nowa na "pustym" buforze (czarne tło). Tylko w bardzo prymitywnych podejściach dorysowujesz coś do poprzednich rzeczy bezpośrednio na pamięci wyświetlacza. W większych projektach (a w grach to już na pewno) takie podejście jest strasznie upierdliwe, bo wciąż musisz pamiętać co gdzie jest, co już zamalowałeś a co jeszcze widać.

Typowo jest tak, że pełny obraz składa się z prymitywów typu linie, trójkąty, prostokąty, okręgi, znaki (litery) rysowanych z generowanej wg potrzeb listy. Kolejność rysowania definiuje "głębokość" położenia. Listę przygotowujesz od nowa tylko raz - gdy tworzysz nowy obraz (np. otwiera się nowy ekran z wykresami, przyciskami, itp) a potem ją modyfikujesz gdy robisz na nim zmiany. To daje możliwość dowolnego przesuwania napisów, robienia żywych wykresów, ruchomych sprite'ów itd.

Link do komentarza
Share on other sites

Szczerze mówiąc nie spotkałem się jeszcze z takim podejściem, ale brzmi ciekawie. Musiałbym przetestować jak to działa, bo nie jestem pewien, czy rysowanie wszystkiego od nowa po kilka razy na pewno byłoby szybsze od kilku transferów SPI. Bardzo dziękuję!

Natomiast ja się spotkałem na platformach z małą pamięcią (takich jak ZX Spectrum czy GameBoy) z zupełnie innym podejściem.

Zamiast bufora dla całego obrazu trzyma się w pamięci mapę "kafelków" (tiles) i listę "duszków" (sprites). Każdy kafelek to nic innego jak numer wskazujący pozycję do kwadratowej grafiki (na przykład 16x16) trzymanej w pamięci stałej gry, a każdy duszek to para współrzędnych i także taki numer. Jeśli wyświetlacz nie obsługuje sprzętowego przewijania, to można jeszcze dla mapy kafelków trzymać poziome i pionowe przesunięcie. Oprócz tego trzymamy w pamięci także listę "brudnych prostokątów".

I teraz tak. Za każdym razem, kiedy zmienimy jakiś kafelek, dodajemy go do tej listy (brudnych prostokątów). Dodajemy do niej także poprzednią i nową pozycję duszka, który się przesunął lub zmienił grafikę. Na koniec możemy jeszcze uruchomić prosty algorytm łączący ze sobą nakładające się lub sąsiadujące prostokąty.

Kiedy przychodzi czas na odświeżenie ekranu, iterujemy po liście brudnych prostokątów i dla każdego ustawiamy obszar komendami SETCOLUMN i SETROW. Następnie wysyłamy dane z pikseli, które obliczamy na żywo w następujący sposób:

* sprawdzamy czy punkt jest wewnątrz któregoś z duszków

* jeśli tak, sprawdzamy w grafice duszka kolor piksela, uwzględniając duszkowe przesunięcie

* jeśli kolor nie jest kolorem przezroczystym, koniec -- znaleźliśmy nasz piksel

* jeśli kolor jest przezroczysty, idziemy dalej

* kiedy sprawdzimy już wszystkie duszki, liczymy w którym kafelku przypada nasz piksel

* sprawdzamy w grafice tego kafelka kolor naszego piksela (uwzględniając przesunięcie), koniec

Procedura wydaje się skomplikowana, ale tak naprawdę jest bardzo szybka -- na tyle szybsza, że kolejny piksel mamy zanim SPI zdąży wysłać nasz poprzedni.

Można ten system wzbogacić, używając wielu warstw kafelków (z przezroczystością i paralaxem) oraz duszków -- w zależności jak szybka jest nasza platforma.

Zaleta tego systemu jest taka, że potrzebuje bardzo mało pamięci, do tego głównie w ROM-ie. Do wad należy to, że spowalnia kiedy w jednej klatce na raz jest bardzo dużo zmian (w przeciwieństwie do odświeżania całego ekranu, które jest wolne cały czas), oraz to, że prawie wszystko się musi składać z kafelków.

Link do komentarza
Share on other sites

Tylko że w Spectrum nie było żadnego SPI. Tam pamięć obrazu była bezpośrednio pamięcią operacyjną procesora Z80 i dostęp do niej był tak samo szybki (z dobrym przybliżeniem) jak do pozostałych obszarów. Po prostu miałeś w RAMie fragment, którego zawartość była wprost wyświetlana na telewizorze. A podział na brudne i czyste (a więc jadro tej metody) wynikał z faktu, że:

a) Tej pamięci było za mało na pełny tryb graficzny więc obszar został podzielony na "monochromatyczną" grafikę (1 bit na pixel) plus kawałek odpowiadający za kolory w polach 8x8 (o ile pamiętam). W każdym takim kwadracie 64 punktów mogłeś zdefiniować jaki kolor mają pixele "białe" a jaki "czarne". No i dlatego nie można było bezkarnie i dowolnie kolorować nieregularnych obszarów.

b) Ogólnie pamięci było mało (48K z tego niecałe 8K sztywno zajęte na obraz) więc w skomplikowanych grach trzeba było sobie radzić tak, by ogólna jej zajętość na podsystem grafiki była minimalna bez uciekania się do buforowania ramek itd.

W Gameboy'ach było podobnie (bitmapa tła dostępna jednocześnie dla procesora i dla sterownika graficznego który wyświetlał jej zawartość) z tym, że to sprzęt obsługiwał widoczne na tle podłoża sprite'y. Mogły być większe i mniejsze i były jakieś ograniczenia na ich liczbę w jednej linii obrazu, ale fakt że do przesunięcia wcześniej zdefiniowanego całego obiektu wystarczyło wpisać nowe współrzędne do rejestrów kontrolera LCD (widocznych jak.. pamięć RAM) był ogromnym skokiem.

Organizacja oprogramowania systemu graficznego zawsze mocno zależy od sprzętu. W sytuacji gdy masz mikrokontroler i "luźno" z nim połączony ekran np. przez SPI, trzeba sobie radzić minimalizacją transferów. Gdyby LCD był wbudowany w chip i korzystał bezpośrednio z pamięci RAM procesora (jak jest w STM-ach z wbudowanym kontrolerem LCD) to wtedy taka idea oczywiście nie ma sensu. Wszystko rysujesz na pamięci obrazu i tyle. Od razu to widać na ekranie a optymalizujesz same algorytmy rysowania. Żeby nie było efektu "budowania" strony z prymitywów możesz wtedy zrobić buforowanie: rysujesz całą stronę na fragmencie pamięci którego nie widać a potem podmieniasz w kontrolerze LCD adresy tak, by teraz pokazywał to co właśnie narysowałeś. A biblioteka graficzna rysuje na tym drugim obszarze pamięci nową stronę i znów je podmienia po zakończeniu.

EDIT: Ale chyba zbyt daleko odeszliśmy od tematu...

Link do komentarza
Share on other sites

No dobra, trochę nakłamałem z tym ZX Spectrum -- tam nie było brudnych prostokątów, były tylko "kafelki", a tak naprawdę to po prostu tryb tekstowy (ale dzięki temu, że można było definiować własne znaki, dało się tego używać jako kafelków).

Natomiast mylisz się co do GameBoy-a -- nie masz tam bitmapy na tło, masz kilka warstw map kafelków i kilka warstw sprite-ów. Owszem, rysowaniem tych kafelków i sprite-ów zajmuje się oddzielny procesor graficzny -- z którym się komunikujesz przez porty w pamięci -- ale nie zmienia to faktu, że taka właśnie technika pozawala zaoszczędzić pamięć.

Samo trzymanie "brudnych prostokątów" to rzeczywiście optymalizacja, która się pojawiła później, kiedy komputery zaczęły mieć osobną pamięć na karcie grafiki. Co nie zmienia faktu, że w tym przypadku jest bardzo użyteczna (także gdy nie używasz kafelków, tylko zwykłego framebuffera).

Link do komentarza
Share on other sites

Cześć,
dzięki za ciekawe i obszerne odpowiedzi. Pozmieniałem kod, instrukcja "wypełniania" ekranu wygląda tak:

void ssd1331_set_active_window (int x1, int y1, int x2, int y2){
ssd1331_write_byte(SET_COLUMN_ADDRESS, SSD1331_CMD);
ssd1331_write_byte(x1, SSD1331_CMD);
ssd1331_write_byte(x2, SSD1331_CMD);
ssd1331_write_byte(SET_ROW_ADDRESS, SSD1331_CMD);
ssd1331_write_byte(y1, SSD1331_CMD);
ssd1331_write_byte(y2, SSD1331_CMD);
}

void ssd1331_clear_screen(uint16_t hwColor)  
{
uint16_t i, j;

ssd1331_set_active_window(0, 0, OLED_WIDTH - 1, OLED_HEIGHT - 1);

for(i = 0; i < OLED_WIDTH; i ++){
	for(j = 0; j < OLED_HEIGHT; j ++){
		ssd1331_write_byte(hwColor, SSD1331_DATA);
	}
}
}

No ALE ... właśnie, ciągle widać migotanie. Próbuję coś kombinować z DMA ale jeszcze nie idzie bo poprawy nie ma (nie działa - tak sądzę).

Jakby co - to moje pierwsze spotkanie z wyświetlaczami OLED - działa to nieco inaczej niż ten od Nokii z tego co widzę 🙂

Przy okazji, próbowałem wykorzystać instrukcję zawartą w manualu do SSD1331

Ale coś nie podziałało bo po wyczyszczeniu ekranu pozostawały artefakty w postaci pojawiających się losowo pikseli na całym wyświetlaczu.

Link do komentarza
Share on other sites

Migotanie zawsze będziesz mieć, to jest jednak bardzo dużo pikseli do przepchnięcia po jednym drucie. W profesjonalnych zastosowaniach się te wyświetlacze używa raczej w trybie szeregowym, gdzie przesyłasz cały bajt w jednym cyklu zegara.

Przy okazji, masz błąd -- powinieneś wysyłać po dwa bajty na piksel, a wysyłasz jeden.

Link do komentarza
Share on other sites

Jeśli Twój kod wygląda tak, że najpierw kasujesz cały ekran a potem coś na nim rysujesz, potem znowu kasujesz i znowu rysujesz itd.. to zawsze będzie migotanie. Tak się nie robi. Przeczytaj jeszcze raz co napisaliśmy. Metod organizacji grafiki w systemie jest wiele. Jedne oszczędzają pamięć inne czas procesora, ale we wszystkich pamięć obrazu jest święta. Musisz zakładać, że to co do niej wpiszesz będzie zauważone. Tak więc masz dwie podstawowe opcje:

1. Przygotowujesz całą zawartość ekranu w RAMie procesora (być może w regularnych porcjach/paskach jak w mojej propozycji lub w buforze obszarów brudnych/czystych w pomyśle deshipu) i przeładowujesz to do pamięci LCD wykorzystując najszybszy możliwy sposób transmisji - SPI przez DMA jest tu idealne.

2. Malujesz wszystko wprost na pamięci ekranu ale wszelkie zmiany wykonujesz bardzo ostrożnie. Jeżeli w grze np. PONG musisz narysować piłkę w nowym położeniu, to najpierw kasujesz stary obiekt a tuż po tym rysujesz nowy by zminimalizować czas gdy na ekranie piłki nie ma. Bo jeśli akurat wtedy sterownik LCD będzie ten fragment rysował, użytkownik zobaczy chwilowy brak. To samo z wszelkimi innymi obiektami ruchomymi. Niedopuszczalne jest skasowanie całego ekranu i namalowanie go od nowa - to będzie wyglądać jak makabryczny pokaz slajdów nawet w systemie z szybkim dostępem do wspólnej pamięci obrazu.

EDIT: Przypadkowe pixele mogą świadczyć o problemach ze spójnością sygnałów. Albo zbyt długi kabel, albo brak dopasowania impedancji albo zbyt szybkie sygnały w stosunku do możliwości kontrolera LCD. Gdy timingi są OK a sygnały mają poprawny kształt, nie ma prawa być żadnych przekłamań. Zacznij od tego, bo jeśli na poziomie sprzętu masz takie kłopoty to nawet najlepszy program tego nie poprawi. Wykluczam oczywiście błędy typu nadpisywanie sobie bufora.

Link do komentarza
Share on other sites

@marek1707 rozumiem to co napisaliście. Trzeba wysłać jak największą paczkę danych na raz, żeby odświeżanie było szybkie. Problem w tym, że mam kłopot wyobrazić sobie realizację tego zamysłu. Kompletnie nie mam pojęcia jak to zrobić.

Wydawało mi się, że poprzednia funkcja, którą zaprezentowałem działa w sposób taki, że określa fragment ekranu, który ma zostać zapełniony kolorem, następnie w każdy piksel wpisuje kolor. Ale chyba nie do końca to jest to o co chodziło Tobie i @deshipu.

Btw. @marek1707 pamiętasz jak pomagałeś mi w ogarnięciu przetwornicy do paneli słonecznych (jest taki temat w dziale ZASILANIE). Jestem na etapie programowania - jak coś będzie już wykonane konkretnie, tak że będzie działać, podzielę się wrażeniami 🙂

Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.