Skocz do zawartości

Wyświetlacze OLED


deshipu

Pomocna odpowiedź

Od pewnego czasu na rynku dostępne są w przystępnych cenach niewielkie moduły z wyświetlaczami OLED. Mogą one z powodzeniem służyć jako wyświetlacz w naszych projektach, szczególnie, że istnieje wiele bibliotek do ich obsługi na praktycznie każdą platformę. Niestety, biblioteki te bardzo często są powolne i zasobożerne, szczególnie jeśli chodzi o pamięć. W tym artykule nauczymy się jak można te wyświetlacze obsługiwać bez bibliotek i jak, odpowiednio przystosowując nasz projekt, możemy to robić bardzo szybko i wydajnie.

Jako przykład użyję płytki Wemos D1 Mini z shieldem OLED, a żeby skupić się bardziej na tym co robimy, niż na szczegółach implementacji, przykładowy kod będzie pisany w MicroPythonie. Nie powinno to jednak stanowić przeszkody przed zaadaptowaniem tych samych metod dla dowolnej platformy i dowolnego języka.

Sprzęt

Wemos D1 Mini to znana i lubiana płytka zawierająca ESP8266, który programuje się poprzez złącze szeregowe, wraz z konwerterem USB, umożliwiającym łatwe podłączenie do komputera. Pod Linuksem wszystko powinno działać od razu (tylko czasem trzeba się dodać do grupy dialout i przelogować), pod Windowsem czy Makiem trzeba zainstalować sterowniki.

Jako wyświetlacza użyjemy shielda OLED dla tej płytki, unikając w ten sposób problemów z podłączeniem czy z rozbieżnościami pomiędzy różnymi modułami. Moduł ten zawiera wyświetlacz o rozdzielczości 64x48 ze sterownikiem SSD1306. Oczywiście te same metody i techniki powinny zadziałać z praktycznie każdym podobnym modułem, gdyż wszystkie one używają praktycznie takiego samego zestawu poleceń. Tam, gdzie występują różnice będzie to zaznaczone.

Oprogramowanie

Na naszą płytkę będziemy musieli wgrać MicroPythona, a na naszym komputerze zainstalować narzędzie Ampy służące do wgrywania programów na płytkę.

Poza tym potrzebny będzie nam dowolny edytor tekstu, którym utworzymy plik "oled.py", który następnie uruchamiać na naszej płytce będziemy poleceniem "ampy -p PORT run oled.py", gdzie "PORT" oznacza nazwę portu, który pojawił się w systemie po podłączeniu płytki. W przypadku Linuksa będzie to "/dev/ttyUSB0", lecz na innych platformach może się nazywać inaczej.

Protokół Komunikacji

Wyświetlacz na naszym module został skonfigurowany do porozumiewania się poprzez protokół I²C używając nóżek SDA (D2, gpio4) oraz SCL (D1, gpio5) naszej płytki. Jako że ten protokół umożliwia podłączenie do tych samych nóżek wielu urządzeń, muszę być one identyfikowane adresem, który w przypadku naszego modułu domyślnie jest ustawiony na 0x3C (czyli 60 dziesiętnie).

Kontroler wyświetlacza przyjmuje dwa rodzaje komunikacji: dane do wyświetlenia, albo komendy ustawiające wartości wewnętrznych rejestrów, zmieniające jak wyświetlacz działa. Te dwa rodzaje odróżniane są przez pierwszy bajt wysłany do kontrolera: jeśli ma on wartość 0x00, to odebrane bajty interpretowane są jako komendy, a jeśli ma wartość 0x40, to są to dane od razu wpisywane do pamięci obrazu.

Spróbujmy zatem wysłać jakieś dane:

import machine

i2c = machine.I2C(-1, machine.Pin(5), machine.Pin(4))
i2c.writeto_mem(0x3C, 0x40, # send data
    b'\xFF\xFF\xFF'
)

Ten kod importuje moduł "machine" zawierający funkcje niezbędne do obsługi peryferiów naszej płytki, następnie tworzy obiekt i2c używając nóżek gpio4 i gpio5, oraz wysyła na adres 0x3C bajt 0x40, po którym wysyła trzy bajty 0xFF.

