Powrót

Kurs Arduino – Kłopoty z OLED

W poprzednim odcinku (UR021)  zaczęliśmy wykorzystywać bodaj najpopularniejsze dziś małe wyświetlacze OLED. Nie zawsze jest to tak proste, jak można by się spodziewać.Wcześniej wykorzystywaliśmy maleńki moduł wyświetlacza OLED o przekątnej 0,96 cala. Znacząco większe są wyświetlacze o przekątnej 1,3 cala, co widać choćby na fotografii 1.

Fotografia 1

Budowa modułu jest praktycznie taka sama, tylko ekran jest większy. Ponieważ w zapasach miałem nieużywany 1,3-calowy moduł, dołączyłem go do płytki Arduino, a było to bardzo łatwe przy wykorzystaniu miniaturowych chwytaków, które widać na fotografii 1. Jednak po zamianie wyświetlacza na większy, na ekranie zobaczyłem śmieci. Dwa przykłady znajdziesz na fotografii 2.

Fotografia 2

Przyczyną nie był błąd w adresie – zły adres uniemożliwia jakąkolwiek komunikację, a u mnie treść ekranu zmieniała się.

Narzucał się wniosek, że moduł jest uszkodzony.

Jednak intrygujące było to, że w górnej części ekranu kilka linijek zawierało fragmenty animacji, znanych z prawidłowo pracującego, mniejszego wyświetlacza.

To mogło świadczyć, że wyświetlacz pracuje, tylko jest jakiś problem z konfiguracją albo transferem danych.

Lektura karty katalogowej SSD1306 niczego nie wyjaśnia. Trzeba dotrzeć do informacji, czy w module pracuje sterownik SSD1306, czy też bardzo podobny SH1106?

Ja tej informacji nie miałem.

W przypadku popularnych wyświetlaczy tekstowych jest inaczej. Mówimy, że mają sterownik Hitachi HD44780, ale w rzeczywistości prawie wszystkie zawierają inne sterowniki, niemniej w pełni kompatybilne z oryginałem HD44780 i niesprawiające kłopotów. O sterowniku Sino Wealth SH1106 mówi się czasem, że jest odpowiednikiem czy zamiennikiem Solomon Systems SSD1306. Rzeczywiście jest bardzo podobny, ale nie identyczny. A właśnie przyczyną problemu okazał się sterownik SH1106, który może obsługiwać wyświetlacze 132×64 pikseli, natomiast SSD1306 wyświetlacze do 128×64 pikseli. Biblioteka Adafruit_SSD1306 po prostu nie może prawidłowo współpracować ze sterownikiem SH1106.

Rozwiązaniem jest zastosowanie odpowiedniej biblioteki sprzętowej. I tu zaczyna się kłopot. Jeżeli w Arduino IDE w Menedżerze bibliotek wpiszemy symbol sterownika SH1106, to znajdziemy co najmniej cztery biblioteki dla tego sterownika. Tak, tylko żadna nie pochodzi z Adafruit i zapewne żadna nie zechce współpracować z jakże pożyteczną biblioteką Adafruit GFX. Firma Adafruit nie sprzedaje modułów wyświetlaczy z SH1106, więc nie przygotowała stosownej biblioteki. Moglibyśmy przerzucić się na wspomnianą wcześniej U8g2, co miałoby swoje zalety, ale to jest inny zestaw i system, tylko dla wyświetlaczy monochromatycznych. My w ramach elementarnego kursu Arduino chcielibyśmy jednak wykorzystywać wygodną bibliotekę Adafruit GFX.

Po chwili poszukiwań w Internecie można znaleźć bibliotekę, a nawet kilka bibliotek z nazwą Adafruit_SH1106.

U mnie w pierwszej kolejności wyszukiwarka wskazała Github i zawartą tam bibliotekę Adafruit_SH1106 użytkownika wonho-master. Sterowniki SH1106 i SSD1306 są bardzo podobne, więc podstawowa przeróbka dotyczy tylko metody .display(). Niestety, różnica szerokości 132 lub 128 pikseli to nie tylko konieczność przesunięcia obrazu o dwa piksele. Daje ona szereg konsekwencji, wskutek czego po prostej przeróbce nie działają niektóre metody, w szczególności dotyczące przewijania (scroll), a czym zresztą poinformował autor przeróbki – rysunek 3.

Rysunek 3

Możemy wykorzystać tę bibliotekę, ale nie przez podstawienie do wcześniejszego przykładowego szkicu dyrektywy #include Adafruit_SH1106.h. Nowa biblioteka znacząco różni się od tej dla SSD1306 i trzeba byłoby dokonać mnóstwa przeróbek i usunąć fragmenty dotyczące przewijania (scrolling). Dlatego trzeba  wykorzystać przykładowy szkic dołączony do tej nowej biblioteki.

