Serwisy partnerskie:
Close icon
Serwisy partnerskie

Wokół Arduino: wyświetlacze graficzne cz.7 - aspekty programowe, przetwarzanie danych, tworzenie obrazu

We wcześniejszych odcinkach omawialiśmy podstawowe zagadnienia sprzętowe. Okazało się, że podstawowe zasady budowy i podłączenia są podobne dla wielu modułów wyświetlaczy. Aby wykorzystać gotowy moduł wyświetlacza, wystarczy za pomocą jednego z popularnych interfejsów wysłać do niego informacje, co ma zostać wyświetlone na ekranie. A to już okazuje się mniej oczywiste!
Article Image
Obecnie największe zainteresowanie elektroników budzą małe kolorowe wyświetlacze i ekrany dotykowe. Budzą zainteresowanie, ale też liczne pytania, wątpliwości i obawy. Nic dziwnego, sytuacja rynkowa szybko się zmienia, dostępne są liczne mniejsze i większe wyświetlacze o różnych właściwościach i co ważne, mające różne, często niezbyt wygodne do wykorzystania interfejsy. W niniejszym kilkuczęściowym artykule przedstawione są różne aspekty zagadnienia, które osobom zainteresowanym rozjaśnią obraz sytuacji i pomogą praktycznie wykorzystać różne wyświetlacze i ekrany.

Aspekty programowe wyświetlaczy graficznych (Arduino)

W zasadzie programowa obsługa wyświetlaczy graficznych okazuje się zaskakująco prosta, a to dzięki gotowym bibliotekom, o których jeszcze będziemy mówić. Jednak i jakość bibliotek, i niezbyt duża szybkość transferu owocują przykrymi niespodziankami, na które trzeba się od początku przygotować. Już wcześniej mówiliśmy, że korzystnie jest, gdy moduł wyświetlacza zawiera pamięć RAM wystarczającą do zapamiętania treści jednego obrazu (stanu wszystkich pikseli wyświetlacza), czyli pamięć GRAM (Graphic RAM) według rysunku 1a.

Natomiast w prostym rozwiązaniu według rysunku 1b mikrokontroler musi na bieżąco, nieustannie, kilkadziesiąt razy na sekundę przekazywać do modułu wyświetlacza informacje o aktualnej treści obrazu. Łącze musi mieć wtedy odpowiednio dużą przepustowość, najczęściej jest to łącze równoległe, np 24-liniowe. Takie rozwiązania są stosowane i mają sens na przykład w przypadku filmów i szybkich animacji.

Tak, ale my w cyklu „Wokół Arduino” mówimy głównie o prostszych wyświetlaczach współpracujących z mikroprocesorami o niewielkiej mocy obliczeniowej. Słaby procesor nie nadążyłby na bieżąco odświeżać ekranu kilkadziesiąt razy na sekundę i niezbędne jest rozwiązanie z rysunku 1a. Wtedy procesor jednorazowo ładuje informacje o treści obrazu do pamięci GRAM wyświetlacza. Kolejne dane z mikrokontrolera zostaną przesłane dopiero wtedy, gdy trzeba zmienić treść wyświetlanego obrazu. Taka koncepcja stosowana jest powszechnie w modułach niewielkich wyświetlaczy, które będziemy dołączać do Arduino.

Rys.1 Rózne rodzaje modułów wyświetlacza (GRAM, bez pamięci, z podwójną pamięcią)

Transfer kompletu danych do większego wyświetlenia kolorowego może trwać wielokrotnie dłużej niż jeden cykl odświeżania trwający do 20 milisekund, na przykład przy korzystaniu z powolnego łącza I2C lub łącza szeregowego (SPI) o małej częstotliwości taktowania. W takiej sytuacji podczas ładowania nowego obrazu na wyświetlaczu z pamięcią buforową będzie widać niepożądane artefakty: powolną, stopniową zmianę treści ekranu.