Jeśli spróbujemy uruchomić ten kod na naszej płytce z podłączonym shieldem, to przekonamy się że... nic on nie robi. Dlaczego?

Włączenie Wyświetlacza

Okazuje się, że nasz wyświetlacz zaraz po włączeniu prądu domyślnie jest w trybie uśpienia — działa tylko kontroler, a sam wyświetlacz nie jest zasilany. Dodatkowo, nie jest też włączony powielacz napięcia generujący wysokie napięcie niezbędne do działania wyświetlacza — w domyślnej konfiguracji wysokie napięcie jest dostarczane z zewnątrz. Zatem aby uruchomić wyświetlacz, musimy wydać trzy komendy: po pierwsze, musimy upewnić się, że jest wyłączony, bo komenda włączająca powielacz może być wydana tylko przy wyłączonym wyświetlaczu; po drugie, musimy włączyć powielacz napięcia; i wreszcie po trzecie, możemy włączyć sam wyświetlacz. Kod, który to robi wygląda tak:

import machine

i2c = machine.I2C(-1, machine.Pin(5), machine.Pin(4))
i2c.writeto_mem(0x3C, 0x00, # send commands
    b'\xAE' # display enable = off
    b'\x8D\x14' # charge pump = enable
    b'\xAF' # display enable = on
)

Po uruchomieniu tego kodu powinieneś zobaczyć na wyświetlaczu losowe piksele.

Pamięć Obrazu

To, co widzisz, to zawartość pamięci wewnątrz kontrolera wyświetlacza. Zaraz po włączeniu jest ona wypełniona losowymi danymi, ale za chwilę nauczymy się jak je zmienić. Zaczniemy od opisania jak ona jest zorganizowana.

Pamięć w naszym wyświetlaczu podzielona jest na 8 stron, każda odpowiadająca poziomemu paskowi o wysokości 8 pikselu przebiegającemu od lewej do prawej strony wyświetlacza przez całą szerokość. Te paski, z kolei, podzielone są na 128 pionowych kolumn, szerokości 1 piksela i wysokości 8, odpowiadających kolejnym bajtom na stronie pomięci. Wreszcie poszczególne bity tych bajtów odpowiadają indywidualnym pikselom w tych kolumnach.

Jeśli dokonasz szybkich obliczeń to zauważysz, że coś się tu nie zgadza. Nasz wyświetlacz ma rozdzielczość 64x48, a pamięci jest na 128x64. Co się stało? Otóż kontroler wyświetlacza, czyli chip zamontowany bezpośrednio na szkle ekranu, przeznaczony jest do obsługi różnych ekranów. W naszym przypadku część jego nóżek po prostu pozostaje niepodłączona i nieużywana. Ale to nic nie szkodzi, bo w dalszej części artykułu zobaczymy, że tę nieużywaną pamięć możemy jeszcze dobrze wykorzystać.

Sam ekran może być podłączony do kontrolera na wiele sposobów. Nasz konkretny jest podłączony dość dziwnie: pierwsza strona pamięci, licząc od góry, ma numer 7, następne to 0, 1, 2, 3, a strony 4, 5, 6 są domyślnie nieużywane. Z kolumnami też jest dziwnie: podłączone są tylko te od 32 do 95, zatem aby cokolwiek było widoczne na ekranie, musimy to rysować przesunięte o 32 piksele w lewo.

Uwaga: inne moduły wyświetlaczy mogą być podłączone inaczej, zatem offsety dla stron i kolumn muszą być dopasowane do konkretnego modelu ekranu. Niektóre ekrany są nawet podłączone z przeplotem i muszą mieć ustawiony odpowiedzialny za to rejestr, w przeciwnym razie widać będzie tylko co drugą linię.

Tryb Adresowania Stron

Czip SSD1306 obsługuje trzy różne tryby adresacji, ale my skupimy się na domyślnym, stronicowym, gdyż jest on najbardziej powszechny w różnych modelach sterowników. O innych trybach adresacji wspomnimy na końcu. Nasz wyświetlacz domyślnie zaczyna w trybie stronicowym, ale żeby się upewnić, że na pewno jest w tym trybie, możemy wysłać do niego bajty 0x20 i 0x02 jako komendy.