Po jego skompilowaniu i załadowaniu rozpocznie się animacja, znacząco jednak różna od tej dla SSD1306. Inny jest obrazek, ekran powitalny, który nie jest przechowywany w pliku splash.h, tylko jest zawarty w pliku Adafruit_SH1106.cpp. Oczywiście nie ma też efektów przesuwania tekstu.

Zachęcam do porównania i przykładowych szkiców, i plików .h oraz .cpp z obu bibliotek. W szczególności w pliku Adafruit_SH1106.cpp znajdziemy przygotowane polecenia przewijania (scroll), ale wyłączone – zakomentowane. Widać, że Autor modyfikacji nad nimi pracował, ale bez powodzenia.

Brak przewijania tekstu nie jest poważną wadą, więc można tę bibliotekę z powodzeniem wykorzystać, pamiętając jednak o różnicach i ograniczeniach względem SSD1306

Można też poszukać innych rozwiązań. Otóż oprócz biblioteki:

github.com/wonho-maker/Adafruit_SH1106

można znaleźć kilka innych bibliotek o identycznej nazwie Adafruit_SH1106.

Przykładowo biblioteka

github.com/shondll/Adafruit_SSD1306

według opisu pozwala obsługiwać zarówno sterownik SSD1306, jak i SH1106.

Z kolei biblioteki

github.com/ghztomash/Adafruit_SH1106

github.com/davidperrenoud/Adafruit_SH1106

zawierają w pliku Adafruit_SH1106.cpp także polecenia – metody przewijania (scroll), przez co na pozór wydają się bardzo atrakcyjne.

Niestety, wszystkie są podobnie stare jak modyfikacja wohno-maker. W niektórych jest informacja, że były testowane tylko z wyświetlaczami z łączem SPI. U mnie wszystkie sprawiały kłopoty przy kompilacji.

Oczywiście można dalej szukać rozwiązań, by także przy sterowniku SH1106 zrealizować przewijanie tekstu za pomocą Adafruit GFX. Jeśli masz ochotę albo taką potrzebę, możesz spróbować we własnym zakresie.

My odnotowujemy kolejny przykład ciemniejszej strony Arduino: Otóż biblioteki są dostępne, ale ich jakość bywa bardzo różna. Zależy to nie tylko od umiejętności programisty, który zdecydował się stworzyć czy zmodyfikować biblioteki, a potem za darmo udostępnić także i nam. Często biblioteki mają już kilka lat i z oczywistych względów nie były testowane przez autora na każdym dostępnym dziś sprzęcie i we wszystkich możliwych kombinacjach sprzętowo-programowych. Trzeba też pamiętać, że tworzenie bibliotek (klas) nie jest zadaniem łatwym, tym bardziej że mają one pracować na różnym sprzęcie, nie tylko na Arduino Uno i podobnych z procesorem ATmega328P.

Nic więc dziwnego, że często znajdujemy biblioteki, zawierające zdecydowanie nieoptymalne rozwiązania. W przypadku Arduino należy cieszyć się, jeżeli system działa. A jeżeli ktoś chce optymalizować jego działanie, to niech zapomni o Arduino i gotowych, uniwersalnych bibliotekach, tylko niech pisze program dokładnie pod swoje potrzeby. A to oczywiście wymaga dużej wiedzy i doświadczenia.

Dlatego w kursie Arduino w EdW pozostajemy jednak przy systemie Adafruit i podstawowej bibliotece graficznej GFX.

Adres I2C

Zgodnie z obietnicą z poprzedniego miesiąca, powinniśmy wyjaśnić kwestię dziwnych adresów.