Problem ten rozwiązuje podwójne buforowanie: jeden bufor pamięci GRAM współpracuje z linią transmisyjną, a drugi z matrycą według rysunku 1c. Dopiero wtedy, gdy z procesora zostanie przesłany komplet danych, zawartość pierwszego bufora zostanie błyskawicznie przepisana do drugiego. Niektóre moduły wyświetlaczy mają podwójne buforowanie, ale użytkownik zwykle nie zna takich szczegółów.

Podwójne buforowanie okazuje się błogosławieństwem przy powolnym przesyłaniu informacji, ale możliwości szybkiej animacji i tak ograniczy prędkość transferu danych. Mówimy o tym, ponieważ wielu początkujących próbuje do Arduino podłączyć kolorowe wyświetlacze o dość dużej rozdzielczości. Biblioteki pozwalają na tworzenie interesujących efektów i animacji, jednak finalny efekt jest mizerny właśnie z uwagi na zbyt małą szybkość transferu danych. Dlatego przygodę z wyświetlaczami graficznymi dołączanymi do Arduino należy rozpoczynać od monochromatycznych wyświetlaczy 128×64 i wyświetlaczy kolorowych o rozdzielczości co najwyżej 240×320.

Przetwarzanie danych (dane są przesyłane z mikrokontrolera do modułu wyświetlacza)

Wiadomo, że wyświetlacze obsługiwane są za pomocą standardowych interfejsów, co jednak ani trochę nie odpowiada na pytanie: jakie dane są przesyłane z mikrokontrolera do modułu wyświetlacza?

Ogólnie biorąc, przesyłane są bajty, czyli ośmiobitowe paczki bitów. Nasuwa się nieodparty wniosek, że w przypadku najprostszych wyświetlaczy graficznych z pikselami typu załącz/wyłącz, na przykład w małych wyświetlaczach OLED, poszczególne bity wskazują po prostu, czy dany piksel ma być wygaszony, czy zaświecony.

W przypadku wyświetlaczy pozwalających na zobrazowanie stopni szarości oczywiste wydaje się przesyłanie w kolejnych bajtach właśnie informacji o jasności. Dziś bardzo popularne są wyświetlacze kolorowe (LCD, a ostatnio także OLED) i można się domyślać, że tam w poszczególnych bajtach przekazywane są informacje o jasności subpikseli RGB. Naturalne wydaje się przesłanie w każdym bajcie 8-bitowej informacji o jasności (sub)piksela, co daje 256 poziomów jasności, a w kolorowych wyświetlaczach RGB aż (256)3, czyli 16 777 216 odcieni. Stan jednego kolorowego piksela określają wtedy trzy bajty.

Tak bywa, ale większość małych kolorowych wyświetlaczy LCD wyświetla mniej odcieni i wtedy informacja o jasności trzech subpikseli RGB każdego piksela zajmuje mniej niż trzy bajty (24 bity).

Bardzo dobra wiadomość jest taka, że ktoś już wcześniej przygotował potrzebne procedury i udostępnił je, między innymi w postaci gotowych bibliotek Arduino. Trzeba jednak mieć przynajmniej ogólne pojęcie o tych sprawach, które bardzo szeroko i drobiazgowo są opisane w kartach katalogowych poszczególnych scalonych sterowników graficznych.

Tworzenie obrazu - wyświetlacze graficzne (Arduino)

W starych telewizorach analogowych, najlepiej w czarno-białych, wyraźnie widoczne było, że obraz składa się z kilkuset poziomych linii, dosłownie rysowanych na ekranie od strony lewej do prawej i od góry ekranu w dół według rysunku 2.

Rys.2 Rysowanie obrazu na ekranie analogowego monitora

Wprawdzie w ekranach cyfrowych mogłoby być (i bywa) zupełnie inaczej, jednak podstawowa zasada przesyłania i sekwencji tworzenia obrazu jest podobna. Ale szczegóły bywają różne.

Na pewno obraz na ekranie składa się z pewnej liczby pikseli, które są uporządkowane w liniach-wierszach (line, row) i kolumnach (column). Standardowo numeruje się linie od góry do dołu i kolumny od strony lewej do prawej, jak to jest pokazane na rysunku 3.

Rys.3 Sposób numeracji rzędów i kolumn