Pamięć wewnętrzna kontrolera ma wirtualny kursor, wskazujący miejsce, do którego wpisywane będą dane, które do niego prześlemy. Po każdym odebranym bajcie danych kursor przesuwa się o jedną kolumnę w prawo, aż do końca ekranu, jednak aby przejść do następnej strony, trzeba specjalnie wydać komendę przełączającą na daną stronę.

Komend takich jest po jednej na każdą stronę, zatem komenda 0xB0 przełącza na stronę 0, 0xB1 na stronę 1, etc. aż do 0xB7, która przełącza na stronę 7. Przesuwanie kursora do zadanej kolumny jest bardziej skomplikowane, bo wymaga wydania dwóch komend, podających oddzielnie pierwsze cztery bity i ostatnie cztery bity (a w zasadzie 3, bo maksymalna wartość to 127). Komendy 0x00 do 0x0F ustawiają młodsze bity (a więc przesuwają co 1 piksel), a komendy 0x10 do 0x17 ustawiają bity starsze (a więc przesuwają co 16 pikseli).

Uzbrojeni w tą wiedzę, możemy wreszcie spróbować wysłać jakieś dane do wyświetlacza:

import machine

i2c = machine.I2C(-1, machine.Pin(5), machine.Pin(4))
i2c.writeto_mem(0x3C, 0x00, # send commands
    b'\xAE' # display enable = off
    b'\x8D\x14' # charge pump = enable
    b'\xAF' # display enable = on
    b'\x20\x02' # address mode = page
    b'\xB7' # page = 7
    b'\x00x\x12' # column = 32
)
i2c.writeto_mem(0x3C, 0x40, # send data
    b'\xFF\xFF\xFF\xFF'
)

Po uruchomieniu tego programu powinniśmy zobaczyć na wyświetlaczu prostokąt 3x8 białych pikseli w jednym z narożników (dla domyślnej orientacji jest to lewy górny narożnik, ale na naszym shieldzie jest on zamontowany do góry nogami — potem zobaczymy co można na to poradzić).

Bity i Piksele

Wiemy zatem już jak narysować prostokąt, ale jak rysować pojedyncze piksele? Krótka odpowiedź brzmi: nie da się. Do wyświetlacza możemy wysyłać tylko całe bajty, a taki bajt to zawsze kolumna ośmiu pikseli. Aby dorysować piksel w kolumnie, w której już jakieś są musielibyśmy najpierw pobrać zawartość pamięci z kontrolera (albo z trzymanego do tego celu w pamięci bufora), wykonać odpowiednią operację bitową na tej wartości i wartości oznaczającej nasz nowy piksel, oraz wysłać to do wyświetlacza. Rysowanie pojedynczych pikseli jest zatem bardzo wolne i to właśnie jest powodem, dla którego tak wolne są także biblioteki do tych wyświetlaczy.

Na szczęście prawie nigdy nie musimy rysować po jednym pikselu. Zazwyczaj wysyłać będziemy do naszego wyświetlacza całe obrazki, fragmenty interfejsu, albo litery tekstu. Jeśli tylko trochę się postaramy aby ich granice pokrywały się z granicami stron, to możemy to robić bardzo szybko i z minimalnym zużyciem pamięci RAM.

Spróbujmy na przykład narysować literę X. Zaczniemy od zaprojektowania jak ona ma wyglądać, w postaci maski bitowej:

0b00110110
0b00001000
0b00110110
0b00000000

Gdy przeliczymy te bity na wartości szesnastkowe, otrzymamy cztery liczby: 0x36, 0x08, 0x36, 0x00. Wyślijmy to do wyświetlacza:

import machine

i2c = machine.I2C(-1, machine.Pin(5), machine.Pin(4))
i2c.writeto_mem(0x3C, 0x00,
    b'\xAE\x8D\x14\xAF' # init
    b'\xB7\x00x\x12' # page=7, column = 32
)
i2c.writeto_mem(0x3C, 0x40, # send data
    b'\x36\x08\x36\x00'
)

I oto na ekranie powinna pojawić się literka X.

Wyświetlanie Tekstu

