Powrót

Kurs Arduino – Wyświetlacze OLED

W poprzednim odcinku (UR021c) poznaliśmy podstawowe funkcje graficzne uniwersalnej biblioteki Adafruit GFX. Wykorzystaliśmy ją do sterowania matrycowego wyświetlacza LED 32 x 8.Teraz wykorzystamy ją dla wyświetlacza OLED.

Pomocą był szkic A2007.ino. Chcieliśmy ten szkic wykorzystać do współpracy z popularnymi wyświetlaczami OLED 128×64 pikseli. I słusznie stwierdziliśmy, że: w szkicu A2007.ino trzeba tylko zmienić informacje o wyświetlaczu, a kluczowe fragmenty programu pozostaną te same.

Zainstalujemy więc bibliotekę „sprzętową” dla wyświetlacza OLED i w szkicu zmienimy tylko parę linijek, dotyczących wyświetlacza.

W zasadzie tak, ale jak to jest z Arduino, często napotykamy zupełnie nieoczekiwane problemy, które mogą wiązać się z ogromną stratą czasu na szukanie przyczyn i rozwiązań.

Tak jest i z najpopularniejszymi dziś wyświetlaczami OLED. Są bardzo często stosowane, więc narzuca się wniosek, że ich wykorzystanie jest lekkie, łatwe i przyjemne. Może być, jeśli unikniemy wstępnych pułapek.

Aby ich uniknąć, zaczniemy od przykładowego szkicu, dostarczonego z biblioteką. Ja ćwiczenia odcinka 20 wykonywałem na innym komputerze i wtedy zacząłem także próby z wyświetlaczem OLED, przy czym od SSD1306 doszedłem do SH1106

Jednak nowy odcinek kursu postanowiłem zrealizować od zera na drugim komputerze. Gdy uruchomiłem pakiet ArduinoIDE, wyświetlił się komunikat, że jest dostępna nowa wersja – pobrałem ją i zainstalowałem.

Jednak instalator „nie zauważył” zainstalowanej wcześniej wersji programu (z angielskojęzycznymi ustawieniami) i w innym miejscu na dysku powstała całkiem nowa instalacja 1.8.10… Oczywiście zainstalowałem wspólną bibliotekę Adafruit GFX za pomocą Menedżera bibliotek.

Na pierwszy ogień poszedł moduł OLED 128×64 o przekątnej 0,96 cala czyli 24,4mm, który według informacji sprzedawcy ma sterownik SSD1306.

Dlatego w oknie Menedżera bibliotek wpisałem OLED i wybrałem Adafruit_SSD1306 według rysunku 1. Można znaleźć inne biblioteki dla SSM1306, ale jeśli chcemy korzystać z uniwersalnej i wspólnej Adafruit GFX, to powinniśmy trzymać się bibliotek Adarfuit, licząc na zgodną ich współpracę.

Rysunek 1

Oczywiście druga instalacja ArduinoIDE zainstalowała biblioteki w …/Dokumenty/Arduino/libraries, gdzie były już wcześniej dodawane biblioteki

Tylna strona mojego modułu pokazana jest na fotografii 2.

Fotografia 2

Scalony sterownik SSD1306, a ściślej jego obwody logiczne, nie mogą być zasilane napięciem wyższym niż 3,3V. Jednak fotografia 2 wskazuje, że układ zawiera stabilizator LDO 3,3V w postaci układu oznaczonego U2, a jeżeli tak jest, to moduł można zasilić napięciem 5V. Na rynku jest wiele podobnych modułów wyświetlaczy OLED o różnej budowie. Sama obecność układu scalonego pozwala wnioskować, że jest to stabilizator LDO 3,3V.

Rysunek 3

Najprawdopodobniej mój moduł ma schemat identyczny lub bardzo podobny jak na rysunku 3, tylko z innymi wartościami niektórych elementów. Nie wnikając w szczegóły zgodności interfejsu I2C z 5-woltową płytką Arduino, a za to zgodnie z licznymi przykładami znalezionymi w Internecie, dołączyłem moduł do płytki bez żadnego konwertera poziomów, według rysunku 4.

Rysunek 4

Następnym krokiem jest przygotowanie programu – szkicu. W starszych materiałach można znaleźć wskazówki, że trzeba ingerować w plik biblioteczny Adafruit_SSD1306.h i tam ustawić rozdzielczość użytego wyświetlacza. Owszem, kiedyś było to konieczne, ale dziś biblioteka Adafruit_SSD1306 jest poprawiona i rozdzielczość wyświetlacza zadeklarujemy w szkicu .ino.

