Jak działa sterownik wyświetlacza OLED?
W pierwszej części tego artykułu zapoznamy się z obsługą sterowników wyświetlaczy OLED, takich jak SH1106, SSD1306, SSD1309, itp. Wszystkie z nich działają bardzo podobnie. Jeżeli masz już doświadczenie z kontrolerami tego typu, to możesz pominąć ten fragment i przejść od razu do kolejnego rozdziału.
Weźmy na warsztat wyświetlacz o rozdzielczości 128×64 pikseli. Jest to bardzo typowa rozdzielczość w przypadku wyświetlaczy OLED, choć oczywiście zdarzają się także modele większe, jak i mniejsze. Spójrz na rysunek 1. Punkt zerowy układu współrzędnych znajduje się w lewym górnym rogu. Oś X jest skierowana w prawo, a oś Y w dół.
Wyświetlacz jest podzielony na osiem poziomych pasów, zwanych stronami (ang. page), a każda strona ma wysokość 8 pikseli. Jest to spowodowane faktem, że interfejsy (SPI, I²C, 8080), przez które sterownik otrzymuje dane do wyświetlenia, są 8-bitowe, a wyświetlacz jest monochromatyczny. Przesyłając jeden bajt danych do wyświetlacza jesteśmy zatem w stanie ustawić kolumnę ośmiu pikseli jednocześnie. Zobacz niebieską kolumnę widoczną na rysunku 1, która symbolicznie przedstawia jeden bajt danych do wyświetlenia, odpowiedzialnych za osiem pikseli. Stan wysoki bitu oznacza zaświecenie piksela, a stan niski powoduje, że piksel staje się niewidoczny.
Sterowniki wyświetlaczy, oprócz przesyłania danych, obsługują sporo różnych komend, które konfigurują rozmaite parametry, takie jak obsługiwana rozdzielczość, rotacja, kontrast, napięcie przetwornicy, częstotliwość odświeżania, tryby uśpienia itp.
Przed pierwszym transferem danych graficznych wymagane jest przesłanie konfiguracji do sterownika, aby mógł on poprawnie obsługiwać matrycę OLED. Konfiguracja jest zapisywana w pamięci RAM, zatem po każdym włączeniu zasilania wymagane jest ponowne przesłanie konfiguracji.
Dwoma najczęściej używanymi poleceniami jest ustawienie pozycji kursora w pionie i poziomie, przy czym ustawienie w pionie polega na wybraniu numeru aktywnej strony. Po ustawieniu kursora na wybraną pozycję, zwykle następuje przesyłanie danych grafiki do wyświetlenia. Po przesłaniu bajtu obrazu jest on natychmiast pokazywany na wyświetlaczu, a kursor przesuwa się o jedną pozycję w prawo, po czym można od razu przesyłać kolejne bajty do wyświetlenia. W ten sposób możemy zdecydować, czy chcemy przesłać całą klatkę obrazu w jednej transmisji, czy też wolimy zaktualizować tylko wybrany fragment obrazu.
Oprócz pinów typowych dla interfejsu I²C czy SPI, sterowniki te mają także linię DC (data/command). Jej zadaniem jest informowanie sterownika o tym, czy bajty przesyłane z procesora są poleceniem do wykonania, czy fragmentem obrazu do pokazania na matrycy. Stan wysoki tego pinu oznacza, że przesyłane są dane, a stan niski odpowiada za transfer polecenia.
Wyświetlacz o rozdzielczości 128×64 px ma 8192 piksele. Skoro w jednym bajcie kodujemy osiem pikseli, to jedna klatka obrazu zajmuje 1024 bajty.
Założenia projektu
W tym odcinku kursu użyjemy FPGA MachXO2 oraz mikrokontrolera ESP32, które dostępne są na płytce User Interface Board, zaprezentowanej w EP 09/2023. Program na mikrokontrolerze, napisany w MicroPythonie, będzie generować obraz do bufora, znajdującego się w pamięci RAM mikrokontrolera ESP32. Następnie obraz ma zostać przesłany z RAM (w ESP32) przez interfejs SPI do FPGA, gdzie zapisany zostanie do dwuportowej pamięci RAM, stanowiącej drugi bufor obrazu. W FPGA potrzebny będzie także sterownik VGA, odczytujący odpowiednie piksele z dwuportowej pamięci RAM i generujący na ich podstawie sygnały na wyjściu VGA.
W ten sposób opracujemy prosty silnik graficzny z podwójnym buforowaniem obrazu i jednocześnie przećwiczymy w praktyce obsługę modułów, jakie stworzyliśmy w poprzednich odcinkach kursu – w szczególności SPI i VGA.
Sterowniki wyświetlaczy OLED najczęściej są monochromatyczne. Moglibyśmy na monitorze VGA wyświetlać białe piksele na czarnym tle, ale żeby było trochę ciekawiej, będziemy pokazywać piksele w kolorze żółtym na tle niebieskim.
Moduł VGA, jaki opracowaliśmy w poprzednim odcinku kursu, obsługiwał wyświetlanie obrazu o rozdzielczości 640×480 pikseli. Gdybyśmy chcieli wyświetlać obraz monochromatyczny o takiej rozdzielczości, to potrzebowalibyśmy 38400 bajtów pamięci RAM (640×480/8=38400), co dalece przekracza zasoby nawet największego FPGA z rodziny MachXO2. Musielibyśmy sięgnąć po zewnętrzną pamięć RAM lub zastosować jakąś cwaną sztuczkę.
Spróbujmy przeskalować obraz. Gdybyśmy 640 podzielili przez 5 to dostaniemy 128. Bingo! Dokładnie tyle, ile wynosi rozdzielczość pozioma typowego wyświetlacza OLED. Natomiast jeżeli rozdzielczość pionową monitora VGA, tzn 480, podzielimy przez 5 to otrzymamy 96. Jest to trochę więcej, niż spotyka się w wyświetlaczach OLED, ale lepiej mieć za dużo niż za mało. Rozdzielczość pionowa 96 pikseli oznacza, że będziemy mieć do dyspozycji 12 stron o wysokości 8 pikseli.
Zatem z punktu widzenia sterownika będziemy mieć obraz o rozdzielczości 128×96 px, ale przez monitor VGA będzie on widziany jako 640×480 i składać będzie się zawsze z bloków o rozmiarach 5×5 pikseli. Zastanówmy się też, ile pamięci potrzebujemy: pomnóżmy 128 przez 96 i podzielmy przez 8. Daje to 1536 bajtów. Bez problemu pomieścimy tyle wewnątrz FPGA.
Zastanówmy się jeszcze nad pinem DC. Moglibyśmy pobawić się w implementację różnych poleceń, jak np. ustawianie kursora, lecz stwierdziłem, że na potrzeby kursu nie ma to większego sensu. W związku z tym uprościmy komunikację tylko do przesyłania danych obrazu, a wszystkie polecenia będą ignorowane. Nasz sterownik nie będzie miał żadnych możliwości zmiany konfiguracji i po włączeniu zasilania będzie od razu gotowy do pracy.
Ponadto zrobimy jeszcze jedno uproszczenie. Rozpoczęcie transmisji na magistrali SPI, tzn. zbocze opadające na linii CS, będzie powodować postawienie kursora w lewym górnym rogu wyświetlacza, czyli w punkcie zerowym układu współrzędnych. Następnie trzeba będzie przesłać cały bufor obrazu w jednej transmisji, tzn. paczkę o rozmiarze 1536 bajtów danych.