Spróbujmy zatem wykorzystać tą technikę w praktyce, wyświetlając na ekranie tekst. W poniższym programie uporządkowaliśmy sobie trochę kod, przenosząc go do odpowiednich funkcji. Funkcja move ustawia kursor w odpowiednim miejscu, natomiast funkcja letter wysyła do wyświetlacza dane odpowiadające podanemu znakowi ASCII. Stała bajtowa FONT zawiera dane z obrazami liter, po cztery bajty na znak.

import machine


FONT = (b'\x00\x00\x00\x00\x00^\x00\x00\x06\x00\x06\x00~$~\x00,~4\x00b\x18F'
        b'\x004Jt\x00\x00\x06\x00\x00<BB\x00BB<\x00*\x1c*\x00\x08\x1c\x08\x00'
        b'@0\x00\x00\x08\x08\x08\x00\x00@\x00\x00`\x18\x06\x00<"\x1e\x00\x04>'
        b'\x00\x00:*.\x00**>\x00\x0e\x08>\x00.*:\x00>*:\x00\x02\x02>\x00>*>'
        b'\x00.*>\x00\x00\x14\x00\x00@4\x00\x00\x08\x14"\x00\x14\x14\x14\x00"'
        b'\x14\x08\x00\x02Z\x0e\x00~Z^\x00>\n>\x00>*4\x00>"6\x00>"\x1c\x00>*"'
        b'\x00>\n\x02\x00>":\x00>\x08>\x00">"\x000 >\x00>\x086\x00>  \x00>\x04'
        b'>\x00>\x02>\x00>">\x00>\n\x0e\x00>"~\x00>\x1a.\x00.*:\x00\x02>\x02'
        b'\x00> >\x00\x1e0\x1e\x00>\x10>\x006\x086\x00\x0e8\x0e\x002*&\x00~BB'
        b'\x00\x06\x18`\x00BB~\x00\x0c\x06\x0c\x00@@@@\x00\x02\x04\x004,<\x00>'
        b'$<\x00<$$\x00<$>\x00<4,\x00\x08~\n\x00\\T|\x00>\x04<\x00\x08: \x00`H'
        b'z\x00>\x084\x00\x02> \x00<\x1c<\x00<\x04<\x00<$<\x00|$<\x00<$|\x00<'
        b'\x04\x0c\x00,,4\x00\x04>$\x00< <\x00\x1c \x1c\x00<0<\x004\x084\x00\\'
        b'P|\x004,$\x00\x08vB\x00\x00~\x00\x00Bv\x08\x00\x10*\x04\x00')


def move(x, y):
    x += 32
    buffer = bytearray(3)
    buffer[0] = 0xB0 | y
    buffer[1] = x & 0x0f
    buffer[2] = 0x10 | (x >> 4) & 0x0f
    i2c.writeto_mem(0x3C, 0x00, buffer)


def letter(c):
    index = min(95, max(0, ord(c) - 32)) * 4
    buffer = FONT[index:index + 4]
    i2c.writeto_mem(0x3C, 0x40, buffer)

i2c = machine.I2C(-1, machine.Pin(5), machine.Pin(4))
i2c.writeto_mem(0x3C, 0x00, b'\xAE\x8D\x14\xAF')
row, col = 0, 0
move(col, row)
for c in "Hello world! This is a test! Testing 1, 2, 3.":
    letter(c)
    col += 1
    if col >= 16:
        col = 0
        row += 1
        if row >= 5:
            row = 0
        move(col, row)

Po uruchomieniu tego programu powinniśmy zobaczyć na ekranie coś takiego:

IMG_20181218_114911.thumb.jpg.dcf9df8208e0955f4a630a052a3f9604.jpg

Używając podobnych sztuczek możemy też wyświetlać symbole, elementy ramek, kursorów, wskaźników i w zasadzie dowolne inne rzeczy.

Przewijanie

A co z tą dodatkową pamięcią? Czy możemy ją jakoś wykorzystać? Okazuje się, że tak! Nasz kontroler obsługuje przewijanie w pionie, dzięki czemu możemy wybrać od której linii nasz wyświetlacz ma zaczynać wyświetlanie obrazu. Linię tę wybieramy komendami od 0x40 (linia 0, domyślna) do 0x7F (linia 63). Dodatkowo, pamięć się zawija, więc przy odrobinie sprytu możemy łatwo uzyskać nieskończone przewijanie. Zdefiniujemy sobie dla wygody funkcję:

def scroll(dy):
    _byte = bytearray(1)
    _byte[0] = 0x40 | dy & 0x3F
    i2c.writeto_mem(0x3C, 0x00, _byte)

Zabawę z tą funkcją pozostawiamy jako zadanie dla czytelnika.

Dostrajanie do Konkretnego Ekranu

Jak wspomnieliśmy wcześniej, ten sam kontroler jest stosowany do wielu różnych ekranów, zatem domyślne ustawienia niekoniecznie są optymalne. Poniższe komendy pozwalają na lepsze dopasowanie, umożliwiając zmianę orientacji, jasności, zmniejszenie migotania, czy oszczędzenie energii.

i2c.writeto_mem(0x3C, 0x00, # send commands
    b'\xA1'  # horizontal flip
    b'\xC8'  # vertical flip
    b'\xD3\x00'  # display offset = 0
    b'\x81\xFF'  # contrast = 255
    b'\xA8\x30'  # multiplex lines = 48
    b'\xA4'  # not all white
    b'\xA6'  # not inverted

    b'\xDA\x12'  # com pin = not interlaced
    b'\xD5\x80'  # clock/oscillator
    b'\xD9\xf1'  # pre-charge period
    b'\xDB\x30'  # VcomH level
)

Szczególnej ostrożności wymagają ostatnie cztery komendy, które są niestandardowe, dostępne tylko w SSD1306, a które kontrolują sposób zasilania i odświeżania ekranu. Ustawiając tam niewłaściwe wartości można nasz ekran uszkodzić. Niestety, o ile sprzedawcy często podają dane na temat samego kontrolera, to dane ekranu jest trudno zdobyć, więc pozostaje macanie po omacku (albo pozostawienie wartości domyślnych).

Inne Tryby Adresacji

Wspomnieliśmy na początku, że istnieją też inne tryby adresowania pamięci. W trybach tych nie ustawiamy tylko kursora, ale cały prostokąt, który chcemy modyfikować, a dane będą automatycznie wpisywane do następnej strony gdy tylko dojdą do brzegu naszego prostokąta. Jest to znacznie wygodniejsze, bo umożliwia na przykład wysłanie danych dla całego ekranu w jednym transferze. Używane do tego są komendy 0x21 i 0x22. W celu poznania szczegółów polecamy zapoznanie się z notą katalogową.

Dodatkowe Komendy

Dobrze jest wiedzieć jaki dokładnie kontroler jest na naszym wyświetlaczu, bo czasem moją one dodatkowe, niestandardowe komendy, które mocno ułatwiają życie. Na przykład SSD1306 ma funkcję automatycznego przewijania zawartości ekranu, zarówno w pionie jak i w poziomie. Inne kontrolery mogą mieć inne funkcję, warto więc zapoznać się z ich notami katalogowymi.

Inne Protokoły

Nasz shield skonfigurowany jest do używania protokołu I²C, ale ten sam kontroler może też używać 3- lub 4-przewodowego SPI, a także jednego z dwóch protokołów szeregowych.

Dostępne na rynku moduły używają albo I²C albo 4-przewodowego SPI, więc na tym się skupiamy. W przypadku użycia SPI oczywiście użyjemy machine.SPI, będziemy musieli ściągać pin CS do masy zawsze, gdy wysyłamy coś do kontrolera, a to, czy wysyłamy dane czy komendy sygnalizować będziemy nie dodatkowym bajtem, jak w I²C, ale pinem D/C — i to w zasadzie tyle różnic. Tryb SPI nie pozwala nam na odczyt, za to pozwala na znacznie większe prędkości transmisji.

  • Lubię! 1
  • Pomogłeś! 1
Link do komentarza
Share on other sites

@deshipu, @Elvis - przepraszam, że usunąłem Wasze posty z tego tematu, ale poleciały do kosza razem z wpisami @es2, który otrzymał ostrzeżenie skutkujące całkowitą blokadą pisania (za całokształt). Nie miałem już jak wydzielić z Waszych postów tych fragmentów, które nie były związane z zaczepkami es2 - nie chcę ręcznie ingerować w treści Waszych wiadomości, aby nie wprowadzać większego zamieszania.

  • Lubię! 2
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.