Można byłoby oczekiwać, że mikroprocesor ma wpisywać dane dla kolejnych pikseli według kolejności z rysunku 2. Taka kolejność obsługi pikseli ekranu jest dostępna i bardzo często wykorzystywana. Trzeba jednak wiedzieć, że wiele sterowników obsługuje też odmienne sekwencje, które na razie pominiemy, żeby nie zaciemniać istoty sprawy.

Nawet gdy dane do poszczególnych pikseli miałyby być przekazywane w kolejności z rysunku 2, to zależnie od rodzaju wyświetlacza pojawiają się różne wątpliwości, które warto rozumieć.

Weźmy na przykład moduły popularnych malutkich monochromatycznych wyświetlaczy OLED 128×64 o przekątnej 0,91 oraz 1,3 cala, gdzie jeden bajt określa stan ośmiu pikseli. Fotografia 4 pokazuje fragment ekranu produkcji Winstar. Rysunek 2 sugeruje, że kolejne bajty będą zaświecać kolejne piksele jednej linii-rzędu.

Fot.4 Fragment ekranu produkcji Winstar

Tak jednak nie jest!

W tego rodzaju małych modułach pracują scalone sterowniki SSD1306 albo pokrewne SH1106. Piksele ekranu odzwierciedlają stan pamięci GRAM kostki SSD1306, która w zasadzie ma organizację 128×64 bitów, ale w postaci 128×8 bajtów według rysunku 5. W sterowniku SH1106 pamięć ma organizację 132×8 bajtów, co pomimo innych podobieństw wymusza wykorzystanie innej biblioteki. W każdym razie w najpopularniejszych wyświetlaczach OLED 128×64 przychodzący z mikroprocesora jeden bajt określa stan ośmiu sąsiednich pikseli, ale ustawionych nie w poziomie, tylko w pionie!

Rys.5 SSD1306 - struktura pamięci GDDRAM

Kolejne nadchodzące dane (bajty) są zapisywane do komórek pamięci GRAM, automatycznie adresowanych przez dwa liczniki adresowe: licznik kolumn, w przypadku SSD1306 zmieniający stan w zakresie 0…127 oraz licznik stron zmieniający swój stan w zakresie 0…7. Zapisanie kolejnego bajtu powoduje automatyczne zwiększenie licznika kolumn, a po dojściu do końca linii-rzędu, zwiększenie stanu licznika rzędów – stron.

Stan całego ekranu określa przychodzący z mikroprocesora ciąg 1024 bajtów, które są automatycznie zapisywane do pamięci najczęściej w kolejności pokazanej na rysunku 6a, a możliwe jest też przełączenie na sekwencję zapisu według rysunku 6b, co znakomicie ułatwia obsługę, jeśli ekran miałby być obrócony o 90 stopni. Aby ułatwić obsługę ekranu umieszczonego „do góry nogami”, czyli obróconego o 180 i 270 stopni, przewidziano też możliwość odwrócenia numeracji rzędów i kolumn (normal/remapped), co zasygnalizowane jest na rysunku 5.

Rys.6 Sekwencje zapisu stanu ekranu

Zauważ, że po obróceniu ekranu o 90 lub 270 stopni w pewnym sensie rzędy stają się kolumnami i odwrotnie, więc generalna zasada z rysunku 2 jest jednak zachowana, tylko jeden bajt w różny sposób określa stan ośmiu pikseli.

Generalna zasada „przemiatania” z rysunku 2, czyli kolejności przesyłania danych, jest też zachowana w wyświetlaczach, gdzie stan jednego piksela określa jeden lub więcej bajtów. Tak, ale różnie traktowane są bity kolejno przesyłanych bajtów. Jako przykład może posłużyć układ scalony SSD1351, który jest sterownikiem małych kolorowych wyświetlaczy OLED RGB o rozdzielczości 128×128 i pozwala wyświetlić albo 65536 odcieni (kolor 16-bitowy 5:6:5), albo 262144 odcieni (kolor 18-bitowy 6:6:6).

