Kurs STM32 F1 HAL – #10 – wyświetlacz graficzny na SPI

Kurs STM32 F1 HAL – #10 – wyświetlacz graficzny na SPI

W poprzedniej części kursu poznaliśmy działanie interfejsu SPI. W praktyce wykorzystaliśmy go do komunikacji z ekspanderem portów MCP23S08.

Teraz zajmiemy się ciekawszym tematem, czyli wyświetlaczem graficznym z telefonu Nokia 5110. Jest to tani i popularny moduł, który może ułatwić interakcję z budowanym urządzeniem.

Wyświetlacz graficzny

Zanim zaczniemy podłączać przewody i programować, powinniśmy poświęcić chwilę na zapoznanie się z naszym modułem. Ten typ wyświetlacza był kiedyś wykorzystywany w telefonach Nokia 5110, stąd nazwa modułu. Ma on rozdzielczość 84 na 48 pikseli i monochromatyczną matrycę LCD. Wyświetlacz ten wchodzi w skład rozszerzonych zestawów dedykowanych do tego kursu.

04614_5

W załączniku do tego artykułu można znaleźć dokumentację sterownika PCD8544, na którym bazuje wyświetlacz. Jak widać na zdjęciu (oraz w dokumentacji), jest on sterowany przez interfejs SPI oraz kilka dodatkowych linii.

  • RST - linia resetująca rejestry wyświetlacza. Przed rozpoczęciem pracy z wyświetlaczem, należy wygenerować na niej stan zero przez co najmniej 100 ns. Podczas normalnej pracy wyświetlacza, na tej linii powinien być ciągle stan wysoki.
  • CE - jest to linia CS interfejsu SPI, nazwa pochodzi od Chip Enable.
  • DC - linia ustalająca, czy przesyłamy dane (stan wysoki), czy komendy (stan niski).
  • Din - linia danych, czyli MOSI interfejsu SPI.
  • CLK - linia zegarowa SPI, odpowiada SCLK.
  • VCC - napięcie zasilające moduł (3.3V).
  • BL - podświetlanie wyświetlacza.
  • GND - masa.

Jak widzimy znajdziemy tutaj jednokierunkowy interfejs SPI, czyli nie możemy odczytywać danych z wyświetlacza. Producent zastosował inne oznaczenia linii (CE zamiast CS, Din zamiast MOSI i CLK zamiast SCLK), jednak sam interfejs działa bez zmian.

Dodatkowe linie sterujące to: RST, która resetuje zawartość pamięci i rejestrów sterownika wyświetlacza oraz DC, która pozwala na wybór typu przesyłanych danych. Można przesyłać dane przeznaczone do wyświetlania lub komendy nim sterujące. Poza tym mamy trzy linie zasilające: masę (GND), zasilanie 3.3V (VCC) oraz zasilanie dla podświetlania (BL).

Czas na praktykę!

Czas na praktykę!

Podłączenie wyświetlacza do Nucleo

Gdy już poznaliśmy nasz moduł, czas połączyć go z płytką Nucleo-STM32. Wykorzystamy ten sam interfejs SPI, co w poprzedniej części kursu. Nowe linie sterujące (DC, CE, RST) podłączymy do portu C. Zostawimy pin PC0 wolny - dzięki temu, będziemy mogli w przyszłości podłączyć jednocześnie MCP23S08 oraz wyświetlacz.

Wykorzystamy więc kolejne piny PC1, PC2 i PC3. Pin sterujący podświetlaniem możemy podłączyć do PC4, ale jeśli chcemy regulować jasność świecenia, wykorzystajmy do tego wyjście PWM - omawialiśmy je w 7 części kursu. Podłączymy więc linię BL do pinu PB6.

Zestaw elementów do kursu

Gwarancja pomocy na forum Błyskawiczna wysyłka

Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!

Kup w Botland.com.pl

Kompletny schemat montażowy wygląda następująco:

spi-lcd_bb

Podłączenie wyświetlacza graficznego do STM32.

Komunikacja z wyświetlaczem

Ponieważ nowych linii sterujących jest nieco więcej, zdefiniujemy stałe, które będą określały, do których pinów podłączyliśmy wyświetlacz (wszystkie dotyczą portu C):

Konfiguracja SPI została omówiona w poprzedniej części, teraz kod wygląda dokładnie tak samo. Dodajemy tylko konfigurację nowych linii:

Linia CE oraz RST jest aktywowana stanem niskim, więc wystawiamy na nich logiczną jedynkę. Pierwszym krokiem podczas komunikacji z wyświetlaczem jest zresetowanie jego kontrolera. W tym celu, na wyjściu RST musimy wygenerować stan 0 przez co najmniej 100 ns.

Napiszmy więc prostą procedurę:

Opóźnienie nie było konieczne, ale w razie problemów zawsze można dodać małe opóźnienie do procedury resetowania.

