Wyświetlacze OLED są chyba jednymi z najczęściej używanych peryferiów przez hobbystów. Oferują znacznie więcej możliwości niż klasyczne wyświetlacze LCD, a ich obsługa jest może nawet nieco prostsza. W tym krótkim cyklu poradników pokażę ci jak takie wyświetlacze (w wersji I2C) zmusić do pracy, oraz kilka innych sztuczek związanych z ich użytkowaniem.
Ja komunikuje się z wyświetlaczem za pomocą mikrokontrolera ATmega16. Polecenia jednak bez problemu będziesz mógł przenieść na jakąkolwiek, inną rodzinę mikrokontrolerów.
Na rynku dostęp mamy do kilku wersji oled’ów. Różnią się one kolorem podświetlenia – można znaleźć niebieskie, białe i zółte, oraz oczywiście wielkością. Wyróżnia się 3 główne przekroje – 0.96″, 1.3″ i 0.91″. To jaki wyświetlacz posiadasz nie ma znaczenia, ich obsługa jest praktycznie taka sama.
Komunikacja OLED – ATmega
Jako, że komunikacja przebiega za pośrednictwem magistrali I2C to wystarczą nam 3 typowe dla niej polecenia: i2c_start, i2c_stop, i2c_write. Poniżej przykładowe implementacje tych poleceń dla atmega16.
void I2C_start() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTA); while (!(TWCR & (1<<TWINT))); } void I2C_stop() { TWCR = (1<<TWINT) | (1<<TWEN) | (1<<TWSTO); while (!(TWCR & (1<<TWSTO))); } void I2C_write(uint8_t data) { TWDR = data; TWCR = (1<<TWINT) | (1<<TWEN); while (!(TWCR & (1<<TWINT))); }
Następnym krokiem będzie stworzenie dwóch podstawowych funkcji do obsługi wyświetlacza. Funkcja do wysyłania komend sterujących, oraz funkcja do wysyłania danych.
// WYSYŁANIE KOMEND DO MODUŁU void OLED_cmd(uint8_t cmd) { uint8_t control_data = 0x00; I2C_start(); I2C_write(oled_addr); I2C_write(control_data); I2C_write(cmd); I2C_stop(); } // WYSYŁANIE DANYCH DO MODUŁU void OLED_data(uint8_t data) { uint8_t control_data = 0x40; I2C_start(); I2C_write(oled_addr); I2C_write(control_data); I2C_write(data); I2C_stop(); }
Jak możesz zauważyć, funkcje te różnią się tylko wartością zmiennej „control_data”, którą przekazaujemy jako pierwszy bajt do wyświetlacza. Wartość ta mówi modułowi oled, czy chcemy wysłać do niego komendę, czy dane. W przypadku komendy wysyłamy 0x00, a danych 0x40 i dopiero wtedy możemy wysłać konkretny bajt.
Oczywiście, jeszcze przed bajtem kontrolnym wysyłam adres oled’a, by wyświetlacz zareagował na kolejne dane. Adres do zapisu takiego oleda to zwykle 0x78.
AVR OLED Inicjalizacja
// INICJALIZACJA MODUŁU void OLED_init(void) { I2C_init(); OLED_cmd(OLED_DISPLAY_OFF); OLED_cmd(OLED_SET_DISPLAY_CLOCK_DIV_RATIO); OLED_cmd(0x80); OLED_cmd(OLED_SET_MULTIPLEX_RATIO); OLED_cmd(0x3F); OLED_cmd(OLED_SET_DISPLAY_OFFSET); OLED_cmd(0x0); OLED_cmd(OLED_SET_START_LINE | 0x0); OLED_cmd(OLED_CHARGE_PUMP); OLED_cmd(0x14); OLED_cmd(OLED_MEMORY_ADDR_MODE); OLED_cmd(0x00); OLED_cmd(OLED_SET_SEGMENT_REMAP | 0x01); OLED_cmd(OLED_COM_SCAN_DIR_DEC); OLED_cmd(OLED_SET_COM_PINS); OLED_cmd(0x12); // Jeżeli 128x32 - 0x02 OLED_cmd(OLED_SET_CONTRAST_CONTROL); OLED_cmd(0xCF); OLED_cmd(OLED_SET_PRECHARGE_PERIOD); OLED_cmd(0xF1); OLED_cmd(OLED_SET_VCOM_DESELECT); OLED_cmd(0x40); OLED_cmd(OLED_DISPLAY_ALL_ON_RESUME); OLED_cmd(OLED_NORMAL_DISPLAY); OLED_cmd(OLED_DISPLAY_ON); }
To co widzisz powyżej, to sekwencja inicjalizacyjna naszego wyświetlacza. Schemat tej sekwencji możesz znaleźć w dokumentacji modułu SSD1306, na stronie 64. Dokumentacja – pdf
Makrodefinicje, natomiast znajdziesz w pliku nagłówkowym na githubie.
Skoro mamy już funkcje do podstawowej komunikacji, a także funkcję inicjalizacyjną to możemy załadować pierwszy program do mikrokontrolera. W funkcji „main” wywołaj tylko polecenie „OLED_init()”. Efekt inicjalizacji powinien wyglądać następująco.
Sterowanie wyświetlaczem
Nasz wyświetlacz po inicjalizacji jest pełen syfu. By go oczyścić wystarczy wystawić wartość 0 na każdy pixel. Możemy to zrobić w bardzo prosty sposób:
for (uint16_t i=0; i<1024; i++) { OLED_data(0); //_delay_ms(10); }
Po dodaniu tej operacji nie zauważysz tego co wcześniej.
Zwróć jednak uwagę na pętlę, którą utworzyłem – wykonuje się ona zaledwie 1024 razy. A jednak wystawia wartość 0 na każdy piksel, czyli na 8192 pikseli (128 * 64). Dzieje się tak ponieważ nasz wyświetlacz podzielony jest na wiersze – dokładnie 8 wierszy, z których każdy składa się z ośmiu pikseli.
Odkomentuj delay, który umieściłem w pętli to dokładnie zauważysz jak zanika każdy z wierszy. Zapamiętaj więc, że do poruszania się po takim wyświetlaczu masz do dyspozycji 128 kolum i 8 wierszy.
Możesz także spróbować zmienić wartość w funkcji „OLED_data” na, przykładowo 8. Wtedy w każdym wierszu powinien się pojawić rząd zapalonych pikseli. Dokładniej mówiąc, w każdym wierszu, każdej kolumny zapali się czwarty od góry piksel. Jak się nie trudno domyśleć operujemy tutaj na potęgach liczby 2.
Dlatego piksel ostatni zapalimy wysyłając do modułu liczbę 128. Jeśli natomiast chcielibyśmy zapalić więcej niż jeden piksel wystarczy wysłać ich sumę. Jeśli wyślesz liczbę 136, to zapali się piksel czwarty, oraz ósmy.
Budujemy prostą bibliotekę
Rysowanie piksela
Zaczniemy od czegoś postego, napisania funkcji rysującej piksel w dowolnym miejscu na wyświetlaczu 128×64. Funkcja przyjmowała będzie więc dwie zmienne typu uint8_t – x(0-127), oraz y(0-63).
void OLED_draw_pixel(uint8_t x, uint8_t y) { uint8_t row = 0; uint8_t pixel = 2; OLED_cmd(0x20); OLED_cmd(0x00); row = y/8; OLED_cmd(OLED_SET_COLUMN_ADDR); // Ustawianie kursora na kolumne OLED_cmd(x); OLED_cmd(x); OLED_cmd(OLED_SET_PAGE_ADDR); // Ustawianie kursora na wiersz OLED_cmd(row); row = y % 8; for (uint8_t i=1; i<row; i++) pixel = pixel*2; OLED_data(pixel); }
Na początku zdefiniowałem dwie zmienne, które posłużą nam do przeprowadzania obliczeń. Kolejno wysłałem dwie komendy do wyświetlacza, które ustawiają tryb adresacji na horyzontalny. Często bez ustawienia tego trybu przed wysłaniem danych, te nie są interpretowane tak jakbyśmy tego chcieli.
Do zmiennej „row” przypisałem wynik działania „y/8”, to nam da numer wiersza, w którym nasz pixel się znajduje. Następne 5 komend ustawia kursor w konkretnym miejscu, wpierw ustawiamy numer kolumny, a potem wiersza. Możesz zauważyć, że x wysłałem dwukrotnie – to dlatego, że tutaj mówimy wyświetlaczowi jaki będzie zakres wysyłanych przez nas danych.
Przykładowo, jeśli rysowałbym linie rozciągającą się na 20 kolumn powinienem w drugim poleceniu po komendzie „OLED_SET_COLUMN_ADDR” wysłać wartość „x+20”. Piksel zajmuje tylko jedną kolumnę więc wysłałem podwójnie – x.
Podobnie z ustawianiem kursora na wiersz, tutaj nie musimy podawać zakresu – przynajmniej w tym przypadku.
Ostatnie trzy polecenia są odpowiedzialne za wyznaczanie adresu piksela. Jak już wiesz, adres każdego piksela to po prostu liczba 2 podniesiona do potęgi jego numeru, ale w zakresie 0-7.
I dokładne ten adres obliczają dwa przedostatnie polecenia. Załóżmy, że chcemy podświetlić piksel (127,63). Wartość jaką przyjmie zmienna row to 7, bo tyle wychodzi reszty z dzielenia 63/8. Więc teraz wystarczy tylko podnieść liczbę 2 do potęgi naszej reszty (7) i otrzymamy 128. A więc adres ostatniego piksela, każdej kolumny!
Podnoszenie do potęgi zrealizowałem za pomocą pętli for nie bez przyczyny. W języku C polecenie potęgowania znajduje się w bibliotece math, a jej import od razu, znacznie zwiększa zajętość pamięci flash.
Czyszczenie ekranu
Teraz wrócimy do czyszczenia ekranu i zrobimy sobie do tego prostą funkcję. Napiszę ją jednak w taki sposób by można było wybrać miejsce, z którego chcemy zacząć czyszczenie, oraz jego zakres. Ponieważ nie zawsze będzie potrzeba czyszczenia całego ekranu a tylko wybranego obszaru.
Tylko jeszcze przed tym, w pliku nagłówkowym stwórzmy sobie strukturę o nazwie cursor, a w niej dwa elementy „x”, oraz „y”. Jak nie trudno się domyśleć będziemy tam przetrzymywali pozycje naszego kursoru.
struct{ uint8_t x; uint8_t y; } cursor;
Funkcja czyszcząca wyświetlacz oled:
void OLED_clear(uint16_t val) { OLED_cmd(0x20); OLED_cmd(0x00); if (val != 1024) { OLED_cmd(0x21); OLED_cmd(cursor.x); OLED_cmd(cursor.x+val); OLED_cmd(0x22); OLED_cmd(cursor.y); } else { OLED_cmd(0x21); OLED_cmd(0); OLED_cmd(127); OLED_cmd(0x22); OLED_cmd(0); OLED_cmd(7); } for (uint16_t i=0; i<v; i++) OLED_data(0x00); }
Jeżeli funkcji tej przekażemy wartość 1024, to po prostu wyczyści cały ekran. Natomiast gdy podamy inną wartość jako argument val, to zacznie czyścić ekran od miejsca zdefiniowanego przez nasz programowy kursor.
Powinniśmy więc stworzyć funkcję ustawiającą kursor w wybranym przez nas miejscu:
void set_cursor(uint8_t x,uint8_t y) { if (x > 127 || x < 0 || y > 7 || y < 0) return; cursor.x = x; cursor.y = y; }
Rysowanie prostej linii
Będzie to kolejna opcja by lepiej zaznajomić się z obsługą naszych wyświetlaczy. Napisałem, na szybko dwie funkcje, z których jedna rysuje linię w pionie, a drugia w poziomie. Jako wyzwanie możesz spróbować tak je zmodyfikować by możliwe było rysowanie linii pod kątem, czy o różnej grubości.
Linia pionowa
void OLED_draw_lineV(uint8_t x, uint8_t y, uint8_t length) { uint8_t row = 0; uint8_t pixel = 1; OLED_cmd(0x20); //set addressing OLED_cmd(0x00); //horizontal addresing row = y/8; OLED_cmd(0x21); //set column OLED_cmd(x); OLED_cmd(x); OLED_cmd(0x22); //set row OLED_cmd(row); uint8_t suma = 255; row = (row+1) * 8 - y; for (uint8_t i=0; i<8-row; i++) { suma -= pixel; pixel = pixel * 2; } OLED_data(suma); length -= row; while (1) { if (length - 8 >= 0) { OLED_data(0xff); length -= 8; } else break; } if (length != 0) { suma = 0; pixel = 1; for (uint8_t i=0; i<length; i++) { suma += pixel; pixel = pixel * 2; } OLED_data(suma); } }
Tylko skrótowo opiszę co tutaj się dzieje… Funkcji przekazujemy trzy argumenty, miejsce startu (x,y), oraz długość rysowanej linii. Od początku, funkcja wylicza ile pikseli podświetlić ma w wierszu, w którym zaczyna rysować. Pętla while podświetla tyle pełnych wierszy (kolumienek 8-pikseli) ile może, równocześnie cały czas aktualizując zmienną length, która trzyma liczbe pozostałych pikseli do podświetlenia. Ostatni blok sprawdza czy są jeszcze piksele do podświetlenia, jeśli tak to podświetla tyle ile zostało.
Funkcja nie jest idealną implementacją, a pierwszą jaką napisałem. Na pierwszy rzut oka – działa.
Linia pozioma
Poniżej funkcja rysująca linię poziomą.
void OLED_draw_lineH(uint8_t x, uint8_t y, uint8_t length) { uint8_t b=2; uint8_t t=0; OLED_cmd(0x20); //set addressing OLED_cmd(0x00); //horizontal addresing OLED_cmd(0x21); //set column OLED_cmd(x); OLED_cmd(x+length); t = y/8; OLED_cmd(0x22); //set row OLED_cmd(t); t = y%8; for (uint8_t i=1; i<t; i++) b = b*2; if (t==0) b=1; for (uint8_t i=0; i<length; i++) OLED_data(b); }
Biblioteka OLED – podsumowanie
Wyświetlacze oled są świetnym sposobem na prezentowanie danych, zarówno graficznych, jak i tekstowych w nawet bardziej zaawansowanych urządzeniach.
A po przeczytwaniu tego wpisu, wiesz już jak obsługiwać wyświetlacze oled za pomocą mikrokontrolera atmega i nie tylko. Jest to pierwsza część z przewidywanych trzech, kolejne dotyczyć będą już generowaniu bitmap, oraz czcionek. A także wypisywaniu tekstu za pomocą, właśnie wygenerowanych przez nas czcionek.
Dodaj komentarz