Zawiera on pamięć GRAM o organizacji 128×128 komórek zawierających po 18 bitów. Zależnie od użytego łącza (8-bitowe szeregowe lub równoległe oraz 16- i 18-bitowego) informacje o odcieniu danego piksela (16 albo 18 bitów) są przesyłane do komórek pamięci w różny sposób. Dostępne sposoby rozłożenia informacji o odcieniach RGB pokazuje rysunek 7, pochodzący z karty katalogowej SSD1351.

Rys.7 Sposoby rozłożenia informacji o odcieniach RGB (SSD1351)

W innych sterownikach takie reguły transmisji są inne. Nie trzeba się w to wgłębiać, ponieważ te szczegóły realizują procedury zawarte w bibliotece przeznaczonej dla konkretnego sterownika SSD1351. Nieprzypadkowo jednak o tym mówimy: już rysunek 7 pokazuje, że biblioteka po pierwsze przeznaczona jest tylko dla jednego sterownika.

Po drugie wskazuje, że biblioteka na pewno jest skomplikowana, ponieważ zapewne uwzględnia różne opcje, nie tylko związane z transferem danych. A to prowadzi do bardzo ważnego wniosku: biblioteki na pewno są skomplikowane, a przecież dostępne są całkowicie za darmo. Nie zawsze wykorzystują procedury udostępnione przez producenta, a mogą być pisane przez osoby, których największą zaletą jest szczera chęć podzielenia się z drugimi owocami swojej pracy. Doceniając to, można się jednak słusznie zastanawiać, na ile takie biblioteki są dopracowane?

Otóż nawet jeśli nie zawierają błędów, mogą zawierać, i często zawierają, procedury nieoptymalne. Czasem zresztą jest to realizowane świadomie, żeby ułatwić pewne zadania i ujednolicić ich realizację. Nie będziemy się w to wgłębiać, ale nie oczekuj od bibliotek Arduino ani doskonałości, ani pełnej potencjalnie dostępnej szybkości. Musimy jednak wspomnieć o jeszcze jednym szczególe.

Zmiany stanu fragmentów ekranu  i pojedynczych pikseli

Podkreślmy, że do sterownika wyświetlacza za jednym zamachem w kolejnych bajtach może być wysłana zawartość całego ekranu.

Ale koniecznie trzeba wiedzieć o innej możliwości. Otóż scalone sterowniki graficzne oferują inną cenną możliwość. Mianowicie przed wysłaniem danych można wysłać rozkaz, który określi, dla jakiego prostokątnego wycinka ekranu przeznaczone są informacje. W rozkazie tym określone zostaną przeciwległe wierzchołki prostokąta i wysłane potem bajty danych zmienią stan tylko tak określonego prostokątnego wycinka ekranu, pozostawiając resztę bez zmian. Ilustruje to pochodzący z kart katalogowych Solomon Systems rysunek 8.

Rys.8 Rysunek z kart katalogowych Solomon Systems

Jest to bardzo cenna właściwość, ponieważ bardzo często nie musimy zmieniać treści całego obrazu, a jedynie modyfikować jego fragmenty. Już tu wspomnijmy, że takie „prostokąciki” mogą być niezależnymi małymi grafikami (sprajtami – sprite), ale co bardzo ważne, mogą być literami, cyframi i innymi symbolami.

Zamiast żmudnie przygotowywać cały tekst i zawartość całego ekranu w pamięci mikroprocesora, możemy znakomicie ułatwić wyświetlanie tekstów i uprościć program. Mianowicie możemy wysyłać kolejno na ekran wcześniej przygotowane malutkie obrazki, które będą literami, cyframi i innymi symbolami. Obrazki te mogą być przygotowane w różny sposób, więc możemy zaprezentować tekst różnej wielkości i o różnych krojach pisma.

Wkraczamy w kolejną ważną i szeroką dziedzinę. Do kwestii wyświetlania tekstów i innych znaków wrócimy, a na razie podkreślmy, że można w stosunkowo prosty sposób modyfikować zawartość drobnych części ekranu, w szczególności pojedynczych pikseli. Na istniejącą treść obrazu można więc „nadpisywać” drobne fragmenty, przy czym w wyświetlaczach kolorowych trzeba uwzględniać też kolor tła.