Następny etap to wysyłanie komend sterujących pracą wyświetlacza. Do rozróżnienia między danymi, a komendami służy linia DC, stan niski informuje o wysyłaniu komendy sterującej. Teraz możemy napisać prostą procedurę, która wyśle komendę do wyświetlacza:

Ostatni etap, to wysłanie danych do wyświetlania. Przygotujemy procedurę, która za jednym wywołaniem prześle cały bufor z pamięci RAM lub Flash do wyświetlacza. Jako parametry będziemy podawali adres bufora oraz liczbę bajtów do przesłania:

Skąd weźmiemy dane do wyświetlania? Możemy je wygenerować programowo. Inna możliwość to wykorzystanie konwertera bitmap do przetłumaczenia obrazka na tablicę wartości. Wykorzystamy drugą opcję i wyświetlimy teraz logo Forbota.

Konwerter, po wczytaniu mapy bitowej z grafiką i ustawieniem odpowiedniej rozdzielczości ekranu, przygotuje dla nas tablicę, taką jak poniższa. Obsługa programu jest bardzo intuicyjna. Gdyby ktoś miał z nią jednak problemy, to proszę pytać w komentarzach.

Fragment tablicy, w której zapisany został kawałek loga Forbota:

Mamy już wszystkie dane potrzebne do obsługi wyświetlacza. Nie będziemy w tej chwili wnikać w wartości rejestrów sterownika ekranu, napiszemy więc kod z magicznymi liczbami. Jednak ciekawskich odsyłam do wspomnianej wcześniej dokumentacji.

Tylko tyle kodu jest niezbędne, aby zobaczyć logo na wyświetlaczu. Oczywiście musieliśmy wcześniej przygotować sporo funkcji pomocniczych, ale teraz obsługa LCD jest już łatwa.

Pełny kod programu:

Efekt działania programu w praktyce wygląda następująco:

STM32 i wyświetlacz graficzny.

STM32 i wyświetlacz graficzny.

Reorganizacja kodu

Dotychczas wszystkie przykłady były pisane w jednym pliku. Taki styl programowania jest odpowiedni do prostych i krótkich przykładów, ale w miarę jak programy stają się bardziej rozbudowane jest bardzo niewygodny i nieczytelny.

Znacznie lepszym rozwiązaniem jest podzielenie dużego programu na mniejsze moduły. Takie moduły są czytelniejsze, a co więcej możemy je wykorzystywać w kolejnych programach.

Spróbujemy podzielić program sterujący wyświetlaczem na mniejsze moduły, a jednocześnie dodać więcej funkcji do samego sterowania wyświetlaczem.

W pierwszym przykładzie po prostu skopiowaliśmy gotowy bufor danych z pamięci Flash do wyświetlacza. Teraz będziemy postępować dwustopniowo - w pamięci RAM mikrokontrolera zadeklarujemy tablicę z buforem danych obrazu. Będziemy w niej przygotowywać dane do wyświetlenia, a następnie w jednym kroku kopiować całą tablicę do LCD.

Takie działanie ma kilka zalet. Po pierwsze dostęp do pamięci RAM jest o wiele szybszy niż do danych wyświetlacza. Po drugie nie możemy odczytywać danych z wyświetlacza, więc manipulacja zawartością ekranu byłaby dużo trudniejsza.

Najpierw podzielmy nasz program na mniejsze moduły. W poprzednich częściach kursu, często wykorzystywaliśmy procedurę delay_ms(). Jest ona bardzo wygodna, ale za każdym razem musieliśmy ją wklejać do kodu. Zamiast tego przygotujemy dla niej moduł, który w przyszłości może jeszcze nam się przydać.

W języku C moduł składa się z dwóch części: pliku nagłówkowego z rozszerzeniem .h oraz kodu źródłowego, z rozszerzeniem .c. Plik nagłówkowy powinien zawierać tylko deklaracje zmiennych i funkcji, natomiast cały kod powinien zostać wstawiony do pliku .c.

Zobaczmy to na przykładzie. Najpierw przygotowujemy plik bitmap.h. W celu dodania nowego pliku najlepiej w widoku projektu, kliknąć na katalog src i wybrać opcję New, a następnie Header File lub Source File (zależnie, czy dodajemy plik .h, czy .c):

Następnie treść wypełniamy w poniższy sposób:

Dyrektywy #ifndef służą unikaniu wielokrotnego włączania tego samego pliku nagłówkowego. Natomiast, to co ważne, to deklaracja tablicy forbot_logo(). Jest opatrzona słówkiem kluczowym extern, żeby zasygnalizować iż jest to tylko deklaracja - sama tablica zdefiniowana jest w innym pliku.

Definicję tablicy z logo forbota umieszczamy w pliku bitmap.c, który tworzymy analogicznie do wcześniejszego, musimy tylko wybrać odpowiedni typ, czyli Source File:

Jaka jest zaleta takiego postępowania? Najważniejsza to podział większego problemu na mniejsze. Kiedy używamy modułu bitmap, interesuje nas tylko plik nagłówkowy bitmap.h - implementacja jest dla nas nieistotna. Zakładamy, że to, co zadeklarowane zostało przetestowane i działa. Jak zobaczymy później pliki z kodem mogą być o wiele dłuższe niż pliki nagłówkowe. Dużo łatwiej jest wiec pamiętać tylko o zawartości pliku bitmap.h niż zastanawiać się która funkcja do czego służy.

Ponieważ rysowanie gotowych bitmap może nam się wydać niewystarczające, dodamy procedurę wyświetlającą napisy. Niestety w tym celu będziemy potrzebowali fontów. Wykorzystamy gotowy font o wymiarach 5x8 pikseli.

Plik font.h:

Implementacja w pliku font.c:

Mając już wszystkie moduły pomocnicze możemy napisać naszą bibliotekę graficzną. Plik nagłówkowy lcd.h wygląda następująco:

Jak widzimy, przygotowujemy 5 procedur:

  • lcd_setup - konfiguruje wyświetlacz, powinna być wywołana na początku programu
  • lcd_clear - czyści bufor ekranu
  • lcd_draw_bitmap - rysuje bitmapę na całym ekranie
  • lcd_draw_text - wypisuje tekst na ekranie
  • lcd_copy - przesyła zawartość bufora do wyświetlacza

Jest to prosta biblioteka, ale jej kod jest już dość długi. Tutaj pojawia się jednak zaleta modułowości - nie musimy go czytać. A nawet jeśli przeczytamy (do czego zachęcam), nie będziemy musieli pamiętać wszystkich jego detali. Do używania wystarczy znajomość pliku nagłówkowego.

Kod lcd.c:

Program główny

Mamy już przygotowaną bibliotekę. Napiszmy teraz prosty programik, który będzie jej używał. Najpierw wyświetlimy logo Forbot-a. Poczekamy 1 s, po czym wypiszemy w kolejnych wierszach napis "Forbot!". Kod jest bardzo prosty:

Pełny kod programu (main.c):

Efekt działania powyższego programu widoczny jest na poniższym zdjęciu:

Wyświetlanie napisów na wyświetlaczu przez SPI na STM32.

Wyświetlanie napisów na wyświetlaczu przez SPI na STM32.

Rozbudowa biblioteki graficznej

Poprzednia wersja biblioteki jest bardzo uboga. Dodajmy więc jeszcze dwie funkcje:

  • lcd_draw_pixel - do zapalania wybranego punktu na ekranie
  • lcd_draw_line - rysującą linię między wybranymi punktami

Dzięki temu będziemy mogli rozszerzyć możliwości naszego wyświetlacza. Do pliku nagłówkowego warto również dodać stałe opisujące wymiary ekranu - będzie się z nich łatwiej korzystać. Nowy nagłówek biblioteki wygląda następująco (lcd.h):

Do rysowania linii wykorzystywany jest algorytm Brensenhama. Kod lcd.c:

Aby zaprezentować nowe możliwości, napiszemy program, który narysuje ramkę dookoła ekranu, po czym będzie wyświetlał obracającą się linię:

Działanie programu widoczne jest na poniższej animacji (słabo jakość wynika z kompresji obrazu):

Prosta animacja na wyświetlaczu.

Prosta animacja na wyświetlaczu.

Podświetlanie ekranu

Dotychczas nie wykorzystywaliśmy podświetlania ekranu. Niewątpliwą zaletą technologii LCD w porównaniu z TFT jest możliwość pracy wyświetlacza nawet bez podświetlania (czyli wykorzystując światło z otoczenia).

Możemy jednak dodać odpowiedni kod i sprawdzić jak łatwo jest sterować jasnością podświetlania. Sterowanie PWM omawialiśmy już w kursie, więc po prostu wykorzystamy omówiony tam kod:

W praktyce program działał następująco:

Podsumowanie

W tej części napisaliśmy własną bibliotekę obsługi wyświetlacza graficznego. Łącznie poznaliśmy dwa układy peryferyjne podłączane za pomocą interfejsu SPI: I/O ekspander oraz wyświetlacz LCD. Podłączanie innych układów przebiega podobnie - samo SPI działa właściwie zawsze tak samo.

W kolejnym odcinku kursu omówimy kolejny interfejs - I2C. Dzięki jego pomocy będzie mogli użyć zewnętrznej pamięci EEPROM oraz w wersji bardziej zaawansowanej będziemy odczytywać dane z akcelerometru oraz magnetometru!

Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk

Redakcja: Damian Szymański

kurs, kursSTM32F1HAL, lcd, nokia, SPI, stm32, wyświetlacz

Załączniki

Komentarze

Dodaj komentarz