Otóż mając moduł wyświetlacza, zawsze można go „odpytać” skanerem – programem skanującym szynę I2C (https://playground.arduino.cc/Main/I2cScanner/). W praktyce nie ma takiej potrzeby. Na wielu płytkach są do wyboru adresy 0x78 i 0x7A, co w postaci dwójkowej ma postać odpowiednio: 1111000 i 1111010. Tymczasem według karty katalogowej sterownika SSD1306 do wyboru są adresy: albo 0x3C (0111100), albo 0x3D (0111101). Aby wyjaśnić zagadkę, należy przypomnieć zasady pracy łącza I2C. Po pierwsze, w systemie I2C podstawowe adresy są 7-bitowe („rozszerzone” adresy 10-bitowe pomijamy), czyli teoretycznie do jednej szyny może być dołączonych do 127 urządzeń slave (adres 0 jest adresem rozgłoszeniowym). Natomiast przesyłane paczki danych są 8-bitowe, a każda przesłana paczka jest potwierdzana przez urządzenie odbierające dane dodatkowym, dziewiątym bitem. Transmisję inicjuje zawsze master i transmisja rozpoczyna się od wysłania przez urządzenie master ośmiu bitów, z których siedem to (7-bitowy) adres urządzenia, z którym master chce się komunikować, a ósmy, najmłodszy bit określa, czy master będzie wysyłał i zapisywał dane do tego zaadresowanego urządzenia (Write), czy chce z tego urządzenia dane odczytać (Read). Decyduje o tym właśnie ósmy bit, jak pokazuje rysunek 4.

Rysunek 4

W naszym przypadku gdy master, czyli procesor w płytce Arduino, chce wysłać dane do modułu wyświetlacza OLED, wysyła pierwszy bajt, który zawsze ma ostatni bit równy zeru (Write\). Wysyłany bajt w starszych bitach zawiera adres modułu wyświetlacza: 0x3C (0111100) albo 0x3D (0111101) i kończy się zerem. Jeżeli do takiej liczby dwójkowej dodamy na końcu zero (polecenie Write\), to otrzymamy 01111000 albo 01111010 czyli szesnastkowo 0xF8 albo 0x7A.

Nie wiadomo, dlaczego na chińskich modułach podawane są takie pseudoadresy, bo przecież rzeczywiste adresy I2C są liczbowo dwukrotnie mniejsze. W każdym razie w przypadku korzystania z biblioteki Adafruit_SSD1306 w szkicu trzeba podać prawidłowy adres I2C:  0x3C albo 0x3D, a nie adres z płytki.

Wyświetlanie obrazów

Biblioteka Adafruit GFX nie potrafi wyświetlić obrazków zawartych w plikach z popularnymi rozszerzeniami .jpg, .gif, .png, ani nawet .bmp. Oferuje jednak sposoby wyświetlania obrazków (grafiki), zarówno kolorowych, jak i czarno-białych.

Między innymi biblioteka Adafruit GFX oferuje metodę .drawBitmap(), która wyświetla najprostsze obrazy monochromatyczne, zawierające jedynie piksele jasne i ciemne.

Od razu warto też podkreślić, że oferuje też metodę .drawXBitmap(), za pomocą której można łatwo wyświetlić grafiki w formacie .xbm, uzyskiwane w programie GIMP. Ponadto biblioteka oferuje też wyświetlanie grafik w skali szarości za pomocą metody .drawGrayscaleBitmap() oraz kolorowych za pomocą metody .drawRGBBitmap().

W przypadku malutkich monochromatycznych wyświetlaczy OLED interesuje nas przede wszystkim metoda .drawBitmap(). Nie będziemy korzystać z .drawXBitmap(), co też jest proste i polega na zapisaniu w GIMP-ie obrazka w prościutkim formacie .xbm, zmiany rozszerzenia z .xbm na .c i wykorzystaniu w programie takiego pliku .c. My wykorzystamy funkcję – metodę .draw­Bitmap(), która przesyła na ekran obrazek – bitmapę, zawartą w pamięci procesora.

Metoda .drawBitmap() wykorzystuje bardzo prostą postać obrazka w postaci ciągu bitów, określających stan kolejnych pikseli. Wartość 1 to piksel „zaświecony”, wartość 0 to piksel nieaktywny. W programie, a potem w pamięci procesora jest umieszczony tylko taki jednolity ciąg bitów bez żadnych dodatkowych informacji. W szczególności nie ma tam informacji o szerokości i wysokości obrazka. Dlatego w funkcji – metodzie .drawBitmap() jako argumenty podajemy gdzie, co i jak wyświetlić.

Najpierw wskazujemy, gdzie wyświetlić, podając współrzędne x, y górnego lewego rogu obrazka. Potem wskazujemy, co wyświetlić, podając, powiedzmy najprościej, nazwę obrazka. Potem wskazujemy, jak wyświetlić taką surową treść, podając szerokość (w – width) i wysokość (h – height) obrazka, a także kolor (i ewentualnie kolor tła). Dopiero wtedy program wie, jak ma potraktować kolejne bity i jak je ułożyć na ekranie. Pokazane jest to na rysunku 5.

Rysunek 5

Podane  współrzędne x, y traktowane są jako liczby całkowite 16-bitowe ze znakiem (typu int16), rozmiary w, h jako liczby 16-bitowe bez znaku (typu uint16), podobnie kolor to liczba 16-bitowa (uint16). Trochę kłopotu jest z obrazkiem. Najogólniej biorąc, nazwa obrazka to miejsce w pamięci procesora, gdzie są informacje o kolejnych pikselach. Nieco dokładniej jest to etykieta, wskazująca, gdzie w pamięci zaczyna się treść obrazka.  Zapis uint8_t *nazwaObrazka wcale nie znaczy, że obrazek jest liczbą 8-bitową bez znaku (typu uint8 czyli char), bo byłoby to bezsensownie mało. Obecność gwiazdki pokazuje, że chodzi o tablicę (o nieokreślonych tu rozmiarach), składającą się z liczb 8-bitowych typu uint8.

By to zadziałało, taka tablica musi być zdefiniowana w programie. Rozsądek być może podpowiada, że będzie   to tablica dwuwymiarowa o liczbie kolumn i rzędów odpowiadającym szerokości i wysokości obrazka.

NIE! Tablica jest jednowymiarowa, czyli jest to ciąg liczb, określających kolory kolejnych pikseli skanowanych według rysunku 6. Metoda .drawBitmap() dotyczy tylko obrazków dwukolorowych (czarno-białych), treść obrazka zapisana jest w tablicy składającej się z bajtów (uint8), więc jeden bajt określa stan ośmiu pikseli.

Rysunek 6

Tworzenie obrazów

Możliwe jest ręczne przygotowanie obrazka, czyli ciągu bitów.  Zróbmy to! Spróbujemy wyświetlić na ekranie prościutki obrazek 5×5 pikseli. Kolejne etapy przygotowania pokazane są na rysunku 7.

Rysunek 7

Zgodnie z rysunkiem 6 tworzymy ciąg bitów, określający stan kolejnych bitów naszego obrazka. W ten sposób najpierw otrzymujemy 25-bitową liczbę dwójkową:

1100111010001000101010001

To jest bitowy opis naszego obrazka i powinniśmy go umieścić w jednowymiarowej tablicy. Już widzimy drobny problem: zgodnie z rysunkiem 5 ma to być tablica składająca się z liczb ośmiobitowych (uint8), czyli bajtów, a my mamy 25 bitów, czyli trzy bajty i „kawałek”. Słusznie domyślamy się, że „kawałki” są niedopuszczalne i niewiele myśląc, rozwiązujemy problem dopisując na końcu siedem jakichkolwiek bitów – najlepiej zer. Otrzymujemy cztery pełne bajty, które w programie – szkicu możemy zapisać albo w postaci dwójkowej, albo wygodniej szesnastkowej. Piszemy więc prościutki program, wyświetlający opis i nasze arcydzieło – szkic 1:

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // szerokość w pikselach
#define SCREEN_HEIGHT 64 // wysokość w pikselach
// tworzymy  obiekt wyświetlacza o nazwie: wysw
Adafruit_SSD1306 wysw(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
//definiujemy w pamięci programu obrazek: naszaBitmapa
const uint8_t naszaBitmapa [] PROGMEM = {0xCE, 0x88, 0xA8, 0x80};
 
void setup() { // jednorazowo:
 wysw.begin(SSD1306_SWITCHCAPVCC, 0x3C);//inicjalizacja obiektu wysw
  // pokaż ekran powitalny – logo Adafruit przez 1 sekundę:
 wysw.display();   delay(1000);
 wysw.clearDisplay();  // wyczyść ekran
 wysw.setCursor(0, 0); //kursor w punkcie „zerowym”
 wysw.setTextColor(1, 0);
 wysw.setTextSize(1);
 wysw.print(„nasza\nbitmapa:”); //w dwóch liniach
 wysw.drawBitmap(20, 20, naszaBitmapa, 5, 5, 1); //do bufora
 wysw.display(); } //teraz wyświetl na ekranie
 
void loop() { } //pusta pętla – kręć się w kółko…

Na pewno treść obrazka musi być zawarta w programie (w pamięci programu FLASH), skąd mogłaby być przepisana – skopiowana do odpowiedniej zmiennej tablicowej w pamięci RAM. Szkoda jednak cennej pamięci RAM. Zajmujący cały ekran monochromatyczny obrazek 128×64 pikseli to 1024 bity, a więc zająłby połowę pamięci RAM procesora ATmega328P. Dlatego obrazki z reguły wyświetlamy wprost z pamięci programu (FLASH), wykorzystując dyrektywę PROGMEM.

Po wgraniu programu do Arduino ekran wyglądał jak na fotografii 8.

Fotografia 8

Porażka! Jakiś obrazek jest, ale nie taki, jak chcieliśmy. Aby wyjaśnić problem, zajrzymy do biblioteki GFX, a konkretnie do pliku Adafruit_GFX.cpp i zawartej tam definicji metody drawBitmap(). Zrobimy to w następnym odcinku (UR023), a Ty już teraz spróbuj samodzielnie rozwiązać problem, by uzyskać na wyświetlaczu obraz pokazany na fotografii 9!

Fotografia 9

Piotr Górecki