Już tu widzimy, jak prosta i atrakcyjna jest idea zmiany stanu pojedynczych pikseli ekranu. Tak, ale nietrudno się domyślić, że jest to czasochłonne i że realizacja tego rodzaju wartościowych sposobów spowalnia prace systemu. Ma to ścisły związek z funkcjami, zawartymi w bibliotekach. To co pod jednym względem jest zaletą, pod innym okazuje się wadą. Nie sposób tego omówić szczegółowo, ponieważ zagadnienie jest naprawdę skomplikowane, a my dotykamy jedynie czubka góry lodowej.

Podsumowanie - moduły wyświetlaczy graficznych do Arduino 

Aktualnie na rynku dostępne są monochromatyczne i kolorowe wyświetlacze graficzne różnego typu, o różnej rozdzielczości i mające różne interfejsy. Wyświetlacze takie stają się coraz tańsze i ich zakup nie jest problemem nawet dla przeciętnego hobbysty. Dlatego hobbyści także do współpracy z Arduino chcą stosować wyświetlacze coraz większe, o coraz większej rozdzielczości.

Jednym z kluczowych ograniczeń jest omówiona wcześniej prędkość transferu danych z procesora do modułu wyświetlacza. Problem jest największy, gdy chodzi o transmisje sygnałów wideo (filmów), a także szybkich animacji. Tam wymagana jest ogromna szybkość transferu danych, absolutnie nieosiągalna w Arduino.

Jednak gdy na ekranie prezentowane mają być obrazy statyczne i zmiany treści obrazu mają być niewielkie i niezbyt częste, wtedy nawet powolne Arduino, wykorzystując obrazy zapisane na karcie SD, mogłoby obsłużyć nawet kolorowy wyświetlacz o bardzo dużej rozdzielczości. Owszem, tylko w praktyce obsługa takiego wyświetlacza będzie bardzo powolna.

To można byłoby zaakceptować, o ile w trakcie zmiany na ekranie nie występowałyby niepożądane artefakty, co jest możliwe do osiągnięcia w modułach wyświetlaczy z podwójnym (lub potrójnym) buforowaniem. Ale nie wszystkie wyświetlacze mają podwójne buforowanie, a niektóre celowo nie mają buforowania w ogóle. Można się tu naciąć na przykre niespodzianki.

Drugi problem to biblioteki. Z jednej strony jeśli jakiś moduł wyświetlacza jest dostępny na rynku, to pewne jest, że istnieje dla niego biblioteka Arduino. Tak, ale niestety jakość licznych bibliotek pozostawia wiele do życzenia. Często niedopracowane biblioteki znacznie zmniejszają realną szybkość transferu danych do modułu wyświetlacza.

Ponadto, jak mówi przysłowie: apetyt rośnie w miarę jedzenia. Początkowa euforia, związana z pierwszym wyświetleniem na ekranie grafiki, szybko mija. A zrealizowanie naprawdę użytecznego systemu Arduino z większym i „szybkim” wyświetlaczem graficznym okazuje się trudne, a wręcz niemożliwe.

Dlatego od początku trzeba mieć realistyczne oczekiwania. Owszem, Arduino w bardzo łatwy sposób pozwala wejść w skomplikowaną i obszerną tematykę wyświetlaczy graficznych. Ale pozwoli zrealizować użyteczne systemy tylko z „małymi” i „powolnymi” wyświetlaczami. Natomiast realizacja ambitniejszych zadań wymaga przejścia od płytek Arduino z procesorami ATmega328 i pokrewnymi do modułów ze znacznie silniejszymi i szybszymi procesorami (np. STM32), ewentualnie do Raspberry Pi.

Odcinek 7 kończy cykl dotyczący elementarnych zagadnień, związanych z wyświetlaczami graficznymi.

Tematyka materiału: wyświetlacze graficzne, interfejsy, ekrany dotykowe
AUTOR
Źródło
Elektronika dla Wszystkich wrzesień 2020
Udostępnij
UK Logo