Do biblioteki Adafruit SSD1306 dołączone są przykłady, a ściślej dwa przykłady, z czego jeden w czterech bardzo podobnych wersjach. Oczywiście do mojego wyświetlacza wykorzystałem szkic ssd1306_128x64_i2c.ino. Jego analiza nie jest najłatwiejsza. Myląca jest obecność dyrektywy:

#include <SPI.h>

która jest niepotrzebna przy wyświetlaczu z interfejsem I2C.

Poważniejsze wątpliwości budzą inne linie. Korzystamy z biblioteki Adafruit, a w ofercie handlowej firma ta ma moduły podobnych wyświetlaczy z wyprowadzoną końcówką Reset, co pokazuje fotografia 5 (z materiałów sklepu Adafruit).

Fotografia 5

A mój tani moduł ma tylko cztery wyprowadzenia, więc linia resetu nie jest dostępna. Nasuwa się więc pytanie, czy w szkicu w linii:

#define OLED_RESET     4
// Reset pin # (or –1 if sharing Arduino reset pin)

pozostawić wartość domyślną 4, czy może wpisać –1 lub coś innego?

Nie wchodząc w szczegóły: wyświetlacz powinien działać z wartością domyślną 4. Jednak w niektórych źródłach można znaleźć zalecenie, by w przypadku modułów bez wyprowadzenia Reset, wpisywać w szkicu wartość –1.

Drugi problem jest poważniejszy i dotyczy adresu. Mianowicie na początku obowiązkowej funkcji setup() mamy sprytną procedurę realizującą inicjalizację modułu:

if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) {
Serial.println(F(„SSD1306 allocation failed”));
for(;;); // Don’t proceed, loop forever

Inicjalizacja polega na wykonaniu funkcji display.begin(). Jeżeli inicjalizacja się nie powiedzie, to na konsoli dołączonego kablem USB komputera zostanie wypisany komunikat: SSD1306 allocation failed i program nie pójdzie dalej. Problem polega na tym, że do funkcji display.begin() przekazujemy dwa argumenty, w tym adres naszego modułu OLED. Według oryginalnego przykładowego szkicu adres ten, zapisany szesnastkowo wynosi 0x3D. Natomiast na tylnej stronie modułu wyświetlacza mamy możliwość wyboru jednego z dwóch adresów, tylko żaden z nich nie wynosi 0x3D. Fotografia 2 pokazuje, że do wyboru mamy: 0x78 oraz 0x7A.

Co więc wpisać w szkicu?

Wyjaśnienie szczegółów znajdziesz w dalszej części artykułu.

Na razie w skrócie: jeżeli w posiadanym przez Ciebie module OLED zworka ustawia adres 0x78 (tak jest w module pokazanym na fotografii 2), to w szkicu trzeba zmienić adres z domyślnego 0x3D na o jeden mniejszy, czyli 0x3C. Po zmianie tego jednego szczegółu kompilujemy szkic i ładujemy do płytki Arduino.

Program najpierw wyświetla logo Adafruit, co na moim dwukolorowym wyświetlaczu (16 rzędów żółtych, reszta niebieskie) wyglądało tak, jak na fotografii 6.

Fotografia 6

Fotografia 7

Następnie prezentowany jest szereg efektów graficznych i tekstowych, a potem program przechodzi do „opadów śniegu” według fotografii 7, co jest też związane z wyświetleniem na konsoli współpracującego komputera współrzędnych tych dziesięciu zadeklarowanych w szkicu „płatków” i prędkości ich opadania (1…6). U mnie wyglądało to jak na rysunku 8, ale u Ciebie wartości x, y i dy będą inne.

Rysunek 8

Warto kilkakrotnie obejrzeć na ekranie OLED sekwencję efektów, naciskając w płytce Arduino przycisk RESET, gdy na ekranie zaczną „opadać płatki śniegu”. Potem trzeba przeanalizować szkic i zidentyfikować, które fragmenty programu są odpowiedzialne za poszczególne efekty. Podczas takiej analizy natkniemy się na niespodzianki.

W szkicu 1 znajdziemy kluczowe fragmenty programu testowego.

Szkic 1:      –czy potrzebne kolory takie  jak są ???
#include <Wire.h> // 3 niezbedne biblioteki
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// tworzymy obiekt wyswietlacza o nazwie: display
Adafruit_SSD1306 display(128, 64, &Wire, -1);
(…)
//definiujemy jakąś niewielką bitmapę
static const unsigned char PROGMEM logo_bmp[] =
{ B00000000, B11000000,
B00000001, B11000000,
… i tak dalej ….
void setup() {
//inicjalizacja obiektu wyświetlacza:
display.begin(SSD1306_SWITCHCAPVCC, 0x3D)
//ku naszemu zdziwieniu tu wyświetlane jest logo Adafruit:
display.display();
delay(2000);  display.clearDisplay();
// zaświecamy na 2 sekundy pojedynczy piksel:
display.drawPixel(10, 10, SSD1306_WHITE);
display.display();   delay(2000);
//a potem wyświetlamy kolejne efekty z funkcji:
testdrawline();      // Draw many lines
testdrawrect();      // Draw rectangles (outlines)
testfillrect();      // Draw rectangles (filled)
testdrawcircle();    // Draw circles (outlines)
testfillcircle();    // Draw circles (filled)
testdrawroundrect(); // rounded rectangles (outlines)
testfillroundrect(); // rounded rectangles (filled)
testdrawtriangle();  // Draw triangles (outlines)
testfilltriangle();  // Draw triangles (filled)
testdrawchar();  //Draw characters of the default font
testdrawstyles();    // Draw 'stylized’ characters
testscrolltext();    // Draw scrolling text
testdrawbitmap();    // Draw a small bitmap image
//na 1 sekundę „odwracamy” wyświetlanie:
display.invertDisplay(true);   delay(1000);
display.invertDisplay(false);  delay(1000);
//przechodzimy do pętli animacji: „opady śniegu”:
 testanimate(logo_bmp, LOGO_WIDTH, LOGO_HEIGHT);
} // koniec funkcji setup
void loop() { } //program nigdy tu nie dochodzi
//dalej są definicje wykorzystywanych funkcji – efektów:
void testdrawline() { (…) }
void testdrawrect(void) { (…) )
void testfillrect(void) { (…) }
void testdrawcircle(void) { (…) }
… i tak dalej aż do funkcji …
void testdrawbitmap(void) { (…) }
i na końcu funkcja – animacja „opady śniegu”:
(…)
void testanimate(*bitmap, w, h) {
//tworzymy licznik f i tablicę „płatków śniegu”
int8_t f, icons[NUMFLAKES][3];
//inicjalizacja „płatków śniegu” losowymi wartościami:
for(f=0; f< NUMFLAKES; f++) {
icons[f][XPOS]=random(1-LOGO_WIDTH, display.width());
icons[f][YPOS]   = -LOGO_HEIGHT;
icons[f][DELTAY] = random(1, 6); //prędkość opadania
//wyświetlamy te współrzędne na konsoli komputera:
Serial.print(F(„x: „));
Serial.print(icons[f][XPOS], DEC);
Serial.print(F(” y: „));
Serial.print(icons[f][YPOS], DEC);
Serial.print(F(” dy: „));
Serial.println(icons[f][DELTAY], DEC);   }
// a potem nieskończona petla „opadów śniegu”:
for(;;) { // Loop forever…
// co 200ms aktualizujemy wygląd ekranu:
for(f=0; f< NUMFLAKES; f++) {display.drawBitmap(…)}
display.display(); delay(200);
//i realizujemy „opadanie” poszczególnych „płatków”:
for(f=0; f< NUMFLAKES; f++) {
icons[f][YPOS] += icons[f][DELTAY];
// a jeżeli płatek opadł do samego dołu ekranu:
if (icons[f][YPOS] >= display.height()) {
// to zostanie wyświetlony na  górze w losowym miejscu
//i określona zostanie losowa prędkość jego opadania
icons[f][XPOS]=random(1-LOGO_WIDTH, display.width());
icons[f][YPOS]   = -LOGO_HEIGHT;
icons[f][DELTAY] = random(1, 6); (…)   }

Na początku oczywiście dołączane są potrzebne biblioteki i tworzony jest obiekt reprezentujący wyświetlacz. Dokładniej mówiąc, w tym przypadku tworzony jest obiekt o nazwie display, należący do klasy Adafruit_SSD1306. Tworząc obiekt reprezentujący wyświetlacz, podajemy szerokość i wysokość wyświetlacza, określamy, jakim interfejsem będzie obsługiwany (Wire, czyli TwoWire = I2C) oraz określamy pin (nieistniejącego w naszym przypadku) wyprowadzenia Reset.

Dalej w programie zdefiniowana jest jakaś malutka bitmapa (16×16), która okaże się płatkiem śniegu. Dyrektywa PROGMEM powoduje, że dla tej bitmapy nie będzie tworzona zmienna w pamięci RAM, tylko do wyświetlenia będzie ona pobierana wprost z pamięci programu (FLASH), co oszczędzi cenną pamięć RAM.

Dalej w obowiązkowej funkcji setup() dokonywana jest inicjalizacja obiektu wyświetlacza poleceniem

display.begin(SSD1306_SWITCHCAPVCC, 0x3D)

co obejmuje wysłanie do sterownika rozkazu, by podwyższone napięcie dla diod OLED było wytwarzane przez wewnętrzną przetwornicę – wbudowany powielacz pojemnościowy. Podczas inicjalizacji podajemy także adres wyświetlacza w systemie I2C: albo 0x3C, albo 0x3D, bo takie dwa adresy może mieć wyświetlacz, zależnie od ustawienia zworki na płytce.

Obserwując sekwencję zmian treści wyświetlacza, zauważamy zaświecenie pojedynczego piksela o współrzędnych (10, 10), do czego wykorzystywana jest funkcja – metoda .drawPixel(). Ale wcześniej wyświetlane jest logo firmy Adafruit.

Następuje to po standardowym poleceniu: display.display(), a jak wiemy, albo powinniśmy wiedzieć, metoda .display() powoduje jedynie wyświetlenie na ekranie aktualnej zawartości bufora, do którego wcześniej wpisano jakąś treść lub nie wpisano.

We wcześniejszej części szkicu nie ma żadnych instrukcji ładowania czegokolwiek do bufora. Jest tylko polecenie wyświetlenia zawartości bufora. Nie znaczy to jednak, że w moim tanim chińskim module w buforze sterownika SSD1306 fabrycznie wpisane jest logo firmy Adafruit.

Nie! Jest to przykład dziwnej i niekorzystnej dla użytkownika decyzji autorów biblioteki. Mianowicie wśród plików bibliotecznych znajdziemy plik splash.h, co pokazane jest na rysunku 9.

Rysunek 9

Okazuje się, że po pierwsze, w tym pliku zawarte jest wyświetlane na początku prezentacji logo Adafruit. Po drugie, w pliku bibliotecznym Adafruit_SSD1306.cpp mamy polecenie dołączenia tego pliku i wykorzystania go z pomocą metody drawBitmap(), co jest pokazane na szkicu 2.

Szkic 2:    –czy potrzebne kolory takie  jak są ???

(…) fragmenty pliku bibliotecznego Adafruit_SSD1306.cpp
#include <Adafruit_GFX.h>
#include „Adafruit_SSD1306.h”
#include „splash.h”
(…)
inicjalizacja – definicja metody .begin()
boolean Adafruit_SSD1306::begin(vcs, addr, reset, periphBegin) {
if((!buffer) && !(buffer=(uint8_t *)malloc(WIDTH*((HEIGHT + 7)/8))))
return false;
clearDisplay();
if(HEIGHT > 32) {
drawBitmap((WIDTH – splash1_width) / 2, (HEIGHT – splash1_height)/2,
splash1_data, splash1_width, splash1_height, 1);
} else {
drawBitmap((WIDTH – splash2_width) / 2, (HEIGHT – splash2_height)/2,
splash2_data, splash2_width, splash2_height, 1);
(…) tu dalsza część biblioteki…

Bez naszej wiedzy biblioteka podczas inicjalizacji ładuje do bufora wyświetlacza logo Adafruit! W dołączonym przykładowym szkicu tę bitmapę wyświetlamy, ale w innych przypadkach nie tylko jej nie wykorzystamy, ale może ona poważnie przeszkadzać.

Między innymi dlatego, że bez naszej wiedzy w treści programu, a potem w pamięci FLASH procesora umieszczana jest bitmapa, która powiększa objętość programu. Z rysunku 9 wynika, że plik splash.h ma aż 8 kilobajtów, jednak w programie umieszczana jest tylko jedna z dwóch zawartych tam wersji i to w postaci bardziej zwartej.

Dla ciekawości najpierw skompilowałem przykładowy szkic z biblioteką oryginalną. Następnie zmodyfikowałem plik biblioteczny Adafruit_SSD1306.cpp, dodając znaczniki komentarza, czyli wyłączając wpisywanie bitmapy do bufora według rysunku 10.

Rysunek 10

Zamknąłem Arduino IDE, ponownie otworzyłem i jeszcze raz skompilowałem przykładowy szkic. Rysunek 11 pokazuje różnicę. Dostępna pamięć RAM jest taka sama, bo obrazek – logo pobierany jest wprost z pamięci programu. Zajęta pamięć FLASH w wersji zmodyfikowanej jest mniejsza o niebagatelne 1398 bajtów!

Rysunek 11

Warto o tym wiedzieć, bo często taka zmiana objętości programu decyduje o sukcesie lub porażce.

Dalej w szkicu 1 z niewielkim zdziwieniem zauważymy, że program nigdy nie dochodzi do pętli loop(), tylko całe działanie zamyka się w funkcji setup(). Najogólniej biorąc, po konfiguracji, inicjalizacji i zaświeceniu pojedynczego piksela wykonywanych jest szereg funkcji, zdefiniowanych w dalszej części szkicu, poczynając od funkcji testdrawline().

Warto starannie przeanalizować wszystkie te funkcje i zobaczyć, jak wykorzystywane są poszczególne metody biblioteki Adafruit GFX.

Okaże się, że procedury realizujące te interesujące animacje są zaskakująco proste.

Najbardziej intrygująca i troszkę trudniejsza do zrozumienia jest animacja spadania płatków śniegu, która zawarta jest w funkcji testanimate(). Na początku szkicu określamy liczbę widocznych na ekranie płatków (NUMFLAKES). Program tworzy  wtedy  dwuwymiarową tablicę icons[NUMFLAKES][3], która dla każdego płatka będzie zawierać jego pozycję w osi X, w osi Y oraz prędkość opadania w zakresie 1…6 (DELTAY). Pozycja w osi X i prędkość opadania na początku działania funkcji testanimate() są generowane losowo za pomocą funkcji random(). Później z upływem czasu zwiększane są współrzędne Y, przez co „płatek opada w dół”. Gdy opadnie do dolnej krawędzi wyświetlacza, tworzony jest u góry nowy płatek z losowo ustaloną pozycją X i prędkością opadania.

W sumie zrozumienie działania omawianego przykładowego programu nie jest trudne. Ale samo przeczytanie i zrozumienie działania to nie wszystko. Przydałoby się trochę praktyki.

Wcześniej wykorzystywaliśmy wyświetlacz matrycowy LED 32 × 8 i ostatnio sterowaliśmy nim z wykorzystaniem biblioteki Adafruit GFX. Teraz mamy wyświetlacz OLED 128 x 64 i też wykorzystujemy bibliotekę Adafruit GFX.

Mam propozycję nie do odrzucenia, żebyś samodzielnie zmodyfikował wcześniejszy szkic A2007.ino, by dostosować go do wyświetlacza OLED.

Zadanie nie jest trudne. Trzeba tylko pamiętać o prawidłowym adresie I2C. Wcześniej do przepisywania zawartości bufora na ekran wykorzystywaliśmy metodę .write(), teraz trzeba ją zastąpić metodą .display().

Tak prosto przerobiony szkic A2007.in0 dostępny jako plik A2101.ino.

Oczywiście teraz na maleńkim wyświetlaczu wcześniejsze figury są prawie nieczytelne. Ale nic nie stoi na przeszkodzie, byś poćwiczył wykorzystanie możliwości biblioteki Adafruit GFX i pozmieniał działanie programu, by uzyskać rozmaite interesujące efekty i animacje. Nie żałuj czasu na tego rodzaju ćwiczenia, ponieważ obecnie małe wyświetlacze OLED są najpopularniejszymi wyświetlaczami graficznymi i zapewne będziesz je wykorzystywał.

W następnym odcinku (UR022) przedstawiam perypetie z podobnym wyświetlaczem OLED o przekątnej 1,3 cala, który jest widoczny z lewej strony fotografii 7. Wyjaśniamy też zagadkę, dlaczego adresy podane na płytce nie zgadzają się z adresami, które trzeba wpisać w szkicu.

Piotr Górecki