Zazwyczaj wyświetlacze są wykonane jako płytka szklana z przyklejonym do niej płaskim złączem taśmowym. Z punktu widzenia konstruktora chcącego zastosować taki element w produkcji małoseryjnej nie jest to wygodne, ponieważ wymaga wykonania mocowania szklanego panelu, zastosowania złącza dla taśmy itp. To również praktycznie dyskwalifikuje taki wyświetlacz, jeżeli chcemy go po prostu przetestować.
Wyświetlacz opisywany w artykule nie ma tych wad, bo został przymocowany na stałe do płytki drukowanej, na której umieszczono wszystkie niezbędne elementy dodatkowe: stabilizator LDO napięcia +3,3 V, goldpiny do podłączenia sygnałów magistrali przeznaczonej mikrokontrolera-hosta, pola lutownicze do przylutowania taśmy wyświetlacza (fotografia 1).
Sterownik SH1106
Do sterowania matrycą i komunikacji z hostem zastosowano sterownik SH1106 (rysunek 2). Jest to specjalizowany układ przeznaczony do sterowania matryc OLED/PLED o rozdzielczości maksymalnej 132×64 piksele. Wbudowany driver steruje 132 segmentami i 64 kolumnami matrycy typu Common Cathode. Sterownik ma tryb oszczędzania energii Sleep Mode z poborem prądu nieprzekraczającym 5 mA. Nie bez znaczenia jest tez szeroki zakres temperatury pracy - od -40°C do +85°C.
Magistrala sterująca
Sterownik komunikuje się z hostem za pomocą magistrali. Jej budowa jest determinowana przez ilość przesyłanych danych. Dla wyświetlaczy kolorowych z głębią 24-bitową ilość przesyłanych danych jest stosunkowo duża. Wtedy najlepszym rozwiązaniem jest zastosowanie magistrali równoległej, 8- lub 16- bitowej charakteryzującej się duża przepływnością danych.
W małych wyświetlaczach monochromatycznych ilość przesyłanych danych jest stosunkowo nieduża i dlatego można stosować magistrale szeregowe, zajmujące mniej miejsca i wymagające mniejszej liczby portów procesora-hosta. Sterownik SH1106 ma 8-bitową magistralę równoległą mogącą pracować w trybach zgodnych z Intel 8080 lub Motorola 6800 oraz 2 interfejsy szeregowe: SPI i I²C. Wyboru interfejsu aktywnego dokonuje się przez wymuszenie odpowiednich poziomów logicznych na wejściach konfiguracyjnych IM0...IM2.
Często zdarza się, że producent wyświetlacza nie wyprowadza wejść konfiguracyjnych i magistrala jest ustawiona na stałe, bez możliwości zamiany przez użytkownika. Tak jest i w tym przypadku. Uznano, że wyświetlacz będzie sterowany za pomocą I²C i innej możliwości nie ma.
Pod sposobu wykonania połączeń magistrala I²C jest jedną z mniej skomplikowanych magistral szeregowych, ponieważ wymaga tylko 2 linii (SDA i SCL) oraz 2 rezystorów podciągających te linie do plusa zasilania. Prosty interfejs jest za to bardziej skomplikowany w obsłudze programowej w porównaniu np. z SPI lub interfejsem równoległym. Sterownik jest układem Slave mającym swój własny adres, a nadrzędny system sterujący jest układem Master.
Każdy transfer danych na magistrali I²C rozpoczyna się od wysłania sekwencji Start, a następnie bajta z adresem Slave (7 starszych bitów) i najmłodszego bitu R/W. Adres Slave ma wartość 0111100b, czyli kompletny bajt zapisu danych do SH1106 to 0x78 (R/W=0) i 0x79 dla odczytu danych (R/W=1). Po przesłaniu adresu protokół transmisji SH1106 wymaga przesłania bajta kontrolnego.
Mają w nim znaczenie 2 najstarsze bity: Co i D/C. Pozostałych 6 młodszych bitów musi być zerami. Jeżeli bit Co jest wyzerowany, to następne bajty są bajtami danych. Po wpisaniu do Co jedynki sterownik interpretuje dwa następne bajty jako dane i kolejny bajt jako bajt kontrolny. Wyzerowania bitu R/W oznacza, że następny bajt jest komendą. Dla R/W=1 kolejne bajty są bajtami danych wpisywanych do pamięci obrazu sterownika. Sekwencję zapisu pokazano na rysunku 3.
STM32F100 - interfejs I²C
Do sterowania wyświetlaczem użyłem modułu STM32F100 Discovery z mikrokontrolerem STM32F100RB8T6. Mikrokontroler ma wbudowany sprzętowy interfejs I²C, który zostanie wykorzystany do komunikacji z wyświetlaczem. Na początku programu skonfigurujemy ten interfejs.
Sprzętowy interfejs STM32 może pracować w trybie z wieloma masterami i obsługuje adresowanie 7 i 10 bitowe. Dostępne są standardowe prędkości transmisji (100 kHz) i szybkie prędkości (Fast Speed - 400 kHz). Do obsługi transmisji można wykorzystać mechanizm przerwań lub odpytywanie (pooling). Możliwe jest też przesyłanie danych z wykorzystaniem kanału DMA.
W naszym przypadku będziemy wykorzystywać wyłącznie tryb Master ( z jednym masterem na magistrali), a dane będą wysyłane tylko przez Mastera - ze sterownika wyświetlacza nic nie będziemy odczytywali.
Oprogramowanie skomplikowanego interfejsu, mimo że nie jest specjalnie trudne, to jest zadaniem żmudnym, na które trzeba poświęcić sporo czasu. Aby tego uniknąć można skorzystać z gotowego rozwiązania. Firma STM udostępnia notę aplikacyjną AN 2824 opisującą sposób obsługi I²C oraz dołączony do niej program demonstracyjny.
Po krótkim przetestowaniu procedur z tej noty postanowiłem zastosować je do sterowania wyświetlaczem. Projekt przykładowy jest skonfigurowany dla taktowania z częstotliwością 72 MHz, natomiast rdzeń zastosowanego mikrokontrolera może pracować z maksymalną częstotliwością taktowania 24 MHz. Jeżeli tego nie zmienimy, to mikrokontroler nie będzie działał. Predefiniowane ustawienia taktowania są umieszone w pliku system_stm32f10x.c, którego zawartość pokazano na listingu 1.
Przed użyciem moduł I²C musi być zainicjowany. Pierwsza cześć inicjacji dotyczy konfigurowania linii portów dla sygnałów SDA i SCL, druga część, to konfiguracja samego modułu I²C, a trzecia opcjonalnie konfiguruje kanał DMA - listing 2.
Na rysunku 4 pokazano sekwencja zapisywania danych przez Mastera do układu Slave.
Z zadaniami wykonywanymi przez mastera na magistrali są związane zdarzenia (events, EV):
- EW5: SB1, zerowana po odczytaniu rejestru SR1 . Po tym jest zapisywany rejestr DR z bajtem adresu Slave uzupełnionym o bit R/W=0.
- EV6: ADDR=1, ustawiany w rejestrze SR1, zerowany przez odczytanie rejestru SR2.
- EV8_1: TxE=1, rejestr przesuwny jest pusty, rejestr danych jest pusty, zapisanie danej do rejestru DR.
- EV8: TxE=1, rejestr przesuwny nie jest pusty, rejestr danych jest pusty, zerowany przez zapis rejestru DR.
- EV8_2:, TxE=1, BTF=1 - żądanie sekwencji STOP. TxE i BTF zerowany sprzętowo przez sekwencję Stop.
Nota aplikacyjna zawiera procedurę wysyłania przez I²C zawartości bufora z danymi. Jej argumentami są: numer interfejsu sprzętowego (I²C1 lub I²C2), wskaźnik do bufora z danymi, liczba bajtów do wysłania, tryb pracy i adres Slave.
Możliwe są 3 tryby pracy: polling, interrupt i DMA. Sprawdziłem działanie wszystkich. Z jakichś powodów nie pracował poprawnie tryb interrupt. Nie sprawdzałem, dlaczego tak było - zapewne przez różnice pomiędzy rdzeniami STM32F100 i STM32F103. Za to tryby polling i DMA działały bardzo dobrze.
Na listingu 3 pokazano procedurę wysyłania zawartości bufora w trybie polling. W pierwszej kolejności jest inicjowana sekwencja Start. Zakończenie tej sekwencji jest sygnalizowane przez wyzerowanie bitu SB - zdarzenie EV5 (rysunek 4). Jeżeli sekwencja Start nie zakończy się w określonym czasie, to procedura kończy działanie i zwraca koda błędu.
Po sekwencji Start jest wysyłany adres slave przez zapisanie jego wartości do rejestru DR. Wysłanie adresu i odebranie bitu potwierdzenia A jest sygnalizowane ustawieniem bitu ADDR. Jeżeli wszystko przebiega poprawnie, to można zapisywać w pętli kolejne dane do rejestru DR. Wysłanie kompletnej danej jest sygnalizowane ustawieniem bitu TxE. Na listingu 4 pokazano procedurę wysyłania zawartości bufora z wykorzystaniem kanału DMA.
Mając do dyspozycji procedurę wysyłania bajtów można napisać dwie funkcje podstawowe - do wysyłania komendy i do wysyłania jednej danej. Pokazano je na listingu 5 i listingu 6.
Funkcja OledCmd jest przeznaczona tylko do wysyłania komend bez argumentu. Zgodnie z tym, co napisano w bajcie kontrolnym przesyłanym po adresie slave bit Co=1 i bit D/C=0. Kiedy jest przesyłana dana przez funkcję OledData, to w bajcie kontrolnym Co=0 i D/C=1.
Każde przesłanie danej z wykorzystaniem funkcji OledData wymaga wysłania adresu slave, bajta kontrolnego i właściwego bajta danej. W przypadku przesyłania dużej ilości danych, na przykład przy wyświetlaniu bitmap, można skorzystać z procedury I²C_Master_BufferWrite, tylko należy pamiętać, że pierwszy bajt bufora wysyłany po adresie slave jest bajtem kontrolnym o wartości 0x40.
Inicjalizacja sterownika
Każdy sterownik wyświetlacza graficznego wymaga zainicjowania. Konieczność inicjalizacji wynika na przykład z możliwości ustalania współrzędnej początkowej (0,0) zależnie od mocowania mechanicznego panelu, różnych źródeł zasileń driverów matrycy (wewnętrzna przetwornica vs napięcie podawane zewnętrznie), poziomu kontrastu, częstotliwości odświeżania itp. Inicjalizacja polega na wysłaniu szeregu komend ustalających początkowe parametry pracy.
W tabeli 1 umieszczono wykaz ważniejszych komend sterownika SH1106. Wszystkie komendy są dokładnie opisane w dokumentacji sterownika i powielanie tego opisu nie ma sensu. Dla nas najważniejsze jest takie zainicjowanie sterownika, aby pracował poprawnie z panelem wyświetlacza. Inicjalizacja powinna być przeprowadzana na podstawie danych technicznych producenta matrycy OLED. Niestety, w wypadku tego wyświetlacza nie mamy takich danych i trzeba niektóre ustawienia dobrać eksperymentalnie.
Ponieważ część komend wymaga oprócz samej komendy dodatkowego argumentu, to nie użyłem do inicjowania funkcji OledCmd, tylko wszystkie bajty komend inicjalizacji łącznie z niezbędnymi argumentami umieściłem w buforze Buffer_Init. Zawartość bufora z pierwszym bajtem kontrolnym 0x80 jest wysyłana do sterownika wyświetlacza za pomocą funkcji I²C_Master_BufferWrite. Zawartość tego bufora pokazano na listingu 7.
Inicjalizacje można podzielić na kilka etapów:
- Wyzerowanie liczników: kolumn, stron pamięci i linii początkowej.
- Ustawienie powiązania zawartości pamięci RAM obrazu z pozycją na panelu OLED (orientacja wyświetlania).
- Ustawienie kontrastu, przetwornicy DC/DC zasilającej drivery, i częstotliwości taktowania, oraz włączenie wyświetlenia.
Żeby prawidłowo zapisywać dane do zainicjalizowanego sterownika trzeba znać powiązanie zawartości pamięci obrazu z wyświetlanymi pikselami na matrycy OLED. Matryca jest monochromatyczna, a w takim wypadku jednemu pikselowi odpowiada jeden bit w pamięci obrazu. Pomimo że pamięć ma wielkość 132×64 bity, to w rzeczywistości ma ona organizacje bajtową.
Host wysyła kolejne bajty za pomocą I²C pod lokacje określone przez liczniki kolumn i stron. Przyporządkowanie tak zaadresowanej pamięci obrazu do pikseli na matrycy pokazano na rysunku 5. Jeden bajt w pamięci obrazu odpowiada pionowej linijce o długości 8 pikseli . Najmłodszy bit tego bajta jest pikselem położonym najwyżej w linijce, a bit najstarszy pikselem położonym najniżej.
Położenie linijki w poziomie określa licznik kolumn zmieniający się w zakresie 0...131, a położenie w pionie określa licznik stron zmienia się w zakresie 0...7. Po ustaleniu numeru strony kolejne zapisywane bajty tworzą pasek o szerokości 8 pikseli. Każdy zapis danej powoduje inkrementację licznika kolumn i nowa dana jest zapisywana po kolejna lokację. Kiedy licznik kolumn osiągnie wartość 131, to po następnym zapisie danych jest zerowany.
Przepełnienia licznika kolumn nie powoduje inkrementacji licznika stron i jeżeli nie zmodyfikujemy go wykorzystując do tego celu komendę "ustawienie licznika stron", to kolejne wpisy do pamięci będą nadpisywać dane począwszy od pozycji zerowej (wyzerowanie licznika kolumn). To ważna właściwość, o której trzeba pamiętać, tym bardziej, że w podobnych rozwiązaniach (na przykład SSD1306) przepełnienie licznika kolumn może powodować inkrementację licznika stron.
Tryb tekstowy
Wyświetlanie tekstu jest bardzo ważnym elementem tworzenia graficznego interfejsu użytkownika. W odróżnieniu od klasycznych wyświetlaczy, alfanumerycznych można tu definiować czcionki o różnej wielkości i stosować je zależnie od potrzeb.
Przypuszczalnie im mniejszy wyświetlacz, tym większe znaki trzeba będzie definiować, aby były dobrze widoczne. Tablice wzorców dużych znaków zajmują sporo miejsca w pamięci programu mikrokontrolera i dlatego warto definiować tylko te, które są niezbędne. Typowym przykładem takiego podejścia jest definiowanie tylko wzorców cyfr 0...9, plus parę znaków dodatkowych potrzebnych do wyświetlania wartości cyfrowych.
W czasie testów wyświetlacza najpierw wykorzystałem tablicę z wzorcami znaków 8×6 pikseli stosowaną przeze mnie przy wyświetlaniu tekstów na monochromatycznym wyświetlaczu LCD od telefonu komórkowego Nokia 3310. Przy używanym w sterowniku trybie adresowania wystarczy ustawić adres kolumny i numer strony (listing 8) i wysłać do sterownika kolejnych 6 bajtów (listing 9).
Definiowanie tablicy wzorców znaków jest zajęciem pracochłonnym i dlatego warto skorzystać ze specjalnych programów, które zrobią to za nas. W testach wyświetlacza użyłem darmowego programu TheDotFactory autorstwa Erana Duchana. Najnowsza wersja programu generuje wzorce w konwencji adresowania pamięci stosowanej w sterowniku SH1106 (rys. 5).
Dodatkowo, można zdefiniować tablicę wzorca tylko dla wybranych znaków. Ze względów praktycznych (zajętość pamięci Flash) zdefiniowałem tablicę znaków (głównie cyfr) dla symboli "średnich" o wielkości będącej wielokrotnością 8 pikseli (1 bajta), wysokości 16 pikseli (2 bajty) i szerokości 10 pikseli oraz "dużych" o wysokości 24 pikseli (3 bajty) i szerokości 15 pikseli. Procedurę wyświetlania symboli "średnich" pokazano na listingu 10, a "dużych" na listingu 11.
W przypadku znaku mającego 16×10 pikseli najpierw jest wyświetlana górna połowa znaku, czyli linijka o wysokości 8 pikseli i szerokości 10 pikseli, przez wysłanie do sterownika 10 bajtów z tablicy wzorca. Potem jest ustawiana wartość początkowa licznika kolumn, licznik stron jest inkrementowany i do sterownika jest wysłanych kolejnych 10 bajtów z tablicy wzorca.
Zasada wyświetlania dużych znaków jest identyczna z tym, że wysyłane są bajty wzorca w trzech porcjach po 15 bajtów. Na fotografii 6 pokazano ekran z wyświetlonymi znakami o 3 różnych wielkościach, od największej do najmniejszej. Nawet przy tak małym wyświetlaczu i ustawieniu kontrastu na średnią wartość duże znaki są dobrze widoczne i czytelne z odległości około 2 metrów. To zapewne zasługa technologii OLED, bo trudno byłoby uzyskać taki efekt z wyświetlaczem LCD o identycznych wymiarach.
Wyświetlanie bitmap
Żeby wyświetlić bitmapę trzeba ją najpierw przekształcić do postaci monochromatycznej oraz dostosować jej wielkości do rozdzielczości matrycy. W naszym wypadku bitmapa musi mieć 128×64 piksele. Do zmiany wielkości obrazu można użyć programów graficznych, na przykład Paint, Microsoft Office Picture Manager, Paint.net lub innych.
Tak przygotowaną bitmapę trzeba następnie przekonwertować do tablicy w języku C. Tutaj jest niezbędne zastosowanie specjalnego programu, który potrafi to zrobić zgodnie z zasadą powiązania zawartości pamięci programu z wyświetlanymi pikselami na matrycy - ja użyłem bmp2c.exe. Po wczytaniu bitmapy, zaznaczeniu opcji V-8 i kliknięciu na przycisk "C", program generuje tablicę w języku C i umieszcza ją w schowku systemu Windows.
We własnym programie wystarczy wkleić tę tablicę i pobierać z niej dane w porcjach po 128 bajtów, a po każdej porcji inkrementować licznik stron. Procedurę wyświetlająca pełnowymiarową bitmapę pokazano na listingu 12, a efekt jej działania na fotografii 8.
Funkcję OledBmp można zoptymalizować czasowo. Każdy bajt z tablicy bitmapy jest wysyłany do sterownika wyświetlacza przez funkcję OledData, Jak już wspomniałem, wymaga to wysłania adresu slave, bajta kontrolnego i bajta danej. Nie jest zbyt optymalne czasowo, ale w trakcie testów okazało się, że użyty mikrokontroler i jego interfejs I²C wysyła dane na tyle szybko, że bitmapa ładuje się bez widocznego migotania.
Tomasz Jabłoński, EP