Podstawowe właściwości sterownika zostały pokazane w tabeli 1, a schemat blokowy sterownika pokazano na rysunku 1. Układ SSD1306 ma możliwość komunikowania się z systemem nadrzędnym za pomocą magistrali równoległej w standardzie Intel 8080 lub Motorola 6800 oraz za pomocą interfejsu szeregowego SPI lub I²C.
Dla aplikacji, w których jest ważna prędkość transmisji danych jest wybierana któraś z magistrali równoległych, a w pozostałych wypadkach - magistrale szeregowe.
Dla wielu typowych aplikacji jest wystarczająca prędkość transmisji dostępna za pomocą interfejsu szeregowego. Aktywny interfejs jest wybierany sprzętowo przez wymuszenie poziomów logicznych na wejściach konfiguracyjnych BS0...BS2 (tabela 2).
Poziom na wejściu BS2 określa czy będzie użyty interfejs szeregowy (BS2=0), czy równoległy (BS2=1). W tym wyświetlaczu BS2 jest na stałe dołączony do masy i mamy możliwość wyboru tylko jednego z trzech interfejsów szeregowych.
Na płytce drukowanej wyświetlacza są umieszczone dwa pola lutownicze umożliwiające ustawianie poziomów na BS0 i BS1 (fotografia 2).
Fabrycznie są tam wymuszone poziomy BS0=0 i BS1=0, czyli 4-przewodowy interfejs SPI. Zgodnie z tym ustawieniem są opisane wyprowadzenia płytki wyświetlacza. Oczywiście, można zmienić interfejs, ale nie widziałem powodu, aby to robić w układzie testowym, więc pozostawiłem ustawienia fabryczne.
Ponieważ nie przewidziano odczytywania jakichkolwiek danych ze sterownika, to SPI ma tylko linię danych DIN (MOSI z punktu widzenia hosta) oraz linię zegarową CLK. Oprócz tych linii, do przesyłania danych jest używana linia CS i D/C.
Wejście CS jest standardowym sygnałem SPI uaktywniającym układ, do którego są wysyłane dane. Wykorzystując CS można do jednej magistrali dołączyć kilka wyświetlaczy - każdy wybierany za pomocą odrębnej linii CS.
Linia D/C jest wykorzystywana do kierowania danych albo do rejestru komend (D/C=0), albo do pamięci obrazu (D/C=1).
Przy 3-przewodowym interfejsie SPI brak jest sygnału D/C i jest konieczne przesłanie dodatkowego bitu informującego sterownik, gdzie maja trafić dane z magistrali. Komplikuje to obsługę transmisji, bo dane są 9-bitowe i nie można wykorzystać standardowych, sprzętowych interfejsów SPI wysyłających słowa 1-bajtowe.
Używając interfejs I²C trzeba dodatkowo wysłać po adresie slave bajt kontrolny. Z jednej strony dodatkowa linia D/C wymaga linii portu sterownika, a z drugiej strony jej użycie przyśpiesza i upraszcza transfer danych. Przebiegi czasowe na liniach magistrali 4-przewodowej pokazano na rysunku 3.
Pamięć obrazu GDDRAM (Graphic Dispaly Data)
Pamięć obrazu jest statyczną pamięcią RAM o organizacji 128×64 bity. Każdemu bitowi pamięci odpowiada pojedynczy piksel na matrycy OLED. Pamięć o takiej organizacji nie może być bezpośrednio zapisywana przez mikrokontroler, dlatego została logicznie podzielona na 8 stron (Page0....Page7, rysunek 4).
Jedna strona zawiera 128 bajtów. Zapisanie bajtu do pamięci odpowiada pionowej linijce o długości 8 pikseli (rysunek 5). Aby zapewnić elastyczność przy mechanicznym mocowaniu wyświetlacza jest możliwe remapowanie segmentów: są one wybierane licząc od lewej do prawej, od 127 do 0. Podobnie są remapowane linie matrycy. Remapowaniem sterują dwie komendy: Set Segment Re-map (A0/ A1) i Set COM Output Scan Direction (C0/C8).
Zapisywanie danych do pamięci RAM jest adresowane przez dwa liczniki adresowe: licznik kolumn zmieniający się w zakresie 0...127 i licznik stron zmieniający się w zakresie 0...7.
Dostęp do pamięci RAM powoduje automatyczne zwiększenie licznika kolumn i/lub licznika stron. Zakres zmiany adresów i sposób inkrementacji jest określany przez tryb adresowania ustawiany komendą Set Memory Addressing (20h).
Pierwszym z tych trybów jest Page Addresing Mode. Zasadę adresowania w tym trybie zilustrowano na rysunku 6. Początkowy adres w pamięci jest ustalany przez komendy:
- Set Lower Column Start Address for Page Addressing Mode (00...0x0F). Ta komenda zapisuje 4 młodsze bity licznika startowego adresu w pamięci RAM dla trybu adresowania stron.
- Set Higer Column Start Address for Page Addressing Mode (0x10...0x1F). Ta komenda zapisuje 4 starsze bity licznika startowego adresu w pamięci RAM dla trybu adresowania stron.
- Set Page Address for Page Addressing Mode (0xB0... 0xB7). Ta komenda ustala licznik adresu stron w pamięci RAM dla trybu adresowania stron.
Po zapisaniu liczników każdy dostęp do pamięci RAM powoduje inkrementowanie licznika kolumn. Jeżeli licznik osiągnie wartość 127, to następny dostęp (zapis) do pamięci RAM powoduje wyzerowanie licznika kolumn, a licznik stron pozostanie bez zmian. Żeby go zmienić trzeba użyć komendy Set Page Address for Page Addressing Mode.
Kolejnym trybem jest tryb adresowania horyzontalnego Horizontal Addressing Mode pokazany na rysunku 7. Po każdym zapisaniu danej do pamięci RAM jest zwiększany licznik kolumn.
Jeżeli licznik osiągnie wartość końcową, to jest ustawiany na wartość początkową i jednocześnie licznik stron jest automatycznie inkrementowany. Jeżeli licznik stron i licznik kolumn osiągną wartość końcową, to są one automatycznie ustawiane na wartość początkową.
Ostatni tryb, to tryb adresowania wertykalnego Vertical Addresing Mode ( rysunek 8). Po każdym zapisaniu danej do pamięci RAM jest inkrementowany licznik stron.
Jeżeli licznik osiągnie wartość końcową, to jest ustawiany na wartość początkową i jednocześnie jest automatycznie inkrementowany licznik kolumn. Jeżeli licznik kolumn i licznik stron osiągną wartość końcową, to są one automatycznie ustawiane na wartość początkową.
Wartości początkowe i końcowe liczników kolumn i stron nie muszą mieć wartości maksymalnych adresujących całą pamięć wyświetlacza. Zakresy adresowania dla trybu horyzontalnego i wertykalnego są ustawiane przez dwie komendy:
- Set Column Address (0x21) komenda z dwoma argumentami z zakresu 0...127 określającymi adres kolumny początkowej i adres kolumny końcowej dla trybów adresowania horyzontalnego i wertykalnego.
- Set Page Address (0x22) komenda z dwoma argumentami z zakresu 0...7 określającymi adres strony początkowej i adres strony końcowej dla trybów adresowania horyzontalnego i wertykalnego.
Oba te tryby w połączeniu z komendami ustawiania adresów początku i końca obszaru umożliwiają łatwe wyświetlanie fontów mających wysokość większą niż 8 pikseli oraz małych bitmap (ikonek). Przykład takiego adresowania został pokazany na rysunku 9.
Komendy sterownika SSD1306
Przy okazji omawiania adresowania pamięci RAM obrazu opisałem kilka komend przeznaczonych do wyboru trybu adresowania i ustawiania liczników adresu. Sterownik SSD1306 ma spory zestaw komend sterujących. Dokładne opisywanie wszystkich znacznie wykracza poza zakres tego artykułu i nie jest też konieczne, bo dane te można znaleźć w dokumentacji. W tabeli 3 umieszczono zestawienie wszystkich komend.
Programowa obsługa wyświetlacza
Moduł wyświetlacza jest skonfigurowany do pracy z 4-przewodową magistralą SPI. Jako sterownik użyłem modułu Nucleo z mikrokontrolerem z rodziny STM-32F401RE produkowanym przez firmę STMicroelectronics. Główne powody tego wyboru to wbudowany programator i wsparcie w systemie bezpłatnego środowiska projektowego mbed. Wsparcie Nucleo przez mbed zapewnia łatwą konfigurację układów peryferyjnych i pozwala skupić się na głównym problemie, czyli na obsłudze wyświetlacza.
Na początek zajmiemy się interfejsem komunikacyjnym SPI. W środowisku mbed trzeba zainicjalizować interfejs SPI podając linie, na których będą dostępne sygnały: MOSI (dane wyjściowe), MISO (dane wejściowe) i SCK (zegar taktujący przesyłaniem danych).
Standardowo te sygnały są dostępne na złączu zgodnym ze standardem Arduino Uno modułu Nucleo. Jeżeli chcemy z nich skorzystać, wystarczy zadeklarować interfejs z predefiniowanymi definicjami. Interfejs SPI, oprócz linii MOSI, MISO i SCK, wymaga linii CS (standardowa linii interfejsu SPI) i linii DC określającej miejsce docelowe przesyłanych danych.
Poza tym, sterownik SSD1306 musi zostać sprzętowo wyzerowany przez wyzerowanie linii RESET. Linie interfejsu SPI, łącznie z linią RESET, należy również zdefiniować. Sposób wykonania konfiguracji sprzętowej pokazano na listingu 1.
Do sterowania wyświetlaczem będą nam potrzebne dwie użyteczne procedury: zapisania rejestrów stepujących, czyli kodu komendy i ewentualnie jej argumentów, oraz zapisania danej do pamięci obrazu wyświetlacza - zamieszczono je na listingach 2 i 3.
Inicjalizowanie sterownika
Jak większość sterowników wyświetlaczy, układ SSD1306 po włączeniu zasilania ustawia w rejestrach konfiguracyjnych wartości domyślne. To, jakie wartości trzeba wpisać do tych rejestrów zależy od rodzaju użytej matrycy OLED i zwykle program użytkownika musi je zapisać w procesie inicjalizacji.
Kompletną funkcję inicjalizacji void InitSSD1306() pokazano na listingu 4. Najpierw jest przeprowadzana sekwencja sprzętowa generująca impuls zerujący o długości 10 ms, ustawiająca bit CS i zerująca DC. Właściwa programowa inicjalizacja zaczyna się od wysłania komendy AE wyłączającej sterowanie matrycą OLED.
Potem są inicjowane (zerowane) liczniki adresowe kolumn i stron oraz jest wprowadzany tryb adresowania Page Addressing Mode. Zakładamy, że wyświetlacz będzie pracował w położeniu normalnym to znaczy, że dół wyświetlacza będzie przy krawędzi płytki z wyprowadzeniami. Do tej orientacji mechanicznej jest ustawiane remapowanie, kierunek skanowania i konfiguracja wyprowadzeń sterownika.
Sterownik może współpracować z różnymi matrycami, więc ma możliwość ustawienia wielu parametrów, takich jak: divide ratio, częstotliwość odświeżania oraz napięcie zasilające (prąd segmentów) matrycy OLED. Z tymi ustawieniami jest bardzo często problem.
Albo mamy wyświetlacz, o którym wiemy tylko, jaki ma sterownik albo w danych katalogowych nie są podane istotne informacje tego typu. Pozostaje inicjowanie pracy układu sterującego metodą prób i błędów. W opisywanym wyświetlaczu istnieje coś w rodzaju dokumentacji technicznej, ale niestety, opisuje ona głównie komunikację ze sterownikiem, a te informacje są doskonale opisane w dokumentacji sterownika SSD1306.
Zacznijmy inicjowanie sterownika od ustawienia kontrastu. Prąd segmentu OLED można wyliczyć z równania ISEG=Contrast/256×IREF×scale factor, w którym Contrast to wartość argumentu komendy 0x81 zmieniająca się w zakresie 0...255, a Scale Factor jest wartością stałą, równą 8.
Aby móc zasilić matrycę OLED napięciem wyższym od napięcia zasilającego układy cyfrowe, w układ SSD1306 wbudowano przetwornicę podwyższająca napięcie działającą na zasadzie pompy ładunkowej. Do działania takiego układu potrzebne są tylko dwa kondensatory zewnętrzne dołączone do wyprowadzeń C1N, C1P i C2N oraz C2P.
Wartość napięcia wyjściowego określa się za pomocą rezystora zewnętrznego dołączonego do wyprowadzenia IREF. Napięcie na tym wyjściu jest równe Vcc-2,5, gdzie Vcc jest napięciem zasilającym matrycę (rysunek 10). W opisywanym wyświetlaczu rezystor R1 ma wartość 1 MV i powinien wymuszać prąd o wartości ok 12,5 µA ±2 µA.
Powoduje on spadek na rezystorze R1 równy 12,5 V, zatem matryca jest zasilana napięciem 12,5 V+2,5 V=15 V. Po włączeniu zasilania sterownika przetwornica jest domyślnie wyłączona. Dlatego w procedurze inicjalizacji należy ją włączyć wysyłając komendę 8D z argumentem 0x14 (A2=1). Po wyliczeniu prądu jednego segmentu można optymalnie dobrać wartość kontrastu dla matrycy. Ja wpisałem 0xDF, ale eksperymentowałem również z innymi wartościami.
Sterownik SSD1306 ma wbudowany układ taktowania zbudowany z wewnętrznego oscylatora RC i programowanego dzielnika (rysunek 11). Do taktowania można tez użyć zewnętrznego zegara podawanego na wejście CL. Stopnień podziału częstotliwości programuje się w zakresie od 1 do 16 za pomocą komendy D5 Set Display Clock Divide Ratio/Oscillator Frequency.
Najmłodsze 4 bity zawierają wartość D, a DCLK=FOSC/D+1. Jak można zobaczyć w procedurze inicjalizacji argument komendy D5 ma wartość 0x80, czyli D+1=1. Sterownik jest taktowany częstotliwością równą częstotliwości FOSC. Cztery starsze bity argumentu zawierają współczynnik K określający ile cykli zegara jest przeznaczonych na wyświetlenie jednego rzędu (linijki). Częstotliwość, z którą są wyświetlane ramki można wyliczyć z równania
Mamy D=1, K=8, współczynnik multipleksowania Mux jest ustawiany komendą A8 i wynosi 64. Zatem FFRM=FOSC/(8×64)=FOSC/512. Częstotliwość oscylatora to około 370 kHz, a FFRM wynosi ok. 720 Hz.
Większość ustawień inicjalizacji została dobrana na podstawie informacji zawartych w dokumentacji sterownika. Ostatnią czynnością inicjalizacji jest wyczyszczenie (wyzerowanie) pamięci obrazu RAM, aby po inicjalizacji na ekranie nie wyświetlały się przypadkowe wartości.
Funkcję czyszczenia wyświetlacza pokazano na listingu 5. Działa ona poprawnie dla trybu adresowania Page Addressing Mode ustawionego w czasie inicjalizacji sterownika. Funkcja void DisplayCls() najpierw zeruje bufor w pamięci mikrokontrolera (dwuwymiarowa tablica), a potem zawartość tego bufora przepisuje do pamięci obrazu RAM sterownika - funkcja RefreshRAM(). Na listingu 6 pokazano procedurę void SetCol-Start() ustawiająca adres kolumny na początek (0x00).
Praca w trybie tekstowym
Wyświetlacz graficzny doskonale nadaje się do wyświetlania tekstu. W typowych wyświetlaczach alfanumerycznych czcionki mają jednakową wielkość zależną od wielkości wyświetlacza i mogą być wyświetlane w ustalonych wierszach. W wyświetlaczu graficznym można definiować czcionki o różnych rozmiarach (zależnie od potrzeb) i umieszczać je w dowolnym miejscu wyświetlacza, o ile tylko tam się zmieszczą.
Przy adresowaniu Page Addressing Mode jest łatwo zdefiniować znaki o wysokości 8 pikseli lub wielokrotności 8. Najłatwiej jest definiować znaki np. 8×6 pikseli umieszczając w pamięci generatora znaków 6 kolejnych bajtów - łatwo to sobie wyobrazić patrząc na rysunek 7.
W programowych bibliotekach obsługujących wyświetlacze graficzne stosowałem z powodzeniem tę metodę. Mam zdefiniowaną tablice generatora znaków i funkcje służące do wyświetlania znaku na podstawie jego kodu ASCII. Niestety w tym wyświetlaczu znaki o wysokości 8 pikseli są nieczytelne. Wynika to po prostu z małych wymiarów - w tym wyświetlaczu wartością graniczną jest wysokość 12 pikseli, a najlepiej, gdyby znaki miały 16 lub więcej pikseli.
Do rysowania znaków o dowolnej wielkości najlepiej nadaje się funkcja, która potrafizaświecić lub zgasić pojedynczy piksel o dowolnych współrzędnych x, y niezależnych od trybu adresowania. Taką funkcję pokazano na listingu 7.
Jak łatwo zauważyć, funkcja void DrawPoint() z list. 7 modyfikuje zawartość dwuwymiarowego bufora DispBuff umieszczonego w pamięci RAM mikrokontrolera hosta i nie zapisuje modyfikacji do sterownika SSD1306. Jak zobaczymy dalej, wszystkie procedury wyświetlania modyfikują tylko ten bufor i aby zobaczyć efekt tych modyfikacji trzeba przepisać całą zawartość DispBuff do pamięci sterownika wywołując funkcję void RefreshRAM() pokazaną na list. 5.
Mając do dyspozycji procedurę umożliwiającą zaświecenie/zgaszenie piksela o konkretnej współrzędnej możemy rysować proste, figury, okręgi, ale też rysować znaki alfanumeryczne o dowolnej wielkości. Żeby to robić, potrzebne są wzorce znaków umieszczone w tablicy - generatorze znaków.
Ręczne tworzenie wzorców znaków jest możliwe, ale to żmudne zajęcie. Istnieje szereg programów tworzących wzorce o zadanej wielkości znaku. W Internecie można też znaleźć gotowe tablice z wzorcami. Ja skorzystałem z gotowych tablic ze zdefiniowanymi znakami o wielkości 12×6 pikseli i 16×8 pikseli.
Tablice są tak zbudowane, że kod ASCII znaku adresuje grupę bajtów definiujących ten znak. To znacznie upraszcza procedury wyświetlające łańcuchy znaków (napisy). Na przykład, tablica dla znaków o wysokości 12 i szerokości 6 pikseli jest dwuwymiarową tablicą const char c_chFont1206[95][12] i zawiera 95 wzorców znaków.
Każdy element wzorca znaków ma 12 bajtów i definiuje znak o szerokości 6 pikseli, ale wykorzystuje przestrzeń 12×8 pikseli (listing 8). Dla znaków 16×8 pikseli jest zdefiniowana druga tablica const uint8_t c_chFont1608[95][16].
Mając tablicę generatora znaków i procedurę DrawPoint() potrafiącą "zapalać i gasić" bit w buforze pamięci mikrokontrolera odpowiadający zawartości pamięci RAM sterownika, a tym samym odpowiadający pikselowi na matrycy OLED można napisać procedurę "rysującą" znak w pamięci RAM mikrokontrolera. Pamiętamy, że aby ten znak się wyświetlił, trzeba przepisać zawartość DispBuff do pamięci RAM wywołując funkcję void RefreshRAM(). Na listingu 9 pokazano procedurę void DisplayChar() z argumentami:
- X i Y - współrzędne początku znaku na ekranie.
- Chr - kod ASCII wyświetlanego znaku.
- Size - wielkość znaku 12 lub 16 pikseli.
- Mode= 1 wyświetlanie normalne, mode=0 wyświetlanie w negatywie.
Zależnie od wartości argumentu size, bajty wzorca są pobierane z tablicy c_chFont1206[95][12] lub z tablicy c_chFont1608[95][16]. Kiedy argument mode jest wyzerowany, to dodatkowo wartość pobranego bajtu jest negowana. Potem jest analizowany każdy bit bajtu wzorca i zależnie od jego wartości DrawPoint() zapisuje do bufora DispBuff odpowiednią wartość.
Mając procedurę wyświetlania jednego znaku o dowolnych współrzędnych można napisać procedurę wyświetlającą łańcuch znaków od określonej pozycji. Na listingu 10 pokazano taką procedurę - void DispTxt() przyjmującą następujące argumenty:
- X i Y - współrzędne początku znaku na ekranie.
- *txt - wskaźnik na początek bufora z łańcuchem znaków.
- Size - wielkość znaku 12, lub 16 pikseli.
- Mode=1 wyświetlanie normalne, mode=0 wyświetlanie w negatywie.
Po każdym wyświetleniu jednego znaku na podstawie informacji o maksymalnych współrzędnych ekranu wyświetlacza (SSD1306_WIDTH i SSD1306_HEIGHT) oraz wartości argumentu size są wyliczane współrzędne kolejnego znaku.
Jeżeli kolejny znak nie zmieści się w całości w linijce, to tekst jest automatycznie przenoszony na początek kolejnej linijki (CR, LF). Efekt działania krótkiego fragmentu programu z listingu 11 jest pokazany na rysunku 12.
Wyświetlanie bitmap
Każda czcionka wyświetlana w trybie tekstowym jest bitmapą rysowaną na ekranie. Te bitmapy - wzorce znaków umieszczane są w tablicy generatora znaków i mają jednakową wielkość. Jednak często zachodzi konieczność wyświetlania bitmap o różnych rozmiarach.
Pewnym problemem jest konwertowanie monochromatycznych - ja używam programu bmp2.exe służącego do konwertowania bitmap monochromatycznych o niewielkich rozmiarach na tablicę w języku C. Kiedy już mamy skonwertowana bitmapę, to procedura jej wyświetlania jest stosunkowo prosta - pokazano ją na fotografii 12. Na fotografii 13 pokazano pełnowymiarową bitmapę 128×64 piksele, a na fotografii 14 kilka małych bitmap - ikon wyświetlanych w żółtym obszarze matrycy.
Tomasz Jabłoński, EP